From 023e2bcd2fc61d227294cf311c39982bd4ff42b2 Mon Sep 17 00:00:00 2001 From: bvcxza <175357591+bvcxza@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:16:13 -0300 Subject: [PATCH 001/371] Fix deviation percent for fixed-price crypto offers (#1411) --- .../desktop/main/offer/offerbook/OfferBookViewModel.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index f5fdd74509..b0a0f7c1df 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -402,9 +402,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { } public Optional getMarketBasedPrice(Offer offer) { - OfferDirection displayDirection = offer.isTraditionalOffer() ? direction : - direction.equals(OfferDirection.BUY) ? OfferDirection.SELL : OfferDirection.BUY; - return priceUtil.getMarketBasedPrice(offer, displayDirection); + return priceUtil.getMarketBasedPrice(offer, direction); } String formatMarketPriceMarginPct(Offer offer) { From 5221782ba044b1ffc23131b8c828e903b9d62d99 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 13 Nov 2024 20:12:48 -0500 Subject: [PATCH 002/371] return empty list if no backup files exist --- common/src/main/java/haveno/common/file/FileUtil.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/haveno/common/file/FileUtil.java b/common/src/main/java/haveno/common/file/FileUtil.java index 8bb6ae75a8..ca533cc0d2 100644 --- a/common/src/main/java/haveno/common/file/FileUtil.java +++ b/common/src/main/java/haveno/common/file/FileUtil.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Date; @@ -76,11 +77,11 @@ public class FileUtil { public static List getBackupFiles(File dir, String fileName) { File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString()); - if (!backupDir.exists()) return null; + if (!backupDir.exists()) return new ArrayList(); String dirName = "backups_" + fileName; if (dirName.contains(".")) dirName = dirName.replace(".", "_"); File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString()); - if (!backupFileDir.exists()) return null; + if (!backupFileDir.exists()) return new ArrayList(); File[] files = backupFileDir.listFiles(); return Arrays.asList(files); } From 59d8a8ee44325d525f18a5027fdbe3c37ff47405 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 13 Nov 2024 12:00:24 -0500 Subject: [PATCH 003/371] trader can re-open dispute unless payout confirmed --- .../core/support/dispute/DisputeManager.java | 214 +++++++++--------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index 6fca89f8ea..d6b2469744 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -359,6 +359,13 @@ public abstract class DisputeManager> extends Sup return; } + // skip if payout is confirmed + if (trade.isPayoutConfirmed()) { + String errorMsg = "Cannot open dispute because payout is already confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId(); + faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg)); + return; + } + synchronized (disputeList.getObservableList()) { if (disputeList.contains(dispute)) { String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId(); @@ -368,116 +375,109 @@ public abstract class DisputeManager> extends Sup } Optional storedDisputeOptional = findDispute(dispute); - boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed(); - if (!storedDisputeOptional.isPresent() || reOpen) { + boolean reOpen = storedDisputeOptional.isPresent(); - // add or re-open dispute - if (reOpen) { - dispute = storedDisputeOptional.get(); - } else { - disputeList.add(dispute); - } - - String disputeInfo = getDisputeInfo(dispute); - String sysMsg = dispute.isSupportTicket() ? - Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) : - Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); - - ChatMessage chatMessage = new ChatMessage( - getSupportType(), - dispute.getTradeId(), - keyRing.getPubKeyRing().hashCode(), - false, - Res.get("support.systemMsg", sysMsg), - p2PService.getAddress()); - chatMessage.setSystemMessage(true); - dispute.addAndPersistChatMessage(chatMessage); - - // export latest multisig hex - try { - trade.exportMultisigHex(); - } catch (Exception e) { - log.error("Failed to export multisig hex", e); - } - - // create dispute opened message - NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); - DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, - p2PService.getAddress(), - UUID.randomUUID().toString(), - getSupportType(), - trade.getSelf().getUpdatedMultisigHex(), - trade.getArbitrator().getPaymentSentMessage()); - log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}", - disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, - disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), - chatMessage.getUid()); - recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName()); - - // send dispute opened message - trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); - mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, - dispute.getAgentPubKeyRing(), - disputeOpenedMessage, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}", - disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, - disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), - chatMessage.getUid()); - clearPendingMessage(); - - // We use the chatMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setArrived(true); - trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); - requestPersistence(); - resultHandler.handleResult(); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}", - disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, - disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), - chatMessage.getUid()); - clearPendingMessage(); - - // We use the chatMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setStoredInMailbox(true); - requestPersistence(); - resultHandler.handleResult(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}, errorMessage={}", - disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, - disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), - chatMessage.getUid(), errorMessage); - - clearPendingMessage(); - // We use the chatMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setSendMessageError(errorMessage); - trade.setDisputeState(Trade.DisputeState.NO_DISPUTE); - requestPersistence(); - faultHandler.handleFault("Sending dispute message failed: " + - errorMessage, new DisputeMessageDeliveryFailedException()); - } - }); + // add or re-open dispute + if (reOpen) { + dispute = storedDisputeOptional.get(); } else { - String msg = "We got a dispute already open for that trade and trading peer.\n" + - "TradeId = " + dispute.getTradeId(); - log.warn(msg); - faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); + disputeList.add(dispute); } + + String disputeInfo = getDisputeInfo(dispute); + String sysMsg = dispute.isSupportTicket() ? + Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) : + Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); + + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + keyRing.getPubKeyRing().hashCode(), + false, + Res.get("support.systemMsg", sysMsg), + p2PService.getAddress()); + chatMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(chatMessage); + + // export latest multisig hex + try { + trade.exportMultisigHex(); + } catch (Exception e) { + log.error("Failed to export multisig hex", e); + } + + // create dispute opened message + NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); + DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType(), + trade.getSelf().getUpdatedMultisigHex(), + trade.getArbitrator().getPaymentSentMessage()); + log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), + chatMessage.getUid()); + recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName()); + + // send dispute opened message + trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); + mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, + dispute.getAgentPubKeyRing(), + disputeOpenedMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), + chatMessage.getUid()); + clearPendingMessage(); + + // We use the chatMessage wrapped inside the openNewDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setArrived(true); + trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); + requestPersistence(); + resultHandler.handleResult(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), + chatMessage.getUid()); + clearPendingMessage(); + + // We use the chatMessage wrapped inside the openNewDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setStoredInMailbox(true); + requestPersistence(); + resultHandler.handleResult(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}, errorMessage={}", + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), + chatMessage.getUid(), errorMessage); + + clearPendingMessage(); + // We use the chatMessage wrapped inside the openNewDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setSendMessageError(errorMessage); + trade.setDisputeState(Trade.DisputeState.NO_DISPUTE); + requestPersistence(); + faultHandler.handleFault("Sending dispute message failed: " + + errorMessage, new DisputeMessageDeliveryFailedException()); + } + }); } requestPersistence(); From 86e67d384cc5038ed6a10f151f7ca7966b62df10 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 13 Nov 2024 18:41:57 -0500 Subject: [PATCH 004/371] new dispute state is considered open --- core/src/main/java/haveno/core/support/dispute/Dispute.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/support/dispute/Dispute.java b/core/src/main/java/haveno/core/support/dispute/Dispute.java index 268cb3e272..15595b8893 100644 --- a/core/src/main/java/haveno/core/support/dispute/Dispute.java +++ b/core/src/main/java/haveno/core/support/dispute/Dispute.java @@ -467,7 +467,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload { } public boolean isOpen() { - return this.disputeState == State.OPEN || this.disputeState == State.REOPENED; + return isNew() || this.disputeState == State.OPEN || this.disputeState == State.REOPENED; } public boolean isClosed() { From c9e992442c3a746e3cd8c7f7dd5a98fd1bbe60ef Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 15 Nov 2024 09:27:06 -0500 Subject: [PATCH 005/371] bump version to 1.0.14 --- build.gradle | 2 +- common/src/main/java/haveno/common/app/Version.java | 2 +- desktop/package/macosx/Info.plist | 4 ++-- seednode/src/main/java/haveno/seednode/SeedNodeMain.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 81a315ad50..d35c6bd05c 100644 --- a/build.gradle +++ b/build.gradle @@ -610,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.13-SNAPSHOT' + version = '1.0.14-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 14e5a6c7c8..2b4b01b036 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.0.13"; + public static final String VERSION = "1.0.14"; /** * Holds a list of the tagged resource files for optimizing the getData requests. diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index 2d4618f7a0..31325683fd 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.13 + 1.0.14 CFBundleShortVersionString - 1.0.13 + 1.0.14 CFBundleExecutable Haveno diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index b1da3d3e19..e0f5ad7c3e 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.0.13"; + private static final String VERSION = "1.0.14"; private SeedNode seedNode; private Timer checkConnectionLossTime; From 264cb5f0acd2ebc4523de2e092eb7697dcc52956 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 15 Nov 2024 10:41:42 -0500 Subject: [PATCH 006/371] fix inverted buy/sell label on make or take crypto offer --- .../haveno/desktop/main/offer/MutableOfferView.java | 8 ++++---- .../desktop/main/offer/takeoffer/TakeOfferView.java | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index db008e93ff..3c6ed09788 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -308,7 +308,7 @@ public abstract class MutableOfferView> exten if (CurrencyUtil.isTraditionalCurrency(tradeCurrency.getCode())) { placeOfferButtonLabel = Res.get("createOffer.placeOfferButton", Res.get("shared.buy")); } else { - placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.buy"), tradeCurrency.getCode()); + placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.sell"), tradeCurrency.getCode()); } nextButton.setId("buy-button"); fundFromSavingsWalletButton.setId("buy-button"); @@ -317,7 +317,7 @@ public abstract class MutableOfferView> exten if (CurrencyUtil.isTraditionalCurrency(tradeCurrency.getCode())) { placeOfferButtonLabel = Res.get("createOffer.placeOfferButton", Res.get("shared.sell")); } else { - placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.sell"), tradeCurrency.getCode()); + placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.buy"), tradeCurrency.getCode()); } nextButton.setId("sell-button"); fundFromSavingsWalletButton.setId("sell-button"); @@ -707,10 +707,10 @@ public abstract class MutableOfferView> exten triggerPriceInputTextField.clear(); if (!CurrencyUtil.isTraditionalCurrency(newValue)) { if (model.isShownAsBuyOffer()) { - placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.buy"), + placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.sell"), model.getTradeCurrency().getCode())); } else { - placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.sell"), + placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.buy"), model.getTradeCurrency().getCode())); } } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index 1b0867ef7d..6f4e9635c1 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -306,12 +306,12 @@ public class TakeOfferView extends ActivatableViewAndModel Date: Sun, 17 Nov 2024 10:27:12 -0500 Subject: [PATCH 007/371] update hyperlink to f2f payment method --- core/src/main/resources/i18n/displayStrings.properties | 2 +- core/src/main/resources/i18n/displayStrings_tr.properties | 2 +- .../content/traditionalaccounts/TraditionalAccountsView.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 6e9ad25fb2..4525c6a33e 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -3016,7 +3016,7 @@ payment.f2f.info='Face to Face' trades have different rules and come with differ of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to \ an agreement.\n\n\ To be sure you fully understand the differences with 'Face to Face' trades please read the instructions and \ - recommendations at: [HYPERLINK:https://haveno.exchange/wiki/Face-to-face_(payment_method)] + recommendations at: [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/F2F] payment.f2f.info.openURL=Open web page payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} payment.f2f.offerbook.tooltip.extra=Additional information: {0} diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index d50061a138..c405e3c2e1 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -3009,7 +3009,7 @@ payment.f2f.info='Yüz Yüze' ticaretler farklı kurallara sahiptir ve çevrimi yardımcı olamaz. Bu tür durumlarda XMR fonları süresiz olarak veya ticaret eşleri anlaşmaya varana \ kadar kilitlenebilir.\n\n\ 'Yüz Yüze' ticaretlerin farklarını tam olarak anladığınızdan emin olmak için lütfen şu adresteki talimatları \ - ve tavsiyeleri okuyun: [HYPERLINK:https://haveno.exchange/wiki/Face-to-face_(payment_method)] + ve tavsiyeleri okuyun: [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/F2F] payment.f2f.info.openURL=Web sayfasını aç payment.f2f.offerbook.tooltip.countryAndCity=Ülke ve şehir: {0} / {1} payment.f2f.offerbook.tooltip.extra=Ek bilgi: {0} diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java index 191bce5dc9..6ed91e956b 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java @@ -275,7 +275,7 @@ public class TraditionalAccountsView extends PaymentAccountsView GUIUtil.openWebPage("https://haveno.exchange/wiki/Face-to-face_(payment_method)")) + .onClose(() -> GUIUtil.openWebPage("https://docs.haveno.exchange/the-project/payment_methods/F2F")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); From 24657c6c57de950ac458eee9aef15ffdc66ae928 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 18 Nov 2024 09:20:39 -0500 Subject: [PATCH 008/371] update translation for funding wallet on create offer --- core/src/main/resources/i18n/displayStrings_cs.properties | 7 ++++++- core/src/main/resources/i18n/displayStrings_de.properties | 7 ++++++- core/src/main/resources/i18n/displayStrings_es.properties | 7 ++++++- core/src/main/resources/i18n/displayStrings_fa.properties | 7 ++++++- core/src/main/resources/i18n/displayStrings_fr.properties | 7 ++++++- core/src/main/resources/i18n/displayStrings_it.properties | 7 ++++++- core/src/main/resources/i18n/displayStrings_ja.properties | 7 ++++++- .../main/resources/i18n/displayStrings_pt-br.properties | 7 ++++++- core/src/main/resources/i18n/displayStrings_pt.properties | 7 ++++++- core/src/main/resources/i18n/displayStrings_ru.properties | 7 ++++++- core/src/main/resources/i18n/displayStrings_th.properties | 7 ++++++- core/src/main/resources/i18n/displayStrings_tr.properties | 6 +++--- core/src/main/resources/i18n/displayStrings_vi.properties | 7 ++++++- .../main/resources/i18n/displayStrings_zh-hans.properties | 7 ++++++- .../main/resources/i18n/displayStrings_zh-hant.properties | 7 ++++++- 15 files changed, 87 insertions(+), 17 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index c47bb44032..ada00d8344 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -458,7 +458,12 @@ createOffer.placeOfferButton=Přehled: Umístěte nabídku {0} monero createOffer.createOfferFundWalletInfo.headline=Financujte svou nabídku # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Výše obchodu: {0}\n -createOffer.createOfferFundWalletInfo.msg=Do této nabídky musíte vložit {0}.\n\nTyto prostředky jsou rezervovány ve vaší lokální peněžence a budou uzamčeny na vkladové multisig adrese, jakmile někdo příjme vaši nabídku.\n\nČástka je součtem:\n{1}- Vaše kauce: {2}\n- Obchodní poplatek: {3}\n- Poplatek za těžbu: {4}\n\nPři financování obchodu si můžete vybrat ze dvou možností:\n- Použijte svou peněženku Haveno (pohodlné, ale transakce mohou být propojitelné) NEBO\n- Přenos z externí peněženky (potenciálně více soukromé)\n\nPo uzavření tohoto vyskakovacího okna se zobrazí všechny možnosti a podrobnosti financování. +createOffer.createOfferFundWalletInfo.msg=Potřebujete vložit {0} do této nabídky.\n\n\ + Tyto prostředky jsou rezervovány ve vaší místní peněžence a budou zablokovány v multisig peněžence, jakmile někdo přijme vaši nabídku.\n\n\ + Částka je součtem:\n\ + {1}\ + - Vaše záloha: {2}\n\ + - Poplatek za obchodování: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Při zadávání nabídky došlo k chybě:\n\n{0}\n\nPeněženku ještě neopustily žádné finanční prostředky.\nRestartujte aplikaci a zkontrolujte síťové připojení. diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 5e691f539e..79f6704ade 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -458,7 +458,12 @@ createOffer.placeOfferButton=Überprüfung: Anbieten moneros zu {0} createOffer.createOfferFundWalletInfo.headline=Ihr Angebot finanzieren # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Handelsbetrag: {0} \n -createOffer.createOfferFundWalletInfo.msg=Sie müssen zum Annehmen dieses Angebots {0} einzahlen.\n\nDiese Gelder werden in Ihrer lokalen Wallet reserviert und in die MultiSig-Kautionsadresse eingesperrt, wenn jemand Ihr Angebot annimmt.\n\nDer Betrag ist die Summe aus:\n{1}- Kaution: {2}\n- Handelsgebühr: {3}\n- Mining-Gebühr: {4}\n\nSie haben zwei Möglichkeiten, Ihren Handel zu finanzieren:\n- Nutzen Sie Ihre Haveno-Wallet (bequem, aber Transaktionen können nachverfolgbar sein) ODER\n- Von einer externen Wallet überweisen (möglicherweise vertraulicher)\n\nSie werden nach dem Schließen dieses Dialogs alle Finanzierungsmöglichkeiten und Details sehen. +createOffer.createOfferFundWalletInfo.msg=Sie müssen {0} in dieses Angebot einzahlen.\n\n\ + Diese Gelder werden in Ihrer lokalen Wallet reserviert und in eine Multisig-Wallet gesperrt, sobald jemand Ihr Angebot annimmt.\n\n\ + Der Betrag ist die Summe aus:\n\ + {1}\ + - Ihre Sicherheitskaution: {2}\n\ + - Handelsgebühr: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Es gab einen Fehler beim Erstellen des Angebots:\n\n{0}\n\nEs haben noch keine Gelder Ihre Wallet verlassen.\nBitte starten Sie Ihre Anwendung neu und überprüfen Sie Ihre Netzwerkverbindung. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 26be43ee98..43a92fe7a7 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -458,7 +458,12 @@ createOffer.placeOfferButton=Revisar: Poner oferta para {0} monero createOffer.createOfferFundWalletInfo.headline=Dote de fondos su trato. # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Cantidad a intercambiar: {0}\n -createOffer.createOfferFundWalletInfo.msg=Necesita depositar {0} para completar esta oferta.\n\nEsos fondos son reservados en su cartera local y se bloquearán en la dirección de depósito multifirma una vez que alguien tome su oferta.\nLa cantidad es la suma de:\n{1}- Su depósito de seguridad: {2}\n- Comisión de intercambio: {3}\n- Comisión de minado: {4}\n\nPuede elegir entre dos opciones a la hora de depositar fondos para realizar su intercambio:\n- Usar su cartera Haveno (conveniente, pero las transacciones pueden ser trazables) O también\n- Transferir desde una cartera externa (potencialmente con mayor privacidad)\n\nConocerá todos los detalles y opciones para depositar fondos al cerrar esta ventana. +createOffer.createOfferFundWalletInfo.msg=Necesitas depositar {0} para esta oferta.\n\n\ + Estos fondos están reservados en tu billetera local y se bloquearán en una billetera multisig una vez que alguien acepte tu oferta.\n\n\ + El monto es la suma de:\n\ + {1}\ + - Tu depósito de seguridad: {2}\n\ + - Comisión de comercio: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Ocurrió un error al colocar la oferta:\n\n{0}\n\nNingún importe de su cartera ha sido deducido aún.\nPor favor, reinicie su aplicación y compruebe su conexión a la red. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 171f3929ef..90613ad019 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -457,7 +457,12 @@ createOffer.placeOfferButton=بررسی: پیشنهاد را برای {0} بی createOffer.createOfferFundWalletInfo.headline=پیشنهاد خود را تامین وجه نمایید # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=مقدار معامله:{0}\n -createOffer.createOfferFundWalletInfo.msg=شما باید {0} برای این پیشنهاد، سپرده بگذارید.\nآن وجوه در کیف پول محلی شما ذخیره شده اند و هنگامی که کسی پیشنهاد شما را دریافت می کند، به آدرس سپرده چند امضایی قفل خواهد شد.\n\nمقدار مذکور، مجموع موارد ذیل است:\n{1} - سپرده‌ی اطمینان شما: {2}\n-هزینه معامله: {3}\n-هزینه تراکنش شبکه: {4}\nشما هنگام تامین مالی معامله‌ی خود، می‌توانید بین دو گزینه انتخاب کنید:\n- از کیف پول Haveno خود استفاده کنید (این روش راحت است، اما ممکن است تراکنش‌ها قابل رصد شوند)، یا\n- از کیف پول خارجی انتقال دهید (به طور بالقوه‌ای این روش ایمن‌تر و محافظ حریم خصوصی شما است)\n\nشما تمام گزینه‌ها و جزئیات تامین مالی را پس از بستن این پنجره، خواهید دید. +createOffer.createOfferFundWalletInfo.msg=شما باید {0} را برای این پیشنهاد واریز کنید.\n\n\ +این وجوه در کیف پول محلی شما رزرو می‌شوند و هنگامی که کسی پیشنهاد شما را قبول کند، به یک کیف پول مولتی‌سیگ قفل خواهند شد.\n\n\ +مقدار این مبلغ مجموع موارد زیر است:\n\ +{1}\ +- ودیعه امنیتی شما: {2}\n\ +- هزینه معامله: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=یک خطا هنگام قرار دادن پیشنهاد، رخ داده است:\n\n{0}\n\nهیچ پولی تاکنون از کیف پول شما کم نشده است.\nلطفاً برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی نمایید. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 1dfb4e2b96..ab196d4eff 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -458,7 +458,12 @@ createOffer.placeOfferButton=Review: Placer un ordre de {0} monero createOffer.createOfferFundWalletInfo.headline=Financer votre ordre # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=Montant du trade: {0}\n\n -createOffer.createOfferFundWalletInfo.msg=Vous devez déposer {0} pour cet ordre.\n\nCes fonds sont réservés dans votre portefeuille local et seront bloqués sur une adresse de dépôt multisig une fois que quelqu''un aura accepté votre ordre.\n\nLe montant correspond à la somme de:\n{1}- Votre dépôt de garantie: {2}\n- Frais de trading: {3}\n- Frais d''exploitation minière: {4}\n\nVous avez le choix entre deux options pour financer votre transaction :\n- Utilisez votre portefeuille Haveno (pratique, mais les transactions peuvent être associables) OU\n- Transfert depuis un portefeuille externe (potentiellement plus privé)\n\nVous pourrez voir toutes les options de financement et les détails après avoir fermé ce popup. +createOffer.createOfferFundWalletInfo.msg=Vous devez déposer {0} à cette offre.\n\n\ + Ces fonds sont réservés dans votre portefeuille local et seront verrouillés dans un portefeuille multisignature dès qu'une personne acceptera votre offre.\n\n\ + Le montant est la somme de :\n\ + {1}\ + - Votre dépôt de garantie : {2}\n\ + - Frais de transaction : {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Une erreur s''est produite lors du placement de cet ordre:\n\n{0}\n\nAucun fonds n''a été prélevé sur votre portefeuille pour le moment.\nVeuillez redémarrer l''application et vérifier votre connexion réseau. diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 6964d462b9..e7dd6f01f8 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -457,7 +457,12 @@ createOffer.placeOfferButton=Revisione: piazza l'offerta a {0} monero createOffer.createOfferFundWalletInfo.headline=Finanzia la tua offerta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Importo di scambio: {0} \n -createOffer.createOfferFundWalletInfo.msg=Devi depositare {0} a questa offerta.\n\nTali fondi sono riservati nel tuo portafoglio locale e verranno bloccati nell'indirizzo di deposito multisig una volta che qualcuno accetta la tua offerta.\n\nL'importo è la somma di:\n{1} - Il tuo deposito cauzionale: {2}\n- Commissione di scambio: {3}\n- Commissione di mining: {4}\n\nPuoi scegliere tra due opzioni quando finanzi il tuo scambio:\n- Usa il tuo portafoglio Haveno (comodo, ma le transazioni possono essere collegabili) OPPURE\n- Effettua il trasferimento da un portafoglio esterno (potenzialmente più privato)\n\nVedrai tutte le opzioni di finanziamento e i dettagli dopo aver chiuso questo popup. +createOffer.createOfferFundWalletInfo.msg=Devi depositare {0} per questa offerta.\n\n\ + Questi fondi sono riservati nel tuo portafoglio locale e verranno bloccati in un portafoglio multisig una volta che qualcuno accetta la tua offerta.\n\n\ + L'importo è la somma di:\n\ + {1}\ + - Il tuo deposito di sicurezza: {2}\n\ + - Tassa di trading: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Si è verificato un errore durante l'immissione dell'offerta:\n\n{0}\n\nNon sono ancora usciti fondi dal tuo portafoglio.\nRiavvia l'applicazione e controlla la connessione di rete. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 1229dbe19f..55a0352d37 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -458,7 +458,12 @@ createOffer.placeOfferButton=再確認: ビットコインを{0}オファーを createOffer.createOfferFundWalletInfo.headline=あなたのオファーへ入金 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 取引額: {0}\n -createOffer.createOfferFundWalletInfo.msg=このオファーに対して {0} のデポジットを送金する必要があります。\n\nこの資金はあなたのローカルウォレットに予約済として保管され、オファーが受け入れられた時にマルチシグデポジットアドレスに移動しロックされます。\n\n金額の合計は以下の通りです\n{1} - セキュリティデポジット: {2}\n- 取引手数料: {3}\n- マイニング手数料: {4}\n\nこのオファーにデポジットを送金するには、以下の2つの方法があります。\n- Havenoウォレットを使う (便利ですがトランザクションが追跡される可能性があります)\n- 外部のウォレットから送金する (機密性の高い方法です)\n\nこのポップアップを閉じると全ての送金方法について詳細な情報が表示されます。 +createOffer.createOfferFundWalletInfo.msg=このオファーには {0} をデポジットする必要があります。\n\n\ + この資金はあなたのローカルウォレットに予約され、誰かがあなたのオファーを受け入れるとマルチシグウォレットにロックされます。\n\n\ + 金額は以下の合計です:\n\ + {1}\ + - あなたの保証金: {2}\n\ + - 取引手数料: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=オファーを出す時にエラーが発生しました:\n\n{0}\n\nウォレットにまだ資金がありません。\nアプリケーションを再起動してネットワーク接続を確認してください。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 7c79f99361..20f897fcee 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -460,7 +460,12 @@ createOffer.placeOfferButton=Revisar: Criar oferta para {0} monero createOffer.createOfferFundWalletInfo.headline=Financiar sua oferta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Quantia da negociação: {0} \n -createOffer.createOfferFundWalletInfo.msg=Você precisa depositar {0} para esta oferta.\n\nEsses fundos ficam reservados na sua carteira local e ficarão travados no endereço de depósito multisig quando alguém aceitar a sua oferta.\n\nA quantia equivale à soma de:\n{1}- Seu depósito de segurança: {2}\n- Taxa de negociação: {3}\n- Taxa de mineração: {4}\n\nVocê pode financiar sua negociação das seguintes maneiras:\n- Usando a sua carteira Haveno (conveniente, mas transações poderão ser associadas entre si) OU\n- Usando uma carteira externa (maior privacidade)\n\nVocê verá todas as opções de financiamento e detalhes após fechar esta janela. +createOffer.createOfferFundWalletInfo.msg=Você precisa depositar {0} para esta oferta.\n\n\ + Esses fundos são reservados em sua carteira local e serão bloqueados em uma carteira multisig assim que alguém aceitar sua oferta.\n\n\ + O valor é a soma de:\n\ + {1}\ + - Seu depósito de segurança: {2}\n\ + - Taxa de negociação: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Um erro ocorreu ao emitir uma oferta:\n\n{0}\n\nNenhum fundo foi retirado de sua carteira até agora.\nPor favor, reinicie o programa e verifique sua conexão de internet. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index fd6d3c6260..604640c56c 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -457,7 +457,12 @@ createOffer.placeOfferButton=Rever: Colocar oferta para {0} monero createOffer.createOfferFundWalletInfo.headline=Financiar sua oferta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Quantia de negócio: {0} \n -createOffer.createOfferFundWalletInfo.msg=Você precisa depositar {0} para esta oferta.\n\nEsses fundos estão reservados na sua carteira local e serão bloqueados no endereço de depósito multi-assinatura assim que alguém aceitar a sua oferta.\n\nA quantia é a soma de:\n{1} - Seu depósito de segurança: {2}\n- Taxa de negociação: {3}\n- Taxa de mineração: {4}\n\nVocê pode escolher entre duas opções ao financiar o seu negócio:\n- Use sua carteira Haveno (conveniente, mas as transações podem ser conectadas) OU\n- Transferência de uma carteira externa (potencialmente mais privada)\n\nVocê verá todas as opções de financiamento e detalhes depois de fechar este popup. +createOffer.createOfferFundWalletInfo.msg=Você precisa depositar {0} para esta oferta.\n\n\ + Esses fundos são reservados em sua carteira local e serão bloqueados em uma carteira multisig assim que alguém aceitar sua oferta.\n\n\ + O valor é a soma de:\n\ + {1}\ + - Seu depósito de segurança: {2}\n\ + - Taxa de negociação: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Ocorreu um erro ao colocar a oferta:\n\n{0}\n\nAinda nenhuns fundos saíram da sua carteira.\nPor favor, reinicie seu programa e verifique sua conexão de rede. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 5f976febdf..fef440d3eb 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -457,7 +457,12 @@ createOffer.placeOfferButton=Проверка: разместить предло createOffer.createOfferFundWalletInfo.headline=Обеспечить своё предложение # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Сумма сделки: {0} \n -createOffer.createOfferFundWalletInfo.msg=Вы должны внести {0} для обеспечения этого предложения.\n\nЭти средства будут зарезервированы в вашем локальном кошельке, а когда кто-то примет ваше предложение — заблокированы на депозитном multisig-адресе.\n\nСумма состоит из:\n{1}- вашего залога: {2},\n- комиссии за сделку: {3},\n- комиссии майнера: {4}.\n\nВы можете выбрать один из двух вариантов финансирования сделки:\n - использовать свой кошелёк Haveno (удобно, но сделки можно отследить) ИЛИ\n - перевести из внешнего кошелька (потенциально более анонимно).\n\nВы увидите все варианты обеспечения предложения и их подробности после закрытия этого окна. +createOffer.createOfferFundWalletInfo.msg=Вам нужно внести депозит {0} для этого предложения.\n\n\ + Эти средства резервируются в вашем локальном кошельке и будут заблокированы в мультиподписном кошельке, как только кто-то примет ваше предложение.\n\n\ + Сумма состоит из:\n\ + {1}\ + - Ваш залог: {2}\n\ + - Торговая комиссия: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Ошибка при создании предложения:\n\n{0}\n\nВаши средства остались в кошельке.\nПерезагрузите приложение и проверьте сетевое соединение. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 15486e225f..e3eca2f0d6 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -457,7 +457,12 @@ createOffer.placeOfferButton=รีวิว: ใส่ข้อเสนอไ createOffer.createOfferFundWalletInfo.headline=เงินทุนสำหรับข้อเสนอของคุณ # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- ปริมาณการซื้อขาย: {0} -createOffer.createOfferFundWalletInfo.msg=คุณต้องวางเงินมัดจำ {0} ข้อเสนอนี้\n\nเงินเหล่านั้นจะถูกสงวนไว้ใน wallet ภายในประเทศของคุณและจะถูกล็อคไว้ในที่อยู่ที่ฝากเงิน multisig เมื่อมีคนรับข้อเสนอของคุณ\n\nผลรวมของจำนวนของ: \n{1} - เงินประกันของคุณ: {2} \n- ค่าธรรมเนียมการซื้อขาย: {3} \n- ค่าขุด: {4} \n\nคุณสามารถเลือกระหว่างสองตัวเลือกเมื่อมีการระดุมทุนการซื้อขายของคุณ: \n- ใช้กระเป๋าสตางค์ Haveno ของคุณ (สะดวก แต่ธุรกรรมอาจเชื่อมโยงกันได้) หรือ\n- โอนเงินจากเงินภายนอกเข้ามา (อาจเป็นส่วนตัวมากขึ้น) \n\nคุณจะเห็นตัวเลือกและรายละเอียดการระดมทุนทั้งหมดหลังจากปิดป๊อปอัปนี้ +createOffer.createOfferFundWalletInfo.msg=คุณจำเป็นต้องฝากเงิน {0} เพื่อข้อเสนอนี้\n\n\ + เงินเหล่านี้จะถูกสงวนไว้ในกระเป๋าเงินในเครื่องของคุณ และจะถูกล็อกในกระเป๋าเงินมัลติซิกเมื่อมีคนรับข้อเสนอของคุณ\n\n\ + จำนวนเงินคือผลรวมของ:\n\ + {1}\ + - เงินประกันของคุณ: {2}\n\ + - ค่าธรรมเนียมการซื้อขาย: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=เกิดข้อผิดพลาดขณะใส่ข้อเสนอ: \n\n{0} \n\nยังไม่มีการโอนเงินจาก wallet ของคุณเลย\nโปรดเริ่มแอปพลิเคชันใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณ diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index c405e3c2e1..a7e587ce11 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -495,10 +495,10 @@ createOffer.createOfferFundWalletInfo.headline=Teklifinizi finanse edin # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Ticaret miktarı: {0} \n createOffer.createOfferFundWalletInfo.msg=Bu teklife {0} yatırmanız gerekiyor.\n\n\ - Bu fonlar yerel cüzdanınızda ayrılır ve birisi teklifinizi aldığında multisig bir cüzdana kilitlenir.\n\n\ - Miktar, şunların toplamıdır:\n\ + Bu fonlar yerel cüzdanınızda rezerve edilir ve birisi teklifinizi kabul ettiğinde bir multisig cüzdanda kilitlenir.\n\n\ + Tutarın toplamı şudur:\n\ {1}\ - - Güvenlik teminatınız: {2}\n\ + - Güvenlik depozitonuz: {2}\n\ - İşlem ücreti: {3} # only first part "Bir teklif verirken bir hata oluştu:" has been used before. We added now the rest (need update in existing translations!) diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index c3d7548e11..c29e335fb0 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -457,7 +457,12 @@ createOffer.placeOfferButton=Kiểm tra:: Đặt báo giá cho {0} monero createOffer.createOfferFundWalletInfo.headline=Nộp tiền cho báo giá của bạn # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Khoản tiền giao dịch: {0} \n -createOffer.createOfferFundWalletInfo.msg=Bạn cần đặt cọc {0} cho báo giá này.\n\nCác khoản tiền này sẽ được giữ trong ví nội bộ của bạn và sẽ bị khóa vào địa chỉ đặt cọc multisig khi có người nhận báo giá của bạn.\n\nKhoản tiền này là tổng của:\n{1}- tiền gửi đại lý của bạn: {2}\n- Phí giao dịch: {3}\n- Phí đào: {4}\n\nBạn có thể chọn giữa hai phương án khi nộp tiền cho giao dịch:\n- Sử dụng ví Haveno của bạn (tiện lợi, nhưng giao dịch có thể bị kết nối) OR\n- Chuyển từ ví bên ngoài (riêng tư hơn)\n\nBạn sẽ xem các phương án nộp tiền và thông tin chi tiết sau khi đóng cửa sổ này. +createOffer.createOfferFundWalletInfo.msg=Bạn cần nạp {0} cho lời đề nghị này.\n\n\ + Số tiền này sẽ được giữ trong ví cục bộ của bạn và sẽ được khóa vào ví multisig ngay khi có người chấp nhận lời đề nghị của bạn.\n\n\ + Số tiền bao gồm:\n\ + {1}\ + - Tiền đặt cọc bảo đảm của bạn: {2}\n\ + - Phí giao dịch: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Có lỗi xảy ra khi đặt chào giá:\n\n{0}\n\nKhông còn tiền trong ví của bạn.\nHãy khởi động lại ứng dụng và kiểm tra kết nối mạng. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index f8af6af20e..8c0db74048 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -458,7 +458,12 @@ createOffer.placeOfferButton=复审:报价挂单 {0} 比特币 createOffer.createOfferFundWalletInfo.headline=为您的报价充值 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 交易数量:{0}\n -createOffer.createOfferFundWalletInfo.msg=这个报价您需要 {0} 作为保证金。\n\n这些资金保留在您的本地钱包并会被冻结到多重验证保证金地址直到报价交易成功。\n\n总数量:{1}\n- 保证金:{2}\n- 挂单费:{3}\n- 矿工手续费:{4}\n\n您有两种选项可以充值您的交易:\n- 使用您的 Haveno 钱包(方便,但交易可能会被链接到)或者\n- 从外部钱包转入(或许这样更隐秘一些)\n\n关闭此弹出窗口后,您将看到所有资金选项和详细信息。 +createOffer.createOfferFundWalletInfo.msg=您需要为此报价存入 {0}。\n\n\ + 这些资金将保留在您的本地钱包中,并在有人接受您的报价后锁定到多签钱包中。\n\n\ + 金额是以下各项的总和:\n\ + {1}\ + - 您的保证金:{2}\n\ + - 交易费用:{3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=提交报价发生错误:\n\n{0}\n\n没有资金从您钱包中扣除。\n请检查您的互联网连接或尝试重启应用程序。 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 1dd25d90b4..a7810b86fb 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -458,7 +458,12 @@ createOffer.placeOfferButton=複審:報價掛單 {0} 比特幣 createOffer.createOfferFundWalletInfo.headline=為您的報價充值 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 交易數量:{0}\n -createOffer.createOfferFundWalletInfo.msg=這個報價您需要 {0} 作為保證金。\n\n這些資金保留在您的本地錢包並會被凍結到多重驗證保證金地址直到報價交易成功。\n\n總數量:{1}\n- 保證金:{2}\n- 掛單費:{3}\n- 礦工手續費:{4}\n\n您有兩種選項可以充值您的交易:\n- 使用您的 Haveno 錢包(方便,但交易可能會被鏈接到)或者\n- 從外部錢包轉入(或許這樣更隱祕一些)\n\n關閉此彈出窗口後,您將看到所有資金選項和詳細信息。 +createOffer.createOfferFundWalletInfo.msg=您需要為此報價存入 {0}。\n\n\ + 這些資金會保留在您的本地錢包中,並在有人接受您的報價後鎖定到多重簽名錢包中。\n\n\ + 金額總和為:\n\ + {1}\ + - 您的保證金:{2}\n\ + - 交易費:{3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=提交報價發生錯誤:\n\n{0}\n\n沒有資金從您錢包中扣除。\n請檢查您的互聯網連接或嘗試重啟應用程序。 From 8fd7f17317cebccab648c8695314ce3d8b2f0a29 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 18 Nov 2024 09:29:30 -0500 Subject: [PATCH 009/371] update translation for funding wallet on take offer --- core/src/main/resources/i18n/displayStrings_es.properties | 2 +- core/src/main/resources/i18n/displayStrings_fa.properties | 2 +- core/src/main/resources/i18n/displayStrings_fr.properties | 2 +- core/src/main/resources/i18n/displayStrings_it.properties | 2 +- core/src/main/resources/i18n/displayStrings_ja.properties | 2 +- core/src/main/resources/i18n/displayStrings_pt-br.properties | 2 +- core/src/main/resources/i18n/displayStrings_pt.properties | 2 +- core/src/main/resources/i18n/displayStrings_ru.properties | 2 +- core/src/main/resources/i18n/displayStrings_th.properties | 2 +- core/src/main/resources/i18n/displayStrings_tr.properties | 2 +- core/src/main/resources/i18n/displayStrings_vi.properties | 2 +- core/src/main/resources/i18n/displayStrings_zh-hans.properties | 2 +- core/src/main/resources/i18n/displayStrings_zh-hant.properties | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 43a92fe7a7..c9d60a7020 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -519,7 +519,7 @@ takeOffer.noPriceFeedAvailable=No puede tomar esta oferta porque utiliza un prec takeOffer.takeOfferFundWalletInfo.headline=Dotar de fondos su intercambio # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Cantidad a intercambiar: {0}\n -takeOffer.takeOfferFundWalletInfo.msg=Necesita depositar {0} para tomar esta oferta.\n\nLa cantidad es la suma de:\n{1} - Su depósito de seguridad: {2}\n- Comisión de intercambio: {3}\n- Comisiones de minado totales: {4}\n\nPuede elegir entre dos opciones al depositar fondos para realizar su intercambio:\n- Usar su cartera Haveno (conveniente, pero las transacciones pueden ser trazables) O también\n- Transferir desde una cartera externa (potencialmente con mayor privacidad)\n\nVerá todos los detalles y opciones para depositar fondos al cerrar esta ventana. +takeOffer.takeOfferFundWalletInfo.msg=Necesitas depositar {0} para aceptar esta oferta.\n\nLa cantidad es la suma de:\n{1}- Tu depósito de seguridad: {2}\n- Tarifa de transacción: {3} takeOffer.alreadyPaidInFunds=Si ya ha depositado puede retirarlo en la pantalla \"Fondos/Disponible para retirar\". takeOffer.paymentInfo=Información de pago takeOffer.setAmountPrice=Establecer cantidad diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 90613ad019..7f18e3c321 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -518,7 +518,7 @@ takeOffer.noPriceFeedAvailable=امکان پذیرفتن پیشنهاد وجود takeOffer.takeOfferFundWalletInfo.headline=معامله خود را تأمین وجه نمایید # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=مقدار معامله: {0}\n -takeOffer.takeOfferFundWalletInfo.msg=شما باید {0} برای قبول این پیشنهاد، سپرده بگذارید.\nاین مقدار مجموع موارد ذیل است:\n{1} - سپرده‌ی اطمینان شما: {2}\n-هزینه معامله: {3}\n-تمامی هزینه های تراکنش شبکه: {4}\nشما هنگام تامین مالی معامله‌ی خود، می‌توانید بین دو گزینه انتخاب کنید:\n- از کیف پول Haveno خود استفاده کنید (این روش راحت است، اما ممکن است تراکنش‌ها قابل رصد شوند)، یا\n- از کیف پول خارجی انتقال دهید (به طور بالقوه‌ای این روش ایمن‌تر و محافظ حریم خصوصی شما است)\n\nشما تمام گزینه‌ها و جزئیات تامین مالی را پس از بستن این پنجره، خواهید دید. +takeOffer.takeOfferFundWalletInfo.msg=باید {0} را برای پذیرش این پیشنهاد واریز کنید.\n\nمبلغ مجموع موارد زیر است:\n{1}- سپرده امنیتی شما: {2}\n- هزینه معامله: {3} takeOffer.alreadyPaidInFunds=اگر شما در حال حاضر در وجوه، پرداختی داشته اید، می توانید آن را در صفحه ی \"وجوه/ارسال وجوه\" برداشت کنید. takeOffer.paymentInfo=اطلاعات پرداخت takeOffer.setAmountPrice=تنظیم مقدار diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index ab196d4eff..a510fbac58 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -519,7 +519,7 @@ takeOffer.noPriceFeedAvailable=Vous ne pouvez pas accepter cet ordre, car celui- takeOffer.takeOfferFundWalletInfo.headline=Provisionner votre trade # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Montant du trade: {0}\n -takeOffer.takeOfferFundWalletInfo.msg=Vous devez envoyer {0} pour cet odre.\n\nLe montant est la somme de:\n{1}--Dépôt de garantie: {2}\n- Frais de transaction: {3}\n- Frais de minage: {4}\n\nVous avez deux choix pour payer votre transaction :\n- Utiliser votre portefeuille local Haveno (pratique, mais vos transactions peuvent être tracées) OU\n- Transférer d''un portefeuille externe (potentiellement plus confidentiel)\n\nVous retrouverez toutes les options de provisionnement après fermeture de ce popup. +takeOffer.takeOfferFundWalletInfo.msg=Vous devez déposer {0} pour accepter cette offre.\n\nLe montant est la somme de :\n{1}- Votre dépôt de garantie : {2}\n- Frais de transaction : {3} takeOffer.alreadyPaidInFunds=Si vous avez déjà provisionner des fonds vous pouvez les retirer dans l'onglet \"Fonds/Envoyer des fonds\". takeOffer.paymentInfo=Informations de paiement takeOffer.setAmountPrice=Définir le montant diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index e7dd6f01f8..09e98ca4d6 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -518,7 +518,7 @@ takeOffer.noPriceFeedAvailable=Non puoi accettare questa offerta poiché utilizz takeOffer.takeOfferFundWalletInfo.headline=Finanzia il tuo scambio # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Importo di scambio: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Devi depositare {0} per accettare questa offerta.\n\nL'importo è la somma de:\n{1} - Il tuo deposito cauzionale: {2}\n- La commissione di trading: {3}\n- I costi di mining: {4}\n\nPuoi scegliere tra due opzioni quando finanzi il tuo scambio:\n- Usare il tuo portafoglio Haveno (comodo, ma le transazioni possono essere collegabili) OPPURE\n- Trasferimento da un portafoglio esterno (potenzialmente più privato)\n\nVedrai tutte le opzioni di finanziamento e i dettagli dopo aver chiuso questo popup. +takeOffer.takeOfferFundWalletInfo.msg=Devi depositare {0} per accettare questa offerta.\n\nL'importo è la somma di:\n{1}- Il tuo deposito di sicurezza: {2}\n- Commissione di trading: {3} takeOffer.alreadyPaidInFunds=Se hai già pagato in fondi puoi effettuare il ritiro nella schermata \"Fondi/Invia fondi\". takeOffer.paymentInfo=Informazioni sul pagamento takeOffer.setAmountPrice=Importo stabilito diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 55a0352d37..8e6085bc80 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -519,7 +519,7 @@ takeOffer.noPriceFeedAvailable=そのオファーは市場価格に基づくパ takeOffer.takeOfferFundWalletInfo.headline=あなたのオファーへ入金 # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount= - 取引額: {0}\n -takeOffer.takeOfferFundWalletInfo.msg=このオファーに対して {0} のデポジットを送金する必要があります。\n\n金額の合計は以下の通りです\n{1} - セキュリティデポジット: {2}\n- 取引手数料: {3}\n- マイニング手数料: {4}\n\nこのオファーにデポジットを送金するには、以下の2つの方法があります。\n- Havenoウォレットを使う (便利ですがトランザクションが追跡される可能性があります)\nまたは\n- 外部のウォレットから送金する (機密性の高い方法です)\n\nこのポップアップを閉じると全ての送金方法について詳細な情報が表示されます。 +takeOffer.takeOfferFundWalletInfo.msg=このオファーを受けるには、{0} を預ける必要があります。\n\n金額は以下の合計です:\n{1}- あなたの保証金: {2}\n- 取引手数料: {3} takeOffer.alreadyPaidInFunds=あなたがすでに資金を支払っている場合は「資金/送金する」画面でそれを出金することができます。 takeOffer.paymentInfo=支払い情報 takeOffer.setAmountPrice=金額を設定 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 20f897fcee..51f2e33388 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -521,7 +521,7 @@ takeOffer.noPriceFeedAvailable=Você não pode aceitar essa oferta pois ela usa takeOffer.takeOfferFundWalletInfo.headline=Financiar sua negociação # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Quantia a negociar: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Você precisa depositar {0} para aceitar esta oferta.\n\nA quantia equivale a soma de:\n{1}- Seu depósito de segurança: {2}\n- Taxa de negociação: {3}\n\nVocê pode escolher entre duas opções para financiar sua negociação:\n- Usar a sua carteira Haveno (conveniente, mas transações podem ser associadas entre si) OU\n- Transferir a partir de uma carteira externa (potencialmente mais privado)\n\nVocê verá todas as opções de financiamento e detalhes após fechar esta janela. +takeOffer.takeOfferFundWalletInfo.msg=Você precisa depositar {0} para aceitar esta oferta.\n\nO valor é a soma de:\n{1}- Seu depósito de segurança: {2}\n- Taxa de negociação: {3} takeOffer.alreadyPaidInFunds=Se você já pagou por essa oferta, você pode retirar seus fundos na seção \"Fundos/Enviar fundos\". takeOffer.paymentInfo=Informações de pagamento takeOffer.setAmountPrice=Definir quantia diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 604640c56c..9be31eefa8 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -518,7 +518,7 @@ takeOffer.noPriceFeedAvailable=Você não pode aceitar aquela oferta pois ela ut takeOffer.takeOfferFundWalletInfo.headline=Financiar seu negócio # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Quantia de negócio: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Você precisa depositar {0} para aceitar esta oferta.\n\nA quantia é a soma de:\n{1} - Seu depósito de segurança: {2}\n- Taxa de negociação: {3}\n\nVocê pode escolher entre duas opções ao financiar o seu negócio:\n- Use sua carteira Haveno (conveniente, mas as transações podem ser conectas) OU\n- Transferência de uma carteira externa (potencialmente mais privada)\n\nVocê verá todas as opções de financiamento e detalhes depois de fechar este popup. +takeOffer.takeOfferFundWalletInfo.msg=Você precisa depositar {0} para aceitar esta oferta.\n\nO valor é a soma de:\n{1}- Seu depósito de segurança: {2}\n- Taxa de negociação: {3} takeOffer.alreadyPaidInFunds=Se você já pagou com seus fundos você pode levantá-los na janela \"Fundos/Enviar fundos\". takeOffer.paymentInfo=Informações de pagamento takeOffer.setAmountPrice=Definir quantia diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index fef440d3eb..49b37f4c98 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -518,7 +518,7 @@ takeOffer.noPriceFeedAvailable=Нельзя принять это предлож takeOffer.takeOfferFundWalletInfo.headline=Обеспечьте свою сделку # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Сумма сделки: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Вы должны внести {0} для принятия этого предложения.\n\nСумма состоит из:\n{1}- вашего залога: {2},\n- комиссии за сделку: {3}\n\nВы можете выбрать один из двух вариантов финансирования сделки:\n - использовать свой кошелёк Haveno (удобно, но сделки можно отследить) ИЛИ\n - перевести из внешнего кошелька (потенциально более анонимно).\n\nВы увидите все варианты обеспечения предложения и их подробности после закрытия этого окна. +takeOffer.takeOfferFundWalletInfo.msg=Вам нужно внести депозит в размере {0} для принятия этого предложения.\n\nСумма составляет:\n{1}- Ваш залог: {2}\n- Торговая комиссия: {3} takeOffer.alreadyPaidInFunds=Если вы уже внесли средства, их можно вывести в разделе \«Средства/Отправить средства\». takeOffer.paymentInfo=Информация о платеже takeOffer.setAmountPrice=Задайте сумму diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index e3eca2f0d6..15b8751843 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -518,7 +518,7 @@ takeOffer.noPriceFeedAvailable=คุณไม่สามารถรับข takeOffer.takeOfferFundWalletInfo.headline=ทุนการซื้อขายของคุณ # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- ปริมาณการซื้อขาย: {0} -takeOffer.takeOfferFundWalletInfo.msg=คุณต้องวางเงินประกัน {0} เพื่อรับข้อเสนอนี้\n\nจำนวนเงินคือผลรวมของ: \n{1} - เงินประกันของคุณ: {2} \n- ค่าธรรมเนียมการซื้อขาย: {3} \n\nคุณสามารถเลือกระหว่างสองตัวเลือกเมื่อลงทุนการซื้อขายของคุณ: \n- ใช้กระเป๋าสตางค์ Haveno ของคุณ (สะดวก แต่ธุรกรรมอาจเชื่อมโยงกันได้) หรือ\n- โอนเงินจากแหล่งเงินภายนอก (อาจเป็นส่วนตัวมากขึ้น) \n\nคุณจะเห็นตัวเลือกและรายละเอียดการลงทุนทั้งหมดหลังจากปิดป๊อปอัปนี้ +takeOffer.takeOfferFundWalletInfo.msg=คุณต้องฝากเงิน {0} เพื่อรับข้อเสนอนี้。\n\nจำนวนเงินคือผลรวมของ:\n{1}- เงินมัดจำของคุณ: {2}\n- ค่าธรรมเนียมการซื้อขาย: {3} takeOffer.alreadyPaidInFunds=หากคุณได้ชำระเงินแล้วคุณสามารถถอนเงินออกได้ในหน้าจอ \"เงิน / ส่งเงิน \" takeOffer.paymentInfo=ข้อมูลการชำระเงิน takeOffer.setAmountPrice=ตั้งยอดจำนวน diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index a7e587ce11..e7161baaa3 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -561,7 +561,7 @@ takeOffer.noPriceFeedAvailable=Bu teklifi alamazsınız çünkü piyasa fiyatın takeOffer.takeOfferFundWalletInfo.headline=İşleminizi finanse edin # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- İşlem miktarı: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Bu teklifi almak için {0} yatırmanız gerekiyor.\n\nBu miktar şunların toplamıdır:\n{1}- Güvenlik teminatınız: {2}\n- İşlem ücreti: {3} +takeOffer.takeOfferFundWalletInfo.msg=Bu teklifi kabul etmek için {0} yatırmanız gerekiyor.\n\nMiktar, şu kalemlerin toplamıdır:\n{1}- Güvenlik depozitonuz: {2}\n- İşlem ücreti: {3} takeOffer.alreadyPaidInFunds=Eğer zaten fon yatırdıysanız, \"Fonlar/Fon gönder\" ekranında çekebilirsiniz. takeOffer.setAmountPrice=Miktar ayarla takeOffer.alreadyFunded.askCancel=Bu teklifi zaten finanse ettiniz.\nŞimdi iptal ederseniz, fonlarınız yerel Haveno cüzdanınızda kalacak ve \"Fonlar/Fon gönder\" ekranında çekilebilir olacaktır.\nİptal etmek istediğinizden emin misiniz? diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index c29e335fb0..5929dd94ad 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -518,7 +518,7 @@ takeOffer.noPriceFeedAvailable=Bạn không thể nhận báo giá này do sử takeOffer.takeOfferFundWalletInfo.headline=Nộp tiền cho giao dịch của bạn # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Giá trị giao dịch: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Bạn cần nộp {0} để nhận báo giá này.\n\nGiá trị này là tổng của:\n{1}- Tiền ứng trước của bạn: {2}\n- phí giao dịch: {3}\n\nBạn có thể chọn một trong hai phương án khi nộp tiền cho giao dịch của bạn:\n- Sử dụng ví Haveno (tiện lợi, nhưng giao dịch có thể bị kết nối) OR\n- Chuyển từ ví ngoài (riêng tư hơn)\n\nBạn sẽ thấy các phương án nộp tiền và thông tin chi tiết sau khi đóng cửa sổ này. +takeOffer.takeOfferFundWalletInfo.msg=Bạn cần phải deposit {0} để chấp nhận đề nghị này.\n\nSố tiền là tổng của:\n{1}- Khoản tiền đặt cọc của bạn: {2}\n- Phí giao dịch: {3} takeOffer.alreadyPaidInFunds=Bạn đã thanh toán, bạn có thể rút số tiền này tại màn hình \"Vốn/Gửi vốn\". takeOffer.paymentInfo=Thông tin thanh toán takeOffer.setAmountPrice=Cài đặt số tiền diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 8c0db74048..0ea093e121 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -519,7 +519,7 @@ takeOffer.noPriceFeedAvailable=您不能对这笔报价下单,因为它使用 takeOffer.takeOfferFundWalletInfo.headline=为交易充值 # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- 交易数量:{0}\n -takeOffer.takeOfferFundWalletInfo.msg=这个报价您需要付出 {0} 保证金。\n\n这些资金保留在您的本地钱包并会被冻结到多重验证保证金地址直到报价交易成功。\n\n总数量:{1}\n- 保证金:{2}\n- 挂单费:{3}\n\n您有两种选项可以充值您的交易:\n- 使用您的 Haveno 钱包(方便,但交易可能会被链接到)或者\n- 从外部钱包转入(或许这样更隐秘一些)\n\n关闭此弹出窗口后,您将看到所有资金选项和详细信息。 +takeOffer.takeOfferFundWalletInfo.msg=您需要存入 {0} 以接受此报价。\n\n该金额为以下总和:\n{1}- 您的保证金:{2}\n- 交易费用:{3} takeOffer.alreadyPaidInFunds=如果你已经支付,你可以在“资金/提现”提现它。 takeOffer.paymentInfo=付款信息 takeOffer.setAmountPrice=设置数量 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index a7810b86fb..95fad5ea7f 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -519,7 +519,7 @@ takeOffer.noPriceFeedAvailable=您不能對這筆報價下單,因為它使用 takeOffer.takeOfferFundWalletInfo.headline=為交易充值 # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- 交易數量:{0}\n -takeOffer.takeOfferFundWalletInfo.msg=這個報價您需要付出 {0} 保證金。\n\n這些資金保留在您的本地錢包並會被凍結到多重驗證保證金地址直到報價交易成功。\n\n總數量:{1}\n- 保證金:{2}\n- 掛單費:{3}\n\n您有兩種選項可以充值您的交易:\n- 使用您的 Haveno 錢包(方便,但交易可能會被鏈接到)或者\n- 從外部錢包轉入(或許這樣更隱祕一些)\n\n關閉此彈出窗口後,您將看到所有資金選項和詳細信息。 +takeOffer.takeOfferFundWalletInfo.msg=您需要存入 {0} 才能接受此報價。\n\n該金額是以下總和:\n{1}- 您的保證金:{2}\n- 交易費用:{3} takeOffer.alreadyPaidInFunds=如果你已經支付,你可以在“資金/提現”提現它。 takeOffer.paymentInfo=付款信息 takeOffer.setAmountPrice=設置數量 From 68b4a0fafbbbb5621d30d18ff05010338173f49c Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 21 Nov 2024 09:54:04 -0500 Subject: [PATCH 010/371] update links to #haveno-development --- README.md | 2 +- docs/CONTRIBUTING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e8d40e8ec5..13b96948f5 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ If you wish to help, take a look at the repositories above and look for open iss Haveno is a community-driven project. For it to be successful it's fundamental to have the support and help of the community. Join the community rooms on our Matrix server: - General discussions: **Haveno** ([#haveno:monero.social](https://matrix.to/#/#haveno:monero.social)) relayed on IRC/Libera (`#haveno`) -- Development discussions: **Haveno Development** ([#haveno-dev:monero.social](https://matrix.to/#/#haveno-dev:monero.social)) relayed on IRC/Libera (`#haveno-dev`) +- Development discussions: **Haveno Development** ([#haveno-development:monero.social](https://matrix.to/#/#haveno-development:monero.social)) relayed on IRC/Libera (`#haveno-development`) Email: contact@haveno.exchange Website: [haveno.exchange](https://haveno.exchange) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 54bebbbf12..caa76ac1a9 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Haveno -Thanks for wishing to help! Here there are some guidelines and information about the development process. We suggest you to join the [matrix](https://app.element.io/#/room/#haveno-dev:haveno.network) room `#haveno-dev` (relayed on [IRC/Libera](irc://irc.libera.chat/#haveno-dev)) and have a chat with the devs, so that we can help get you started. +Thanks for wishing to help! Here there are some guidelines and information about the development process. We suggest you to join the [matrix](https://app.element.io/#/room/#haveno-development:monero.social) room `#haveno-development` (relayed on [IRC/Libera](irc://irc.libera.chat/#haveno-development)) and have a chat with the devs, so that we can help get you started. Issues are tracked on GitHub. We use [a label system](https://github.com/haveno-dex/haveno/issues/50) and GitHub's [project boards](https://github.com/haveno-dex/haveno/projects) to simplify development. Make sure to take a look at those and to follow the priorities suggested. From ae80935f3a81eae3e3c145c552a7faf33d407099 Mon Sep 17 00:00:00 2001 From: ohchase Date: Sun, 24 Nov 2024 07:51:55 -0500 Subject: [PATCH 011/371] enable hidden files in cache node dependencies --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0340da084..3d66321d71 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,7 @@ jobs: - name: cache nodes dependencies uses: actions/upload-artifact@v3 with: + include-hidden-files: true name: cached-localnet path: .localnet - name: Install dependencies From c40e0bea5a614af8b1c3564def2d3d57c7fbfa65 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 24 Nov 2024 11:08:26 -0500 Subject: [PATCH 012/371] build instructions warn that mainnet is not supported --- README.md | 9 +++++++-- docs/installing.md | 10 +++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 13b96948f5..46fb3528ca 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,16 @@ See the [FAQ on our website](https://haveno.exchange/faq/) for more information. ## Installing Haveno -Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network. We do not endorse any networks at this time. +Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network. + +> [!note] +> The official Haveno repository does not support making real trades directly. +> +> To make real trades with Haveno, first find a third party network, and then use their installer or build their repository. We do not endorse any networks at this time. A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the test network. -Alternatively, you can [start your own network](https://github.com/haveno-dex/haveno/blob/master/docs/create-mainnet.md). +Alternatively, you can [create your own mainnet network](create-mainnet.md). Note that Haveno is being actively developed. If you find issues or bugs, please let us know. diff --git a/docs/installing.md b/docs/installing.md index 3edb2f2330..29a15b54f5 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -1,13 +1,13 @@ # Build and run Haveno -These are the steps needed to build and run Haveno. You can test it locally or on our test network using the official Haveno repository. +These are the steps to build and run Haveno using the *official test network*. -> [!note] -> Trying to use Haveno on mainnet? +> [!warning] +> The official Haveno repository does not support making real trades directly. > -> The official Haveno repository does not operate or endorse any mainnet network. +> To make real trades with Haveno, first find a third party network, and then use their installer or build their repository. We do not endorse any networks at this time. > -> Find a third party network and use their installer or build their repository. Alternatively [create your own mainnet network](create-mainnet.md). +> Alternatively, you can [create your own mainnet network](create-mainnet.md). ## Install dependencies From a5417994d65954d93269e477c918d839b7820156 Mon Sep 17 00:00:00 2001 From: ohchase Date: Mon, 25 Nov 2024 10:40:27 -0500 Subject: [PATCH 013/371] flatpak icon support (#1428) --- .../Haveno.AppDir/{haveno.svg => exchange.haveno.Haveno.svg} | 0 desktop/package/linux/Haveno.desktop | 2 +- desktop/package/linux/exchange.haveno.Haveno.yml | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename desktop/package/linux/Haveno.AppDir/{haveno.svg => exchange.haveno.Haveno.svg} (100%) diff --git a/desktop/package/linux/Haveno.AppDir/haveno.svg b/desktop/package/linux/Haveno.AppDir/exchange.haveno.Haveno.svg similarity index 100% rename from desktop/package/linux/Haveno.AppDir/haveno.svg rename to desktop/package/linux/Haveno.AppDir/exchange.haveno.Haveno.svg diff --git a/desktop/package/linux/Haveno.desktop b/desktop/package/linux/Haveno.desktop index a0e32a6347..b6d62c222c 100644 --- a/desktop/package/linux/Haveno.desktop +++ b/desktop/package/linux/Haveno.desktop @@ -3,7 +3,7 @@ Comment=A decentralized, Tor-based, P2P Monero exchange network. Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; bin/Haveno %u" GenericName[en_US]=Monero Exchange GenericName=Monero Exchange -Icon=haveno +Icon=exchange.haveno.Haveno Categories=Office;Finance;Java;P2P; Name[en_US]=Haveno Name=Haveno diff --git a/desktop/package/linux/exchange.haveno.Haveno.yml b/desktop/package/linux/exchange.haveno.Haveno.yml index 2924684435..50843f8f75 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.yml +++ b/desktop/package/linux/exchange.haveno.Haveno.yml @@ -35,7 +35,7 @@ modules: - mkdir -p /app/share/icons/hicolor/128x128/apps/ - mkdir -p /app/share/applications/ - mkdir -p /app/share/metainfo/ - - mv icon.png /app/share/icons/hicolor/128x128/apps/haveno.png + - mv icon.png /app/share/icons/hicolor/128x128/apps/exchange.haveno.Haveno.png - mv Haveno.desktop /app/share/applications/exchange.haveno.Haveno.desktop - mv exchange.haveno.Haveno.metainfo.xml /app/share/metainfo/ From bf452c91da27bd3ff9c98109ce4e29fdde20c10b Mon Sep 17 00:00:00 2001 From: coinstudent2048 <87281755+coinstudent2048@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:42:09 +0800 Subject: [PATCH 014/371] add flatpak version (#1429) --- desktop/package/linux/exchange.haveno.Haveno.metainfo.xml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index 405d8aa657..6805134589 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -15,7 +15,6 @@ monero - CC-BY-4.0 AGPL-3.0-only @@ -40,7 +39,6 @@
  • There is No token, because we don't need it. Transactions between traders are secured by non-custodial multisignature transactions on the Monero network.
  • - @@ -61,6 +59,7 @@ intense - - Haveno.desktop + + + From c9cf5351c02111b0d99961b4a5d4f9bbec54002d Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 25 Nov 2024 10:48:27 -0500 Subject: [PATCH 015/371] support usdc (#1439) --- .../haveno/asset/tokens/{USDCoin.java => USDCoinERC20.java} | 6 +++--- .../src/main/resources/META-INF/services/haveno.asset.Asset | 3 ++- core/src/main/java/haveno/core/locale/CurrencyUtil.java | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) rename assets/src/main/java/haveno/asset/tokens/{USDCoin.java => USDCoinERC20.java} (85%) diff --git a/assets/src/main/java/haveno/asset/tokens/USDCoin.java b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java similarity index 85% rename from assets/src/main/java/haveno/asset/tokens/USDCoin.java rename to assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java index b3e5c121e5..a65c021df9 100644 --- a/assets/src/main/java/haveno/asset/tokens/USDCoin.java +++ b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java @@ -19,9 +19,9 @@ package haveno.asset.tokens; import haveno.asset.Erc20Token; -public class USDCoin extends Erc20Token { +public class USDCoinERC20 extends Erc20Token { - public USDCoin() { - super("USD Coin", "USDC"); + public USDCoinERC20() { + super("USD Coin (ERC20)", "USDC-ERC20"); } } diff --git a/assets/src/main/resources/META-INF/services/haveno.asset.Asset b/assets/src/main/resources/META-INF/services/haveno.asset.Asset index 350f6f1501..709b951227 100644 --- a/assets/src/main/resources/META-INF/services/haveno.asset.Asset +++ b/assets/src/main/resources/META-INF/services/haveno.asset.Asset @@ -8,4 +8,5 @@ haveno.asset.coins.Ether haveno.asset.coins.Litecoin haveno.asset.coins.Monero haveno.asset.tokens.TetherUSDERC20 -haveno.asset.tokens.TetherUSDTRC20 \ No newline at end of file +haveno.asset.tokens.TetherUSDTRC20 +haveno.asset.tokens.USDCoinERC20 \ No newline at end of file diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java index 93f579f413..bd66667da5 100644 --- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java +++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java @@ -201,6 +201,7 @@ public class CurrencyUtil { result.add(new CryptoCurrency("ETH", "Ether")); result.add(new CryptoCurrency("LTC", "Litecoin")); result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)")); + result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)")); result.sort(TradeCurrency::compareTo); return result; } @@ -328,13 +329,14 @@ public class CurrencyUtil { private static boolean isCryptoCurrencyBase(String currencyCode) { if (currencyCode == null) return false; currencyCode = currencyCode.toUpperCase(); - return currencyCode.equals("USDT"); + return currencyCode.equals("USDT") || currencyCode.equals("USDC"); } public static String getCurrencyCodeBase(String currencyCode) { if (currencyCode == null) return null; currencyCode = currencyCode.toUpperCase(); if (currencyCode.contains("USDT")) return "USDT"; + if (currencyCode.contains("USDC")) return "USDC"; return currencyCode; } From 103c45d4125bae4818cf3cc00379062035ef241e Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 23 Nov 2024 13:41:08 -0500 Subject: [PATCH 016/371] fix showing offer created popup after canceled --- .../java/haveno/desktop/main/offer/MutableOfferViewModel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index 66ca647551..fabad8570f 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -613,7 +613,7 @@ public abstract class MutableOfferViewModel ext dataModel.onPlaceOffer(offer, transaction -> { resultHandler.run(); - placeOfferCompleted.set(true); + if (!createOfferCanceled) placeOfferCompleted.set(true); errorMessage.set(null); }, errMessage -> { createOfferRequested = false; From 98e2df3c7ea881918afe5ebbd2c0923c436f2281 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 25 Nov 2024 11:01:15 -0500 Subject: [PATCH 017/371] fix scheduling offers with funds sent to self --- .../haveno/core/offer/OpenOfferManager.java | 22 +++++++++++++------ .../core/xmr/wallet/XmrWalletService.java | 19 ++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index af8984598b..c8390a5af0 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1169,16 +1169,24 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe Set scheduledTxs = new HashSet(); for (MoneroTxWallet tx : xmrWalletService.getTxs()) { - // skip if outputs unavailable - if (tx.getIncomingTransfers() == null || tx.getIncomingTransfers().isEmpty()) continue; + // skip if no funds available + BigInteger sentToSelfAmount = xmrWalletService.getAmountSentToSelf(tx); // amount sent to self always shows 0, so compute from destinations manually + if (sentToSelfAmount.equals(BigInteger.ZERO) && (tx.getIncomingTransfers() == null || tx.getIncomingTransfers().isEmpty())) continue; if (!isOutputsAvailable(tx)) continue; if (isTxScheduledByOtherOffer(openOffers, openOffer, tx.getHash())) continue; - // add scheduled tx - for (MoneroIncomingTransfer transfer : tx.getIncomingTransfers()) { - if (transfer.getAccountIndex() == 0) { - scheduledAmount = scheduledAmount.add(transfer.getAmount()); - scheduledTxs.add(tx); + // schedule transaction if funds sent to self, because they are not included in incoming transfers // TODO: fix in libraries? + if (sentToSelfAmount.compareTo(BigInteger.ZERO) > 0) { + scheduledAmount = scheduledAmount.add(sentToSelfAmount); + scheduledTxs.add(tx); + } else if (tx.getIncomingTransfers() != null) { + + // schedule transaction if incoming tranfers to account 0 + for (MoneroIncomingTransfer transfer : tx.getIncomingTransfers()) { + if (transfer.getAccountIndex() == 0) { + scheduledAmount = scheduledAmount.add(transfer.getAmount()); + scheduledTxs.add(tx); + } } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 6857766521..759dabd086 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1219,10 +1219,29 @@ public class XmrWalletService extends XmrWalletBase { return cachedAvailableBalance; } + public boolean hasAddress(String address) { + for (MoneroSubaddress subaddress : getSubaddresses()) { + if (subaddress.getAddress().equals(address)) return true; + } + return false; + } + public List getSubaddresses() { return cachedSubaddresses; } + public BigInteger getAmountSentToSelf(MoneroTxWallet tx) { + BigInteger sentToSelfAmount = BigInteger.ZERO; + if (tx.getOutgoingTransfer() != null && tx.getOutgoingTransfer().getDestinations() != null) { + for (MoneroDestination destination : tx.getOutgoingTransfer().getDestinations()) { + if (hasAddress(destination.getAddress())) { + sentToSelfAmount = sentToSelfAmount.add(destination.getAmount()); + } + } + } + return sentToSelfAmount; + } + public List getOutputs(MoneroOutputQuery query) { List filteredOutputs = new ArrayList(); for (MoneroOutputWallet output : cachedOutputs) { From 1f385328deeccc8dff7ad5a0f9ffc2aa11b661db Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 25 Nov 2024 11:50:36 -0500 Subject: [PATCH 018/371] increase rate limit to get offers on testnet --- .../src/main/java/haveno/daemon/grpc/GrpcOffersService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index 04b294e451..081a0949f7 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -202,9 +202,9 @@ class GrpcOffersService extends OffersImplBase { new HashMap<>() {{ put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, SECONDS)); put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, SECONDS)); - put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 1, SECONDS)); - put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 1, SECONDS)); + put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); From dc8d854709ec23cf39f6ac6b45b1f43e7030e7f1 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 29 Nov 2024 09:52:02 -0500 Subject: [PATCH 019/371] show available monero nodes in network settings --- .../haveno/core/api/XmrConnectionService.java | 41 +++++------- .../java/haveno/core/app/AppStartupState.java | 2 +- .../java/haveno/core/app/P2PNetworkSetup.java | 2 +- .../resources/i18n/displayStrings.properties | 2 + .../i18n/displayStrings_cs.properties | 2 + .../i18n/displayStrings_de.properties | 2 + .../i18n/displayStrings_es.properties | 2 + .../i18n/displayStrings_fa.properties | 2 + .../i18n/displayStrings_fr.properties | 2 + .../i18n/displayStrings_it.properties | 2 + .../i18n/displayStrings_ja.properties | 2 + .../i18n/displayStrings_pt-br.properties | 2 + .../i18n/displayStrings_pt.properties | 2 + .../i18n/displayStrings_ru.properties | 2 + .../i18n/displayStrings_th.properties | 2 + .../i18n/displayStrings_tr.properties | 2 + .../i18n/displayStrings_vi.properties | 2 + .../i18n/displayStrings_zh-hans.properties | 2 + .../i18n/displayStrings_zh-hant.properties | 2 + .../network/MoneroNetworkListItem.java | 29 ++++---- .../settings/network/NetworkSettingsView.fxml | 22 ++---- .../settings/network/NetworkSettingsView.java | 67 +++++++++---------- 22 files changed, 100 insertions(+), 95 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 75be338503..96458a27ac 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -40,7 +40,6 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -64,7 +63,6 @@ import monero.common.MoneroRpcConnection; import monero.common.TaskLooper; import monero.daemon.MoneroDaemonRpc; import monero.daemon.model.MoneroDaemonInfo; -import monero.daemon.model.MoneroPeer; @Slf4j @Singleton @@ -85,9 +83,9 @@ public final class XmrConnectionService { private final XmrLocalNode xmrLocalNode; private final MoneroConnectionManager connectionManager; private final EncryptedConnectionList connectionList; - private final ObjectProperty> peers = new SimpleObjectProperty<>(); + private final ObjectProperty> connections = new SimpleObjectProperty<>(); + private final IntegerProperty numConnections = new SimpleIntegerProperty(0); private final ObjectProperty connectionProperty = new SimpleObjectProperty<>(); - private final IntegerProperty numPeers = new SimpleIntegerProperty(0); private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter @@ -390,12 +388,12 @@ public final class XmrConnectionService { // ----------------------------- APP METHODS ------------------------------ - public ReadOnlyIntegerProperty numPeersProperty() { - return numPeers; + public ReadOnlyIntegerProperty numConnectionsProperty() { + return numConnections; } - public ReadOnlyObjectProperty> peerConnectionsProperty() { - return peers; + public ReadOnlyObjectProperty> connectionsProperty() { + return connections; } public ReadOnlyObjectProperty connectionProperty() { @@ -403,7 +401,7 @@ public final class XmrConnectionService { } public boolean hasSufficientPeersForBroadcast() { - return numPeers.get() >= getMinBroadcastConnections(); + return numConnections.get() >= getMinBroadcastConnections(); } public LongProperty chainHeightProperty() { @@ -782,16 +780,15 @@ public final class XmrConnectionService { downloadListener.progress(percent, blocksLeft, null); } - // set peer connections - // TODO: peers often uknown due to restricted RPC call, skipping call to get peer connections - // try { - // peers.set(getOnlinePeers()); - // } catch (Exception err) { - // // TODO: peers unknown due to restricted RPC call - // } - // numPeers.set(peers.get().size()); - numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections()); - peers.set(new ArrayList()); + // set available connections + List availableConnections = new ArrayList<>(); + for (MoneroRpcConnection connection : connectionManager.getConnections()) { + if (Boolean.TRUE.equals(connection.isOnline()) && Boolean.TRUE.equals(connection.isAuthenticated())) { + availableConnections.add(connection); + } + } + connections.set(availableConnections); + numConnections.set(availableConnections.size()); // notify update numUpdates.set(numUpdates.get() + 1); @@ -821,12 +818,6 @@ public final class XmrConnectionService { } } - private List getOnlinePeers() { - return daemon.getPeers().stream() - .filter(peer -> peer.isOnline()) - .collect(Collectors.toList()); - } - private boolean isFixedConnection() { return !"".equals(config.xmrNode) || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; } diff --git a/core/src/main/java/haveno/core/app/AppStartupState.java b/core/src/main/java/haveno/core/app/AppStartupState.java index 85da2af9d4..89fe61576a 100644 --- a/core/src/main/java/haveno/core/app/AppStartupState.java +++ b/core/src/main/java/haveno/core/app/AppStartupState.java @@ -73,7 +73,7 @@ public class AppStartupState { isWalletSynced.set(true); }); - xmrConnectionService.numPeersProperty().addListener((observable, oldValue, newValue) -> { + xmrConnectionService.numConnectionsProperty().addListener((observable, oldValue, newValue) -> { if (xmrConnectionService.hasSufficientPeersForBroadcast()) hasSufficientPeersForBroadcast.set(true); }); diff --git a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java index 4e90faeb55..69609a4bba 100644 --- a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java +++ b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java @@ -87,7 +87,7 @@ public class P2PNetworkSetup { BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty(); p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(), - xmrConnectionService.numPeersProperty(), hiddenServicePublished, initialP2PNetworkDataReceived, + xmrConnectionService.numConnectionsProperty(), hiddenServicePublished, initialP2PNetworkDataReceived, (state, warning, numP2pPeers, numXmrPeers, hiddenService, dataReceived) -> { String result; int p2pPeers = (int) numP2pPeers; diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 4525c6a33e..f3a67e4013 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1315,6 +1315,8 @@ settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=My onion address settings.net.xmrNodesLabel=Use custom Monero nodes settings.net.moneroPeersLabel=Connected peers +settings.net.connection=Connection +settings.net.connected=Connected settings.net.useTorForXmrJLabel=Use Tor for Monero network settings.net.useTorForXmrAfterSyncRadio=After wallet is synchronized settings.net.useTorForXmrOffRadio=Never diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index ada00d8344..161a6be719 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -1043,6 +1043,8 @@ settings.net.p2pHeader=Síť Haveno settings.net.onionAddressLabel=Moje onion adresa settings.net.xmrNodesLabel=Použijte vlastní Monero node settings.net.moneroPeersLabel=Připojené peer uzly +settings.net.connection=Připojení +settings.net.connected=Připojeno settings.net.useTorForXmrJLabel=Použít Tor pro Monero síť settings.net.moneroNodesLabel=Monero nody, pro připojení settings.net.useProvidedNodesRadio=Použijte nabízené Monero Core nody diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 79f6704ade..198b5999f2 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1043,6 +1043,8 @@ settings.net.p2pHeader=Haveno-Netzwerk settings.net.onionAddressLabel=Meine Onion-Adresse settings.net.xmrNodesLabel=Spezifische Monero-Knoten verwenden settings.net.moneroPeersLabel=Verbundene Peers +settings.net.connection=Verbindung +settings.net.connected=Verbunden settings.net.useTorForXmrJLabel=Tor für das Monero-Netzwerk verwenden settings.net.moneroNodesLabel=Mit Monero-Knoten verbinden settings.net.useProvidedNodesRadio=Bereitgestellte Monero-Core-Knoten verwenden diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index c9d60a7020..17115d059d 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -1044,6 +1044,8 @@ settings.net.p2pHeader=Red Haveno settings.net.onionAddressLabel=Mi dirección onion settings.net.xmrNodesLabel=Utilizar nodos Monero personalizados settings.net.moneroPeersLabel=Pares conectados +settings.net.connection=Conexión +settings.net.connected=Conectado settings.net.useTorForXmrJLabel=Usar Tor para la red Monero settings.net.moneroNodesLabel=Nodos Monero para conectarse settings.net.useProvidedNodesRadio=Utilizar nodos Monero Core proporcionados diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 7f18e3c321..f6db7fffd2 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1040,6 +1040,8 @@ settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=آدرس onion من settings.net.xmrNodesLabel=استفاده از گره‌های Monero اختصاصی settings.net.moneroPeersLabel=همتایان متصل +settings.net.connection=اتصال +settings.net.connected=متصل settings.net.useTorForXmrJLabel=استفاده از Tor برای شبکه مونرو settings.net.moneroNodesLabel=گره‌های Monero در دسترس settings.net.useProvidedNodesRadio=استفاده از نودهای بیتکوین ارائه شده diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index a510fbac58..400f58cdc4 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -1045,6 +1045,8 @@ settings.net.p2pHeader=Le réseau Haveno settings.net.onionAddressLabel=Mon adresse onion settings.net.xmrNodesLabel=Utiliser des nœuds Monero personnalisés settings.net.moneroPeersLabel=Pairs connectés +settings.net.connection=Connexion +settings.net.connected=Connecté settings.net.useTorForXmrJLabel=Utiliser Tor pour le réseau Monero settings.net.moneroNodesLabel=Nœuds Monero pour se connecter à settings.net.useProvidedNodesRadio=Utiliser les nœuds Monero Core fournis diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 09e98ca4d6..4a8e6844d0 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1042,6 +1042,8 @@ settings.net.p2pHeader=Rete Haveno settings.net.onionAddressLabel=Il mio indirizzo onion settings.net.xmrNodesLabel=Usa nodi Monero personalizzati settings.net.moneroPeersLabel=Peer connessi +settings.net.connection=Connessione +settings.net.connected=Connesso settings.net.useTorForXmrJLabel=Usa Tor per la rete Monero settings.net.moneroNodesLabel=Nodi Monero a cui connettersi settings.net.useProvidedNodesRadio=Usa i nodi Monero Core forniti diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 8e6085bc80..afd3cd69d3 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -1043,6 +1043,8 @@ settings.net.p2pHeader=Havenoネットワーク settings.net.onionAddressLabel=私のonionアドレス settings.net.xmrNodesLabel=任意のモネロノードを使う settings.net.moneroPeersLabel=接続されたピア +settings.net.connection=接続 +settings.net.connected=接続されました settings.net.useTorForXmrJLabel=MoneroネットワークにTorを使用 settings.net.moneroNodesLabel=接続するMoneroノード: settings.net.useProvidedNodesRadio=提供されたMonero Core ノードを使う diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 51f2e33388..da457db45b 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -1044,6 +1044,8 @@ settings.net.p2pHeader=Rede Haveno settings.net.onionAddressLabel=Meu endereço onion settings.net.xmrNodesLabel=Usar nodos personalizados do Monero settings.net.moneroPeersLabel=Pares conectados +settings.net.connection=Conexão +settings.net.connected=Conectado settings.net.useTorForXmrJLabel=Usar Tor na rede Monero settings.net.moneroNodesLabel=Conexão a nodos do Monero settings.net.useProvidedNodesRadio=Usar nodos do Monero Core fornecidos diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 9be31eefa8..b3985a5692 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1041,6 +1041,8 @@ settings.net.p2pHeader=Rede do Haveno settings.net.onionAddressLabel=O meu endereço onion settings.net.xmrNodesLabel=Usar nós de Monero personalizados settings.net.moneroPeersLabel=Pares conectados +settings.net.connection=Conexão +settings.net.connected=Conectado settings.net.useTorForXmrJLabel=Usar Tor para a rede de Monero settings.net.moneroNodesLabel=Nós de Monero para conectar settings.net.useProvidedNodesRadio=Usar nós de Monero Core providenciados diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 49b37f4c98..51d98e1ef4 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1040,6 +1040,8 @@ settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=Мой onion-адрес settings.net.xmrNodesLabel=Использовать особые узлы Monero settings.net.moneroPeersLabel=Подключенные пиры +settings.net.connection=Соединение +settings.net.connected=Подключено settings.net.useTorForXmrJLabel=Использовать Tor для сети Monero settings.net.moneroNodesLabel=Узлы Monero для подключения settings.net.useProvidedNodesRadio=Использовать предоставленные узлы Monero Core diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 15b8751843..5460e1074f 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1040,6 +1040,8 @@ settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=ที่อยู่ onion ของฉัน settings.net.xmrNodesLabel=ใช้โหนดเครือข่าย Monero ที่กำหนดเอง settings.net.moneroPeersLabel=เชื่อมต่อกับเน็ตเวิร์ก peers แล้ว +settings.net.connection=การเชื่อมต่อ +settings.net.connected=เชื่อมต่อ settings.net.useTorForXmrJLabel=ใช้ Tor สำหรับเครือข่าย Monero settings.net.moneroNodesLabel=ใช้โหนดเครือข่าย Monero เพื่อเชื่อมต่อ settings.net.useProvidedNodesRadio=ใช้โหนดเครือข่าย Monero ที่ให้มา diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index e7161baaa3..d807f48aa5 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -1310,6 +1310,8 @@ settings.net.p2pHeader=Haveno ağı settings.net.onionAddressLabel=Onion adresim settings.net.xmrNodesLabel=Özel Monero düğümleri kullan settings.net.moneroPeersLabel=Bağlı eşler +settings.net.connection=Bağlantı +settings.net.connected=Bağlı settings.net.useTorForXmrJLabel=Monero ağı için Tor kullan settings.net.useTorForXmrAfterSyncRadio=Cüzdan senkronize edildikten sonra settings.net.useTorForXmrOffRadio=Asla diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 5929dd94ad..321d1bb6b6 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1042,6 +1042,8 @@ settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=Địa chỉ onion của tôi settings.net.xmrNodesLabel=Sử dụng nút Monero thông dụng settings.net.moneroPeersLabel=Các đối tác được kết nối +settings.net.connection=Kết nối +settings.net.connected=Kết nối settings.net.useTorForXmrJLabel=Sử dụng Tor cho mạng Monero settings.net.moneroNodesLabel=nút Monero để kết nối settings.net.useProvidedNodesRadio=Sử dụng các nút Monero Core đã cung cấp diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 0ea093e121..2709bb2645 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -1043,6 +1043,8 @@ settings.net.p2pHeader=Haveno 网络 settings.net.onionAddressLabel=我的匿名地址 settings.net.xmrNodesLabel=使用自定义比特币主节点 settings.net.moneroPeersLabel=已连接节点 +settings.net.connection=连接 +settings.net.connected=连接 settings.net.useTorForXmrJLabel=使用 Tor 连接 Monero 网络 settings.net.moneroNodesLabel=需要连接 Monero settings.net.useProvidedNodesRadio=使用公共比特币核心节点 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 95fad5ea7f..64f5082a45 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -1043,6 +1043,8 @@ settings.net.p2pHeader=Haveno 網絡 settings.net.onionAddressLabel=我的匿名地址 settings.net.xmrNodesLabel=使用自定义Monero节点 settings.net.moneroPeersLabel=已連接節點 +settings.net.connection=連接 +settings.net.connected=連接完成 settings.net.useTorForXmrJLabel=使用 Tor 連接 Monero 網絡 settings.net.moneroNodesLabel=需要連接 Monero settings.net.useProvidedNodesRadio=使用公共比特幣核心節點 diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/MoneroNetworkListItem.java b/desktop/src/main/java/haveno/desktop/main/settings/network/MoneroNetworkListItem.java index 80bd094fc9..fea0ef6f34 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/MoneroNetworkListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/MoneroNetworkListItem.java @@ -17,28 +17,23 @@ package haveno.desktop.main.settings.network; -import monero.daemon.model.MoneroPeer; +import haveno.core.locale.Res; +import monero.common.MoneroRpcConnection; public class MoneroNetworkListItem { - private final MoneroPeer peer; - - public MoneroNetworkListItem(MoneroPeer peer) { - this.peer = peer; + private final MoneroRpcConnection connection; + private final boolean connected; + + public MoneroNetworkListItem(MoneroRpcConnection connection, boolean connected) { + this.connection = connection; + this.connected = connected; } - public String getOnionAddress() { - return peer.getHost() + ":" + peer.getPort(); + public String getAddress() { + return connection.getUri(); } - public String getVersion() { - return ""; - } - - public String getSubVersion() { - return ""; - } - - public String getHeight() { - return String.valueOf(peer.getHeight()); + public String getConnected() { + return connected ? Res.get("settings.net.connected") : ""; } } diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml index 3e9d25428e..1f3e8840d7 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml @@ -45,27 +45,17 @@ - - + + - + - + - + - - - - - - - - - - - + diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java index 931a6e3526..546a55505f 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java @@ -44,7 +44,6 @@ import haveno.desktop.main.overlays.windows.TorNetworkSettingsWindow; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import haveno.network.p2p.network.Statistic; -import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static javafx.beans.binding.Bindings.createStringBinding; @@ -63,7 +62,6 @@ import javafx.scene.control.TextField; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.GridPane; -import monero.daemon.model.MoneroPeer; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @@ -79,7 +77,7 @@ public class NetworkSettingsView extends ActivatableView { @FXML TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField; @FXML - Label p2PPeersLabel, moneroPeersLabel; + Label p2PPeersLabel, moneroConnectionsLabel; @FXML RadioButton useTorForXmrAfterSyncRadio, useTorForXmrOffRadio, useTorForXmrOnRadio; @FXML @@ -87,13 +85,12 @@ public class NetworkSettingsView extends ActivatableView { @FXML TableView p2pPeersTableView; @FXML - TableView moneroPeersTableView; + TableView moneroConnectionsTableView; @FXML TableColumn onionAddressColumn, connectionTypeColumn, creationDateColumn, roundTripTimeColumn, sentBytesColumn, receivedBytesColumn, peerTypeColumn; @FXML - TableColumn moneroPeerAddressColumn, moneroPeerVersionColumn, - moneroPeerSubVersionColumn, moneroPeerHeightColumn; + TableColumn moneroConnectionAddressColumn, moneroConnectionConnectedColumn; @FXML Label rescanOutputsLabel; @FXML @@ -116,7 +113,7 @@ public class NetworkSettingsView extends ActivatableView { private final SortedList moneroSortedList = new SortedList<>(moneroNetworkListItems); private Subscription numP2PPeersSubscription; - private Subscription moneroPeersSubscription; + private Subscription moneroConnectionsSubscription; private Subscription moneroBlockHeightSubscription; private Subscription nodeAddressSubscription; private ChangeListener xmrNodesInputTextFieldFocusListener; @@ -156,17 +153,15 @@ public class NetworkSettingsView extends ActivatableView { p2pHeader.setText(Res.get("settings.net.p2pHeader")); onionAddress.setPromptText(Res.get("settings.net.onionAddressLabel")); xmrNodesLabel.setText(Res.get("settings.net.xmrNodesLabel")); - moneroPeersLabel.setText(Res.get("settings.net.moneroPeersLabel")); + moneroConnectionsLabel.setText(Res.get("settings.net.moneroPeersLabel")); useTorForXmrLabel.setText(Res.get("settings.net.useTorForXmrJLabel")); useTorForXmrAfterSyncRadio.setText(Res.get("settings.net.useTorForXmrAfterSyncRadio")); useTorForXmrOffRadio.setText(Res.get("settings.net.useTorForXmrOffRadio")); useTorForXmrOnRadio.setText(Res.get("settings.net.useTorForXmrOnRadio")); moneroNodesLabel.setText(Res.get("settings.net.moneroNodesLabel")); - moneroPeerAddressColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.onionAddressColumn"))); - moneroPeerAddressColumn.getStyleClass().add("first-column"); - moneroPeerVersionColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.versionColumn"))); - moneroPeerSubVersionColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.subVersionColumn"))); - moneroPeerHeightColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.heightColumn"))); + moneroConnectionAddressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); + moneroConnectionAddressColumn.getStyleClass().add("first-column"); + moneroConnectionConnectedColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.connection"))); localhostXmrNodeInfoLabel.setText(Res.get("settings.net.localhostXmrNodeInfo")); useProvidedNodesRadio.setText(Res.get("settings.net.useProvidedNodesRadio")); useCustomNodesRadio.setText(Res.get("settings.net.useCustomNodesRadio")); @@ -192,19 +187,19 @@ public class NetworkSettingsView extends ActivatableView { rescanOutputsLabel.setVisible(false); rescanOutputsButton.setVisible(false); - GridPane.setMargin(moneroPeersLabel, new Insets(4, 0, 0, 0)); - GridPane.setValignment(moneroPeersLabel, VPos.TOP); + GridPane.setMargin(moneroConnectionsLabel, new Insets(4, 0, 0, 0)); + GridPane.setValignment(moneroConnectionsLabel, VPos.TOP); GridPane.setMargin(p2PPeersLabel, new Insets(4, 0, 0, 0)); GridPane.setValignment(p2PPeersLabel, VPos.TOP); - moneroPeersTableView.setMinHeight(180); - moneroPeersTableView.setPrefHeight(180); - moneroPeersTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); - moneroPeersTableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData"))); - moneroPeersTableView.getSortOrder().add(moneroPeerAddressColumn); - moneroPeerAddressColumn.setSortType(TableColumn.SortType.ASCENDING); - + moneroConnectionAddressColumn.setSortType(TableColumn.SortType.ASCENDING); + moneroConnectionConnectedColumn.setSortType(TableColumn.SortType.DESCENDING); + moneroConnectionsTableView.setMinHeight(180); + moneroConnectionsTableView.setPrefHeight(180); + moneroConnectionsTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + moneroConnectionsTableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData"))); + moneroConnectionsTableView.getSortOrder().add(moneroConnectionConnectedColumn); p2pPeersTableView.setMinHeight(180); p2pPeersTableView.setPrefHeight(180); @@ -309,11 +304,11 @@ public class NetworkSettingsView extends ActivatableView { rescanOutputsButton.setOnAction(event -> GUIUtil.rescanOutputs(preferences)); - moneroPeersSubscription = EasyBind.subscribe(connectionService.peerConnectionsProperty(), - this::updateMoneroPeersTable); + moneroConnectionsSubscription = EasyBind.subscribe(connectionService.connectionsProperty(), + connections -> updateMoneroConnectionsTable()); moneroBlockHeightSubscription = EasyBind.subscribe(connectionService.chainHeightProperty(), - this::updateChainHeightTextField); + height -> updateMoneroConnectionsTable()); nodeAddressSubscription = EasyBind.subscribe(p2PService.getNetworkNode().nodeAddressProperty(), nodeAddress -> onionAddress.setText(nodeAddress == null ? @@ -333,8 +328,8 @@ public class NetworkSettingsView extends ActivatableView { Statistic.numTotalReceivedMessagesPerSecProperty().get()), Statistic.numTotalReceivedMessagesPerSecProperty())); - moneroSortedList.comparatorProperty().bind(moneroPeersTableView.comparatorProperty()); - moneroPeersTableView.setItems(moneroSortedList); + moneroSortedList.comparatorProperty().bind(moneroConnectionsTableView.comparatorProperty()); + moneroConnectionsTableView.setItems(moneroSortedList); p2pSortedList.comparatorProperty().bind(p2pPeersTableView.comparatorProperty()); p2pPeersTableView.setItems(p2pSortedList); @@ -355,8 +350,8 @@ public class NetworkSettingsView extends ActivatableView { if (nodeAddressSubscription != null) nodeAddressSubscription.unsubscribe(); - if (moneroPeersSubscription != null) - moneroPeersSubscription.unsubscribe(); + if (moneroConnectionsSubscription != null) + moneroConnectionsSubscription.unsubscribe(); if (moneroBlockHeightSubscription != null) moneroBlockHeightSubscription.unsubscribe(); @@ -521,13 +516,15 @@ public class NetworkSettingsView extends ActivatableView { .collect(Collectors.toList())); } - private void updateMoneroPeersTable(List peers) { - moneroNetworkListItems.clear(); - if (peers != null) { - moneroNetworkListItems.setAll(peers.stream() - .map(MoneroNetworkListItem::new) + private void updateMoneroConnectionsTable() { + UserThread.execute(() -> { + if (connectionService.isShutDownStarted()) return; // ignore if shutting down + moneroNetworkListItems.clear(); + moneroNetworkListItems.setAll(connectionService.getConnections().stream() + .map(connection -> new MoneroNetworkListItem(connection, Boolean.TRUE.equals(connection.isConnected()) && connection == connectionService.getConnection())) .collect(Collectors.toList())); - } + updateChainHeightTextField(connectionService.chainHeightProperty().get()); + }); } private void updateChainHeightTextField(Number chainHeight) { From cfaf163bbc42540e3fa594aaa5928c2261bfc259 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 30 Nov 2024 18:46:32 -0500 Subject: [PATCH 020/371] update account limit hyperlinks --- core/src/main/resources/i18n/displayStrings.properties | 6 +++--- core/src/main/resources/i18n/displayStrings_cs.properties | 4 ++-- core/src/main/resources/i18n/displayStrings_de.properties | 4 ++-- core/src/main/resources/i18n/displayStrings_es.properties | 4 ++-- core/src/main/resources/i18n/displayStrings_fa.properties | 4 ++-- core/src/main/resources/i18n/displayStrings_fr.properties | 4 ++-- core/src/main/resources/i18n/displayStrings_it.properties | 4 ++-- core/src/main/resources/i18n/displayStrings_ja.properties | 4 ++-- .../src/main/resources/i18n/displayStrings_pt-br.properties | 4 ++-- core/src/main/resources/i18n/displayStrings_pt.properties | 4 ++-- core/src/main/resources/i18n/displayStrings_ru.properties | 4 ++-- core/src/main/resources/i18n/displayStrings_th.properties | 4 ++-- core/src/main/resources/i18n/displayStrings_tr.properties | 6 +++--- core/src/main/resources/i18n/displayStrings_vi.properties | 4 ++-- .../main/resources/i18n/displayStrings_zh-hans.properties | 4 ++-- .../main/resources/i18n/displayStrings_zh-hant.properties | 4 ++-- .../desktop/components/AccountStatusTooltipLabel.java | 2 +- 17 files changed, 35 insertions(+), 35 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index f3a67e4013..c4becc9357 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -414,7 +414,7 @@ offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due t offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\n\ After successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\n\ - For more information on account signing, please see the documentation at [HYPERLINK:https://haveno.exchange/wiki/Account_limits#Account_signing]. + For more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits/#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\ - The buyer''s account has not been signed by an arbitrator or a peer\n\ @@ -2651,7 +2651,7 @@ payment.limits.info=Please be aware that all bank transfers carry a certain amou \n\ This limit only applies to the size of a single trade—you can place as many trades as you like.\n\ \n\ - See more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. + See more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based \ on the following 2 factors:\n\n\ @@ -2669,7 +2669,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade \n\ These limits only apply to the size of a single trade—you can place as many trades as you like. \n\ \n\ - See more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. + See more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Please confirm your bank allows you to send cash deposits into other peoples' accounts. \ For example, Bank of America and Wells Fargo no longer allow such deposits. diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 161a6be719..cb8af0709f 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -1969,9 +1969,9 @@ payment.moneyGram.info=Při používání MoneyGram musí XMR kupující zaslat payment.westernUnion.info=Při používání služby Western Union musí kupující XMR zaslat prodejci XMR e-mailem MTCN (sledovací číslo) a fotografii potvrzení. Potvrzení musí jasně uvádět celé jméno prodejce, město, zemi a částku. E-mail prodávajícího se kupujícímu zobrazí během procesu obchodování. payment.halCash.info=Při používání HalCash musí kupující XMR poslat prodejci XMR kód HalCash prostřednictvím textové zprávy z mobilního telefonu.\n\nUjistěte se, že nepřekračujete maximální částku, kterou vám banka umožňuje odesílat pomocí HalCash. Min. částka za výběr je 10 EUR a max. částka je 600 EUR. Pro opakované výběry je to 3000 EUR za příjemce za den a 6000 EUR za příjemce za měsíc. Zkontrolujte prosím tyto limity u své banky, abyste se ujistili, že používají stejné limity, jaké jsou zde uvedeny.\n\nČástka pro výběr musí být násobkem 10 EUR, protože z bankomatu nemůžete vybrat jiné částky. Uživatelské rozhraní na obrazovce vytvořit-nabídku and přijmout-nabídku upraví částku XMR tak, aby částka EUR byla správná. Nemůžete použít tržní cenu, protože částka v EURECH se mění s měnícími se cenami.\n\nV případě sporu musí kupující XMR poskytnout důkaz, že zaslal EURA. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Uvědomte si, že u všech bankovních převodů existuje určité riziko zpětného zúčtování. Aby se toto riziko zmírnilo, stanoví Haveno limity pro jednotlivé obchody na základě odhadované úrovně rizika zpětného zúčtování pro použitou platební metodu.\n\nU této platební metody je váš limit pro jednotlivé obchody pro nákup a prodej {2}.\n\nToto omezení se vztahuje pouze na velikost jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\nDalší podrobnosti najdete na wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Uvědomte si, že u všech bankovních převodů existuje určité riziko zpětného zúčtování. Aby se toto riziko zmírnilo, stanoví Haveno limity pro jednotlivé obchody na základě odhadované úrovně rizika zpětného zúčtování pro použitou platební metodu.\n\nU této platební metody je váš limit pro jednotlivé obchody pro nákup a prodej {2}.\n\nToto omezení se vztahuje pouze na velikost jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\nDalší podrobnosti najdete na wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=Aby se omezilo riziko zpětného zúčtování, Haveno stanoví limity pro jednotlivé obchody pro tento typ platebního účtu na základě následujících 2 faktorů:\n\n1. Obecné riziko zpětného zúčtování pro platební metodu\n2. Stav podepisování účtu\n\nTento platební účet ještě není podepsán, takže je omezen na nákup {0} za obchod. Po podpisu se limity nákupu zvýší následovně:\n\n● Před podpisem a 30 dní po podpisu bude váš limit nákupu podle obchodu {0}\n● 30 dní po podpisu bude váš limit nákupu podle obchodu {1}\n● 60 dní po podpisu bude váš limit nákupu podle obchodu {2}\n\nPodpisy účtu neovlivňují prodejní limity. Můžete okamžitě prodat {2} v jednom obchodu.\n\nTato omezení platí pouze pro objem jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\nDalší podrobnosti najdete na wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=Aby se omezilo riziko zpětného zúčtování, Haveno stanoví limity pro jednotlivé obchody pro tento typ platebního účtu na základě následujících 2 faktorů:\n\n1. Obecné riziko zpětného zúčtování pro platební metodu\n2. Stav podepisování účtu\n\nTento platební účet ještě není podepsán, takže je omezen na nákup {0} za obchod. Po podpisu se limity nákupu zvýší následovně:\n\n● Před podpisem a 30 dní po podpisu bude váš limit nákupu podle obchodu {0}\n● 30 dní po podpisu bude váš limit nákupu podle obchodu {1}\n● 60 dní po podpisu bude váš limit nákupu podle obchodu {2}\n\nPodpisy účtu neovlivňují prodejní limity. Můžete okamžitě prodat {2} v jednom obchodu.\n\nTato omezení platí pouze pro objem jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\nDalší podrobnosti najdete na wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Potvrďte, že vám vaše banka umožňuje odesílat hotovostní vklady na účty jiných lidí. Například Bank of America a Wells Fargo již takové vklady nepovolují. diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 198b5999f2..b920ec9c48 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1970,9 +1970,9 @@ payment.moneyGram.info=Bei der Nutzung von MoneyGram, muss der XMR Käufer die M payment.westernUnion.info=Bei der Nutzung von Western Union, muss der XMR Käufer die MTCN (Tracking-Nummer) Foto der Quittung per E-Mail an den XMR-Verkäufer senden. Die Quittung muss den vollständigen Namen, das Land, die Stadt des Verkäufers und den Betrag deutlich zeigen. Der Käufer bekommt die E-Mail-Adresse des Verkäufers im Handelsprozess angezeigt. payment.halCash.info=Bei Verwendung von HalCash muss der XMR-Käufer dem XMR-Verkäufer den HalCash-Code per SMS vom Mobiltelefon senden.\n\nBitte achten Sie darauf, dass Sie den maximalen Betrag, den Sie bei Ihrer Bank mit HalCash versenden dürfen, nicht überschreiten. Der Mindestbetrag pro Auszahlung beträgt 10 EUR und der Höchstbetrag 600 EUR. Bei wiederholten Abhebungen sind es 3000 EUR pro Empfänger pro Tag und 6000 EUR pro Empfänger pro Monat. Bitte überprüfen Sie diese Limits bei Ihrer Bank, um sicherzustellen, dass sie die gleichen Limits wie hier angegeben verwenden.\n\nDer Auszahlungsbetrag muss ein Vielfaches von 10 EUR betragen, da Sie keine anderen Beträge an einem Geldautomaten abheben können. Die Benutzeroberfläche beim Erstellen und Annehmen eines Angebots passt den XMR-Betrag so an, dass der EUR-Betrag korrekt ist. Sie können keinen marktbasierten Preis verwenden, da sich der EUR-Betrag bei sich ändernden Preisen ändern würde.\n\nIm Streitfall muss der XMR-Käufer den Nachweis erbringen, dass er die EUR geschickt hat. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Bitte beachten Sie, dass alle Banküberweisungen mit einem gewissen Rückbuchungsrisiko verbunden sind. Um dieses Risiko zu mindern, setzt Haveno Limits pro Trade fest, je nachdem wie hoch das Rückbuchungsrisiko der Zahlungsmethode ist. \n\nFür diese Zahlungsmethode beträgt Ihr Pro-Trade-Limit zum Kaufen oder Verkaufen {2}.\nDieses Limit gilt nur für die Größe eines einzelnen Trades - Sie können soviele Trades platzieren wie Sie möchten.\n\nFinden Sie mehr Informationen im Wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Bitte beachten Sie, dass alle Banküberweisungen mit einem gewissen Rückbuchungsrisiko verbunden sind. Um dieses Risiko zu mindern, setzt Haveno Limits pro Trade fest, je nachdem wie hoch das Rückbuchungsrisiko der Zahlungsmethode ist. \n\nFür diese Zahlungsmethode beträgt Ihr Pro-Trade-Limit zum Kaufen oder Verkaufen {2}.\nDieses Limit gilt nur für die Größe eines einzelnen Trades - Sie können soviele Trades platzieren wie Sie möchten.\n\nFinden Sie mehr Informationen im Wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=Um das Risiko einer Rückbuchung zu minimieren, setzt Haveno für diese Zahlungsmethode Limits pro Trade auf der Grundlage der folgenden 2 Faktoren fest:\n\n1. Allgemeines Rückbuchungsrisiko für die Zahlungsmethode\n2. Status der Kontounterzeichnung\n\nDieses Zahlungskonto ist noch nicht unterzeichnet. Es ist daher auf den Kauf von {0} pro Trade beschränkt ist. Nach der Unterzeichnung werden die Kauflimits wie folgt erhöht:\n\n● Vor der Unterzeichnung und für 30 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {0}\n● 30 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {1}\n● 60 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {2}\n\nVerkaufslimits sind von der Kontounterzeichnung nicht betroffen. Sie können {2} in einem einzigen Trade sofort verkaufen.\n\nDieses Limit gilt nur für die Größe eines einzelnen Trades - Sie können soviele Trades platzieren wie sie möchten.\n\nWeitere Informationen gibt es im Wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=Um das Risiko einer Rückbuchung zu minimieren, setzt Haveno für diese Zahlungsmethode Limits pro Trade auf der Grundlage der folgenden 2 Faktoren fest:\n\n1. Allgemeines Rückbuchungsrisiko für die Zahlungsmethode\n2. Status der Kontounterzeichnung\n\nDieses Zahlungskonto ist noch nicht unterzeichnet. Es ist daher auf den Kauf von {0} pro Trade beschränkt ist. Nach der Unterzeichnung werden die Kauflimits wie folgt erhöht:\n\n● Vor der Unterzeichnung und für 30 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {0}\n● 30 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {1}\n● 60 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {2}\n\nVerkaufslimits sind von der Kontounterzeichnung nicht betroffen. Sie können {2} in einem einzigen Trade sofort verkaufen.\n\nDieses Limit gilt nur für die Größe eines einzelnen Trades - Sie können soviele Trades platzieren wie sie möchten.\n\nWeitere Informationen gibt es im Wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. 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. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 17115d059d..9fd52c5773 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -1971,9 +1971,9 @@ payment.moneyGram.info=Al utilizar MoneyGram, el comprador de XMR tiene que envi payment.westernUnion.info=Al utilizar Western Union, el comprador de XMR tiene que enviar el número de seguimiento (MTCN) y una foto del recibo al vendedor de XMR por correo electrónico. El recibo debe mostrar claramente el como el nombre completo del vendedor, país, ciudad y cantidad. Al comprador se le mostrará el correo electrónico del vendedor en el proceso de intercambio. payment.halCash.info=Al usar HalCash el comprador de XMR necesita enviar al vendedor de XMR el código HalCash a través de un mensaje de texto desde el teléfono móvil.\n\nPor favor asegúrese de que no excede la cantidad máxima que su banco le permite enviar con HalCash. La cantidad mínima por retirada es de 10 EUR y el máximo son 600 EUR. Para retiros frecuentes es 3000 por receptor al día y 6000 por receptor al mes. Por favor compruebe estos límites con su banco y asegúrese que son los mismos aquí expuestos.\n\nLa cantidad de retiro debe ser un múltiplo de 10 EUR ya que no se puede retirar otras cantidades desde el cajero automático. La Interfaz de Usuario en la pantalla crear oferta y tomar oferta ajustará la cantidad de XMR para que la cantidad de EUR sea correcta. No puede usar precios basados en el mercado ya que la cantidad de EUR cambiaría con el cambio de precios.\n\nEn caso de disputa el comprador de XMR necesita proveer la prueba de que ha enviado EUR. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Por favor, tenga en cuenta que todas las transferencias bancarias tienen cierto riesgo de reversión de pago.\n\nPara disminuir este riesgo, Haveno establece límites por intercambio en función del nivel estimado de riesgo de reversión de pago para el método usado.\n\nPara este método de pago, su límite por intercambio para comprar y vender es {2}.\n\nEste límite solo aplica al tamaño de un intercambio: puede poner tantos intercambios como quira.\n\nConsulte detalles en la wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Por favor, tenga en cuenta que todas las transferencias bancarias tienen cierto riesgo de reversión de pago.\n\nPara disminuir este riesgo, Haveno establece límites por intercambio en función del nivel estimado de riesgo de reversión de pago para el método usado.\n\nPara este método de pago, su límite por intercambio para comprar y vender es {2}.\n\nEste límite solo aplica al tamaño de un intercambio: puede poner tantos intercambios como quira.\n\nConsulte detalles en la wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=Para limitar el riesgo de devolución de cargo, Haveno establece límites por compra basados en los 2 siguientes factores:\n\n1. Riesgo general de devolución de cargo para el método de pago\n2. Estado de firmado de cuenta\n\nEsta cuenta de pago aún no ha sido firmada, con lo que ha sido limitada para comprar {0} por intercambio. Después de firmarse, los límites de compra se incrementarán de esta manera:\n\n● Antes de ser firmada, y hasta 30 días después de la firma, su límite por intercambio de compra será {0}\n● 30 días después de la firma, su límite de compra por intercambio será de {1}\n● 60 días después de la firma, su límite de compra por intercambio será de {2}\n\nLos límites de venta no se ven afectados por el firmado de cuentas. Puede vender {2} en un solo \nintercambio inmediatamente.\n\nEstos límites solo aplican al tamaño de un intercambio. Puede hacer tantos intercambios como quiera.\n\n Consulte detalles en la wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits].\n\n +payment.limits.info.withSigning=Para limitar el riesgo de devolución de cargo, Haveno establece límites por compra basados en los 2 siguientes factores:\n\n1. Riesgo general de devolución de cargo para el método de pago\n2. Estado de firmado de cuenta\n\nEsta cuenta de pago aún no ha sido firmada, con lo que ha sido limitada para comprar {0} por intercambio. Después de firmarse, los límites de compra se incrementarán de esta manera:\n\n● Antes de ser firmada, y hasta 30 días después de la firma, su límite por intercambio de compra será {0}\n● 30 días después de la firma, su límite de compra por intercambio será de {1}\n● 60 días después de la firma, su límite de compra por intercambio será de {2}\n\nLos límites de venta no se ven afectados por el firmado de cuentas. Puede vender {2} en un solo \nintercambio inmediatamente.\n\nEstos límites solo aplican al tamaño de un intercambio. Puede hacer tantos intercambios como quiera.\n\n Consulte detalles en la wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits].\n\n 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. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index f6db7fffd2..539c3287ab 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1963,9 +1963,9 @@ payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Author payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=زمانی که از HalCash استفاده می‌کنید، خریدار باید کد HalCash را از طریق پیام کوتاه موبایل به فروشنده XMR ارسال کند.\n\nلطفا مطمئن شوید که از حداکثر میزانی که بانک شما برای انتقال از طریق HalCash مجاز می‌داند تجاوز نکرده‌اید. حداقل مقداردر هر برداشت معادل 10 یورو و حداکثر مقدار 600 یورو می‌باشد. این محدودیت برای برداشت‌های تکراری برای هر گیرنده در روز 3000 یورو و در ماه 6000 یورو می‌باشد. لطفا این محدودیت‌ها را با بانک خود مطابقت دهید و مطمئن شوید که آنها هم همین محدودی‌ها را دارند.\n\nمقدار برداشت باید شریبی از 10 یورو باشد چرا که مقادیر غیر از این را نمی‌توانید از طریق ATM برداشت کنید. رابط کاربری در صفحه ساخت پینشهاد و پذیرش پیشنهاد مقدار XMR را به گونه‌ای تنظیم می‌کنند که مقدار EUR درست باشد. شما نمی‌توانید از قیمت بر مبنای بازار استفاده کنید چون مقدار یورو با تغییر قیمت‌ها عوض خواهد شد.\n\nدر صورت بروز اختلاف خریدار XMR باید شواهد مربوط به ارسال یورو را ارائه دهد. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=لطفا مطمئن شوید که بانک شما اجازه پرداخت سپرده نفد به حساب دیگر افراد را می‌دهد. برای مثال، Bank of America و Wells Fargo دیگر اجازه چنین پرداخت‌هایی را نمی‌دهند. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 400f58cdc4..cacd919ca6 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -1972,9 +1972,9 @@ payment.moneyGram.info=Lors de l'utilisation de MoneyGram, l'acheteur de XMR doi payment.westernUnion.info=Lors de l'utilisation de Western Union, l'acheteur XMR doit envoyer le MTCN (numéro de suivi) et une photo du reçu par e-mail au vendeur de XMR. Le reçu doit indiquer clairement le nom complet du vendeur, la ville, le pays et le montant. L'acheteur verra ensuite s'afficher l'email du vendeur pendant le processus de transaction. payment.halCash.info=Lors de l'utilisation de HalCash, l'acheteur de XMR doit envoyer au vendeur de XMR le code HalCash par SMS depuis son téléphone portable.\n\nVeuillez vous assurer de ne pas dépasser le montant maximum que votre banque vous permet d'envoyer avec HalCash. Le montant minimum par retrait est de 10 EUR et le montant maximum est de 600 EUR. Pour les retraits récurrents, il est de 3000 EUR par destinataire par jour et 6000 EUR par destinataire par mois. Veuillez vérifier ces limites auprès de votre banque pour vous assurer qu'elles utilisent les mêmes limites que celles indiquées ici.\n\nLe montant du retrait doit être un multiple de 10 EUR car vous ne pouvez pas retirer d'autres montants à un distributeur automatique. Pendant les phases de create-offer et take-offer l'affichage de l'interface utilisateur ajustera le montant en XMR afin que le montant en euros soit correct. Vous ne pouvez pas utiliser le prix basé sur le marché, car le montant en euros varierait en fonction de l'évolution des prix.\n\nEn cas de litige, l'acheteur de XMR doit fournir la preuve qu'il a envoyé la somme en EUR. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Sachez que tous les virements bancaires comportent un certain risque de rétrofacturation. Pour mitiger ce risque, Haveno fixe des limites par trade en fonction du niveau estimé de risque de rétrofacturation pour la méthode de paiement utilisée.\n\nPour cette méthode de paiement, votre limite de trading pour l'achat et la vente est de {2}.\n\nCette limite ne s'applique qu'à la taille d'une seule transaction. Vous pouvez effectuer autant de transactions que vous le souhaitez.\n\nVous trouverez plus de détails sur le wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Sachez que tous les virements bancaires comportent un certain risque de rétrofacturation. Pour mitiger ce risque, Haveno fixe des limites par trade en fonction du niveau estimé de risque de rétrofacturation pour la méthode de paiement utilisée.\n\nPour cette méthode de paiement, votre limite de trading pour l'achat et la vente est de {2}.\n\nCette limite ne s'applique qu'à la taille d'une seule transaction. Vous pouvez effectuer autant de transactions que vous le souhaitez.\n\nVous trouverez plus de détails sur le wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=Afin de limiter le risque de rétrofacturation des achats, Haveno fixe des limites d'achat par transaction pour ce compte de paiement basé sur les 2 facteurs suivants :\n\n1. Risque de rétrofacturation pour le mode de paiement\n2. Statut de signature du compte\n\nCe compte de paiement n'est pas encore signé, il est donc limité à l'achat de {0} par trade. Après sa signature, les limites d'achat augmenteront comme suit :\n\n● Avant la signature, et jusqu'à 30 jours après la signature, votre limite d'achat par trade sera de {0}\n● 30 jours après la signature, votre limite d'achat par trade sera de {1}\n● 60 jours après la signature, votre limite d'achat par trade sera de {2}\n\nLes limites de vente ne sont pas affectées par la signature du compte. Vous pouvez vendre {2} en un seul trade immédiatement.\n\nCes limites s'appliquent uniquement à la taille d'un seul trade-vous pouvez placer autant de trades que vous voulez.\n\n Pour plus d''nformations, rendez vous à [LIEN:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=Afin de limiter le risque de rétrofacturation des achats, Haveno fixe des limites d'achat par transaction pour ce compte de paiement basé sur les 2 facteurs suivants :\n\n1. Risque de rétrofacturation pour le mode de paiement\n2. Statut de signature du compte\n\nCe compte de paiement n'est pas encore signé, il est donc limité à l'achat de {0} par trade. Après sa signature, les limites d'achat augmenteront comme suit :\n\n● Avant la signature, et jusqu'à 30 jours après la signature, votre limite d'achat par trade sera de {0}\n● 30 jours après la signature, votre limite d'achat par trade sera de {1}\n● 60 jours après la signature, votre limite d'achat par trade sera de {2}\n\nLes limites de vente ne sont pas affectées par la signature du compte. Vous pouvez vendre {2} en un seul trade immédiatement.\n\nCes limites s'appliquent uniquement à la taille d'un seul trade-vous pouvez placer autant de trades que vous voulez.\n\n Pour plus d''nformations, rendez vous à [LIEN:https://docs.haveno.exchange/the-project/account_limits]. 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. diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 4a8e6844d0..2bb5a9bb91 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1966,9 +1966,9 @@ payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Author payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Quando utilizza HalCash, l'acquirente XMR deve inviare al venditore XMR il codice HalCash tramite un messaggio di testo dal proprio telefono cellulare.\n\nAssicurati di non superare l'importo massimo che la tua banca ti consente di inviare con HalCash. L'importo minimo per prelievo è di 10 EURO, l'importo massimo è di 600 EURO. Per prelievi ripetuti è di 3000 EURO per destinatario al giorno e 6000 EURO per destintario al mese. Verifica i limiti con la tua banca per accertarti che utilizzino gli stessi limiti indicati qui.\n\nL'importo del prelievo deve essere un multiplo di 10 EURO in quanto non è possibile prelevare altri importi da un bancomat. L'interfaccia utente nella schermata di creazione offerta e accettazione offerta modificherà l'importo XMR in modo che l'importo in EURO sia corretto. Non è possibile utilizzare il prezzo di mercato poiché l'importo in EURO cambierebbe al variare dei prezzi.\n\nIn caso di controversia, l'acquirente XMR deve fornire la prova di aver inviato gli EURO. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Conferma che la tua banca ti consente di inviare depositi in contanti su conti di altre persone. Ad esempio, Bank of America e Wells Fargo non consentono più tali depositi. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index afd3cd69d3..a6a77ca657 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -1969,9 +1969,9 @@ payment.moneyGram.info=MoneyGramを使用する場合、XMRの買い手は認証 payment.westernUnion.info=Western Unionを使用する場合、XMRの買い手はMTCN(追跡番号)と領収書の写真をEメールでXMRの売り手に送信する必要があります。領収書には、売り手の氏名、市区町村、国、金額を明確に記載する必要があります。トレードプロセスにて、売り手のEメールは買い手に表示されます。 payment.halCash.info=HalCashを使用する場合、XMRの買い手は携帯電話からのテキストメッセージを介してXMRの売り手にHalCashコードを送信する必要があります。\n\n銀行がHalCashで送金できる最大額を超えないようにしてください。 1回の出金あたりの最小金額は10EURで、最大金額は600EURです。繰り返し出金する場合は、1日に受取人1人あたり3000EUR、1ヶ月に受取人1人あたり6000EURです。あなたの銀行でも、ここに記載されているのと同じ制限を使用しているか、これらの制限を銀行と照合して確認してください。\n\n出金額は10の倍数EURでなければ、ATMから出金できません。 オファーの作成画面およびオファー受け入れ画面のUIは、EUR金額が正しくなるようにXMR金額を調整します。価格の変化とともにEURの金額は変化するため、市場ベースの価格を使用することはできません。\n\n係争が発生した場合、XMRの買い手はEURを送ったという証明を提出する必要があります。 # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=すべての銀行振込にはある程度の支払取り消しのリスクがあることに気を付けて下さい。\n\nこのリスクを軽減するために、Havenoは使用する支払い方法での支払取り消しリスクの推定レベルに基づいてトレードごとの制限を設定します。\n\n現在使用する支払い方法では、トレードごとの売買制限は{2}です。\n\n制限は各トレードの量のみに適用されることに注意して下さい。トレードできる合計回数には制限はありません。\n\n詳しくはWikiを調べて下さい [HYPERLINK:https://haveno.exchange/wiki/Account_limits] 。 +payment.limits.info=すべての銀行振込にはある程度の支払取り消しのリスクがあることに気を付けて下さい。\n\nこのリスクを軽減するために、Havenoは使用する支払い方法での支払取り消しリスクの推定レベルに基づいてトレードごとの制限を設定します。\n\n現在使用する支払い方法では、トレードごとの売買制限は{2}です。\n\n制限は各トレードの量のみに適用されることに注意して下さい。トレードできる合計回数には制限はありません。\n\n詳しくはWikiを調べて下さい [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits] 。 # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=支払取り消しのリスクを軽減するために、Havenoはこの支払いアカウントに下記の2つの要因に基づいてトレードごとの制限を設定します。\n\n1.使用する支払い方法での支払取り消しリスクの推定レベル\n2.アカウントの署名状況\n\nこの支払いアカウントはまだ無署名ですので、トレードごとに{0}の買い制限があります。 アカウントが署名される後、トレードごとの制限は以下のように成長します:\n\n●署名の前、そして署名から30日間までに、1トレードあたりの買い制限は{0}になります\n●署名から30日間後に、1トレードあたりの買い制限は{1}になります\n●署名から60日間後に、1トレードあたりの買い制限は{2}になります\n\n売り制限は署名状況に関係がありません。現在のところ、1トレードあたりに{2}を売ることができます。\n\n制限は各トレードの量のみに適用されることに注意して下さい。取引できる合計回数には制限はありません。\n\n詳しくは: [HYPERLINK:https://haveno.exchange/wiki/Account_limits] +payment.limits.info.withSigning=支払取り消しのリスクを軽減するために、Havenoはこの支払いアカウントに下記の2つの要因に基づいてトレードごとの制限を設定します。\n\n1.使用する支払い方法での支払取り消しリスクの推定レベル\n2.アカウントの署名状況\n\nこの支払いアカウントはまだ無署名ですので、トレードごとに{0}の買い制限があります。 アカウントが署名される後、トレードごとの制限は以下のように成長します:\n\n●署名の前、そして署名から30日間までに、1トレードあたりの買い制限は{0}になります\n●署名から30日間後に、1トレードあたりの買い制限は{1}になります\n●署名から60日間後に、1トレードあたりの買い制限は{2}になります\n\n売り制限は署名状況に関係がありません。現在のところ、1トレードあたりに{2}を売ることができます。\n\n制限は各トレードの量のみに適用されることに注意して下さい。取引できる合計回数には制限はありません。\n\n詳しくは: [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits] payment.cashDeposit.info=あなたの銀行が他の人の口座に現金入金を送ることを許可していることを確認してください。たとえば、Bank of America と Wells Fargo では、こうした預金は許可されなくなりました。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index da457db45b..2960b16ca0 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -1973,9 +1973,9 @@ payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Author payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Ao usar o HalCash, o comprador de XMR precisa enviar ao vendedor de XMR o código HalCash através de uma mensagem de texto do seu telefone.\n\nPor favor, certifique-se de não exceder a quantia máxima que seu banco lhe permite enviar com o HalCash. O valor mínimo de saque é de 10 euros e valor máximo é de 600 EUR. Para saques repetidos é de 3000 euros por destinatário por dia e 6000 euros por destinatário por mês. Por favor confirme esses limites com seu banco para ter certeza de que eles usam os mesmos limites mencionados aqui.\n\nO valor de saque deve ser um múltiplo de 10 euros, pois você não pode sacar notas diferentes de uma ATM. Esse valor em XMR será ajustado na telas de criar e aceitar ofertas para que a quantia de EUR esteja correta. Você não pode usar o preço com base no mercado, pois o valor do EUR estaria mudando com a variação dos preços.\n\nEm caso de disputa, o comprador de XMR precisa fornecer a prova de que enviou o EUR. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Certifique-se de que o seu banco permite a realização de depósitos em espécie na conta de terceiros. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index b3985a5692..0f20382ba4 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1963,9 +1963,9 @@ payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Author payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Ao usar o HalCash, o comprador de XMR precisa enviar ao vendedor de XMR o código HalCash através de uma mensagem de texto do seu telemóvel.\n\nPor favor, certifique-se de não exceder a quantia máxima que seu banco lhe permite enviar com o HalCash. A quantia mín. de levantamento é de 10 euros e a quantia máx. é de 600 EUR. Para levantamentos repetidos é de 3000 euros por recipiente por dia e 6000 euros por recipiente por mês. Por favor confirme esses limites com seu banco para ter certeza de que eles usam os mesmos limites mencionados aqui.\n\nA quantia de levantamento deve ser um múltiplo de 10 euros, pois você não pode levantar outras quantias de uma ATM. A interface do utilizador no ecrã para criar oferta e aceitar ofertas ajustará a quantia de XMR para que a quantia de EUR esteja correta. Você não pode usar o preço com base no mercado, pois o valor do EUR estaria mudando com a variação dos preços.\n\nEm caso de disputa, o comprador de XMR precisa fornecer a prova de que enviou o EUR. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. 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. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 51d98e1ef4..864a75b73e 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1964,9 +1964,9 @@ payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Author payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Используя HalCash, покупатель XMR обязуется отправить продавцу XMR код HalCash через СМС с мобильного телефона.\n\nУбедитесь, что не вы не превысили максимальную сумму, которую ваш банк позволяет отправить с HalCash. Минимальная сумма на вывод средств составляет 10 EUR, а и максимальная — 600 EUR. При повторном выводе средств лимит составляет 3000 EUR на получателя в день и 6000 EUR на получателя в месяц. Просьба сверить эти лимиты с вашим банком и убедиться, что лимиты банка соответствуют лимитам, указанным здесь.\n\nВыводимая сумма должна быть кратна 10 EUR, так как другие суммы снять из банкомата невозможно. Приложение само отрегулирует сумму XMR, чтобы она соответствовала сумме в EUR, во время создания или принятия предложения. Вы не сможете использовать текущий рыночный курс, так как сумма в EUR будет меняться с изменением курса.\n\nВ случае спора покупателю XMR необходимо предоставить доказательство отправки EUR. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Убедитесь, что ваш банк позволяет отправлять денежные переводы на счета других лиц. Например, Bank of America и Wells Fargo больше не разрешают такие переводы. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 5460e1074f..214d5d9e39 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1964,9 +1964,9 @@ payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Author payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=เมื่อมีการใช้งาน HalCash ผู้ซื้อ XMR จำเป็นต้องส่งรหัส Halcash ให้กับผู้ขายทางข้อความโทรศัพท์มือถือ\n\nโปรดตรวจสอบว่าไม่เกินจำนวนเงินสูงสุดที่ธนาคารของคุณอนุญาตให้คุณส่งด้วย HalCash จำนวนเงินขั้นต่ำในการเบิกถอนคือ 10 EUR และสูงสุดในจำนวนเงิน 600 EUR สำหรับการถอนซ้ำเป็น 3000 EUR ต่อผู้รับและต่อวัน และ 6000 EUR ต่อผู้รับและต่อเดือน โปรดตรวจสอบข้อจำกัดจากทางธนาคารคุณเพื่อให้มั่นใจได้ว่าทางธนาคารได้มีการใช้มาตรฐานข้อกำหนดเดียวกันกับดังที่ระบุไว้ ณ ที่นี่\n\nจำนวนเงินที่ถอนจะต้องเป็นจำนวนเงินหลาย 10 EUR เนื่องจากคุณไม่สามารถถอนเงินอื่น ๆ ออกจากตู้เอทีเอ็มได้ UI ในหน้าจอสร้างข้อเสนอและรับข้อเสนอจะปรับจำนวนเงิน XMR เพื่อให้จำนวนเงิน EUR ถูกต้อง คุณไม่สามารถใช้ราคาตลาดเป็นจำนวนเงิน EUR ซึ่งจะเปลี่ยนแปลงไปตามราคาที่มีการปรับเปลี่ยน\n\nในกรณีที่มีข้อพิพาทผู้ซื้อ XMR ต้องแสดงหลักฐานว่าได้ส่ง EUR แล้ว # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=โปรดยืนยันว่าธนาคารของคุณได้อนุมัติให้คุณสามารถส่งเงินสดให้กับบัญชีบุคคลอื่นได้ ตัวอย่างเช่น บางธนาคารที่ไม่ได้มีการบริการถ่ายโอนเงินสดอย่าง Bank of America และ Wells Fargo diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index d807f48aa5..cff8e62c70 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -411,7 +411,7 @@ offerbook.warning.counterpartyTradeRestrictions=Karşı taraf ticaret kısıtlam offerbook.warning.newVersionAnnouncement=Bu yazılım sürümü ile, ticaret yapan eşler birbirlerinin ödeme hesaplarını doğrulayabilir ve imzalayabilir, böylece güvenilir ödeme hesapları ağı oluşturulabilir.\n\n\ Doğrulanmış ödeme hesabı olan bir eş ile başarılı bir şekilde ticaret yaptıktan sonra, ödeme hesabınız imzalanır ve ticaret limitleri belirli bir zaman aralığından sonra kaldırılır (bu aralığın uzunluğu doğrulama yöntemine bağlıdır).\n\n\ - Hesap imzalama hakkında daha fazla bilgi için, lütfen [HYPERLINK:https://haveno.exchange/wiki/Account_limits#Account_signing] belgelere bakın. + Hesap imzalama hakkında daha fazla bilgi için, lütfen [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits/#account-signing] belgelere bakın. popup.warning.tradeLimitDueAccountAgeRestriction.seller=İzin verilen ticaret miktarı, aşağıdaki kriterlere dayanan güvenlik kısıtlamaları nedeniyle {0} ile sınırlıdır:\n\ - Alıcının hesabı bir hakem veya eş tarafından imzalanmamış\n\ @@ -2644,7 +2644,7 @@ payment.limits.info=Lütfen tüm banka transferlerinin belirli bir miktarda geri \n\ Bu limit yalnızca tek bir işlemin boyutuna uygulanır—istediğiniz kadar işlem yapabilirsiniz.\n\ \n\ - Daha fazla ayrıntı için wiki sayfasına bakın [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. + Daha fazla ayrıntı için wiki sayfasına bakın [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=Geri ödeme riskini sınırlamak için, Haveno, bu ödeme hesabı türü için işlem başına limitler belirler \ aşağıdaki 2 faktöre dayanır:\n\n\ @@ -2662,7 +2662,7 @@ payment.limits.info.withSigning=Geri ödeme riskini sınırlamak için, Haveno, \n\ Bu limitler yalnızca tek bir işlemin boyutuna uygulanır—istediğiniz kadar işlem yapabilirsiniz. \n\ \n\ - Daha fazla ayrıntı için wiki sayfasına bakın [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. + Daha fazla ayrıntı için wiki sayfasına bakın [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Lütfen bankanızın başka kişilerin hesaplarına nakit yatırma işlemlerine izin verdiğini onaylayın. \ Örneğin, Bank of America ve Wells Fargo gibi bankalar bu tür yatırımlara artık izin vermemektedir. diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 321d1bb6b6..03a23db4fa 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1966,9 +1966,9 @@ payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Author payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Khi sử dụng HalCash người mua XMR cần phải gửi cho người bán XMR mã HalCash bằng tin nhắn điện thoại.\n\nVui lòng đảm bảo là lượng tiền này không vượt quá số lượng tối đa mà ngân hàng của bạn cho phép gửi khi dùng HalCash. Số lượng rút tối thiểu là 10 EUR và tối đa là 600 EUR. Nếu rút nhiều lần thì giới hạn sẽ là 3000 EUR/ người nhận/ ngày và 6000 EUR/người nhận/tháng. Vui lòng kiểm tra chéo những giới hạn này với ngân hàng của bạn để chắc chắn là họ cũng dùng những giới hạn như ghi ở đây.\n\nSố tiền rút phải là bội số của 10 EUR vì bạn không thể rút các mệnh giá khác từ ATM. Giao diện người dùng ở phần 'tạo chào giá' và 'chấp nhận chào giá' sẽ điều chỉnh lượng btc sao cho lượng EUR tương ứng sẽ chính xác. Bạn không thể dùng giá thị trường vì lượng EUR có thể sẽ thay đổi khi giá thay đổi.\n\nTrường hợp tranh chấp, người mua XMR cần phải cung cấp bằng chứng chứng minh mình đã gửi EUR. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. 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. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 2709bb2645..9993a36b81 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -1973,9 +1973,9 @@ payment.moneyGram.info=使用 MoneyGram 时,XMR 买方必须将授权号码和 payment.westernUnion.info=使用 Western Union 时,XMR 买方必须通过电子邮件将 MTCN(运单号)和收据照片发送给 XMR 卖方。收据上必须清楚地显示卖方的全名、城市、国家或地区和金额。买方将在交易过程中显示卖方的电子邮件。 payment.halCash.info=使用 HalCash 时,XMR 买方需要通过手机短信向 XMR 卖方发送 HalCash 代码。\n\n请确保不要超过银行允许您用半现金汇款的最高金额。每次取款的最低金额是 10 欧元,最高金额是 10 欧元。金额是 600 欧元。对于重复取款,每天每个接收者 3000 欧元,每月每个接收者 6000 欧元。请与您的银行核对这些限额,以确保它们使用与此处所述相同的限额。\n\n提现金额必须是 10 欧元的倍数,因为您不能从 ATM 机提取其他金额。 创建报价和下单屏幕中的 UI 将调整 XMR 金额,使 EUR 金额正确。你不能使用基于市场的价格,因为欧元的数量会随着价格的变化而变化。\n # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=请注意,所有银行转账都有一定的退款风险。为了降低这一风险,Haveno 基于使用的付款方式的退款风险。\n\n对于付款方式,您的每笔交易的出售和购买的限额为{2}\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Haveno Wiki 查看更多信息[HYPERLINK:https://haveno.exchange/wiki/Account_limits]。 +payment.limits.info=请注意,所有银行转账都有一定的退款风险。为了降低这一风险,Haveno 基于使用的付款方式的退款风险。\n\n对于付款方式,您的每笔交易的出售和购买的限额为{2}\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Haveno Wiki 查看更多信息[HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]。 # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=为了降低这一风险,Haveno 基于两个因素对该付款方式每笔交易设置了限制:\n\n1. 使用的付款方法的预估退款风险水平\n2. 您的付款方式的账龄\n\n这个付款账户还没有被验证,所以他每个交易最多购买{0}。在验证之后,购买限制会以以下规则逐渐增加:\n\n●签署前,以及签署后30天内,您的每笔最大交易将限制为{0}\n●签署后30天,每笔最大交易将限制为{1}\n●签署后60天,每笔最大交易将限制为{2}\n\n出售限制不会被账户验证状态限制,你可以理科进行单笔为{2}的交易\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Haveno Wiki 上查看更多:\nhttps://haveno.exchange/wiki/Account_limits +payment.limits.info.withSigning=为了降低这一风险,Haveno 基于两个因素对该付款方式每笔交易设置了限制:\n\n1. 使用的付款方法的预估退款风险水平\n2. 您的付款方式的账龄\n\n这个付款账户还没有被验证,所以他每个交易最多购买{0}。在验证之后,购买限制会以以下规则逐渐增加:\n\n●签署前,以及签署后30天内,您的每笔最大交易将限制为{0}\n●签署后30天,每笔最大交易将限制为{1}\n●签署后60天,每笔最大交易将限制为{2}\n\n出售限制不会被账户验证状态限制,你可以理科进行单笔为{2}的交易\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Haveno Wiki 上查看更多:\nhttps://docs.haveno.exchange/the-project/account_limits payment.cashDeposit.info=请确认您的银行允许您将现金存款汇入他人账户。例如,美国银行和富国银行不再允许此类存款。 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 64f5082a45..a41239132a 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -1967,9 +1967,9 @@ payment.moneyGram.info=使用 MoneyGram 時,XMR 買方必須將授權號碼和 payment.westernUnion.info=使用 Western Union 時,XMR 買方必須通過電子郵件將 MTCN(運單號)和收據照片發送給 XMR 賣方。收據上必須清楚地顯示賣方的全名、城市、國家或地區和金額。買方將在交易過程中顯示賣方的電子郵件。 payment.halCash.info=使用 HalCash 時,XMR 買方需要通過手機短信向 XMR 賣方發送 HalCash 代碼。\n\n請確保不要超過銀行允許您用半現金匯款的最高金額。每次取款的最低金額是 10 歐元,最高金額是 10 歐元。金額是 600 歐元。對於重複取款,每天每個接收者 3000 歐元,每月每個接收者 6000 歐元。請與您的銀行核對這些限額,以確保它們使用與此處所述相同的限額。\n\n提現金額必須是 10 歐元的倍數,因為您不能從 ATM 機提取其他金額。 創建報價和下單屏幕中的 UI 將調整 XMR 金額,使 EUR 金額正確。你不能使用基於市場的價格,因為歐元的數量會隨着價格的變化而變化。\n # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=請注意,所有銀行轉賬都有一定的退款風險。為了降低這一風險,Haveno 基於使用的付款方式的退款風險。\n\n對於付款方式,您的每筆交易的出售和購買的限額為{2}\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Haveno Wiki 查看更多信息[HYPERLINK:https://haveno.exchange/wiki/Account_limits]。 +payment.limits.info=請注意,所有銀行轉賬都有一定的退款風險。為了降低這一風險,Haveno 基於使用的付款方式的退款風險。\n\n對於付款方式,您的每筆交易的出售和購買的限額為{2}\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Haveno Wiki 查看更多信息[HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]。 # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=為了降低這一風險,Haveno 基於兩個因素對該付款方式每筆交易設置了限制:\n\n1. 使用的付款方法的預估退款風險水平\n2. 您的付款方式的賬齡\n\n這個付款賬户還沒有被驗證,所以他每個交易最多購買{0}。在驗證之後,購買限制會以以下規則逐漸增加:\n\n●簽署前,以及簽署後30天內,您的每筆最大交易將限制為{0}\n●簽署後30天,每筆最大交易將限制為{1}\n●簽署後60天,每筆最大交易將限制為{2}\n\n出售限制不會被賬户驗證狀態限制,你可以理科進行單筆為{2}的交易\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Haveno Wiki 上查看更多:\nhttps://haveno.exchange/wiki/Account_limits +payment.limits.info.withSigning=為了降低這一風險,Haveno 基於兩個因素對該付款方式每筆交易設置了限制:\n\n1. 使用的付款方法的預估退款風險水平\n2. 您的付款方式的賬齡\n\n這個付款賬户還沒有被驗證,所以他每個交易最多購買{0}。在驗證之後,購買限制會以以下規則逐漸增加:\n\n●簽署前,以及簽署後30天內,您的每筆最大交易將限制為{0}\n●簽署後30天,每筆最大交易將限制為{1}\n●簽署後60天,每筆最大交易將限制為{2}\n\n出售限制不會被賬户驗證狀態限制,你可以理科進行單筆為{2}的交易\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Haveno Wiki 上查看更多:\nhttps://docs.haveno.exchange/the-project/account_limits payment.cashDeposit.info=請確認您的銀行允許您將現金存款匯入他人賬户。例如,美國銀行和富國銀行不再允許此類存款。 diff --git a/desktop/src/main/java/haveno/desktop/components/AccountStatusTooltipLabel.java b/desktop/src/main/java/haveno/desktop/components/AccountStatusTooltipLabel.java index 501efe9a0d..8ae0b607f0 100644 --- a/desktop/src/main/java/haveno/desktop/components/AccountStatusTooltipLabel.java +++ b/desktop/src/main/java/haveno/desktop/components/AccountStatusTooltipLabel.java @@ -106,7 +106,7 @@ public class AccountStatusTooltipLabel extends AutoTooltipLabel { learnMoreLink.setWrapText(true); learnMoreLink.setPadding(new Insets(10, 10, 2, 10)); learnMoreLink.getStyleClass().addAll("very-small-text"); - learnMoreLink.setOnAction((e) -> GUIUtil.openWebPage("https://haveno.exchange/wiki/Account_limits")); + learnMoreLink.setOnAction((e) -> GUIUtil.openWebPage("https://docs.haveno.exchange/the-project/account_limits")); VBox vBox = new VBox(2, titleLabel, infoLabel, buyLabel, waitLabel, learnMoreLink); vBox.setPadding(new Insets(2, 0, 2, 0)); From e05ab6f7ed06196377ced113e25b99e0b2d57b76 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 30 Nov 2024 09:26:31 -0500 Subject: [PATCH 021/371] fix links from offer book chart to buy/sell views --- .../offerbook/OfferBookChartViewModel.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java index 59c9008868..c7d0c91277 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java @@ -212,7 +212,10 @@ class OfferBookChartViewModel extends ActivatableViewModel { } public boolean isSellOffer(OfferDirection direction) { - return direction == OfferDirection.SELL; + // for cryptocurrency, buy direction is to buy XMR, so we need sell offers + // for traditional currency, buy direction is to sell XMR, so we need buy offers + boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(getCurrencyCode()); + return isCryptoCurrency ? direction == OfferDirection.BUY : direction == OfferDirection.SELL; } public boolean isMyOffer(Offer offer) { @@ -423,12 +426,20 @@ class OfferBookChartViewModel extends ActivatableViewModel { private void updateScreenCurrencyInPreferences(OfferDirection direction) { if (isSellOffer(direction)) { - if (CurrencyUtil.isTraditionalCurrency(getCurrencyCode())) { + if (CurrencyUtil.isFiatCurrency(getCurrencyCode())) { preferences.setBuyScreenCurrencyCode(getCurrencyCode()); + } else if (CurrencyUtil.isCryptoCurrency(getCurrencyCode())) { + preferences.setBuyScreenCryptoCurrencyCode(getCurrencyCode()); + } else if (CurrencyUtil.isTraditionalCurrency(getCurrencyCode())) { + preferences.setBuyScreenOtherCurrencyCode(getCurrencyCode()); } } else { - if (CurrencyUtil.isTraditionalCurrency(getCurrencyCode())) { + if (CurrencyUtil.isFiatCurrency(getCurrencyCode())) { preferences.setSellScreenCurrencyCode(getCurrencyCode()); + } else if (CurrencyUtil.isCryptoCurrency(getCurrencyCode())) { + preferences.setSellScreenCryptoCurrencyCode(getCurrencyCode()); + } else if (CurrencyUtil.isTraditionalCurrency(getCurrencyCode())) { + preferences.setSellScreenOtherCurrencyCode(getCurrencyCode()); } } } From 71987400c7c7784c11348fbf9357fec7e1127604 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 26 Nov 2024 13:40:11 -0500 Subject: [PATCH 022/371] make or take offer applies wallet funds and computes remaining amount --- .../resources/i18n/displayStrings.properties | 2 +- .../i18n/displayStrings_cs.properties | 2 +- .../i18n/displayStrings_de.properties | 2 +- .../i18n/displayStrings_es.properties | 2 +- .../i18n/displayStrings_fa.properties | 2 +- .../i18n/displayStrings_fr.properties | 2 +- .../i18n/displayStrings_it.properties | 2 +- .../i18n/displayStrings_ja.properties | 2 +- .../i18n/displayStrings_pt-br.properties | 2 +- .../i18n/displayStrings_pt.properties | 2 +- .../i18n/displayStrings_ru.properties | 2 +- .../i18n/displayStrings_th.properties | 2 +- .../i18n/displayStrings_tr.properties | 2 +- .../i18n/displayStrings_vi.properties | 2 +- .../i18n/displayStrings_zh-hans.properties | 2 +- .../i18n/displayStrings_zh-hant.properties | 2 +- .../main/offer/MutableOfferDataModel.java | 31 +++++++++++++------ .../desktop/main/offer/OfferDataModel.java | 26 ++-------------- .../offer/takeoffer/TakeOfferDataModel.java | 28 +++++++++++------ .../main/offer/takeoffer/TakeOfferView.java | 19 +++++++++++- 20 files changed, 77 insertions(+), 59 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index c4becc9357..9d5de331af 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -107,7 +107,7 @@ shared.chooseTradingAccount=Choose trading account shared.faq=Visit FAQ page shared.yesCancel=Yes, cancel shared.nextStep=Next step -shared.fundFromSavingsWalletButton=Transfer funds from Haveno wallet +shared.fundFromSavingsWalletButton=Apply funds from Haveno wallet shared.fundFromExternalWalletButton=Open your external wallet for funding shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Below % from market price diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index cb8af0709f..72fe292526 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -103,7 +103,7 @@ shared.faq=Navštívit stránku FAQ shared.yesCancel=Ano, zrušit shared.nextStep=Další krok shared.selectTradingAccount=Vyberte obchodní účet -shared.fundFromSavingsWalletButton=Přesunout finance z Haveno peněženky +shared.fundFromSavingsWalletButton=Použít prostředky z peněženky Haveno shared.fundFromExternalWalletButton=Otevřít vaši externí peněženku pro financování shared.openDefaultWalletFailed=Nepodařilo se otevřít aplikaci moneroové peněženky. Jste si jisti, že máte nějakou nainstalovanou? shared.belowInPercent=% pod tržní cenou diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index b920ec9c48..7a7a5643af 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -103,7 +103,7 @@ shared.faq=Zur FAQ Seite shared.yesCancel=Ja, abbrechen shared.nextStep=Nächster Schritt shared.selectTradingAccount=Handelskonto auswählen -shared.fundFromSavingsWalletButton=Gelder aus Haveno-Wallet überweisen +shared.fundFromSavingsWalletButton=Wenden Sie Gelder aus der Haveno-Wallet an shared.fundFromExternalWalletButton=Ihre externe Wallet zum Finanzieren öffnen shared.openDefaultWalletFailed=Das Öffnen des Standardprogramms für Monero-Wallets ist fehlgeschlagen. Sind Sie sicher, dass Sie eines installiert haben? shared.belowInPercent=% unter dem Marktpreis diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 9fd52c5773..2ff762f847 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -103,7 +103,7 @@ shared.faq=Visitar web preguntas frecuentes shared.yesCancel=Sí, cancelar shared.nextStep=Siguiente paso shared.selectTradingAccount=Selecionar cuenta de intercambio -shared.fundFromSavingsWalletButton=Transferir fondos desde la cartera Haveno +shared.fundFromSavingsWalletButton=Aplicar fondos desde la billetera de Haveno shared.fundFromExternalWalletButton=Abrir su monedero externo para agregar fondos shared.openDefaultWalletFailed=Fallo al abrir la aplicación de cartera predeterminada. ¿Tal vez no tenga una instalada? shared.belowInPercent=% por debajo del precio de mercado diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 539c3287ab..ed78e6ec4f 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -103,7 +103,7 @@ shared.faq=Visit FAQ page shared.yesCancel=بله، لغو شود shared.nextStep=گام بعدی shared.selectTradingAccount=حساب معاملات را انتخاب کنید -shared.fundFromSavingsWalletButton=انتقال وجه از کیف Haveno +shared.fundFromSavingsWalletButton=اعمال وجه از کیف پول هاونو shared.fundFromExternalWalletButton=برای تهیه پول، کیف پول بیرونی خود را باز کنید shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent= ٪ زیر قیمت بازار diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index cacd919ca6..252b31466e 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -103,7 +103,7 @@ shared.faq=Visitez la page FAQ shared.yesCancel=Oui, annuler shared.nextStep=Étape suivante shared.selectTradingAccount=Sélectionner le compte de trading -shared.fundFromSavingsWalletButton=Transférer des fonds depuis le portefeuille Haveno +shared.fundFromSavingsWalletButton=Appliquer les fonds depuis le portefeuille Haveno shared.fundFromExternalWalletButton=Ouvrez votre portefeuille externe pour provisionner shared.openDefaultWalletFailed=L'ouverture de l'application de portefeuille Monero par défaut a échoué. Êtes-vous sûr de l'avoir installée? shared.belowInPercent=% sous le prix du marché diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 2bb5a9bb91..b2f67e19cd 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -103,7 +103,7 @@ shared.faq=Visit FAQ page shared.yesCancel=Si, annulla shared.nextStep=Passo successivo shared.selectTradingAccount=Seleziona conto di trading -shared.fundFromSavingsWalletButton=Trasferisci fondi dal portafoglio Haveno +shared.fundFromSavingsWalletButton=Applica fondi dal portafoglio Haveno shared.fundFromExternalWalletButton=Apri il tuo portafoglio esterno per aggiungere fondi shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Sotto % del prezzo di mercato diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index a6a77ca657..478e11353b 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -103,7 +103,7 @@ shared.faq=FAQを参照する shared.yesCancel=はい、取り消します shared.nextStep=次へ shared.selectTradingAccount=取引アカウントを選択 -shared.fundFromSavingsWalletButton=Havenoウォレットから資金を移動する +shared.fundFromSavingsWalletButton=Havenoウォレットから資金を適用 shared.fundFromExternalWalletButton=外部のwalletを開く shared.openDefaultWalletFailed=ビットコインウォレットのアプリを開けませんでした。インストールされているか確認して下さい。 shared.belowInPercent=市場価格から%以下 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 2960b16ca0..8fa3d80a71 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -103,7 +103,7 @@ shared.faq=Visit FAQ page shared.yesCancel=Sim, cancelar shared.nextStep=Próximo passo shared.selectTradingAccount=Selecionar conta de negociação -shared.fundFromSavingsWalletButton=Transferir fundos da carteira Haveno +shared.fundFromSavingsWalletButton=Aplicar fundos da carteira Haveno shared.fundFromExternalWalletButton=Abrir sua carteira externa para prover fundos shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=% abaixo do preço de mercado diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 0f20382ba4..27f94191e5 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -103,7 +103,7 @@ shared.faq=Visit FAQ page shared.yesCancel=Sim, cancelar shared.nextStep=Próximo passo shared.selectTradingAccount=Selecionar conta de negociação -shared.fundFromSavingsWalletButton=Transferir fundos da carteira Haveno +shared.fundFromSavingsWalletButton=Aplicar fundos da carteira Haveno shared.fundFromExternalWalletButton=Abrir sua carteira externa para o financiamento shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Abaixo % do preço de mercado diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 864a75b73e..6f1ba86d90 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -103,7 +103,7 @@ shared.faq=Visit FAQ page shared.yesCancel=Да, отменить shared.nextStep=Далее shared.selectTradingAccount=Выбрать торговый счёт -shared.fundFromSavingsWalletButton=Перевести средства с кошелька Haveno +shared.fundFromSavingsWalletButton=Применить средства из кошелька Haveno shared.fundFromExternalWalletButton=Открыть внешний кошелёк для пополнения shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=% ниже рыночного курса diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 214d5d9e39..b603454ce5 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -103,7 +103,7 @@ shared.faq=Visit FAQ page shared.yesCancel=ใช่ ยกเลิก shared.nextStep=ขั้นถัดไป shared.selectTradingAccount=เลือกบัญชีการซื้อขาย -shared.fundFromSavingsWalletButton=โอนเงินจาก Haveno wallet +shared.fundFromSavingsWalletButton=ใช้เงินจากกระเป๋าเงิน Haveno shared.fundFromExternalWalletButton=เริ่มทำการระดมเงินทุนหาแหล่งเงินจากกระเป๋าสตางค์ภายนอกของคุณ shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=ต่ำกว่า % จากราคาตลาด diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index cff8e62c70..7656d581da 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -107,7 +107,7 @@ shared.chooseTradingAccount=İşlem hesabını seç shared.faq=SSS sayfasını ziyaret et shared.yesCancel=Evet, iptal et shared.nextStep=Sonraki adım -shared.fundFromSavingsWalletButton=Haveno cüzdanından fon transfer et +shared.fundFromSavingsWalletButton=Haveno cüzdanından fonları uygula shared.fundFromExternalWalletButton=Fonlama için harici cüzdanını aç shared.openDefaultWalletFailed=Bir Monero cüzdan uygulaması açılamadı. Yüklü olduğundan emin misiniz? shared.belowInPercent=Piyasa fiyatının altında % diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 03a23db4fa..8530cf4691 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -103,7 +103,7 @@ shared.faq=Visit FAQ page shared.yesCancel=Có, hủy shared.nextStep=Bước tiếp theo shared.selectTradingAccount=Chọn tài khoản giao dịch -shared.fundFromSavingsWalletButton=Chuyển tiền từ Ví Haveno +shared.fundFromSavingsWalletButton=Áp dụng tiền từ ví Haveno shared.fundFromExternalWalletButton=Mở ví ngoài để nộp tiền shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Thấp hơn % so với giá thị trường diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 9993a36b81..91166e64df 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -103,7 +103,7 @@ shared.faq=访问 FAQ 页面 shared.yesCancel=是的,取消 shared.nextStep=下一步 shared.selectTradingAccount=选择交易账户 -shared.fundFromSavingsWalletButton=从 Haveno 钱包资金划转 +shared.fundFromSavingsWalletButton=从 Haveno 钱包申请资金 shared.fundFromExternalWalletButton=从您的外部钱包充值 shared.openDefaultWalletFailed=打开默认的比特币钱包应用程序失败了。您确定您安装了吗? shared.belowInPercent=低于市场价格 % diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index a41239132a..37d5bfbbd9 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -103,7 +103,7 @@ shared.faq=訪問 FAQ 頁面 shared.yesCancel=是的,取消 shared.nextStep=下一步 shared.selectTradingAccount=選擇交易賬户 -shared.fundFromSavingsWalletButton=從 Haveno 錢包資金劃轉 +shared.fundFromSavingsWalletButton=從 Haveno 錢包申請資金 shared.fundFromExternalWalletButton=從您的外部錢包充值 shared.openDefaultWalletFailed=打開默認的比特幣錢包應用程序失敗了。您確定您安裝了嗎? shared.belowInPercent=低於市場價格 % diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index 10ecb181b5..325e7f2930 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -20,6 +20,8 @@ package haveno.desktop.main.offer; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.name.Named; + +import haveno.common.UserThread; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.util.MathUtils; import haveno.common.util.Utilities; @@ -176,7 +178,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { if (isTabSelected) priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); - updateBalance(); + updateBalances(); } @Override @@ -205,7 +207,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { xmrBalanceListener = new XmrBalanceListener(getAddressEntry().getSubaddressIndex()) { @Override public void onBalanceChanged(BigInteger balance) { - updateBalance(); + updateBalances(); } }; @@ -246,7 +248,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { calculateVolume(); calculateTotalToPay(); - updateBalance(); + updateBalances(); setSuggestedSecurityDeposit(getPaymentAccount()); return true; @@ -273,6 +275,19 @@ public abstract class MutableOfferDataModel extends OfferDataModel { priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); } + protected void updateBalances() { + super.updateBalances(); + + // update remaining balance + UserThread.await(() -> { + missingCoin.set(offerUtil.getBalanceShortage(totalToPay.get(), balance.get())); + isXmrWalletFunded.set(offerUtil.isBalanceSufficient(totalToPay.get(), balance.get())); + if (totalToPay.get() != null && isXmrWalletFunded.get() && !showWalletFundedNotification.get()) { + showWalletFundedNotification.set(true); + } + }); + } + /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// @@ -393,11 +408,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { void fundFromSavingsWallet() { this.useSavingsWallet = true; - updateBalance(); - if (!isXmrWalletFunded.get()) { - this.useSavingsWallet = false; - updateBalance(); - } + updateBalances(); } protected void setMarketPriceMarginPct(double marketPriceMargin) { @@ -492,7 +503,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } } - updateBalance(); + updateBalances(); } void calculateMinVolume() { @@ -545,7 +556,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { BigInteger feeAndSecDeposit = getSecurityDeposit().add(makerFee); BigInteger total = isBuyOffer() ? feeAndSecDeposit : feeAndSecDeposit.add(amount.get()); totalToPay.set(total); - updateBalance(); + updateBalances(); } } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java index 533f2834e4..8b92477e2e 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java @@ -65,29 +65,7 @@ public abstract class OfferDataModel extends ActivatableDataModel { this.offerUtil = offerUtil; } - protected void updateBalance() { - updateBalances(); - UserThread.await(() -> { - missingCoin.set(offerUtil.getBalanceShortage(totalToPay.get(), balance.get())); - isXmrWalletFunded.set(offerUtil.isBalanceSufficient(totalToPay.get(), balance.get())); - if (totalToPay.get() != null && isXmrWalletFunded.get() && !showWalletFundedNotification.get()) { - showWalletFundedNotification.set(true); - } - }); - } - - protected void updateAvailableBalance() { - updateBalances(); - UserThread.await(() -> { - missingCoin.set(offerUtil.getBalanceShortage(totalToPay.get(), availableBalance.get())); - isXmrWalletFunded.set(offerUtil.isBalanceSufficient(totalToPay.get(), availableBalance.get())); - if (totalToPay.get() != null && isXmrWalletFunded.get() && !showWalletFundedNotification.get()) { - showWalletFundedNotification.set(true); - } - }); - } - - private void updateBalances() { + protected void updateBalances() { BigInteger tradeWalletBalance = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); BigInteger tradeWalletAvailableBalance = xmrWalletService.getAvailableBalanceForSubaddress(addressEntry.getSubaddressIndex()); BigInteger walletBalance = xmrWalletService.getBalance(); @@ -101,6 +79,8 @@ public abstract class OfferDataModel extends ActivatableDataModel { availableBalance.set(totalToPay.get().min(totalAvailableBalance)); } } else { + totalBalance = tradeWalletBalance; + totalAvailableBalance = tradeWalletAvailableBalance; balance.set(tradeWalletBalance); availableBalance.set(tradeWalletAvailableBalance); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 84210538a9..beae7c11e3 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -20,6 +20,7 @@ package haveno.desktop.main.offer.takeoffer; import com.google.inject.Inject; import haveno.common.ThreadUtils; +import haveno.common.UserThread; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; @@ -131,7 +132,7 @@ class TakeOfferDataModel extends OfferDataModel { addListeners(); - updateAvailableBalance(); + updateBalances(); // TODO In case that we have funded but restarted, or canceled but took again the offer we would need to // store locally the result when we received the funding tx(s). @@ -192,7 +193,7 @@ class TakeOfferDataModel extends OfferDataModel { balanceListener = new XmrBalanceListener(addressEntry.getSubaddressIndex()) { @Override public void onBalanceChanged(BigInteger balance) { - updateAvailableBalance(); + updateBalances(); } }; @@ -227,6 +228,19 @@ class TakeOfferDataModel extends OfferDataModel { ThreadUtils.submitToPool(() -> xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId())); } + protected void updateBalances() { + super.updateBalances(); + + // update remaining balance + UserThread.await(() -> { + missingCoin.set(offerUtil.getBalanceShortage(totalToPay.get(), balance.get())); + isXmrWalletFunded.set(offerUtil.isBalanceSufficient(totalToPay.get(), availableBalance.get())); + if (totalToPay.get() != null && isXmrWalletFunded.get() && !showWalletFundedNotification.get()) { + showWalletFundedNotification.set(true); + } + }); + } + /////////////////////////////////////////////////////////////////////////////////////////// // UI actions @@ -287,11 +301,7 @@ class TakeOfferDataModel extends OfferDataModel { void fundFromSavingsWallet() { useSavingsWallet = true; - updateAvailableBalance(); - if (!isXmrWalletFunded.get()) { - this.useSavingsWallet = false; - updateAvailableBalance(); - } + updateBalances(); } @@ -369,7 +379,7 @@ class TakeOfferDataModel extends OfferDataModel { volume.set(volumeByAmount); - updateAvailableBalance(); + updateBalances(); } } @@ -391,7 +401,7 @@ class TakeOfferDataModel extends OfferDataModel { totalToPay.set(feeAndSecDeposit.add(amount.get())); else totalToPay.set(feeAndSecDeposit); - updateAvailableBalance(); + updateBalances(); log.debug("totalToPay {}", totalToPay.get()); } } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index 6f4e9635c1..6f9991331d 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -154,6 +154,7 @@ public class TakeOfferView extends ActivatableViewAndModel missingCoinListener; private int gridRow = 0; private final HashMap paymentAccountWarningDisplayed = new HashMap<>(); @@ -191,6 +192,8 @@ public class TakeOfferView extends ActivatableViewAndModel { + if (!newValue.toString().equals("")) { + updateQrCode(); + } + }; + } + private void addListeners() { amountTextField.focusedProperty().addListener(amountFocusedListener); model.dataModel.getShowWalletFundedNotification().addListener(getShowWalletFundedNotificationListener); + model.dataModel.getMissingCoin().addListener(missingCoinListener); } private void removeListeners() { amountTextField.focusedProperty().removeListener(amountFocusedListener); model.dataModel.getShowWalletFundedNotification().removeListener(getShowWalletFundedNotificationListener); + model.dataModel.getMissingCoin().removeListener(missingCoinListener); } /////////////////////////////////////////////////////////////////////////////////////////// From 1aef8a6babf7ce5207790d035a54dbffcc8fd4e9 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 30 Nov 2024 10:06:42 -0500 Subject: [PATCH 023/371] fix deadlock on startup while awaiting monero connection --- core/src/main/java/haveno/core/api/XmrConnectionService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 96458a27ac..3f50f4aa45 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -142,7 +142,7 @@ public final class XmrConnectionService { p2PService.addP2PServiceListener(new P2PServiceListener() { @Override public void onTorNodeReady() { - initialize(); + ThreadUtils.submitToPool(() -> initialize()); } @Override public void onHiddenServicePublished() {} From 7f6d28f1fbb5033f85ac35c68d0f5c1f8d4d71de Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 30 Nov 2024 13:17:05 -0500 Subject: [PATCH 024/371] prompt to fall back on startup error with custom node --- .../haveno/core/api/XmrConnectionService.java | 52 +++++++++++++------ .../haveno/core/app/HavenoHeadlessApp.java | 1 + .../java/haveno/core/app/HavenoSetup.java | 13 +++++ .../java/haveno/core/app/WalletAppSetup.java | 2 +- .../core/xmr/wallet/XmrWalletService.java | 7 +-- .../resources/i18n/displayStrings.properties | 3 ++ .../java/haveno/desktop/main/MainView.java | 1 + .../haveno/desktop/main/MainViewModel.java | 30 +++++++++++ 8 files changed, 89 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 3f50f4aa45..664daaca8a 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -43,6 +43,7 @@ import java.util.Set; import org.apache.commons.lang3.exception.ExceptionUtils; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.ObjectProperty; @@ -50,6 +51,7 @@ import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleLongProperty; import javafx.beans.property.SimpleObjectProperty; @@ -89,6 +91,8 @@ public final class XmrConnectionService { private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter + private final BooleanProperty connectionServiceFallbackHandlerActive = new SimpleBooleanProperty(); + @Getter private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final LongProperty numUpdates = new SimpleLongProperty(0); private Socks5ProxyProvider socks5ProxyProvider; @@ -99,6 +103,7 @@ public final class XmrConnectionService { private Boolean isConnected = false; @Getter private MoneroDaemonInfo lastInfo; + private Long lastFallbackInvocation; private Long lastLogPollErrorTimestamp; private long lastLogDaemonNotSyncedTimestamp; private Long syncStartHeight; @@ -115,6 +120,8 @@ public final class XmrConnectionService { private int numRequestsLastMinute; private long lastSwitchTimestamp; private Set excludedConnections = new HashSet<>(); + private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 60 * 1; // offer to fallback up to once every minute + private boolean fallbackApplied; @Inject public XmrConnectionService(P2PService p2PService, @@ -424,6 +431,19 @@ public final class XmrConnectionService { return numUpdates; } + public void fallbackToBestConnection() { + if (isShutDownStarted) return; + if (xmrNodes.getProvidedXmrNodes().isEmpty()) { + log.warn("Falling back to public nodes"); + preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); + } else { + log.warn("Falling back to provided nodes"); + preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); + } + fallbackApplied = true; + initializeConnections(); + } + // ------------------------------- HELPERS -------------------------------- private void doneDownload() { @@ -533,7 +553,7 @@ public final class XmrConnectionService { } // restore connections - if ("".equals(config.xmrNode)) { + if (!isFixedConnection()) { // load previous or default connections if (coreContext.isApiUser()) { @@ -569,10 +589,7 @@ public final class XmrConnectionService { } // restore last connection - if (isFixedConnection()) { - if (getConnections().size() != 1) throw new IllegalStateException("Expected connection list to have 1 fixed connection but was: " + getConnections().size()); - connectionManager.setConnection(getConnections().get(0)); - } else if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) { + if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) { if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get())) { connectionManager.setConnection(connectionList.getCurrentConnectionUri().get()); } @@ -592,7 +609,7 @@ public final class XmrConnectionService { maybeStartLocalNode(); // update connection - if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) { + if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) { MoneroRpcConnection bestConnection = getBestAvailableConnection(); if (bestConnection != null) setConnection(bestConnection); } @@ -614,6 +631,7 @@ public final class XmrConnectionService { } // notify initial connection + lastRefreshPeriodMs = getRefreshPeriodMs(); onConnectionChanged(connectionManager.getConnection()); } @@ -716,16 +734,14 @@ public final class XmrConnectionService { // skip handling if shutting down if (isShutDownStarted) return; - // fallback to provided or public nodes if custom connection fails on startup - if (lastInfo == null && "".equals(config.xmrNode) && preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM) { - if (xmrNodes.getProvidedXmrNodes().isEmpty()) { - log.warn("Failed to fetch daemon info from custom node on startup, falling back to public nodes: " + e.getMessage()); - preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); - } else { - log.warn("Failed to fetch daemon info from custom node on startup, falling back to provided nodes: " + e.getMessage()); - preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); + // invoke fallback handling on startup error + boolean canFallback = isFixedConnection() || isCustomConnections(); + if (lastInfo == null && canFallback) { + if (!connectionServiceFallbackHandlerActive.get() && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { + log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); + lastFallbackInvocation = System.currentTimeMillis(); + connectionServiceFallbackHandlerActive.set(true); } - initializeConnections(); return; } @@ -819,6 +835,10 @@ public final class XmrConnectionService { } private boolean isFixedConnection() { - return !"".equals(config.xmrNode) || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; + return !"".equals(config.xmrNode) && !fallbackApplied; + } + + private boolean isCustomConnections() { + return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; } } diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java index 7235efce7b..0cf18224ba 100644 --- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java @@ -75,6 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp { log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); acceptedHandler.run(); }); + havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.info("onDisplayMoneroConnectionFallbackHandler: show={}", show)); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index d80ca807cc..a291da5001 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -158,6 +158,9 @@ public class HavenoSetup { rejectedTxErrorMessageHandler; @Setter @Nullable + private Consumer displayMoneroConnectionFallbackHandler; + @Setter + @Nullable private Consumer displayTorNetworkSettingsHandler; @Setter @Nullable @@ -426,6 +429,12 @@ public class HavenoSetup { getXmrDaemonSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); + // listen for fallback handling + getConnectionServiceFallbackHandlerActive().addListener((observable, oldValue, newValue) -> { + if (displayMoneroConnectionFallbackHandler == null) return; + displayMoneroConnectionFallbackHandler.accept(newValue); + }); + log.info("Init P2P network"); havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork); p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler); @@ -725,6 +734,10 @@ public class HavenoSetup { return xmrConnectionService.getConnectionServiceErrorMsg(); } + public BooleanProperty getConnectionServiceFallbackHandlerActive() { + return xmrConnectionService.getConnectionServiceFallbackHandlerActive(); + } + public StringProperty getTopErrorMsg() { return topErrorMsg; } diff --git a/core/src/main/java/haveno/core/app/WalletAppSetup.java b/core/src/main/java/haveno/core/app/WalletAppSetup.java index 1f7946eac7..d17c7ba366 100644 --- a/core/src/main/java/haveno/core/app/WalletAppSetup.java +++ b/core/src/main/java/haveno/core/app/WalletAppSetup.java @@ -120,7 +120,7 @@ public class WalletAppSetup { @Nullable Runnable showPopupIfInvalidBtcConfigHandler, Runnable downloadCompleteHandler, Runnable walletInitializedHandler) { - log.info("Initialize WalletAppSetup with monero-java version {}", MoneroUtils.getVersion()); + log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion()); ObjectProperty walletServiceException = new SimpleObjectProperty<>(); xmrInfoBinding = EasyBind.combine( diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 759dabd086..79e9248c64 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1298,9 +1298,10 @@ public class XmrWalletService extends XmrWalletBase { } else { // force restart main wallet if connection changed while syncing - log.warn("Force restarting main wallet because connection changed while syncing"); - forceRestartMainWallet(); - return; + if (wallet != null) { + log.warn("Force restarting main wallet because connection changed while syncing"); + forceRestartMainWallet(); + } } }); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 9d5de331af..c1f98ce420 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2044,6 +2044,9 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=Enter password to unlock +connectionFallback.headline=Connection error +connectionFallback.msg=Error connecting to your custom Monero node(s).\n\nDo you want to try the next best available Monero node? + torNetworkSettingWindow.header=Tor networks settings torNetworkSettingWindow.noBridges=Don't use bridges torNetworkSettingWindow.providedBridges=Connect with provided bridges diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index 7290f76810..9bf5f378e7 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -674,6 +674,7 @@ public class MainView extends InitializableView { } } else { xmrInfoLabel.setId("footer-pane"); + xmrInfoLabel.getStyleClass().remove("error-text"); if (xmrNetworkWarnMsgPopup != null) xmrNetworkWarnMsgPopup.hide(); } diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index 4681431b70..f120b794e7 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -140,6 +140,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener @SuppressWarnings("FieldCanBeLocal") private MonadicBinding tradesAndUIReady; private final Queue> popupQueue = new PriorityQueue<>(Comparator.comparing(Overlay::getDisplayOrderPriority)); + private Popup moneroConnectionFallbackPopup; /////////////////////////////////////////////////////////////////////////////////////////// @@ -334,9 +335,38 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener tacWindow.onAction(acceptedHandler::run).show(); }, 1)); + havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> { + if (moneroConnectionFallbackPopup == null) { + moneroConnectionFallbackPopup = new Popup() + .headLine(Res.get("connectionFallback.headline")) + .warning(Res.get("connectionFallback.msg")) + .closeButtonText(Res.get("shared.no")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + havenoSetup.getConnectionServiceFallbackHandlerActive().set(false); + new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + }) + .onClose(() -> { + log.warn("User has declined to fallback to the next best available Monero node."); + havenoSetup.getConnectionServiceFallbackHandlerActive().set(false); + }); + } + if (show) { + moneroConnectionFallbackPopup.show(); + } else if (moneroConnectionFallbackPopup.isDisplayed()) { + moneroConnectionFallbackPopup.hide(); + } + }); + havenoSetup.setDisplayTorNetworkSettingsHandler(show -> { if (show) { torNetworkSettingsWindow.show(); + + // bring connection fallback popup to front if displayed + if (moneroConnectionFallbackPopup != null && moneroConnectionFallbackPopup.isDisplayed()) { + moneroConnectionFallbackPopup.hide(); + moneroConnectionFallbackPopup.show(); + } } else if (torNetworkSettingsWindow.isDisplayed()) { torNetworkSettingsWindow.hide(); } From b586bc57f67ec3d2d118b7e0a9116bd2b6ab6ab5 Mon Sep 17 00:00:00 2001 From: slrslr <6596726+slrslr@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:30:19 +0100 Subject: [PATCH 025/371] Fixing some words displayStrings_cs.properties (#1454) --- .../i18n/displayStrings_cs.properties | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 72fe292526..f1c8fc6a31 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -105,7 +105,7 @@ shared.nextStep=Další krok shared.selectTradingAccount=Vyberte obchodní účet shared.fundFromSavingsWalletButton=Použít prostředky z peněženky Haveno shared.fundFromExternalWalletButton=Otevřít vaši externí peněženku pro financování -shared.openDefaultWalletFailed=Nepodařilo se otevřít aplikaci moneroové peněženky. Jste si jisti, že máte nějakou nainstalovanou? +shared.openDefaultWalletFailed=Nepodařilo se otevřít aplikaci peněženky Monero. Jste si jisti, že máte nějakou nainstalovanou? shared.belowInPercent=% pod tržní cenou shared.aboveInPercent=% nad tržní cenou shared.enterPercentageValue=Zadejte % hodnotu @@ -124,7 +124,7 @@ shared.noDetailsAvailable=Detaily nejsou k dispozici shared.notUsedYet=Ještě nepoužito shared.date=Datum # suppress inspection "TrailingSpacesInProperty" -shared.sendFundsDetailsDust=Haveno zjistil, že tato transakce by vytvořila drobné mince, které jsou pod limitem drobných mincí (a není to povoleno pravidly pro moneroový konsenzus). Místo toho budou tyto drobné mince ({0} satoshi {1}) přidány k poplatku za těžbu.\n\n\n +shared.sendFundsDetailsDust=Haveno zjistil, že tato transakce by vytvořila drobné mince, které jsou pod limitem drobných mincí (a není to povoleno pravidly pro konsenzus Monero). Místo toho budou tyto drobné mince ({0} satoshi {1}) přidány k poplatku za těžbu.\n\n\n shared.copyToClipboard=Kopírovat do schránky shared.language=Jazyk shared.country=Země @@ -258,19 +258,19 @@ mainView.footer.xmrInfo.synchronizingWalletWith=Synchronizace peněženky s {0} mainView.footer.xmrInfo.syncedWith=Synchronizováno s {0} na bloku {1} mainView.footer.xmrInfo.connectingTo=Připojování mainView.footer.xmrInfo.connectionFailed=Připojení se nezdařilo -mainView.footer.xmrPeers=Monero síťové nody: {0} -mainView.footer.p2pPeers=Haveno síťové nody: {0} +mainView.footer.xmrPeers=Monero síťové uzly: {0} +mainView.footer.p2pPeers=Haveno síťové uzly: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Připojování do sítě Tor... -mainView.bootstrapState.torNodeCreated=(2/4) Tor node vytvořen +mainView.bootstrapState.torNodeCreated=(2/4) Tor uzel vytvořen mainView.bootstrapState.hiddenServicePublished=(3/4) Skrytá služba publikována mainView.bootstrapState.initialDataReceived=(4/4) Iniciační data přijata -mainView.bootstrapWarning.noSeedNodesAvailable=Žádné seed nody nejsou k dispozici -mainView.bootstrapWarning.noNodesAvailable=Žádné seed ani peer nody k dispozici +mainView.bootstrapWarning.noSeedNodesAvailable=Žádné seed uzly nejsou k dispozici +mainView.bootstrapWarning.noNodesAvailable=Žádné seed ani peer uzly k dispozici mainView.bootstrapWarning.bootstrappingToP2PFailed=Zavádění do sítě Haveno se nezdařilo -mainView.p2pNetworkWarnMsg.noNodesAvailable=Pro vyžádání dat nejsou k dispozici žádné seed ani peer nody.\nZkontrolujte připojení k internetu nebo zkuste aplikaci restartovat. +mainView.p2pNetworkWarnMsg.noNodesAvailable=Pro vyžádání dat nejsou k dispozici žádné seed ani peer uzly.\nZkontrolujte připojení k internetu nebo zkuste aplikaci restartovat. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Připojení k síti Haveno selhalo (nahlášená chyba: {0}).\nZkontrolujte připojení k internetu nebo zkuste aplikaci restartovat. mainView.walletServiceErrorMsg.timeout=Připojení k síti Monero selhalo kvůli vypršení časového limitu. @@ -278,8 +278,8 @@ mainView.walletServiceErrorMsg.connectionError=Připojení k síti Monero selhal mainView.walletServiceErrorMsg.rejectedTxException=Transakce byla ze sítě zamítnuta.\n\n{0} -mainView.networkWarning.allConnectionsLost=Ztratili jste připojení ke všem {0} síťovým peer nodům.\nMožná jste ztratili připojení k internetu nebo byl váš počítač v pohotovostním režimu. -mainView.networkWarning.localhostMoneroLost=Ztratili jste připojení k Moneroovému localhost nodu.\nRestartujte aplikaci Haveno a připojte se k jiným Moneroovým nodům nebo restartujte Moneroový localhost node. +mainView.networkWarning.allConnectionsLost=Ztratili jste připojení ke všem {0} síťovým peer uzlům.\nMožná jste ztratili připojení k internetu nebo byl váš počítač v pohotovostním režimu. +mainView.networkWarning.localhostMoneroLost=Ztratili jste připojení k localhost uzlu Monero.\nRestartujte aplikaci Haveno a připojte se k jiným uzlům Monero nebo restartujte localhost Monero uzel. mainView.version.update=(Dostupná aktualizace) @@ -360,7 +360,7 @@ offerbook.timeSinceSigning.notSigned.noNeed=N/A shared.notSigned=Tento účet ještě nebyl podepsán a byl vytvořen před {0} dny shared.notSigned.noNeed=Tento typ účtu nevyžaduje podepisování shared.notSigned.noNeedDays=Tento typ účtu nevyžaduje podepisování a byl vytvořen před {0} dny -shared.notSigned.noNeedAlts=Cryptoové účty neprocházejí kontrolou podpisu a stáří +shared.notSigned.noNeedAlts=Kryptoměnové účty neprocházejí kontrolou podpisu a stáří offerbook.nrOffers=Počet nabídek: {0} offerbook.volume={0} (min - max) @@ -747,7 +747,7 @@ portfolio.pending.step5_buyer.alreadyWithdrawn=Vaše finanční prostředky již portfolio.pending.step5_buyer.confirmWithdrawal=Potvrďte žádost o výběr portfolio.pending.step5_buyer.amountTooLow=Částka k převodu je nižší než transakční poplatek a min. možná hodnota tx (drobné). portfolio.pending.step5_buyer.withdrawalCompleted.headline=Výběr byl dokončen -portfolio.pending.step5_buyer.withdrawalCompleted.msg=Vaše dokončené obchody jsou uloženy na \"Portfolio/Historie\".\nVšechny své moneroové transakce si můžete prohlédnout v sekci \"Prostředky/Transakce\" +portfolio.pending.step5_buyer.withdrawalCompleted.msg=Vaše dokončené obchody jsou uloženy na \"Portfolio/Historie\".\nVšechny své transakce Monero si můžete prohlédnout v sekci \"Finance/Transakce\" portfolio.pending.step5_buyer.bought=Koupili jste portfolio.pending.step5_buyer.paid=Zaplatili jste @@ -1015,8 +1015,8 @@ setting.preferences.prefCurrency=Preferovaná měna setting.preferences.displayTraditional=Zobrazit národní měny setting.preferences.noTraditional=Nejsou vybrány žádné národní měny setting.preferences.cannotRemovePrefCurrency=Vybranou zobrazovanou měnu nelze odebrat. -setting.preferences.displayCryptos=Zobrazit cryptoy -setting.preferences.noCryptos=Nejsou vybrány žádné cryptoy +setting.preferences.displayCryptos=Zobrazit kryptoměny +setting.preferences.noCryptos=Nejsou vybrány žádné kryptoměny setting.preferences.addTraditional=Přidejte národní měnu setting.preferences.addCrypto=Přidejte crypto setting.preferences.displayOptions=Zobrazit možnosti @@ -1038,24 +1038,24 @@ settings.preferences.editCustomExplorer.name=Jméno settings.preferences.editCustomExplorer.txUrl=Transakční URL settings.preferences.editCustomExplorer.addressUrl=Adresa URL -settings.net.xmrHeader=Moneroová síť +settings.net.xmrHeader=Síť Monero settings.net.p2pHeader=Síť Haveno settings.net.onionAddressLabel=Moje onion adresa -settings.net.xmrNodesLabel=Použijte vlastní Monero node +settings.net.xmrNodesLabel=Použijte vlastní Monero uzel settings.net.moneroPeersLabel=Připojené peer uzly settings.net.connection=Připojení settings.net.connected=Připojeno settings.net.useTorForXmrJLabel=Použít Tor pro Monero síť -settings.net.moneroNodesLabel=Monero nody, pro připojení -settings.net.useProvidedNodesRadio=Použijte nabízené Monero Core nody -settings.net.usePublicNodesRadio=Použít veřejnou Moneroovou síť -settings.net.useCustomNodesRadio=Použijte vlastní Monero Core node -settings.net.warn.usePublicNodes=Pokud používáte veřejné Monero nody, jste vystaveni riziku spojenému s používáním nedůvěryhodných vzdálených nodů.\n\nProsím, přečtěte si více podrobností na [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nJste si jistí, že chcete použít veřejné nody? -settings.net.warn.usePublicNodes.useProvided=Ne, použijte nabízené nody +settings.net.moneroNodesLabel=Monero uzly, pro připojení +settings.net.useProvidedNodesRadio=Použijte nabízené Monero Core uzly +settings.net.usePublicNodesRadio=Použít veřejnou síť Monero +settings.net.useCustomNodesRadio=Použijte vlastní Monero Core uzel +settings.net.warn.usePublicNodes=Pokud používáte veřejné Monero uzly, jste vystaveni riziku spojenému s používáním nedůvěryhodných vzdálených uzlů.\n\nProsím, přečtěte si více podrobností na [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nJste si jistí, že chcete použít veřejné uzly? +settings.net.warn.usePublicNodes.useProvided=Ne, použijte nabízené uzly settings.net.warn.usePublicNodes.usePublic=Ano, použít veřejnou síť -settings.net.warn.useCustomNodes.B2XWarning=Ujistěte se, že váš moneroový node je důvěryhodný Monero Core node!\n\nPřipojení k nodům, které nedodržují pravidla konsensu Monero Core, může poškodit vaši peněženku a způsobit problémy v obchodním procesu.\n\nUživatelé, kteří se připojují k nodům, které porušují pravidla konsensu, odpovídají za případné škody, které z toho vyplývají. Jakékoli výsledné spory budou rozhodnuty ve prospěch druhého obchodníka. Uživatelům, kteří ignorují tyto varovné a ochranné mechanismy, nebude poskytována technická podpora! -settings.net.warn.invalidXmrConfig=Připojení k moneroové síti selhalo, protože je vaše konfigurace neplatná.\n\nVaše konfigurace byla resetována, aby se místo toho použily poskytnuté moneroové uzly. Budete muset restartovat aplikaci. -settings.net.localhostXmrNodeInfo=Základní informace: Haveno při spuštění hledá místní Moneroový uzel. Pokud je nalezen, Haveno bude komunikovat se sítí Monero výhradně skrze něj. +settings.net.warn.useCustomNodes.B2XWarning=Ujistěte se, že váš Monero uzel je důvěryhodný Monero Core uzel!\n\nPřipojení k uzlům, které nedodržují pravidla konsensu Monero Core, může poškodit vaši peněženku a způsobit problémy v obchodním procesu.\n\nUživatelé, kteří se připojují k uzlům, které porušují pravidla konsensu, odpovídají za případné škody, které z toho vyplývají. Jakékoli výsledné spory budou rozhodnuty ve prospěch druhého obchodníka. Uživatelům, kteří ignorují tyto varovné a ochranné mechanismy, nebude poskytována technická podpora! +settings.net.warn.invalidXmrConfig=Připojení k síti Monero selhalo, protože je vaše konfigurace neplatná.\n\nVaše konfigurace byla resetována, aby byly místo toho použity poskytnuté uzly Monero. Budete muset restartovat aplikaci. +settings.net.localhostXmrNodeInfo=Základní informace: Haveno při spuštění hledá místní Monero uzel. Pokud je nalezen, Haveno bude komunikovat se sítí Monero výhradně skrze něj. settings.net.p2PPeersLabel=Připojené uzly settings.net.onionAddressColumn=Onion adresa settings.net.creationDateColumn=Založeno @@ -1079,7 +1079,7 @@ settings.net.sentData=Odeslaná data: {0}, {1} zprávy, {2} zprávy/sekundu settings.net.receivedData=Přijatá data: {0}, {1} zprávy, {2} zprávy/sekundu settings.net.chainHeight=Monero Peers: {0} settings.net.ips=[IP adresa:port | název hostitele:port | onion adresa:port] (oddělené čárkou). Pokud je použit výchozí port (8333), lze port vynechat. -settings.net.seedNode=Seed node +settings.net.seedNode=Seed uzel settings.net.directPeer=Peer uzel (přímý) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Peer @@ -1158,10 +1158,10 @@ account.tab.mediatorRegistration=Registrace mediátora account.tab.refundAgentRegistration=Registrace rozhodce pro vrácení peněz account.tab.signing=Podepisování account.info.headline=Vítejte ve vašem účtu Haveno -account.info.msg=Zde můžete přidat obchodní účty pro národní měny & cryptoy a vytvořit zálohu dat vaší peněženky a účtu.\n\nPři prvním spuštění Haveno byla vytvořena nová moneroová peněženka.\n\nDůrazně doporučujeme zapsat si seed slova moneroových peněženek (viz záložka nahoře) a před financováním zvážit přidání hesla. Vklady a výběry moneroů jsou spravovány v sekci \ "Finance \".\n\nOchrana osobních údajů a zabezpečení: protože Haveno je decentralizovaná směnárna, všechna data jsou uložena ve vašem počítači. Neexistují žádné servery, takže nemáme přístup k vašim osobním informacím, vašim finančním prostředkům ani vaší IP adrese. Údaje, jako jsou čísla bankovních účtů, adresy cryptoů a monerou atd., jsou sdíleny pouze s obchodním partnerem za účelem uskutečnění obchodů, které zahájíte (v případě sporu uvidí Prostředník nebo Rozhodce stejná data jako váš obchodní partner). +account.info.msg=Zde můžete přidat obchodní účty pro národní měny & kryptoměny a vytvořit zálohu dat vaší peněženky a účtu.\n\nPři prvním spuštění Haveno byla vytvořena nová peněženka Monero.\n\nDůrazně doporučujeme zapsat si seed slova peněženek (viz záložka nahoře) a před financováním zvážit přidání hesla. Vklady a výběry moneroů jsou spravovány v sekci \ "Finance \".\n\nOchrana osobních údajů a zabezpečení: protože Haveno je decentralizovaná směnárna, všechna data jsou uložena ve vašem počítači. Neexistují žádné servery, takže nemáme přístup k vašim osobním informacím, vašim finančním prostředkům ani vaší IP adrese. Údaje, jako jsou čísla bankovních účtů, adresy cryptoů a monerou atd., jsou sdíleny pouze s obchodním partnerem za účelem uskutečnění obchodů, které zahájíte (v případě sporu uvidí Prostředník nebo Rozhodce stejná data jako váš obchodní partner). account.menu.paymentAccount=Účty v národní měně -account.menu.altCoinsAccountView=Cryptoové účty +account.menu.altCoinsAccountView=Kryptoměnové účty account.menu.password=Heslo peněženky account.menu.seedWords=Seed peněženky account.menu.walletInfo=Info o peněžence @@ -1190,7 +1190,7 @@ account.arbitratorRegistration.removedFailed=Registraci se nepodařilo odebrat. account.arbitratorRegistration.registerSuccess=Úspěšně jste se zaregistrovali do sítě Haveno. account.arbitratorRegistration.registerFailed=Registraci se nepodařilo dokončit. {0} -account.crypto.yourCryptoAccounts=Vaše cryptoové účty +account.crypto.yourCryptoAccounts=Vaše kryptoměnové účty account.crypto.popup.wallet.msg=Ujistěte se, že dodržujete požadavky na používání peněženek {0}, jak je popsáno na webové stránce {1}.\nPoužití peněženek z centralizovaných směnáren, kde (a) nevlastníte své soukromé klíče nebo (b) které nepoužívají kompatibilní software peněženky, je riskantní: může to vést ke ztrátě obchodovaných prostředků!\nMediátor nebo rozhodce není specialista {2} a v takových případech nemůže pomoci. account.crypto.popup.wallet.confirm=Rozumím a potvrzuji, že vím, jakou peněženku musím použít. # suppress inspection "UnusedProperty" @@ -1244,7 +1244,7 @@ account.password.removePw.button=Odstraňte heslo account.password.removePw.headline=Odstraňte ochranu peněženky pomocí hesla account.password.setPw.button=Nastavit heslo account.password.setPw.headline=Nastavte ochranu peněženky pomocí hesla -account.password.info=S ochranou pomocí hesla budete muset zadat heslo při spuštění aplikace, při výběru monera z vaší peněženky a při zobrazení vašich slov z klíčového základu. +account.password.info=S ochranou pomocí hesla budete muset zadat heslo při spuštění aplikace, při výběru monera z vaší peněženky a při zobrazení slov seedu peněženky. account.seed.backup.title=Zálohujte svá klíčová slova peněženky. account.seed.info=Prosím, zapište si jak klíčová slova peněženky, tak datum. Kdykoliv můžete obnovit svou peněženku pomocí klíčových slov a data.\n\nKlíčová slova byste měli zapsat na kus papíru. Neukládejte je na počítač.\n\nVezměte prosím na vědomí, že klíčová slova NEJSOU náhradou za zálohu.\nMusíte vytvořit zálohu celého adresáře aplikace z obrazovky "Účet/Záloha", abyste mohli obnovit stav a data aplikace. @@ -1252,7 +1252,7 @@ account.seed.backup.warning=Prosím, poznamenejte si, že klíčová slova nejso account.seed.warn.noPw.msg=Nenastavili jste si heslo k peněžence, které by chránilo zobrazení seed slov.\n\nChcete zobrazit seed slova? account.seed.warn.noPw.yes=Ano, a už se mě znovu nezeptat account.seed.enterPw=Chcete-li zobrazit seed slova, zadejte heslo -account.seed.restore.info=Před použitím obnovení ze seed slov si vytvořte zálohu. Uvědomte si, že obnova peněženky je pouze pro naléhavé případy a může způsobit problémy s interní databází peněženky.\nNení to způsob, jak použít zálohu! K obnovení předchozího stavu aplikace použijte zálohu z adresáře dat aplikace.\n\nPo obnovení se aplikace automaticky vypne. Po restartování aplikace se bude znovu synchronizovat s moneroovou sítí. To může chvíli trvat a může spotřebovat hodně CPU, zejména pokud byla peněženka starší a měla mnoho transakcí. Vyhněte se přerušování tohoto procesu, jinak budete možná muset znovu odstranit soubor řetězu SPV nebo opakovat proces obnovy. +account.seed.restore.info=Před použitím obnovení ze seed slov si vytvořte zálohu. Uvědomte si, že obnova peněženky je pouze pro naléhavé případy a může způsobit problémy s interní databází peněženky.\nNení to způsob, jak použít zálohu! K obnovení předchozího stavu aplikace použijte zálohu z adresáře dat aplikace.\n\nPo obnovení se aplikace automaticky vypne. Po restartování aplikace se bude znovu synchronizovat se sítí Monero. To může chvíli trvat a může spotřebovat hodně CPU, zejména pokud byla peněženka starší a měla mnoho transakcí. Vyhněte se přerušování tohoto procesu, jinak budete možná muset znovu odstranit soubor řetězu SPV nebo opakovat proces obnovy. account.seed.restore.ok=Dobře, proveďte obnovu a vypněte Haveno @@ -1323,7 +1323,7 @@ inputControlWindow.balanceLabel=Dostupný zůstatek contractWindow.title=Podrobnosti o sporu contractWindow.dates=Datum nabídky / Datum obchodu -contractWindow.xmrAddresses=Moneroová adresa kupujícího XMR / prodávajícího XMR +contractWindow.xmrAddresses=Monero adresa kupujícího XMR / prodávajícího XMR contractWindow.onions=Síťová adresa kupující XMR / prodávající XMR contractWindow.accountAge=Stáří účtu XMR kupující / XMR prodejce contractWindow.numDisputes=Počet sporů XMR kupující / XMR prodejce @@ -1436,10 +1436,10 @@ filterWindow.bannedPrivilegedDevPubKeys=Filtrované privilegované klíče pub d filterWindow.arbitrators=Filtrovaní rozhodci (onion adresy oddělené čárkami) filterWindow.mediators=Filtrovaní mediátoři (onion adresy oddělené čárkami) filterWindow.refundAgents=Filtrovaní rozhodci pro vrácení peněz (onion adresy oddělené čárkami) -filterWindow.seedNode=Filtrované seed nody (onion adresy oddělené čárkami) -filterWindow.priceRelayNode=Filtrované cenové relay nody (onion adresy oddělené čárkami) -filterWindow.xmrNode=Filtrované Moneroové nody (adresy+porty oddělené čárkami) -filterWindow.preventPublicXmrNetwork=Zabraňte použití veřejné moneroové sítě +filterWindow.seedNode=Filtrované seed uzly (onion adresy oddělené čárkami) +filterWindow.priceRelayNode=Filtrované cenové relay uzly (onion adresy oddělené čárkami) +filterWindow.xmrNode=Filtrované uzly Monero (adresy+porty oddělené čárkami) +filterWindow.preventPublicXmrNetwork=Zabraňte použití veřejné sítě Monero filterWindow.disableAutoConf=Zakázat automatické potvrzení filterWindow.autoConfExplorers=Filtrované průzkumníky s automatickým potvrzením (adresy oddělené čárkami) filterWindow.disableTradeBelowVersion=Min. verze nutná pro obchodování @@ -1595,12 +1595,12 @@ popup.warning.startupFailed.twoInstances=Haveno již běží. Nemůžete spustit popup.warning.tradePeriod.halfReached=Váš obchod s ID {0} dosáhl poloviny max. povoleného obchodního období a stále není dokončen.\n\nObdobí obchodování končí {1}\n\nDalší informace o stavu obchodu naleznete na adrese \"Portfolio/Otevřené obchody\". popup.warning.tradePeriod.ended=Váš obchod s ID {0} dosáhl max. povoleného obchodního období a není dokončen.\n\nObdobí obchodování skončilo {1}\n\nZkontrolujte prosím svůj obchod v sekci "Portfolio/Otevřené obchody\", abyste kontaktovali mediátora. popup.warning.noTradingAccountSetup.headline=Nemáte nastaven obchodní účet -popup.warning.noTradingAccountSetup.msg=Než budete moci vytvořit nabídku, musíte si nastavit národní měnu nebo cryptoový účet.\nChcete si založit účet? +popup.warning.noTradingAccountSetup.msg=Než budete moci vytvořit nabídku, musíte si nastavit národní měnu nebo kryptoměnový účet.\nChcete si založit účet? popup.warning.noArbitratorsAvailable=Nejsou k dispozici žádní rozhodci. popup.warning.noMediatorsAvailable=Nejsou k dispozici žádní mediátoři. popup.warning.notFullyConnected=Musíte počkat, až budete plně připojeni k síti.\nTo může při spuštění trvat až 2 minuty. -popup.warning.notSufficientConnectionsToXmrNetwork=Musíte počkat, až budete mít alespoň {0} připojení k moneroové síti. -popup.warning.downloadNotComplete=Musíte počkat, až bude stahování chybějících moneroových bloků kompletní. +popup.warning.notSufficientConnectionsToXmrNetwork=Musíte počkat, až budete mít alespoň {0} připojení k síti Monero. +popup.warning.downloadNotComplete=Musíte počkat, až bude dokončeno stahování chybějících bloků Monero. popup.warning.walletNotSynced=Haveno peněženka není synchronizována s nejnovější výškou blockchainu. Počkejte, dokud se peněženka nesynchronizuje, nebo zkontrolujte své připojení. popup.warning.removeOffer=Opravdu chcete tuto nabídku odebrat? popup.warning.tooLargePercentageValue=Nelze nastavit procento 100% nebo větší. @@ -1622,11 +1622,11 @@ popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Aktualizujte prosím na nejnovější verzi Haveno. Byla vydána povinná aktualizace, která zakazuje obchodování se starými verzemi. Další informace naleznete na fóru Haveno. popup.warning.burnXMR=Tato transakce není možná, protože poplatky za těžbu {0} by přesáhly částku převodu {1}. Počkejte prosím, dokud nebudou poplatky za těžbu opět nízké nebo dokud nenahromadíte více XMR k převodu. -popup.warning.openOffer.makerFeeTxRejected=Transakční poplatek tvůrce za nabídku s ID {0} byl moneroovou sítí odmítnut.\nID transakce = {1}.\nNabídka byla odstraněna, aby se předešlo dalším problémům.\nPřejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. +popup.warning.openOffer.makerFeeTxRejected=Transakční poplatek tvůrce za nabídku s ID {0} byl odmítnut sítí Monero.\nID transakce = {1}.\nNabídka byla odstraněna, aby se předešlo dalším problémům.\nPřejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. popup.warning.trade.txRejected.tradeFee=obchodní poplatek popup.warning.trade.txRejected.deposit=vklad -popup.warning.trade.txRejected=Moneroová síť odmítla {0} transakci pro obchod s ID {1}.\nID transakce = {2}\nObchod byl přesunut do neúspěšných obchodů.\nPřejděte do části \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. +popup.warning.trade.txRejected=Síť Monero odmítla {0} transakci pro obchod s ID {1}.\nID transakce = {2}\nObchod byl přesunut do neúspěšných obchodů.\nPřejděte do části \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. popup.warning.openOfferWithInvalidMakerFeeTx=Transakční poplatek tvůrce za nabídku s ID {0} je neplatný.\nID transakce = {1}.\nPřejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. @@ -1641,9 +1641,9 @@ popup.warn.downGradePrevention=Downgrade z verze {0} na verzi {1} není podporov popup.privateNotification.headline=Důležité soukromé oznámení! popup.securityRecommendation.headline=Důležité bezpečnostní doporučení -popup.securityRecommendation.msg=Chtěli bychom vám připomenout, abyste zvážili použití ochrany heslem pro vaši peněženku, pokud jste ji již neaktivovali.\n\nDůrazně se také doporučuje zapsat seed slova peněženky. Tato seed slova jsou jako hlavní heslo pro obnovení vaší moneroové peněženky.\nV sekci "Seed peněženky" naleznete další informace.\n\nDále byste měli zálohovat úplnou složku dat aplikace v sekci \"Záloha\". +popup.securityRecommendation.msg=Chtěli bychom vám připomenout, abyste zvážili použití ochrany heslem pro vaši peněženku, pokud jste ji již neaktivovali.\n\nDůrazně se také doporučuje zapsat seed slova peněženky. Tato seed slova jsou jako hlavní heslo pro obnovení vaší peněženky Monero.\nV sekci "Seed peněženky" naleznete další informace.\n\nDále byste měli zálohovat úplnou složku dat aplikace v sekci \"Záloha\". -popup.xmrLocalNode.msg=Haveno zjistil, že na tomto stroji (na localhostu) běží Monero node.\n\nUjistěte se, prosím, že tento node je plně synchronizován před spuštěním Havena. +popup.xmrLocalNode.msg=Haveno zjistil, že na tomto stroji (na localhostu) běží Monero uzel.\n\nUjistěte se, prosím, že tento uzel je plně synchronizován před spuštěním Havena. popup.shutDownInProgress.headline=Probíhá vypínání popup.shutDownInProgress.msg=Vypnutí aplikace může trvat několik sekund.\nProsím, nepřerušujte tento proces. @@ -1779,10 +1779,10 @@ peerInfo.age.noRisk=Stáří platebního účtu peerInfo.age.chargeBackRisk=Čas od podpisu peerInfo.unknownAge=Stáří není známo -addressTextField.openWallet=Otevřete výchozí moneroovou peněženku +addressTextField.openWallet=Otevřete výchozí peněženku Monero addressTextField.copyToClipboard=Zkopírujte adresu do schránky addressTextField.addressCopiedToClipboard=Adresa byla zkopírována do schránky -addressTextField.openWallet.failed=Otevření výchozí moneroové peněženky se nezdařilo. Možná nemáte žádnou nainstalovanou? +addressTextField.openWallet.failed=Otevření výchozí peněženky Monero selhalo. Možná nemáte žádnou nainstalovanou? peerInfoIcon.tooltip={0}\nŠtítek: {1} @@ -1872,7 +1872,7 @@ seed.date=Datum peněženky seed.restore.title=Obnovit peněženky z seed slov seed.restore=Obnovit peněženky seed.creationDate=Datum vzniku -seed.warn.walletNotEmpty.msg=Vaše moneroová peněženka není prázdná.\n\nTuto peněženku musíte vyprázdnit, než se pokusíte obnovit starší, protože smíchání peněženek může vést ke zneplatnění záloh.\n\nDokončete své obchody, uzavřete všechny otevřené nabídky a přejděte do sekce Prostředky, kde si můžete vybrat své moneroy.\nV případě, že nemáte přístup ke svým moneroům, můžete použít nouzový nástroj k vyprázdnění peněženky.\nNouzový nástroj otevřete stisknutím kombinace kláves \"Alt+e\" nebo \"Cmd/Ctrl+e\". +seed.warn.walletNotEmpty.msg=Vaše peněženka Monero není prázdná.\n\nTuto peněženku musíte vyprázdnit, než se pokusíte obnovit starší, protože smíchání peněženek může vést ke zneplatnění záloh.\n\nDokončete své obchody, uzavřete všechny otevřené nabídky a přejděte do sekce Prostředky, kde si můžete vybrat své moneroy.\nV případě, že nemáte přístup ke svým moneroům, můžete použít nouzový nástroj k vyprázdnění peněženky.\nNouzový nástroj otevřete stisknutím kombinace kláves \"Alt+e\" nebo \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=Chci přesto obnovit seed.warn.walletNotEmpty.emptyWallet=Nejprve vyprázdním své peněženky seed.warn.notEncryptedAnymore=Vaše peněženky jsou šifrovány.\n\nPo obnovení již nebudou peněženky šifrovány a musíte nastavit nové heslo.\n\nChcete pokračovat? @@ -1939,7 +1939,7 @@ shared.accountSigningState=Stav podpisu účtu #new payment.crypto.address.dyn={0} adresa -payment.crypto.receiver.address=Cryptoová adresa příjemce +payment.crypto.receiver.address=Kryptoměnová adresa příjemce payment.accountNr=Číslo účtu payment.emailOrMobile=E-mail nebo mobilní číslo payment.useCustomAccountName=Použijte vlastní název účtu @@ -2080,7 +2080,7 @@ INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" -BLOCK_CHAINS=Cryptoy +BLOCK_CHAINS=Kryptoměny # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" @@ -2090,7 +2090,7 @@ TRANSFERWISE=TransferWise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" -BLOCK_CHAINS_INSTANT=Instantní Cryptoy +BLOCK_CHAINS_INSTANT=Instantní kryptoměny # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" @@ -2132,7 +2132,7 @@ INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" -BLOCK_CHAINS_SHORT=Cryptoy +BLOCK_CHAINS_SHORT=Kryptoměny # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" @@ -2142,7 +2142,7 @@ TRANSFERWISE_SHORT=TransferWise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" -BLOCK_CHAINS_INSTANT_SHORT=Instantní Cryptoy +BLOCK_CHAINS_INSTANT_SHORT=Instantní kryptoměny # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" From 0275de3ff6991ed7e6e39a12bb7725c5b5ebc631 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 14 Dec 2024 11:40:52 -0500 Subject: [PATCH 026/371] increase limits: crypto to 528, very low risk to 132, pay by mail to 48 Co-authored-by: XMRZombie --- .../java/haveno/core/payment/TradeLimits.java | 2 +- .../core/payment/payload/PaymentMethod.java | 31 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/haveno/core/payment/TradeLimits.java b/core/src/main/java/haveno/core/payment/TradeLimits.java index e2de303ed5..2bfdcdbc6f 100644 --- a/core/src/main/java/haveno/core/payment/TradeLimits.java +++ b/core/src/main/java/haveno/core/payment/TradeLimits.java @@ -30,7 +30,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class TradeLimits { - private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(96.0); // max trade limit for lowest risk payment method. Others will get derived from that. + private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(528); // max trade limit for lowest risk payment method. Others will get derived from that. @Nullable @Getter private static TradeLimits INSTANCE; diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index cf75df3652..49302f610a 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -124,13 +124,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable Date: Sat, 14 Dec 2024 18:15:26 -0500 Subject: [PATCH 027/371] throttle warnings in KeepAlive and PeerExchange handlers #1468 --- .../network/p2p/network/Connection.java | 16 +++++++------- .../p2p/peers/keepalive/KeepAliveHandler.java | 21 +++++++++++++++---- .../peerexchange/PeerExchangeHandler.java | 19 ++++++++++++++--- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/p2p/src/main/java/haveno/network/p2p/network/Connection.java b/p2p/src/main/java/haveno/network/p2p/network/Connection.java index 1a7f1b84df..8165ecd0b3 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/Connection.java +++ b/p2p/src/main/java/haveno/network/p2p/network/Connection.java @@ -175,9 +175,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { // throttle logs of reported invalid requests private static final long LOG_THROTTLE_INTERVAL_MS = 30000; // throttle logging rule violations and warnings to once every 30 seconds private static long lastLoggedInvalidRequestReportTs = 0; - private static int numUnloggedInvalidRequestReports = 0; + private static int numThrottledInvalidRequestReports = 0; private static long lastLoggedWarningTs = 0; - private static int numUnloggedWarnings = 0; + private static int numThrottledWarnings = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -620,7 +620,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { boolean logReport = System.currentTimeMillis() - lastLoggedInvalidRequestReportTs > LOG_THROTTLE_INTERVAL_MS; // count the number of unlogged reports since last log entry - if (!logReport) numUnloggedInvalidRequestReports++; + if (!logReport) numThrottledInvalidRequestReports++; // handle report if (logReport) log.warn("We got reported the ruleViolation {} at connection with address={}, uid={}, errorMessage={}", ruleViolation, connection.getPeersNodeAddressProperty(), connection.getUid(), errorMessage); @@ -654,8 +654,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) { if (logReport) { - if (numUnloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", numUnloggedInvalidRequestReports); - numUnloggedInvalidRequestReports = 0; + if (numThrottledInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", numThrottledInvalidRequestReports); + numThrottledInvalidRequestReports = 0; lastLoggedInvalidRequestReportTs = System.currentTimeMillis(); } } @@ -940,11 +940,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; if (logWarning) { log.warn(msg); - if (numUnloggedWarnings > 0) log.warn("We received {} other log warnings since the last log entry", numUnloggedWarnings); - numUnloggedWarnings = 0; + if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings); + numThrottledWarnings = 0; lastLoggedWarningTs = System.currentTimeMillis(); } else { - numUnloggedWarnings++; + numThrottledWarnings++; } } } diff --git a/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java index 91ba2278c0..07ea9397a5 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java @@ -40,8 +40,10 @@ import java.util.concurrent.TimeUnit; class KeepAliveHandler implements MessageListener { private static final Logger log = LoggerFactory.getLogger(KeepAliveHandler.class); - private static final int DELAY_MS = 10_000; + private static final long LOG_THROTTLE_INTERVAL_MS = 60000; // throttle logging warnings to once every 60 seconds + private static long lastLoggedWarningTs = 0; + private static int numThrottledWarnings = 0; /////////////////////////////////////////////////////////////////////////////////////////// @@ -147,9 +149,8 @@ class KeepAliveHandler implements MessageListener { cleanup(); listener.onComplete(); } else { - log.warn("Nonce not matching. That should never happen.\n\t" + - "We drop that message. nonce={} / requestNonce={}", - nonce, pong.getRequestNonce()); + throttleWarn("Nonce not matching. That should never happen.\n" + + "\tWe drop that message. nonce=" + nonce + ", requestNonce=" + pong.getRequestNonce() + ", peerNodeAddress=" + connection.getPeersNodeAddressOptional().orElseGet(null)); } } else { log.trace("We have stopped already. We ignore that onMessage call."); @@ -167,4 +168,16 @@ class KeepAliveHandler implements MessageListener { delayTimer = null; } } + + private synchronized void throttleWarn(String msg) { + boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; + if (logWarning) { + log.warn(msg); + if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings); + numThrottledWarnings = 0; + lastLoggedWarningTs = System.currentTimeMillis(); + } else { + numThrottledWarnings++; + } + } } diff --git a/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java index 1a929289a0..0b10307ccb 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java @@ -45,6 +45,9 @@ class PeerExchangeHandler implements MessageListener { // We want to keep timeout short here private static final long TIMEOUT = 90; private static final int DELAY_MS = 500; + private static final long LOG_THROTTLE_INTERVAL_MS = 60000; // throttle logging warnings to once every 60 seconds + private static long lastLoggedWarningTs = 0; + private static int numThrottledWarnings = 0; /////////////////////////////////////////////////////////////////////////////////////////// @@ -173,9 +176,8 @@ class PeerExchangeHandler implements MessageListener { cleanup(); listener.onComplete(); } else { - log.warn("Nonce not matching. That should never happen.\n\t" + - "We drop that message. nonce={} / requestNonce={}", - nonce, getPeersResponse.getRequestNonce()); + throttleWarn("Nonce not matching. That should never happen.\n" + + "\tWe drop that message. nonce=" + nonce + ", requestNonce=" + getPeersResponse.getRequestNonce() + ", peerNodeAddress=" + connection.getPeersNodeAddressOptional().orElseGet(null)); } } else { log.trace("We have stopped that handler already. We ignore that onMessage call."); @@ -216,4 +218,15 @@ class PeerExchangeHandler implements MessageListener { } } + private synchronized void throttleWarn(String msg) { + boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; + if (logWarning) { + log.warn(msg); + if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings); + numThrottledWarnings = 0; + lastLoggedWarningTs = System.currentTimeMillis(); + } else { + numThrottledWarnings++; + } + } } From 85acb8aeb33e31dfbda3b2c8cc2870a5420543cd Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 15 Dec 2024 10:39:45 -0500 Subject: [PATCH 028/371] fix sorting of dispute state column --- .../java/haveno/desktop/main/support/dispute/DisputeView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java index 14e85b78c6..7e3cad8de3 100644 --- a/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java @@ -927,6 +927,7 @@ public abstract class DisputeView extends ActivatableView implements buyerOnionAddressColumn.setComparator(Comparator.comparing(this::getBuyerOnionAddressColumnLabel)); sellerOnionAddressColumn.setComparator(Comparator.comparing(this::getSellerOnionAddressColumnLabel)); marketColumn.setComparator((o1, o2) -> CurrencyUtil.getCurrencyPair(o1.getContract().getOfferPayload().getCurrencyCode()).compareTo(o2.getContract().getOfferPayload().getCurrencyCode())); + stateColumn.setComparator(Comparator.comparing(this::getDisputeStateText)); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); From 9ec2794931bfad5bcfef2ad3db647004747ae6f4 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 15 Dec 2024 11:34:26 -0500 Subject: [PATCH 029/371] do not auto complete trades resolved by arbitration --- core/src/main/java/haveno/core/trade/TradeManager.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index dc0427c4af..9a3d84cbb2 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -994,7 +994,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (tradeOptional.isPresent()) { Trade trade = tradeOptional.get(); trade.setDisputeState(disputeState); - onTradeCompleted(trade); xmrWalletService.resetAddressEntriesForTrade(trade.getId()); requestPersistence(); } From 140961d885825628569c594d8583fe984cd54ddb Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 16 Dec 2024 09:34:12 -0500 Subject: [PATCH 030/371] show either dispute payout tx id or normal payout tx id --- .../overlays/windows/TradeDetailsWindow.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java index 6575769db3..0484305792 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java @@ -183,12 +183,12 @@ public class TradeDetailsWindow extends Overlay { rows++; } - if (trade.getPayoutTxId() != null) - rows++; boolean showDisputedTx = arbitrationManager.findOwnDispute(trade.getId()).isPresent() && arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId() != null; if (showDisputedTx) rows++; + else if (trade.getPayoutTxId() != null) + rows++; if (trade.hasFailed()) rows += 2; if (trade.getTradePeerNodeAddress() != null) @@ -244,16 +244,18 @@ public class TradeDetailsWindow extends Overlay { if (trade.getMaker().getDepositTxHash() != null) addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.makerDepositTransactionId"), trade.getMaker().getDepositTxHash()); - if (trade.getTaker().getDepositTxHash() != null) + if (trade.getTaker().getDepositTxHash() != null) addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerDepositTransactionId"), trade.getTaker().getDepositTxHash()); - if (trade.getPayoutTxId() != null && !trade.getPayoutTxId().isBlank()) - addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), - trade.getPayoutTxId()); - if (showDisputedTx) + + if (showDisputedTx) { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.disputedPayoutTxId"), arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId()); + } else if (trade.getPayoutTxId() != null && !trade.getPayoutTxId().isBlank()) { + addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), + trade.getPayoutTxId()); + } if (trade.hasFailed()) { textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("shared.errorMessage"), "", 0).second; From 4b7db9a1ae3fe2f4140f40a430df1dd548e4098e Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 16 Dec 2024 09:38:33 -0500 Subject: [PATCH 031/371] remove colon from disputed payout transaction id --- core/src/main/resources/i18n/displayStrings.properties | 2 +- core/src/main/resources/i18n/displayStrings_cs.properties | 2 +- core/src/main/resources/i18n/displayStrings_de.properties | 2 +- core/src/main/resources/i18n/displayStrings_fr.properties | 2 +- core/src/main/resources/i18n/displayStrings_it.properties | 2 +- core/src/main/resources/i18n/displayStrings_ja.properties | 2 +- core/src/main/resources/i18n/displayStrings_pt-br.properties | 2 +- core/src/main/resources/i18n/displayStrings_pt.properties | 2 +- core/src/main/resources/i18n/displayStrings_th.properties | 2 +- core/src/main/resources/i18n/displayStrings_tr.properties | 2 +- core/src/main/resources/i18n/displayStrings_vi.properties | 2 +- core/src/main/resources/i18n/displayStrings_zh-hans.properties | 2 +- core/src/main/resources/i18n/displayStrings_zh-hant.properties | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index c1f98ce420..c0d2d88fd8 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2016,7 +2016,7 @@ tacWindow.disagree=I disagree and quit tacWindow.arbitrationSystem=Dispute resolution tradeDetailsWindow.headline=Trade -tradeDetailsWindow.disputedPayoutTxId=Disputed payout transaction ID: +tradeDetailsWindow.disputedPayoutTxId=Disputed payout transaction ID tradeDetailsWindow.tradeDate=Trade date tradeDetailsWindow.txFee=Mining fee tradeDetailsWindow.tradePeersOnion=Trading peers onion address diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index f1c8fc6a31..789eb761ca 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -1504,7 +1504,7 @@ tacWindow.disagree=Nesouhlasím a odcházím tacWindow.arbitrationSystem=Řešení sporů tradeDetailsWindow.headline=Obchod -tradeDetailsWindow.disputedPayoutTxId=ID sporné platební transakce: +tradeDetailsWindow.disputedPayoutTxId=ID sporné platební transakce tradeDetailsWindow.tradeDate=Datum obchodu tradeDetailsWindow.txFee=Poplatek za těžbu tradeDetailsWindow.tradePeersOnion=Onion adresa obchodního partnera diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 7a7a5643af..5d03c34b0d 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1504,7 +1504,7 @@ tacWindow.disagree=Ich stimme nicht zu und beende tacWindow.arbitrationSystem=Streitbeilegung tradeDetailsWindow.headline=Handel -tradeDetailsWindow.disputedPayoutTxId=Transaktions-ID der strittigen Auszahlung: +tradeDetailsWindow.disputedPayoutTxId=Transaktions-ID der strittigen Auszahlung tradeDetailsWindow.tradeDate=Handelsdatum tradeDetailsWindow.txFee=Mining-Gebühr tradeDetailsWindow.tradePeersOnion=Onion-Adresse des Handelspartners diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 252b31466e..e87a0caceb 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -1506,7 +1506,7 @@ tacWindow.disagree=Je ne suis pas d'accord et je quitte tacWindow.arbitrationSystem=Règlement du litige tradeDetailsWindow.headline=Échange -tradeDetailsWindow.disputedPayoutTxId=ID de la transaction de versement contestée : +tradeDetailsWindow.disputedPayoutTxId=ID de la transaction de versement contestée tradeDetailsWindow.tradeDate=Date de l'échange tradeDetailsWindow.txFee=Frais de minage tradeDetailsWindow.tradePeersOnion=Adresse onion du pair de trading diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index b2f67e19cd..212c89f3b3 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1503,7 +1503,7 @@ tacWindow.disagree=Non accetto ed esco tacWindow.arbitrationSystem=Risoluzione disputa tradeDetailsWindow.headline=Scambio -tradeDetailsWindow.disputedPayoutTxId=ID transazione di pagamento contestato: +tradeDetailsWindow.disputedPayoutTxId=ID transazione di pagamento contestato tradeDetailsWindow.tradeDate=Data di scambio tradeDetailsWindow.txFee=Commissione di mining tradeDetailsWindow.tradePeersOnion=Indirizzi onion peer di trading diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 478e11353b..80542bc3e8 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -1504,7 +1504,7 @@ tacWindow.disagree=同意せずにに終了 tacWindow.arbitrationSystem=紛争解決 tradeDetailsWindow.headline=トレード -tradeDetailsWindow.disputedPayoutTxId=係争中の支払い取引ID: +tradeDetailsWindow.disputedPayoutTxId=係争中の支払い取引ID tradeDetailsWindow.tradeDate=取引日 tradeDetailsWindow.txFee=マイニング手数料 tradeDetailsWindow.tradePeersOnion=トレード相手のonionアドレス diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 8fa3d80a71..f6659b16ef 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -1507,7 +1507,7 @@ tacWindow.disagree=Eu não concordo e desisto tacWindow.arbitrationSystem=Resolução de disputas tradeDetailsWindow.headline=Negociação -tradeDetailsWindow.disputedPayoutTxId=ID de transação do pagamento disputado: +tradeDetailsWindow.disputedPayoutTxId=ID de transação do pagamento disputado tradeDetailsWindow.tradeDate=Data da negociação tradeDetailsWindow.txFee=Taxa de mineração tradeDetailsWindow.tradePeersOnion=Endereço onion dos parceiros de negociação diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 27f94191e5..d7891bead6 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1500,7 +1500,7 @@ tacWindow.disagree=Eu não concordo e desisto tacWindow.arbitrationSystem=Resolução da disputa tradeDetailsWindow.headline=Negócio -tradeDetailsWindow.disputedPayoutTxId=ID de transação do pagamento disputado: +tradeDetailsWindow.disputedPayoutTxId=ID de transação do pagamento disputado tradeDetailsWindow.tradeDate=Data de negócio tradeDetailsWindow.txFee=Taxa de mineração tradeDetailsWindow.tradePeersOnion=Endereço onion dos parceiros de negociação diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index b603454ce5..db2855c570 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1501,7 +1501,7 @@ tacWindow.disagree=ฉันไม่เห็นด้วยและออก tacWindow.arbitrationSystem=Dispute resolution tradeDetailsWindow.headline=ซื้อขาย -tradeDetailsWindow.disputedPayoutTxId=รหัส ID ธุรกรรมการจ่ายเงินที่พิพาท: +tradeDetailsWindow.disputedPayoutTxId=รหัส ID ธุรกรรมการจ่ายเงินที่พิพาท tradeDetailsWindow.tradeDate=วันที่ซื้อขาย tradeDetailsWindow.txFee=ค่าธรรมเนียมการขุด tradeDetailsWindow.tradePeersOnion=ที่อยู่ของ onion คู่ค้า diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 7656d581da..d8436a0f41 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -2011,7 +2011,7 @@ tacWindow.disagree=Kabul etmiyorum ve çıkıyorum tacWindow.arbitrationSystem=Uyuşmazlık çözümü tradeDetailsWindow.headline=Ticaret -tradeDetailsWindow.disputedPayoutTxId=Uyuşmazlık konusu olan ödeme işlem kimliği: +tradeDetailsWindow.disputedPayoutTxId=Uyuşmazlık konusu olan ödeme işlem kimliği tradeDetailsWindow.tradeDate=Ticaret tarihi tradeDetailsWindow.txFee=Madencilik ücreti tradeDetailsWindow.tradePeersOnion=Ticaret ortaklarının onion adresi diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 8530cf4691..828e81e4b1 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1503,7 +1503,7 @@ tacWindow.disagree=Tôi không đồng ý và thoát tacWindow.arbitrationSystem=Dispute resolution tradeDetailsWindow.headline=giao dịch -tradeDetailsWindow.disputedPayoutTxId=ID giao dịch hoàn tiền khiếu nại: +tradeDetailsWindow.disputedPayoutTxId=ID giao dịch hoàn tiền khiếu nại tradeDetailsWindow.tradeDate=Ngày giao dịch tradeDetailsWindow.txFee=Phí đào tradeDetailsWindow.tradePeersOnion=Địa chỉ onion Đối tác giao dịch diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 91166e64df..684ba4c663 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -1505,7 +1505,7 @@ tacWindow.disagree=我不同意并退出 tacWindow.arbitrationSystem=纠纷解决方案 tradeDetailsWindow.headline=交易 -tradeDetailsWindow.disputedPayoutTxId=纠纷支付交易 ID: +tradeDetailsWindow.disputedPayoutTxId=纠纷支付交易 ID tradeDetailsWindow.tradeDate=交易时间 tradeDetailsWindow.txFee=矿工手续费 tradeDetailsWindow.tradePeersOnion=交易伙伴匿名地址 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 37d5bfbbd9..ebd7194be9 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -1505,7 +1505,7 @@ tacWindow.disagree=我不同意並退出 tacWindow.arbitrationSystem=糾紛解決方案 tradeDetailsWindow.headline=交易 -tradeDetailsWindow.disputedPayoutTxId=糾紛支付交易 ID: +tradeDetailsWindow.disputedPayoutTxId=糾紛支付交易 ID tradeDetailsWindow.tradeDate=交易時間 tradeDetailsWindow.txFee=礦工手續費 tradeDetailsWindow.tradePeersOnion=交易夥伴匿名地址 From ece3b0fec01132b8f8fa9d65bf5088a8e1e44715 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 16 Dec 2024 05:59:43 -0500 Subject: [PATCH 032/371] fix concurrency exception updating capabilities #1473 --- .../java/haveno/common/app/Capabilities.java | 54 ++++++++++++------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/common/src/main/java/haveno/common/app/Capabilities.java b/common/src/main/java/haveno/common/app/Capabilities.java index 39266a25c6..5bb3bd0bc4 100644 --- a/common/src/main/java/haveno/common/app/Capabilities.java +++ b/common/src/main/java/haveno/common/app/Capabilities.java @@ -59,8 +59,10 @@ public class Capabilities { } public Capabilities(Collection capabilities) { - synchronized (this.capabilities) { - this.capabilities.addAll(capabilities); + synchronized (capabilities) { + synchronized (this.capabilities) { + this.capabilities.addAll(capabilities); + } } } @@ -73,9 +75,11 @@ public class Capabilities { } public void set(Collection capabilities) { - synchronized (this.capabilities) { - this.capabilities.clear(); - this.capabilities.addAll(capabilities); + synchronized (capabilities) { + synchronized (this.capabilities) { + this.capabilities.clear(); + this.capabilities.addAll(capabilities); + } } } @@ -87,15 +91,19 @@ public class Capabilities { public void addAll(Capabilities capabilities) { if (capabilities != null) { - synchronized (this.capabilities) { - this.capabilities.addAll(capabilities.capabilities); + synchronized (capabilities.capabilities) { + synchronized (this.capabilities) { + this.capabilities.addAll(capabilities.capabilities); + } } } } public boolean containsAll(final Set requiredItems) { - synchronized (this.capabilities) { - return capabilities.containsAll(requiredItems); + synchronized(requiredItems) { + synchronized (this.capabilities) { + return capabilities.containsAll(requiredItems); + } } } @@ -129,7 +137,9 @@ public class Capabilities { * @return int list of Capability ordinals */ public static List toIntList(Capabilities capabilities) { - return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList()); + synchronized (capabilities.capabilities) { + return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList()); + } } /** @@ -139,11 +149,13 @@ public class Capabilities { * @return a {@link Capabilities} object */ public static Capabilities fromIntList(List capabilities) { - return new Capabilities(capabilities.stream() - .filter(integer -> integer < Capability.values().length) - .filter(integer -> integer >= 0) - .map(integer -> Capability.values()[integer]) - .collect(Collectors.toSet())); + synchronized (capabilities) { + return new Capabilities(capabilities.stream() + .filter(integer -> integer < Capability.values().length) + .filter(integer -> integer >= 0) + .map(integer -> Capability.values()[integer]) + .collect(Collectors.toSet())); + } } /** @@ -181,7 +193,9 @@ public class Capabilities { } public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) { - return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability); + synchronized (capabilities.capabilities) { + return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability); + } } @Override @@ -211,8 +225,10 @@ public class Capabilities { // Neither would support removal of past capabilities, a use case we never had so far and which might have // backward compatibility issues, so we should treat capabilities as an append-only data structure. public int findHighestCapability(Capabilities capabilities) { - return (int) capabilities.capabilities.stream() - .mapToLong(e -> (long) e.ordinal()) - .sum(); + synchronized (capabilities.capabilities) { + return (int) capabilities.capabilities.stream() + .mapToLong(e -> (long) e.ordinal()) + .sum(); + } } } From 775fbc41c2720f5c4a71dff36424dd0288f3fa4f Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 16 Dec 2024 07:04:53 -0500 Subject: [PATCH 033/371] support buying xmr without deposit or fee using passphrase --- .../haveno/apitest/method/MethodTest.java | 6 +- .../apitest/method/offer/CancelOfferTest.java | 2 +- .../offer/CreateOfferUsingFixedPriceTest.java | 6 +- ...CreateOfferUsingMarketPriceMarginTest.java | 10 +- .../method/offer/CreateXMROffersTest.java | 8 +- .../method/offer/ValidateCreateOfferTest.java | 6 +- .../method/trade/TakeBuyBTCOfferTest.java | 2 +- ...keBuyBTCOfferWithNationalBankAcctTest.java | 2 +- .../method/trade/TakeBuyXMROfferTest.java | 2 +- .../method/trade/TakeSellBTCOfferTest.java | 2 +- .../method/trade/TakeSellXMROfferTest.java | 2 +- .../LongRunningOfferDeactivationTest.java | 4 +- .../apitest/scenario/bot/RandomOffer.java | 6 +- .../cli/request/OffersServiceRequest.java | 2 +- .../witness/AccountAgeWitnessService.java | 8 +- .../main/java/haveno/core/api/CoreApi.java | 22 +- .../haveno/core/api/CoreDisputesService.java | 29 +- .../haveno/core/api/CoreOffersService.java | 22 +- .../haveno/core/api/CoreTradesService.java | 2 +- .../java/haveno/core/api/model/OfferInfo.java | 15 +- .../java/haveno/core/api/model/TradeInfo.java | 16 +- .../api/model/builder/OfferInfoBuilder.java | 12 + .../haveno/core/offer/CreateOfferService.java | 112 +- .../main/java/haveno/core/offer/Offer.java | 18 + .../haveno/core/offer/OfferFilterService.java | 2 +- .../java/haveno/core/offer/OfferPayload.java | 25 +- .../java/haveno/core/offer/OfferUtil.java | 19 +- .../java/haveno/core/offer/OpenOffer.java | 14 +- .../haveno/core/offer/OpenOfferManager.java | 109 +- .../tasks/SendOfferAvailabilityRequest.java | 3 +- .../offer/placeoffer/tasks/ValidateOffer.java | 22 +- .../core/offer/takeoffer/TakeOfferModel.java | 3 +- .../core/payment/PaymentAccountUtil.java | 2 +- .../java/haveno/core/payment/TradeLimits.java | 11 + .../validation/SecurityDepositValidator.java | 4 +- .../haveno/core/trade/ArbitratorTrade.java | 10 +- .../haveno/core/trade/BuyerAsMakerTrade.java | 11 +- .../haveno/core/trade/BuyerAsTakerTrade.java | 9 +- .../java/haveno/core/trade/BuyerTrade.java | 6 +- .../main/java/haveno/core/trade/Contract.java | 57 +- .../java/haveno/core/trade/HavenoUtils.java | 43 +- .../haveno/core/trade/SellerAsMakerTrade.java | 9 +- .../haveno/core/trade/SellerAsTakerTrade.java | 9 +- .../java/haveno/core/trade/SellerTrade.java | 6 +- .../main/java/haveno/core/trade/Trade.java | 60 +- .../java/haveno/core/trade/TradeManager.java | 29 +- .../core/trade/messages/DepositRequest.java | 17 +- .../core/trade/messages/InitTradeRequest.java | 11 +- .../trade/messages/SignContractRequest.java | 11 +- .../haveno/core/trade/protocol/TradePeer.java | 1 - .../ArbitratorProcessDepositRequest.java | 87 +- .../tasks/ArbitratorProcessReserveTx.java | 62 +- ...tratorSendInitTradeOrMultisigRequests.java | 3 +- .../tasks/BuyerPreparePaymentSentMessage.java | 2 +- ...MakerSendInitTradeRequestToArbitrator.java | 3 +- .../tasks/MaybeSendSignContractRequest.java | 81 +- .../tasks/ProcessSignContractRequest.java | 20 +- .../protocol/tasks/SendDepositRequest.java | 4 +- .../tasks/TakerReserveTradeFunds.java | 91 +- ...TakerSendInitTradeRequestToArbitrator.java | 7 +- .../TakerSendInitTradeRequestToMaker.java | 8 +- .../java/haveno/core/user/Preferences.java | 27 +- .../haveno/core/user/PreferencesPayload.java | 17 +- .../java/haveno/core/util/coin/CoinUtil.java | 34 +- .../haveno/core/xmr/wallet/Restrictions.java | 37 +- core/src/main/resources/bip39_english.txt | 2048 +++++++++++++++++ .../resources/i18n/displayStrings.properties | 20 +- .../i18n/displayStrings_cs.properties | 17 +- .../i18n/displayStrings_de.properties | 17 +- .../i18n/displayStrings_es.properties | 17 +- .../i18n/displayStrings_fa.properties | 19 +- .../i18n/displayStrings_fr.properties | 17 +- .../i18n/displayStrings_it.properties | 19 +- .../i18n/displayStrings_ja.properties | 17 +- .../i18n/displayStrings_pt-br.properties | 19 +- .../i18n/displayStrings_pt.properties | 21 +- .../i18n/displayStrings_ru.properties | 19 +- .../i18n/displayStrings_th.properties | 19 +- .../i18n/displayStrings_tr.properties | 17 +- .../i18n/displayStrings_vi.properties | 19 +- .../i18n/displayStrings_zh-hans.properties | 19 +- .../i18n/displayStrings_zh-hant.properties | 19 +- .../haveno/daemon/grpc/GrpcOffersService.java | 4 +- .../haveno/daemon/grpc/GrpcTradesService.java | 1 + .../paymentmethods/PaymentMethodForm.java | 6 +- .../src/main/java/haveno/desktop/images.css | 4 + .../main/offer/MutableOfferDataModel.java | 81 +- .../desktop/main/offer/MutableOfferView.java | 167 +- .../main/offer/MutableOfferViewModel.java | 129 +- .../desktop/main/offer/OfferDataModel.java | 4 + .../main/offer/offerbook/OfferBookView.java | 48 +- .../offer/offerbook/OfferBookViewModel.java | 10 + .../offer/takeoffer/TakeOfferDataModel.java | 2 +- .../main/offer/takeoffer/TakeOfferView.java | 42 +- .../main/overlays/editor/PasswordPopup.java | 245 ++ .../main/overlays/windows/ContractWindow.java | 2 +- .../windows/DisputeSummaryWindow.java | 46 +- .../overlays/windows/OfferDetailsWindow.java | 82 +- .../DuplicateOfferDataModel.java | 17 +- .../editoffer/EditOfferDataModel.java | 14 +- .../editoffer/EditOfferViewModel.java | 2 +- .../failedtrades/FailedTradesView.java | 2 +- .../openoffer/OpenOffersViewModel.java | 2 +- .../pendingtrades/PendingTradesDataModel.java | 24 +- .../pendingtrades/PendingTradesView.java | 2 +- .../pendingtrades/steps/TradeStepView.java | 48 +- .../main/java/haveno/desktop/theme-dark.css | 9 + .../main/java/haveno/desktop/theme-light.css | 5 + .../haveno/desktop/util/DisplayUtils.java | 18 +- desktop/src/main/resources/images/lock.png | Bin 22292 -> 0 bytes desktop/src/main/resources/images/lock@2x.png | Bin 0 -> 721 bytes .../createoffer/CreateOfferDataModelTest.java | 2 +- .../createoffer/CreateOfferViewModelTest.java | 3 +- proto/src/main/proto/grpc.proto | 7 +- proto/src/main/proto/pb.proto | 10 +- 115 files changed, 3845 insertions(+), 838 deletions(-) create mode 100644 core/src/main/resources/bip39_english.txt create mode 100644 desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java delete mode 100644 desktop/src/main/resources/images/lock.png create mode 100644 desktop/src/main/resources/images/lock@2x.png diff --git a/apitest/src/test/java/haveno/apitest/method/MethodTest.java b/apitest/src/test/java/haveno/apitest/method/MethodTest.java index 739c7c03c7..01c7a3bfd3 100644 --- a/apitest/src/test/java/haveno/apitest/method/MethodTest.java +++ b/apitest/src/test/java/haveno/apitest/method/MethodTest.java @@ -43,7 +43,7 @@ import java.util.stream.Collectors; import static haveno.apitest.config.ApiTestConfig.BTC; import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig; import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL; -import static haveno.core.xmr.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.stream; @@ -157,8 +157,8 @@ public class MethodTest extends ApiTestCase { return haveno.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER); } - public static final Supplier defaultBuyerSecurityDepositPct = () -> { - var defaultPct = BigDecimal.valueOf(getDefaultBuyerSecurityDepositAsPercent()); + public static final Supplier defaultSecurityDepositPct = () -> { + var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent()); if (defaultPct.precision() != 2) throw new IllegalStateException(format( "Unexpected decimal precision, expected 2 but actual is %d%n." diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java index a7776dd683..1867605507 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java @@ -47,7 +47,7 @@ public class CancelOfferTest extends AbstractOfferTest { 10000000L, 10000000L, 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), paymentAccountId, NO_TRIGGER_PRICE); }; diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java index 38d83f696d..49c01d5ae4 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java @@ -49,7 +49,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { 10_000_000L, 10_000_000L, "36000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), audAccount.getId()); log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -97,7 +97,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { 10_000_000L, 10_000_000L, "30000.1234", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId()); log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -145,7 +145,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { 10_000_000L, 5_000_000L, "29500.1234", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), eurAccount.getId()); log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index cc6f53acdc..f4dff640c1 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -66,7 +66,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { 10_000_000L, 10_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer)); @@ -114,7 +114,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { 10_000_000L, 10_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), nzdAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer)); @@ -162,7 +162,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { 10_000_000L, 5_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), gbpAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer)); @@ -210,7 +210,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { 10_000_000L, 5_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), brlAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer)); @@ -259,7 +259,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { 10_000_000L, 5_000_000L, 0.0, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId(), triggerPrice); assertTrue(newOffer.getIsMyOffer()); diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java index 4b7032c86f..8571c0c59d 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java @@ -62,7 +62,7 @@ public class CreateXMROffersTest extends AbstractOfferTest { 100_000_000L, 75_000_000L, "0.005", // FIXED PRICE IN BTC FOR 1 XMR - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -108,7 +108,7 @@ public class CreateXMROffersTest extends AbstractOfferTest { 100_000_000L, 50_000_000L, "0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -156,7 +156,7 @@ public class CreateXMROffersTest extends AbstractOfferTest { 100_000_000L, 75_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), triggerPrice); log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); @@ -211,7 +211,7 @@ public class CreateXMROffersTest extends AbstractOfferTest { 100_000_000L, 50_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), NO_TRIGGER_PRICE); log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); diff --git a/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java b/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java index f299801c5a..e8745c1b2c 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java @@ -47,7 +47,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest { 100000000000L, // exceeds amount limit 100000000000L, "10000.0000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId())); assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage()); } @@ -63,7 +63,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest { 10000000L, 10000000L, "40000.0000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), chfAccount.getId())); String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId()); assertEquals(expectedError, exception.getMessage()); @@ -80,7 +80,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest { 10000000L, 10000000L, "63000.0000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), audAccount.getId())); String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId()); assertEquals(expectedError, exception.getMessage()); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java index fee6e798ba..abbec38ff6 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -52,7 +52,7 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest { 12_500_000L, 12_500_000L, // min-amount = amount 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesUsdAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java index 10be976c8b..3199a6b053 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java @@ -96,7 +96,7 @@ public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest { 1_000_000L, 1_000_000L, // min-amount = amount 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesPaymentAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java index 40289e1d50..f61203400b 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java @@ -65,7 +65,7 @@ public class TakeBuyXMROfferTest extends AbstractTradeTest { 15_000_000L, 7_500_000L, "0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); genBtcBlocksThenWait(1, 5000); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java index a425759717..4f43c21cd7 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java @@ -58,7 +58,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest { 12_500_000L, 12_500_000L, // min-amount = amount 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesUsdAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java index 68daa64050..9a769b5f04 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java @@ -71,7 +71,7 @@ public class TakeSellXMROfferTest extends AbstractTradeTest { 20_000_000L, 10_500_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), NO_TRIGGER_PRICE); log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); diff --git a/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java b/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java index 13b72ff79f..356b77ef8a 100644 --- a/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java +++ b/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java @@ -57,7 +57,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest { 1_000_000, 1_000_000, 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), paymentAcct.getId(), triggerPrice); log.info("SELL offer {} created with margin based price {}.", @@ -103,7 +103,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest { 1_000_000, 1_000_000, 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), paymentAcct.getId(), triggerPrice); log.info("BUY offer {} created with margin based price {}.", diff --git a/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java b/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java index eebaec8b04..4e8b86afac 100644 --- a/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java +++ b/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java @@ -28,7 +28,7 @@ import java.text.DecimalFormat; import java.util.Objects; import java.util.function.Supplier; -import static haveno.apitest.method.offer.AbstractOfferTest.defaultBuyerSecurityDepositPct; +import static haveno.apitest.method.offer.AbstractOfferTest.defaultSecurityDepositPct; import static haveno.cli.CurrencyFormat.formatInternalFiatPrice; import static haveno.cli.CurrencyFormat.formatSatoshis; import static haveno.common.util.MathUtils.scaleDownByPowerOf10; @@ -119,7 +119,7 @@ public class RandomOffer { amount, minAmount, priceMargin, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), "0" /*no trigger price*/); } else { this.offer = botClient.createOfferAtFixedPrice(paymentAccount, @@ -128,7 +128,7 @@ public class RandomOffer { amount, minAmount, fixedOfferPrice, - defaultBuyerSecurityDepositPct.get()); + defaultSecurityDepositPct.get()); } this.id = offer.getId(); return this; diff --git a/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java b/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java index eaa0cac150..2fcb3426d1 100644 --- a/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java +++ b/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java @@ -81,7 +81,7 @@ public class OffersServiceRequest { .setUseMarketBasedPrice(useMarketBasedPrice) .setPrice(fixedPrice) .setMarketPriceMarginPct(marketPriceMarginPct) - .setBuyerSecurityDepositPct(securityDepositPct) + .setSecurityDepositPct(securityDepositPct) .setPaymentAccountId(paymentAcctId) .setTriggerPrice(triggerPrice) .build(); diff --git a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java index 4d91d75eff..978b6dd715 100644 --- a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java @@ -40,6 +40,7 @@ import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferRestrictions; import haveno.core.payment.ChargeBackRisk; import haveno.core.payment.PaymentAccount; +import haveno.core.payment.TradeLimits; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.support.dispute.Dispute; @@ -498,10 +499,15 @@ public class AccountAgeWitnessService { return getAccountAge(getMyWitness(paymentAccountPayload), new Date()); } - public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction) { + public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) { if (paymentAccount == null) return 0; + if (buyerAsTakerWithoutDeposit) { + TradeLimits tradeLimits = new TradeLimits(); + return tradeLimits.getMaxTradeLimitBuyerAsTakerWithoutDeposit().longValueExact(); + } + AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload()); BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode); if (hasTradeLimitException(accountAgeWitness)) { diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index 14ec0f6d08..c91d4a405b 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -419,10 +419,12 @@ public class CoreApi { double marketPriceMargin, long amountAsLong, long minAmountAsLong, - double buyerSecurityDeposit, + double securityDepositPct, String triggerPriceAsString, boolean reserveExactAmount, String paymentAccountId, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { coreOffersService.postOffer(currencyCode, @@ -432,10 +434,12 @@ public class CoreApi { marketPriceMargin, amountAsLong, minAmountAsLong, - buyerSecurityDeposit, + securityDepositPct, triggerPriceAsString, reserveExactAmount, paymentAccountId, + isPrivateOffer, + buyerAsTakerWithoutDeposit, resultHandler, errorMessageHandler); } @@ -448,8 +452,10 @@ public class CoreApi { double marketPriceMargin, BigInteger amount, BigInteger minAmount, - double buyerSecurityDeposit, - PaymentAccount paymentAccount) { + double securityDepositPct, + PaymentAccount paymentAccount, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit) { return coreOffersService.editOffer(offerId, currencyCode, direction, @@ -458,8 +464,10 @@ public class CoreApi { marketPriceMargin, amount, minAmount, - buyerSecurityDeposit, - paymentAccount); + securityDepositPct, + paymentAccount, + isPrivateOffer, + buyerAsTakerWithoutDeposit); } public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -535,9 +543,11 @@ public class CoreApi { public void takeOffer(String offerId, String paymentAccountId, long amountAsLong, + String challenge, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { Offer offer = coreOffersService.getOffer(offerId); + offer.setChallenge(challenge); coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler); } diff --git a/core/src/main/java/haveno/core/api/CoreDisputesService.java b/core/src/main/java/haveno/core/api/CoreDisputesService.java index f193287edc..f4bb4c803d 100644 --- a/core/src/main/java/haveno/core/api/CoreDisputesService.java +++ b/core/src/main/java/haveno/core/api/CoreDisputesService.java @@ -62,11 +62,12 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class CoreDisputesService { - public enum DisputePayout { + // TODO: persist in DisputeResult? + public enum PayoutSuggestion { BUYER_GETS_TRADE_AMOUNT, - BUYER_GETS_ALL, // used in desktop + BUYER_GETS_ALL, SELLER_GETS_TRADE_AMOUNT, - SELLER_GETS_ALL, // used in desktop + SELLER_GETS_ALL, CUSTOM } @@ -172,17 +173,17 @@ public class CoreDisputesService { // create dispute result var closeDate = new Date(); var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate); - DisputePayout payout; + PayoutSuggestion payoutSuggestion; if (customWinnerAmount > 0) { - payout = DisputePayout.CUSTOM; + payoutSuggestion = PayoutSuggestion.CUSTOM; } else if (winner == DisputeResult.Winner.BUYER) { - payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT; + payoutSuggestion = PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT; } else if (winner == DisputeResult.Winner.SELLER) { - payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT; + payoutSuggestion = PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT; } else { throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner); } - applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount); + applyPayoutAmountsToDisputeResult(payoutSuggestion, winningDispute, winnerDisputeResult, customWinnerAmount); // close winning dispute ticket closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> { @@ -227,26 +228,26 @@ public class CoreDisputesService { * Sets payout amounts given a payout type. If custom is selected, the winner gets a custom amount, and the peer * receives the remaining amount minus the mining fee. */ - public void applyPayoutAmountsToDisputeResult(DisputePayout payout, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) { + public void applyPayoutAmountsToDisputeResult(PayoutSuggestion payoutSuggestion, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) { Contract contract = dispute.getContract(); Trade trade = tradeManager.getTrade(dispute.getTradeId()); BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit(); BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit(); BigInteger tradeAmount = contract.getTradeAmount(); disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER); - if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) { + if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT) { disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit)); disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit); - } else if (payout == DisputePayout.BUYER_GETS_ALL) { + } else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) { disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7) disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO); - } else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) { + } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) { disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit)); - } else if (payout == DisputePayout.SELLER_GETS_ALL) { + } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) { disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit)); - } else if (payout == DisputePayout.CUSTOM) { + } else if (payoutSuggestion == PayoutSuggestion.CUSTOM) { if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance"); long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact(); if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative"); diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index 5bdd2f1605..8232827887 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -172,10 +172,12 @@ public class CoreOffersService { double marketPriceMargin, long amountAsLong, long minAmountAsLong, - double securityDeposit, + double securityDepositPct, String triggerPriceAsString, boolean reserveExactAmount, String paymentAccountId, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { coreWalletsService.verifyWalletsAreAvailable(); @@ -199,8 +201,10 @@ public class CoreOffersService { price, useMarketBasedPrice, exactMultiply(marketPriceMargin, 0.01), - securityDeposit, - paymentAccount); + securityDepositPct, + paymentAccount, + isPrivateOffer, + buyerAsTakerWithoutDeposit); verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); @@ -223,8 +227,10 @@ public class CoreOffersService { double marketPriceMargin, BigInteger amount, BigInteger minAmount, - double buyerSecurityDeposit, - PaymentAccount paymentAccount) { + double securityDepositPct, + PaymentAccount paymentAccount, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit) { return createOfferService.createAndGetOffer(offerId, direction, currencyCode.toUpperCase(), @@ -233,8 +239,10 @@ public class CoreOffersService { price, useMarketBasedPrice, exactMultiply(marketPriceMargin, 0.01), - buyerSecurityDeposit, - paymentAccount); + securityDepositPct, + paymentAccount, + isPrivateOffer, + buyerAsTakerWithoutDeposit); } void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java index 431ab9a652..14f10b4f00 100644 --- a/core/src/main/java/haveno/core/api/CoreTradesService.java +++ b/core/src/main/java/haveno/core/api/CoreTradesService.java @@ -132,7 +132,7 @@ class CoreTradesService { // adjust amount for fixed-price offer (based on TakeOfferViewModel) String currencyCode = offer.getCurrencyCode(); OfferDirection direction = offer.getOfferPayload().getDirection(); - long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction); + long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit()); if (offer.getPrice() != null) { if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) { amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit); diff --git a/core/src/main/java/haveno/core/api/model/OfferInfo.java b/core/src/main/java/haveno/core/api/model/OfferInfo.java index b489aaa8bb..537cc9ab26 100644 --- a/core/src/main/java/haveno/core/api/model/OfferInfo.java +++ b/core/src/main/java/haveno/core/api/model/OfferInfo.java @@ -78,6 +78,8 @@ public class OfferInfo implements Payload { @Nullable private final String splitOutputTxHash; private final long splitOutputTxFee; + private final boolean isPrivateOffer; + private final String challenge; public OfferInfo(OfferInfoBuilder builder) { this.id = builder.getId(); @@ -111,6 +113,8 @@ public class OfferInfo implements Payload { this.arbitratorSigner = builder.getArbitratorSigner(); this.splitOutputTxHash = builder.getSplitOutputTxHash(); this.splitOutputTxFee = builder.getSplitOutputTxFee(); + this.isPrivateOffer = builder.isPrivateOffer(); + this.challenge = builder.getChallenge(); } public static OfferInfo toOfferInfo(Offer offer) { @@ -137,6 +141,7 @@ public class OfferInfo implements Payload { .withIsActivated(isActivated) .withSplitOutputTxHash(openOffer.getSplitOutputTxHash()) .withSplitOutputTxFee(openOffer.getSplitOutputTxFee()) + .withChallenge(openOffer.getChallenge()) .build(); } @@ -177,7 +182,9 @@ public class OfferInfo implements Payload { .withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString()) .withVersionNumber(offer.getOfferPayload().getVersionNr()) .withProtocolVersion(offer.getOfferPayload().getProtocolVersion()) - .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress()); + .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress()) + .withIsPrivateOffer(offer.isPrivateOffer()) + .withChallenge(offer.getChallenge()); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -215,9 +222,11 @@ public class OfferInfo implements Payload { .setPubKeyRing(pubKeyRing) .setVersionNr(versionNumber) .setProtocolVersion(protocolVersion) - .setSplitOutputTxFee(splitOutputTxFee); + .setSplitOutputTxFee(splitOutputTxFee) + .setIsPrivateOffer(isPrivateOffer); Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner); Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash); + Optional.ofNullable(challenge).ifPresent(builder::setChallenge); return builder.build(); } @@ -255,6 +264,8 @@ public class OfferInfo implements Payload { .withArbitratorSigner(proto.getArbitratorSigner()) .withSplitOutputTxHash(proto.getSplitOutputTxHash()) .withSplitOutputTxFee(proto.getSplitOutputTxFee()) + .withIsPrivateOffer(proto.getIsPrivateOffer()) + .withChallenge(proto.getChallenge()) .build(); } } diff --git a/core/src/main/java/haveno/core/api/model/TradeInfo.java b/core/src/main/java/haveno/core/api/model/TradeInfo.java index fa94fd27f2..8df26368ba 100644 --- a/core/src/main/java/haveno/core/api/model/TradeInfo.java +++ b/core/src/main/java/haveno/core/api/model/TradeInfo.java @@ -172,14 +172,14 @@ public class TradeInfo implements Payload { .withAmount(trade.getAmount().longValueExact()) .withMakerFee(trade.getMakerFee().longValueExact()) .withTakerFee(trade.getTakerFee().longValueExact()) - .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().getSecurityDeposit().longValueExact()) - .withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit() == null ? -1 : trade.getSeller().getSecurityDeposit().longValueExact()) - .withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee() == null ? -1 : trade.getBuyer().getDepositTxFee().longValueExact()) - .withSellerDepositTxFee(trade.getSeller().getDepositTxFee() == null ? -1 : trade.getSeller().getDepositTxFee().longValueExact()) - .withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee() == null ? -1 : trade.getBuyer().getPayoutTxFee().longValueExact()) - .withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee() == null ? -1 : trade.getSeller().getPayoutTxFee().longValueExact()) - .withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount() == null ? -1 : trade.getBuyer().getPayoutAmount().longValueExact()) - .withSellerPayoutAmount(trade.getSeller().getPayoutAmount() == null ? -1 : trade.getSeller().getPayoutAmount().longValueExact()) + .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit().longValueExact()) + .withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit().longValueExact()) + .withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee().longValueExact()) + .withSellerDepositTxFee(trade.getSeller().getDepositTxFee().longValueExact()) + .withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee().longValueExact()) + .withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee().longValueExact()) + .withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount().longValueExact()) + .withSellerPayoutAmount(trade.getSeller().getPayoutAmount().longValueExact()) .withTotalTxFee(trade.getTotalTxFee().longValueExact()) .withPrice(toPreciseTradePrice.apply(trade)) .withVolume(toRoundedVolume.apply(trade)) diff --git a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java index 35d532f67f..36801cdbb6 100644 --- a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java +++ b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java @@ -63,6 +63,8 @@ public final class OfferInfoBuilder { private String arbitratorSigner; private String splitOutputTxHash; private long splitOutputTxFee; + private boolean isPrivateOffer; + private String challenge; public OfferInfoBuilder withId(String id) { this.id = id; @@ -234,6 +236,16 @@ public final class OfferInfoBuilder { return this; } + public OfferInfoBuilder withIsPrivateOffer(boolean isPrivateOffer) { + this.isPrivateOffer = isPrivateOffer; + return this; + } + + public OfferInfoBuilder withChallenge(String challenge) { + this.challenge = challenge; + return this; + } + public OfferInfo build() { return new OfferInfo(this); } diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index afd4366a3b..407953b05f 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -33,10 +33,8 @@ import haveno.core.provider.price.PriceFeedService; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatisticsManager; -import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.coin.CoinUtil; -import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; @@ -102,9 +100,10 @@ public class CreateOfferService { Price fixedPrice, boolean useMarketBasedPrice, double marketPriceMargin, - double securityDepositAsDouble, - PaymentAccount paymentAccount) { - + double securityDepositPct, + PaymentAccount paymentAccount, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit) { log.info("create and get offer with offerId={}, " + "currencyCode={}, " + "direction={}, " + @@ -113,7 +112,9 @@ public class CreateOfferService { "marketPriceMargin={}, " + "amount={}, " + "minAmount={}, " + - "securityDeposit={}", + "securityDepositPct={}, " + + "isPrivateOffer={}, " + + "buyerAsTakerWithoutDeposit={}", offerId, currencyCode, direction, @@ -122,7 +123,16 @@ public class CreateOfferService { marketPriceMargin, amount, minAmount, - securityDepositAsDouble); + securityDepositPct, + isPrivateOffer, + buyerAsTakerWithoutDeposit); + + + // verify buyer as taker security deposit + boolean isBuyerMaker = offerUtil.isBuyOffer(direction); + if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) { + throw new IllegalArgumentException("Buyer as taker deposit is required for public offers"); + } // verify fixed price xor market price with margin if (fixedPrice != null) { @@ -143,10 +153,17 @@ public class CreateOfferService { } // adjust amount and min amount for fixed-price offer - long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction); if (fixedPrice != null) { - amount = CoinUtil.getRoundedAmount(amount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId()); - minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId()); + amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); + minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); + } + + // generate one-time challenge for private offer + String challenge = null; + String challengeHash = null; + if (isPrivateOffer) { + challenge = HavenoUtils.generateChallenge(); + challengeHash = HavenoUtils.getChallengeHash(challenge); } long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L; @@ -161,21 +178,16 @@ public class CreateOfferService { String bankId = PaymentAccountUtil.getBankId(paymentAccount); List acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount); long maxTradePeriod = paymentAccount.getMaxTradePeriod(); - - // reserved for future use cases - // Use null values if not set - boolean isPrivateOffer = false; + boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit; + long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit); boolean useAutoClose = false; boolean useReOpenAfterAutoClose = false; long lowerClosePrice = 0; long upperClosePrice = 0; - String hashOfChallenge = null; - Map extraDataMap = offerUtil.getExtraDataMap(paymentAccount, - currencyCode, - direction); + Map extraDataMap = offerUtil.getExtraDataMap(paymentAccount, currencyCode, direction); offerUtil.validateOfferData( - securityDepositAsDouble, + securityDepositPct, paymentAccount, currencyCode); @@ -189,11 +201,11 @@ public class CreateOfferService { useMarketBasedPriceValue, amountAsLong, minAmountAsLong, - HavenoUtils.MAKER_FEE_PCT, - HavenoUtils.TAKER_FEE_PCT, + hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT, + hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT, HavenoUtils.PENALTY_FEE_PCT, - securityDepositAsDouble, - securityDepositAsDouble, + hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers + securityDepositPct, baseCurrencyCode, counterCurrencyCode, paymentAccount.getPaymentMethod().getId(), @@ -211,7 +223,7 @@ public class CreateOfferService { upperClosePrice, lowerClosePrice, isPrivateOffer, - hashOfChallenge, + challengeHash, extraDataMap, Version.TRADE_PROTOCOL_VERSION, null, @@ -219,38 +231,10 @@ public class CreateOfferService { null); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); + offer.setChallenge(challenge); return offer; } - public BigInteger getReservedFundsForOffer(OfferDirection direction, - BigInteger amount, - double buyerSecurityDeposit, - double sellerSecurityDeposit) { - - BigInteger reservedFundsForOffer = getSecurityDeposit(direction, - amount, - buyerSecurityDeposit, - sellerSecurityDeposit); - if (!offerUtil.isBuyOffer(direction)) - reservedFundsForOffer = reservedFundsForOffer.add(amount); - - return reservedFundsForOffer; - } - - public BigInteger getSecurityDeposit(OfferDirection direction, - BigInteger amount, - double buyerSecurityDeposit, - double sellerSecurityDeposit) { - return offerUtil.isBuyOffer(direction) ? - getBuyerSecurityDeposit(amount, buyerSecurityDeposit) : - getSellerSecurityDeposit(amount, sellerSecurityDeposit); - } - - public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) { - return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? buyerSecurityDeposit : - Restrictions.getSellerSecurityDepositAsPercent(); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @@ -259,26 +243,4 @@ public class CreateOfferService { MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); return marketPrice != null && marketPrice.isExternallyProvidedPrice(); } - - private BigInteger getBuyerSecurityDeposit(BigInteger amount, double buyerSecurityDeposit) { - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(buyerSecurityDeposit, amount); - return getBoundedBuyerSecurityDeposit(percentOfAmount); - } - - private BigInteger getSellerSecurityDeposit(BigInteger amount, double sellerSecurityDeposit) { - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(sellerSecurityDeposit, amount); - return getBoundedSellerSecurityDeposit(percentOfAmount); - } - - private BigInteger getBoundedBuyerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinBuyerSecurityDeposit from Restrictions. - return Restrictions.getMinBuyerSecurityDeposit().max(value); - } - - private BigInteger getBoundedSellerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinSellerSecurityDeposit from Restrictions. - return Restrictions.getMinSellerSecurityDeposit().max(value); - } } diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index 675fbeb550..3dc566b308 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -115,6 +115,12 @@ public class Offer implements NetworkPayload, PersistablePayload { @Setter transient private boolean isReservedFundsSpent; + @JsonExclude + @Getter + @Setter + @Nullable + transient private String challenge; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -337,6 +343,18 @@ public class Offer implements NetworkPayload, PersistablePayload { return offerPayload.getSellerSecurityDepositPct(); } + public boolean isPrivateOffer() { + return offerPayload.isPrivateOffer(); + } + + public String getChallengeHash() { + return offerPayload.getChallengeHash(); + } + + public boolean hasBuyerAsTakerWithoutDeposit() { + return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0; + } + public BigInteger getMaxTradeLimit() { return BigInteger.valueOf(offerPayload.getMaxTradeLimit()); } diff --git a/core/src/main/java/haveno/core/offer/OfferFilterService.java b/core/src/main/java/haveno/core/offer/OfferFilterService.java index 51ac8cdee7..e64a1ee6eb 100644 --- a/core/src/main/java/haveno/core/offer/OfferFilterService.java +++ b/core/src/main/java/haveno/core/offer/OfferFilterService.java @@ -201,7 +201,7 @@ public class OfferFilterService { accountAgeWitnessService); long myTradeLimit = accountOptional .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), offer.getMirroredDirection())) + offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit())) .orElse(0L); long offerMinAmount = offer.getMinAmount().longValueExact(); log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index fa05685dee..0dadf882ba 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -156,7 +156,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay // Reserved for possible future use to support private trades where the taker needs to have an accessKey private final boolean isPrivateOffer; @Nullable - private final String hashOfChallenge; + private final String challengeHash; /////////////////////////////////////////////////////////////////////////////////////////// @@ -195,7 +195,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay long lowerClosePrice, long upperClosePrice, boolean isPrivateOffer, - @Nullable String hashOfChallenge, + @Nullable String challengeHash, @Nullable Map extraDataMap, int protocolVersion, @Nullable NodeAddress arbitratorSigner, @@ -238,7 +238,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay this.lowerClosePrice = lowerClosePrice; this.upperClosePrice = upperClosePrice; this.isPrivateOffer = isPrivateOffer; - this.hashOfChallenge = hashOfChallenge; + this.challengeHash = challengeHash; } public byte[] getHash() { @@ -284,7 +284,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay lowerClosePrice, upperClosePrice, isPrivateOffer, - hashOfChallenge, + challengeHash, extraDataMap, protocolVersion, arbitratorSigner, @@ -328,12 +328,17 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) { BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getBuyerSecurityDepositPct()); - return Restrictions.getMinBuyerSecurityDeposit().max(securityDepositUnadjusted); + boolean isBuyerTaker = getDirection() == OfferDirection.SELL; + if (isPrivateOffer() && isBuyerTaker) { + return securityDepositUnadjusted; + } else { + return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted); + } } public BigInteger getSellerSecurityDepositForTradeAmount(BigInteger tradeAmount) { BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getSellerSecurityDepositPct()); - return Restrictions.getMinSellerSecurityDeposit().max(securityDepositUnadjusted); + return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -376,7 +381,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay Optional.ofNullable(bankId).ifPresent(builder::setBankId); Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds); Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes); - Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge); + Optional.ofNullable(challengeHash).ifPresent(builder::setChallengeHash); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage())); Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e))); @@ -392,7 +397,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay null : new ArrayList<>(proto.getAcceptedCountryCodesList()); List reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ? null : new ArrayList<>(proto.getReserveTxKeyImagesList()); - String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge()); + String challengeHash = ProtoUtil.stringOrNullFromProto(proto.getChallengeHash()); Map extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(); @@ -428,7 +433,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay proto.getLowerClosePrice(), proto.getUpperClosePrice(), proto.getIsPrivateOffer(), - hashOfChallenge, + challengeHash, extraDataMapMap, proto.getProtocolVersion(), proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null, @@ -475,7 +480,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay ",\r\n lowerClosePrice=" + lowerClosePrice + ",\r\n upperClosePrice=" + upperClosePrice + ",\r\n isPrivateOffer=" + isPrivateOffer + - ",\r\n hashOfChallenge='" + hashOfChallenge + '\'' + + ",\r\n challengeHash='" + challengeHash + '\'' + ",\r\n arbitratorSigner=" + arbitratorSigner + ",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + "\r\n} "; diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java index 72593ab5e7..3d4c1c376e 100644 --- a/core/src/main/java/haveno/core/offer/OfferUtil.java +++ b/core/src/main/java/haveno/core/offer/OfferUtil.java @@ -58,8 +58,8 @@ import haveno.core.trade.statistics.ReferralIdService; import haveno.core.user.AutoConfirmSettings; import haveno.core.user.Preferences; import haveno.core.util.coin.CoinFormatter; -import static haveno.core.xmr.wallet.Restrictions.getMaxBuyerSecurityDepositAsPercent; -import static haveno.core.xmr.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent; import haveno.network.p2p.P2PService; import java.math.BigInteger; import java.util.HashMap; @@ -120,9 +120,10 @@ public class OfferUtil { public long getMaxTradeLimit(PaymentAccount paymentAccount, String currencyCode, - OfferDirection direction) { + OfferDirection direction, + boolean buyerAsTakerWithoutDeposit) { return paymentAccount != null - ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction) + ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit) : 0; } @@ -228,16 +229,16 @@ public class OfferUtil { return extraDataMap.isEmpty() ? null : extraDataMap; } - public void validateOfferData(double buyerSecurityDeposit, + public void validateOfferData(double securityDeposit, PaymentAccount paymentAccount, String currencyCode) { checkNotNull(p2PService.getAddress(), "Address must not be null"); - checkArgument(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(), + checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(), "securityDeposit must not exceed " + - getMaxBuyerSecurityDepositAsPercent()); - checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(), + getMaxSecurityDepositAsPercent()); + checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(), "securityDeposit must not be less than " + - getMinBuyerSecurityDepositAsPercent() + " but was " + buyerSecurityDeposit); + getMinSecurityDepositAsPercent() + " but was " + securityDeposit); checkArgument(!filterManager.isCurrencyBanned(currencyCode), Res.get("offerbook.warning.currencyBanned")); checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()), diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index b0df3e0e35..1f177959f3 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -96,6 +96,9 @@ public final class OpenOffer implements Tradable { @Getter private String reserveTxKey; @Getter + @Setter + private String challenge; + @Getter private final long triggerPrice; @Getter @Setter @@ -107,7 +110,6 @@ public final class OpenOffer implements Tradable { @Getter @Setter transient int numProcessingAttempts = 0; - public OpenOffer(Offer offer) { this(offer, 0, false); } @@ -120,6 +122,7 @@ public final class OpenOffer implements Tradable { this.offer = offer; this.triggerPrice = triggerPrice; this.reserveExactAmount = reserveExactAmount; + this.challenge = offer.getChallenge(); state = State.PENDING; } @@ -137,6 +140,7 @@ public final class OpenOffer implements Tradable { this.reserveTxHash = openOffer.reserveTxHash; this.reserveTxHex = openOffer.reserveTxHex; this.reserveTxKey = openOffer.reserveTxKey; + this.challenge = openOffer.challenge; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -153,7 +157,8 @@ public final class OpenOffer implements Tradable { long splitOutputTxFee, @Nullable String reserveTxHash, @Nullable String reserveTxHex, - @Nullable String reserveTxKey) { + @Nullable String reserveTxKey, + @Nullable String challenge) { this.offer = offer; this.state = state; this.triggerPrice = triggerPrice; @@ -164,6 +169,7 @@ public final class OpenOffer implements Tradable { this.reserveTxHash = reserveTxHash; this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; + this.challenge = challenge; // reset reserved state to available if (this.state == State.RESERVED) setState(State.AVAILABLE); @@ -184,6 +190,7 @@ public final class OpenOffer implements Tradable { Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash)); Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); + Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge)); return protobuf.Tradable.newBuilder().setOpenOffer(builder).build(); } @@ -199,7 +206,8 @@ public final class OpenOffer implements Tradable { proto.getSplitOutputTxFee(), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), - ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey())); + ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), + ProtoUtil.stringOrNullFromProto(proto.getChallenge())); return openOffer; } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index c8390a5af0..86f45566dd 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -79,6 +79,7 @@ import haveno.core.util.JsonUtil; import haveno.core.util.Validator; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.BtcWalletService; +import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrKeyImageListener; import haveno.core.xmr.wallet.XmrKeyImagePoller; import haveno.core.xmr.wallet.TradeWalletService; @@ -1307,7 +1308,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe NodeAddress thisAddress = p2PService.getNetworkNode().getNodeAddress(); if (thisArbitrator == null || !thisArbitrator.getNodeAddress().equals(thisAddress)) { errorMessage = "Cannot sign offer because we are not a registered arbitrator"; - log.info(errorMessage); + log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } @@ -1315,47 +1316,109 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // verify arbitrator is signer of offer payload if (!thisAddress.equals(request.getOfferPayload().getArbitratorSigner())) { errorMessage = "Cannot sign offer because offer payload is for a different arbitrator"; - log.info(errorMessage); + log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } - // verify maker's trade fee + // private offers must have challenge hash Offer offer = new Offer(request.getOfferPayload()); - if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) { - errorMessage = "Wrong maker fee for offer " + request.offerId; - log.info(errorMessage); + if (offer.isPrivateOffer() && (offer.getChallengeHash() == null || offer.getChallengeHash().length() == 0)) { + errorMessage = "Private offer must have challenge hash for offer " + request.offerId; + log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } - // verify taker's trade fee - if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) { - errorMessage = "Wrong taker fee for offer " + request.offerId; - log.info(errorMessage); - sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); - return; + // verify maker and taker fees + boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0; + if (hasBuyerAsTakerWithoutDeposit) { + + // verify maker's trade fee + if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT) { + errorMessage = "Wrong maker fee for offer " + request.offerId; + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify taker's trade fee + if (offer.getTakerFeePct() != 0) { + errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected 0 but got " + offer.getTakerFeePct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify maker security deposit + if (offer.getSellerSecurityDepositPct() != Restrictions.MIN_SECURITY_DEPOSIT_PCT) { + errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify taker's security deposit + if (offer.getBuyerSecurityDepositPct() != 0) { + errorMessage = "Wrong buyer security deposit for offer " + request.offerId + ". Expected 0 but got " + offer.getBuyerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + } else { + + // verify maker's trade fee + if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) { + errorMessage = "Wrong maker fee for offer " + request.offerId; + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify taker's trade fee + if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) { + errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + HavenoUtils.TAKER_FEE_PCT + " but got " + offer.getTakerFeePct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify seller's security deposit + if (offer.getSellerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) { + errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify buyer's security deposit + if (offer.getBuyerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) { + errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getBuyerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // security deposits must be equal + if (offer.getBuyerSecurityDepositPct() != offer.getSellerSecurityDepositPct()) { + errorMessage = "Buyer and seller security deposits are not equal for offer " + request.offerId + ": " + offer.getSellerSecurityDepositPct() + " vs " + offer.getBuyerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } } // verify penalty fee if (offer.getPenaltyFeePct() != HavenoUtils.PENALTY_FEE_PCT) { errorMessage = "Wrong penalty fee for offer " + request.offerId; - log.info(errorMessage); - sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); - return; - } - - // verify security deposits are equal - if (offer.getBuyerSecurityDepositPct() != offer.getSellerSecurityDepositPct()) { - errorMessage = "Buyer and seller security deposits are not equal for offer " + request.offerId; - log.info(errorMessage); + log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify maker's reserve tx (double spend, trade fee, trade amount, mining fee) BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.PENALTY_FEE_PCT); - BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.MAKER_FEE_PCT); + BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT); BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); MoneroTx verifiedTx = xmrWalletService.verifyReserveTx( @@ -1710,7 +1773,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe originalOfferPayload.getLowerClosePrice(), originalOfferPayload.getUpperClosePrice(), originalOfferPayload.isPrivateOffer(), - originalOfferPayload.getHashOfChallenge(), + originalOfferPayload.getChallengeHash(), updatedExtraDataMap, protocolVersion, originalOfferPayload.getArbitratorSigner(), diff --git a/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java b/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java index 493ded6759..d6080e61e3 100644 --- a/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java +++ b/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java @@ -88,7 +88,8 @@ public class SendOfferAvailabilityRequest extends Task { null, // reserve tx not sent from taker to maker null, null, - payoutAddress); + payoutAddress, + null); // challenge is required when offer taken // save trade request to later send to arbitrator model.setTradeRequest(tradeRequest); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java index dfb1b2fa34..3b1a01beeb 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java @@ -21,6 +21,7 @@ import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.trade.HavenoUtils; import haveno.core.trade.messages.TradeMessage; @@ -63,8 +64,21 @@ public class ValidateOffer extends Task { checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit"); if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct()); if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct()); - if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); - if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); + offer.isPrivateOffer(); + if (offer.isPrivateOffer()) { + boolean isBuyerMaker = offer.getDirection() == OfferDirection.BUY; + if (isBuyerMaker) { + if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); + if (offer.getSellerSecurityDepositPct() < 0) throw new IllegalArgumentException("Seller security deposit percent must be >= 0% but was " + offer.getSellerSecurityDepositPct()); + } else { + if (offer.getBuyerSecurityDepositPct() < 0) throw new IllegalArgumentException("Buyer security deposit percent must be >= 0% but was " + offer.getBuyerSecurityDepositPct()); + if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); + } + } else { + if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); + if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); + } + // We remove those checks to be more flexible with future changes. /*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value, @@ -82,9 +96,9 @@ public class ValidateOffer extends Task { /*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0, "MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/ - long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection()); + long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit()); checkArgument(offer.getAmount().longValueExact() <= maxAmount, - "Amount is larger than " + HavenoUtils.atomicUnitsToXmr(offer.getPaymentMethod().getMaxTradeLimit(offer.getCurrencyCode())) + " XMR"); + "Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR"); checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount"); checkNotNull(offer.getPrice(), "Price is null"); diff --git a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java index 85d0b99bcf..49c6da12e9 100644 --- a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java +++ b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java @@ -148,7 +148,8 @@ public class TakeOfferModel implements Model { private long getMaxTradeLimit() { return accountAgeWitnessService.getMyTradeLimit(paymentAccount, offer.getCurrencyCode(), - offer.getMirroredDirection()); + offer.getMirroredDirection(), + offer.hasBuyerAsTakerWithoutDeposit()); } @NotNull diff --git a/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java b/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java index 4d7b4ad0c4..11575aeb62 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java @@ -124,7 +124,7 @@ public class PaymentAccountUtil { AccountAgeWitnessService accountAgeWitnessService) { boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), offer.getMirroredDirection()) >= offer.getMinAmount().longValueExact(); + offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact(); return !hasChargebackRisk || hasValidAccountAgeWitness; } diff --git a/core/src/main/java/haveno/core/payment/TradeLimits.java b/core/src/main/java/haveno/core/payment/TradeLimits.java index 2bfdcdbc6f..92ac29b0a8 100644 --- a/core/src/main/java/haveno/core/payment/TradeLimits.java +++ b/core/src/main/java/haveno/core/payment/TradeLimits.java @@ -31,6 +31,8 @@ import lombok.extern.slf4j.Slf4j; @Singleton public class TradeLimits { private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(528); // max trade limit for lowest risk payment method. Others will get derived from that. + private static final BigInteger MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT = HavenoUtils.xmrToAtomicUnits(1); // max trade limit without deposit from buyer + @Nullable @Getter private static TradeLimits INSTANCE; @@ -57,6 +59,15 @@ public class TradeLimits { return MAX_TRADE_LIMIT; } + /** + * The maximum trade limit without a buyer deposit. + * + * @return the maximum trade limit for a buyer without a deposit + */ + public BigInteger getMaxTradeLimitBuyerAsTakerWithoutDeposit() { + return MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT; + } + // We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account // age witness is not considered anymore (> 2 months). diff --git a/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java b/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java index 7bf873ce4f..4545a4e210 100644 --- a/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java +++ b/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java @@ -59,7 +59,7 @@ public class SecurityDepositValidator extends NumberValidator { private ValidationResult validateIfNotTooLowPercentageValue(String input) { try { double percentage = ParsingUtils.parsePercentStringToDouble(input); - double minPercentage = Restrictions.getMinBuyerSecurityDepositAsPercent(); + double minPercentage = Restrictions.getMinSecurityDepositAsPercent(); if (percentage < minPercentage) return new ValidationResult(false, Res.get("validation.inputTooSmall", FormattingUtils.formatToPercentWithSymbol(minPercentage))); @@ -73,7 +73,7 @@ public class SecurityDepositValidator extends NumberValidator { private ValidationResult validateIfNotTooHighPercentageValue(String input) { try { double percentage = ParsingUtils.parsePercentStringToDouble(input); - double maxPercentage = Restrictions.getMaxBuyerSecurityDepositAsPercent(); + double maxPercentage = Restrictions.getMaxSecurityDepositAsPercent(); if (percentage > maxPercentage) return new ValidationResult(false, Res.get("validation.inputTooLarge", FormattingUtils.formatToPercentWithSymbol(maxPercentage))); diff --git a/core/src/main/java/haveno/core/trade/ArbitratorTrade.java b/core/src/main/java/haveno/core/trade/ArbitratorTrade.java index 93f03dece5..ea179a655a 100644 --- a/core/src/main/java/haveno/core/trade/ArbitratorTrade.java +++ b/core/src/main/java/haveno/core/trade/ArbitratorTrade.java @@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j; import java.math.BigInteger; import java.util.UUID; +import javax.annotation.Nullable; + /** * Trade in the context of an arbitrator. */ @@ -42,8 +44,9 @@ public class ArbitratorTrade extends Trade { String uid, NodeAddress makerNodeAddress, NodeAddress takerNodeAddress, - NodeAddress arbitratorNodeAddress) { - super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress); + NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { + super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge); } @Override @@ -81,7 +84,8 @@ public class ArbitratorTrade extends Trade { uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null), + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getChallenge())), proto, coreProtoResolver); } diff --git a/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java b/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java index 99fad94ebe..ca38f1ca06 100644 --- a/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java +++ b/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java @@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j; import java.math.BigInteger; import java.util.UUID; +import javax.annotation.Nullable; + @Slf4j public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { @@ -43,7 +45,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { String uid, NodeAddress makerNodeAddress, NodeAddress takerNodeAddress, - NodeAddress arbitratorNodeAddress) { + NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -52,7 +55,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -85,7 +89,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null); + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getChallenge())); trade.setPrice(proto.getPrice()); diff --git a/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java b/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java index 1cb7b20d47..2b9501919c 100644 --- a/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java +++ b/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java @@ -44,7 +44,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -53,7 +54,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } @@ -87,7 +89,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null), + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getChallenge())), proto, coreProtoResolver); } diff --git a/core/src/main/java/haveno/core/trade/BuyerTrade.java b/core/src/main/java/haveno/core/trade/BuyerTrade.java index dbf73db101..9de8f1e03f 100644 --- a/core/src/main/java/haveno/core/trade/BuyerTrade.java +++ b/core/src/main/java/haveno/core/trade/BuyerTrade.java @@ -38,7 +38,8 @@ public abstract class BuyerTrade extends Trade { String uid, @Nullable NodeAddress takerNodeAddress, @Nullable NodeAddress makerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -47,7 +48,8 @@ public abstract class BuyerTrade extends Trade { uid, takerNodeAddress, makerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } @Override diff --git a/core/src/main/java/haveno/core/trade/Contract.java b/core/src/main/java/haveno/core/trade/Contract.java index b0950c552f..9a88eaff56 100644 --- a/core/src/main/java/haveno/core/trade/Contract.java +++ b/core/src/main/java/haveno/core/trade/Contract.java @@ -36,6 +36,7 @@ package haveno.core.trade; import com.google.protobuf.ByteString; import haveno.common.crypto.PubKeyRing; +import haveno.common.proto.ProtoUtil; import haveno.common.proto.network.NetworkPayload; import haveno.common.util.JsonExclude; import haveno.common.util.Utilities; @@ -53,6 +54,7 @@ import org.apache.commons.lang3.StringUtils; import javax.annotation.Nullable; import java.math.BigInteger; +import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; @@ -79,6 +81,7 @@ public final class Contract implements NetworkPayload { private final String makerPayoutAddressString; private final String takerPayoutAddressString; private final String makerDepositTxHash; + @Nullable private final String takerDepositTxHash; public Contract(OfferPayload offerPayload, @@ -99,7 +102,7 @@ public final class Contract implements NetworkPayload { String makerPayoutAddressString, String takerPayoutAddressString, String makerDepositTxHash, - String takerDepositTxHash) { + @Nullable String takerDepositTxHash) { this.offerPayload = offerPayload; this.tradeAmount = tradeAmount; this.tradePrice = tradePrice; @@ -134,6 +137,31 @@ public final class Contract implements NetworkPayload { // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// + @Override + public protobuf.Contract toProtoMessage() { + protobuf.Contract.Builder builder = protobuf.Contract.newBuilder() + .setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()) + .setTradeAmount(tradeAmount) + .setTradePrice(tradePrice) + .setBuyerNodeAddress(buyerNodeAddress.toProtoMessage()) + .setSellerNodeAddress(sellerNodeAddress.toProtoMessage()) + .setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()) + .setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker) + .setMakerAccountId(makerAccountId) + .setTakerAccountId(takerAccountId) + .setMakerPaymentMethodId(makerPaymentMethodId) + .setTakerPaymentMethodId(takerPaymentMethodId) + .setMakerPaymentAccountPayloadHash(ByteString.copyFrom(makerPaymentAccountPayloadHash)) + .setTakerPaymentAccountPayloadHash(ByteString.copyFrom(takerPaymentAccountPayloadHash)) + .setMakerPubKeyRing(makerPubKeyRing.toProtoMessage()) + .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage()) + .setMakerPayoutAddressString(makerPayoutAddressString) + .setTakerPayoutAddressString(takerPayoutAddressString) + .setMakerDepositTxHash(makerDepositTxHash); + Optional.ofNullable(takerDepositTxHash).ifPresent(builder::setTakerDepositTxHash); + return builder.build(); + } + public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver coreProtoResolver) { return new Contract(OfferPayload.fromProto(proto.getOfferPayload()), proto.getTradeAmount(), @@ -153,32 +181,7 @@ public final class Contract implements NetworkPayload { proto.getMakerPayoutAddressString(), proto.getTakerPayoutAddressString(), proto.getMakerDepositTxHash(), - proto.getTakerDepositTxHash()); - } - - @Override - public protobuf.Contract toProtoMessage() { - return protobuf.Contract.newBuilder() - .setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()) - .setTradeAmount(tradeAmount) - .setTradePrice(tradePrice) - .setBuyerNodeAddress(buyerNodeAddress.toProtoMessage()) - .setSellerNodeAddress(sellerNodeAddress.toProtoMessage()) - .setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()) - .setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker) - .setMakerAccountId(makerAccountId) - .setTakerAccountId(takerAccountId) - .setMakerPaymentMethodId(makerPaymentMethodId) - .setTakerPaymentMethodId(takerPaymentMethodId) - .setMakerPaymentAccountPayloadHash(ByteString.copyFrom(makerPaymentAccountPayloadHash)) - .setTakerPaymentAccountPayloadHash(ByteString.copyFrom(takerPaymentAccountPayloadHash)) - .setMakerPubKeyRing(makerPubKeyRing.toProtoMessage()) - .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage()) - .setMakerPayoutAddressString(makerPayoutAddressString) - .setTakerPayoutAddressString(takerPayoutAddressString) - .setMakerDepositTxHash(makerDepositTxHash) - .setTakerDepositTxHash(takerDepositTxHash) - .build(); + ProtoUtil.stringOrNullFromProto(proto.getTakerDepositTxHash())); } diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index 06133e1689..fcd5c556e1 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -28,6 +28,7 @@ import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; import haveno.common.file.FileUtil; +import haveno.common.util.Base64; import haveno.common.util.Utilities; import haveno.core.api.CoreNotificationService; import haveno.core.api.XmrConnectionService; @@ -48,7 +49,10 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.net.InetAddress; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.security.PrivateKey; +import java.security.SecureRandom; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.SimpleDateFormat; @@ -87,13 +91,15 @@ public class HavenoUtils { // configure fees public static final boolean ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS = true; + public static final double PENALTY_FEE_PCT = 0.02; // 2% public static final double MAKER_FEE_PCT = 0.0015; // 0.15% public static final double TAKER_FEE_PCT = 0.0075; // 0.75% - public static final double PENALTY_FEE_PCT = 0.02; // 2% + public static final double MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT = MAKER_FEE_PCT + TAKER_FEE_PCT; // customize maker's fee when no deposit or fee from taker // other configuration public static final long LOG_POLL_ERROR_PERIOD_MS = 1000 * 60 * 4; // log poll errors up to once every 4 minutes public static final long LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS = 1000 * 30; // log warnings when daemon not synced once every 30s + public static final int PRIVATE_OFFER_PASSPHRASE_NUM_WORDS = 8; // number of words in a private offer passphrase // synchronize requests to the daemon private static boolean SYNC_DAEMON_REQUESTS = false; // sync long requests to daemon (e.g. refresh, update pool) // TODO: performance suffers by syncing daemon requests, but otherwise we sometimes get sporadic errors? @@ -286,6 +292,41 @@ public class HavenoUtils { // ------------------------ SIGNING AND VERIFYING ------------------------- + public static String generateChallenge() { + try { + + // load bip39 words + String fileName = "bip39_english.txt"; + File bip39File = new File(havenoSetup.getConfig().appDataDir, fileName); + if (!bip39File.exists()) FileUtil.resourceToFile(fileName, bip39File); + List bip39Words = Files.readAllLines(bip39File.toPath(), StandardCharsets.UTF_8); + + // select words randomly + List passphraseWords = new ArrayList(); + SecureRandom secureRandom = new SecureRandom(); + for (int i = 0; i < PRIVATE_OFFER_PASSPHRASE_NUM_WORDS; i++) { + passphraseWords.add(bip39Words.get(secureRandom.nextInt(bip39Words.size()))); + } + return String.join(" ", passphraseWords); + } catch (Exception e) { + throw new IllegalStateException("Failed to generate challenge", e); + } + } + + public static String getChallengeHash(String challenge) { + if (challenge == null) return null; + + // tokenize passphrase + String[] words = challenge.toLowerCase().split(" "); + + // collect first 4 letters of each word, which are unique in bip39 + List prefixes = new ArrayList(); + for (String word : words) prefixes.add(word.substring(0, Math.min(word.length(), 4))); + + // hash the result + return Base64.encode(Hash.getSha256Hash(String.join(" ", prefixes).getBytes())); + } + public static byte[] sign(KeyRing keyRing, String message) { return sign(keyRing.getSignatureKeyPair().getPrivate(), message); } diff --git a/core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java b/core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java index c31c325342..07f4a16157 100644 --- a/core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java +++ b/core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java @@ -44,7 +44,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -53,7 +54,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } @@ -87,7 +89,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null); + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getChallenge())); trade.setPrice(proto.getPrice()); diff --git a/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java b/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java index afca9346d1..4b5e594a1e 100644 --- a/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java +++ b/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java @@ -44,7 +44,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -53,7 +54,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } @@ -87,7 +89,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null), + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getChallenge())), proto, coreProtoResolver); } diff --git a/core/src/main/java/haveno/core/trade/SellerTrade.java b/core/src/main/java/haveno/core/trade/SellerTrade.java index 457ea10aed..fae3cce7a1 100644 --- a/core/src/main/java/haveno/core/trade/SellerTrade.java +++ b/core/src/main/java/haveno/core/trade/SellerTrade.java @@ -36,7 +36,8 @@ public abstract class SellerTrade extends Trade { String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -45,7 +46,8 @@ public abstract class SellerTrade extends Trade { uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } @Override diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index b540351179..95b067b28a 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -486,6 +486,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private IdlePayoutSyncer idlePayoutSyncer; @Getter private boolean isCompleted; + @Getter + private final String challenge; /////////////////////////////////////////////////////////////////////////////////////////// // Constructors @@ -500,7 +502,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(); this.offer = offer; this.amount = tradeAmount.longValueExact(); @@ -511,6 +514,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { this.uid = uid; this.takeOfferDate = new Date().getTime(); this.tradeListeners = new ArrayList(); + this.challenge = challenge; getMaker().setNodeAddress(makerNodeAddress); getTaker().setNodeAddress(takerNodeAddress); @@ -534,7 +538,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { this(offer, tradeAmount, @@ -544,7 +549,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } // TODO: remove these constructors @@ -559,7 +565,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { NodeAddress arbitratorNodeAddress, XmrWalletService xmrWalletService, ProcessModel processModel, - String uid) { + String uid, + @Nullable String challenge) { this(offer, tradeAmount, @@ -569,7 +576,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); setAmount(tradeAmount); } @@ -1233,7 +1241,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null"); Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null"); BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount(); - BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount(); + BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount(); BigInteger tradeAmount = getAmount(); BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount); BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount); @@ -1324,7 +1332,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { MoneroWallet wallet = getWallet(); Contract contract = getContract(); BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount(); - BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount(); + BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount(); BigInteger tradeAmount = getAmount(); // describe payout tx @@ -2091,9 +2099,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { final long tradeTime = getTakeOfferDate().getTime(); MoneroDaemon daemonRpc = xmrWalletService.getDaemon(); if (daemonRpc == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod"); - if (getMakerDepositTx() == null || getTakerDepositTx() == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?"); + if (getMakerDepositTx() == null || (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?"); - long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight()); + long maxHeight = Math.max(getMakerDepositTx().getHeight(), hasBuyerAsTakerWithoutDeposit() ? 0l : getTakerDepositTx().getHeight()); long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp(); // If block date is in future (Date in blocks can be off by +/- 2 hours) we use our current date. @@ -2125,7 +2133,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public boolean isDepositsPublished() { if (isDepositFailed()) return false; - return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && getTaker().getDepositTxHash() != null; + return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && (getTaker().getDepositTxHash() != null || hasBuyerAsTakerWithoutDeposit()); } public boolean isFundsLockedIn() { @@ -2277,7 +2285,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public BigInteger getTakerFee() { - return offer.getTakerFee(getAmount()); + return hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : offer.getTakerFee(getAmount()); + } + + public BigInteger getSecurityDepositBeforeMiningFee() { + return isBuyer() ? getBuyerSecurityDepositBeforeMiningFee() : getSellerSecurityDepositBeforeMiningFee(); } public BigInteger getBuyerSecurityDepositBeforeMiningFee() { @@ -2288,6 +2300,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(getAmount()); } + public boolean isBuyerAsTakerWithoutDeposit() { + return isBuyer() && isTaker() && BigInteger.ZERO.equals(getBuyerSecurityDepositBeforeMiningFee()); + } + + public boolean hasBuyerAsTakerWithoutDeposit() { + return getBuyer() == getTaker() && BigInteger.ZERO.equals(getBuyerSecurityDepositBeforeMiningFee()); + } + @Override public BigInteger getTotalTxFee() { return getSelf().getDepositTxFee().add(getSelf().getPayoutTxFee()); // sum my tx fees @@ -2303,7 +2323,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isTxChainInvalid() { - return processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null; + return processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit()); } /** @@ -2537,7 +2557,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (isPayoutUnlocked()) return; // skip if deposit txs unknown or not requested - if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return; + if (!isDepositRequested() || processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if daemon not synced if (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance()) return; @@ -2553,7 +2573,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // get txs from trade wallet MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); - Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null); + Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && hasBuyerAsTakerWithoutDeposit())); if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible List txs; if (!updatePool) txs = wallet.getTxs(query); @@ -2565,22 +2585,22 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } setDepositTxs(txs); - if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen + if (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if either deposit tx not seen setStateDepositsSeen(); // set actual security deposits if (getBuyer().getSecurityDeposit().longValueExact() == 0) { - BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); + BigInteger buyerSecurityDeposit = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); getBuyer().setSecurityDeposit(buyerSecurityDeposit); getSeller().setSecurityDeposit(sellerSecurityDeposit); } // check for deposit txs confirmation - if (getMaker().getDepositTx().isConfirmed() && getTaker().getDepositTx().isConfirmed()) setStateDepositsConfirmed(); + if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) setStateDepositsConfirmed(); // check for deposit txs unlocked - if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) { + if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) { setStateDepositsUnlocked(); } } @@ -2750,7 +2770,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { log.warn("Missing maker deposit tx for {} {}", getClass().getSimpleName(), getId()); return true; } - if (getTakerDepositTx() == null) { + if (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit()) { log.warn("Missing taker deposit tx for {} {}", getClass().getSimpleName(), getId()); return true; } @@ -2913,6 +2933,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex)); Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxKey(payoutTxKey)); Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData)); + Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge)); return builder.build(); } @@ -2982,6 +3003,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ",\n refundResultState=" + refundResultState + ",\n refundResultStateProperty=" + refundResultStateProperty + ",\n isCompleted=" + isCompleted + + ",\n challenge='" + challenge + '\'' + "\n}"; } } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 9a3d84cbb2..a3cca84912 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -561,6 +561,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi OpenOffer openOffer = openOfferOptional.get(); if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; Offer offer = openOffer.getOffer(); + + // validate challenge + if (openOffer.getChallenge() != null && !HavenoUtils.getChallengeHash(openOffer.getChallenge()).equals(HavenoUtils.getChallengeHash(request.getChallenge()))) { + log.warn("Ignoring InitTradeRequest to maker because challenge is incorrect, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } // ensure trade does not already exist Optional tradeOptional = getOpenTrade(request.getOfferId()); @@ -583,7 +589,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); + request.getArbitratorNodeAddress(), + openOffer.getChallenge()); else trade = new SellerAsMakerTrade(offer, BigInteger.valueOf(request.getTradeAmount()), @@ -593,7 +600,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); + request.getArbitratorNodeAddress(), + openOffer.getChallenge()); trade.getMaker().setPaymentAccountId(trade.getOffer().getOfferPayload().getMakerPaymentAccountId()); trade.getTaker().setPaymentAccountId(request.getTakerPaymentAccountId()); trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing()); @@ -646,6 +654,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } + // validate challenge hash + if (offer.getChallengeHash() != null && !offer.getChallengeHash().equals(HavenoUtils.getChallengeHash(request.getChallenge()))) { + log.warn("Ignoring InitTradeRequest to arbitrator because challenge hash is incorrect, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } + // handle trade Trade trade; Optional tradeOptional = getOpenTrade(offer.getId()); @@ -679,7 +693,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); + request.getArbitratorNodeAddress(), + request.getChallenge()); // set reserve tx hash if available Optional signedOfferOptional = openOfferManager.getSignedOfferById(request.getOfferId()); @@ -873,7 +888,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi UUID.randomUUID().toString(), offer.getMakerNodeAddress(), P2PService.getMyNodeAddress(), - null); + null, + offer.getChallenge()); } else { trade = new BuyerAsTakerTrade(offer, amount, @@ -883,7 +899,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi UUID.randomUUID().toString(), offer.getMakerNodeAddress(), P2PService.getMyNodeAddress(), - null); + null, + offer.getChallenge()); } trade.getProcessModel().setUseSavingsWallet(useSavingsWallet); trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact()); @@ -1127,7 +1144,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi log.warn("We found a closed trade with locked up funds. " + "That should never happen. trade ID={} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); } - } else { + } else if (!trade.hasBuyerAsTakerWithoutDeposit()) { log.warn("Closed trade with locked up funds missing taker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); } diff --git a/core/src/main/java/haveno/core/trade/messages/DepositRequest.java b/core/src/main/java/haveno/core/trade/messages/DepositRequest.java index c743595ed4..b0ac60af85 100644 --- a/core/src/main/java/haveno/core/trade/messages/DepositRequest.java +++ b/core/src/main/java/haveno/core/trade/messages/DepositRequest.java @@ -33,7 +33,9 @@ import java.util.Optional; public final class DepositRequest extends TradeMessage implements DirectMessage { private final long currentDate; private final byte[] contractSignature; + @Nullable private final String depositTxHex; + @Nullable private final String depositTxKey; @Nullable private final byte[] paymentAccountKey; @@ -43,8 +45,8 @@ public final class DepositRequest extends TradeMessage implements DirectMessage String messageVersion, long currentDate, byte[] contractSignature, - String depositTxHex, - String depositTxKey, + @Nullable String depositTxHex, + @Nullable String depositTxKey, @Nullable byte[] paymentAccountKey) { super(messageVersion, tradeId, uid); this.currentDate = currentDate; @@ -63,13 +65,12 @@ public final class DepositRequest extends TradeMessage implements DirectMessage public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.DepositRequest.Builder builder = protobuf.DepositRequest.newBuilder() .setTradeId(offerId) - .setUid(uid) - .setDepositTxHex(depositTxHex) - .setDepositTxKey(depositTxKey); + .setUid(uid); builder.setCurrentDate(currentDate); Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e))); + Optional.ofNullable(depositTxHex).ifPresent(builder::setDepositTxHex); + Optional.ofNullable(depositTxKey).ifPresent(builder::setDepositTxKey); Optional.ofNullable(contractSignature).ifPresent(e -> builder.setContractSignature(ByteString.copyFrom(e))); - return getNetworkEnvelopeBuilder().setDepositRequest(builder).build(); } @@ -81,8 +82,8 @@ public final class DepositRequest extends TradeMessage implements DirectMessage messageVersion, proto.getCurrentDate(), ProtoUtil.byteArrayOrNullFromProto(proto.getContractSignature()), - proto.getDepositTxHex(), - proto.getDepositTxKey(), + ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()), + ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()), ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey())); } diff --git a/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java b/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java index 4846487598..817162addd 100644 --- a/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java +++ b/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java @@ -58,6 +58,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag private final String reserveTxKey; @Nullable private final String payoutAddress; + @Nullable + private final String challenge; public InitTradeRequest(TradeProtocolVersion tradeProtocolVersion, String offerId, @@ -79,7 +81,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag @Nullable String reserveTxHash, @Nullable String reserveTxHex, @Nullable String reserveTxKey, - @Nullable String payoutAddress) { + @Nullable String payoutAddress, + @Nullable String challenge) { super(messageVersion, offerId, uid); this.tradeProtocolVersion = tradeProtocolVersion; this.tradeAmount = tradeAmount; @@ -99,6 +102,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; this.payoutAddress = payoutAddress; + this.challenge = challenge; } @@ -129,6 +133,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); Optional.ofNullable(payoutAddress).ifPresent(e -> builder.setPayoutAddress(payoutAddress)); + Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge)); Optional.ofNullable(accountAgeWitnessSignatureOfOfferId).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(e))); builder.setCurrentDate(currentDate); @@ -158,7 +163,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), - ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress())); + ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress()), + ProtoUtil.stringOrNullFromProto(proto.getChallenge())); } @Override @@ -183,6 +189,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag ",\n reserveTxHex=" + reserveTxHex + ",\n reserveTxKey=" + reserveTxKey + ",\n payoutAddress=" + payoutAddress + + ",\n challenge=" + challenge + "\n} " + super.toString(); } } diff --git a/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java b/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java index 8945904421..ab82e13dec 100644 --- a/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java @@ -35,7 +35,9 @@ public final class SignContractRequest extends TradeMessage implements DirectMes private final String accountId; private final byte[] paymentAccountPayloadHash; private final String payoutAddress; + @Nullable private final String depositTxHash; + @Nullable private final byte[] accountAgeWitnessSignatureOfDepositHash; public SignContractRequest(String tradeId, @@ -45,7 +47,7 @@ public final class SignContractRequest extends TradeMessage implements DirectMes String accountId, byte[] paymentAccountPayloadHash, String payoutAddress, - String depositTxHash, + @Nullable String depositTxHash, @Nullable byte[] accountAgeWitnessSignatureOfDepositHash) { super(messageVersion, tradeId, uid); this.currentDate = currentDate; @@ -68,10 +70,9 @@ public final class SignContractRequest extends TradeMessage implements DirectMes .setUid(uid) .setAccountId(accountId) .setPaymentAccountPayloadHash(ByteString.copyFrom(paymentAccountPayloadHash)) - .setPayoutAddress(payoutAddress) - .setDepositTxHash(depositTxHash); - + .setPayoutAddress(payoutAddress); Optional.ofNullable(accountAgeWitnessSignatureOfDepositHash).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfDepositHash(ByteString.copyFrom(e))); + Optional.ofNullable(depositTxHash).ifPresent(builder::setDepositTxHash); builder.setCurrentDate(currentDate); return getNetworkEnvelopeBuilder().setSignContractRequest(builder).build(); @@ -87,7 +88,7 @@ public final class SignContractRequest extends TradeMessage implements DirectMes proto.getAccountId(), proto.getPaymentAccountPayloadHash().toByteArray(), proto.getPayoutAddress(), - proto.getDepositTxHash(), + ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash()), ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfDepositHash())); } diff --git a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java index b076826b95..eeef2d4daf 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java @@ -158,7 +158,6 @@ public final class TradePeer implements PersistablePayload { } public BigInteger getSecurityDeposit() { - if (depositTxHash == null) return null; return BigInteger.valueOf(securityDeposit); } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java index 54ced82510..5bd7ab9d80 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java @@ -36,8 +36,9 @@ import monero.daemon.model.MoneroSubmitTxResult; import monero.daemon.model.MoneroTx; import java.math.BigInteger; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.UUID; @Slf4j @@ -83,72 +84,86 @@ public class ArbitratorProcessDepositRequest extends TradeTask { byte[] signature = request.getContractSignature(); // get trader info - TradePeer trader = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); - if (trader == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator"); - PubKeyRing peerPubKeyRing = trader.getPubKeyRing(); + TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); + if (sender == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator"); + PubKeyRing senderPubKeyRing = sender.getPubKeyRing(); // verify signature - if (!HavenoUtils.isSignatureValid(peerPubKeyRing, contractAsJson, signature)) { + if (!HavenoUtils.isSignatureValid(senderPubKeyRing, contractAsJson, signature)) { throw new RuntimeException("Peer's contract signature is invalid"); } // set peer's signature - trader.setContractSignature(signature); + sender.setContractSignature(signature); // collect expected values Offer offer = trade.getOffer(); - boolean isFromTaker = trader == trade.getTaker(); - boolean isFromBuyer = trader == trade.getBuyer(); + boolean isFromTaker = sender == trade.getTaker(); + boolean isFromBuyer = sender == trade.getBuyer(); BigInteger tradeFee = isFromTaker ? trade.getTakerFee() : trade.getMakerFee(); BigInteger sendTradeAmount = isFromBuyer ? BigInteger.ZERO : trade.getAmount(); BigInteger securityDeposit = isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); String depositAddress = processModel.getMultisigAddress(); + sender.setSecurityDeposit(securityDeposit); // verify deposit tx - MoneroTx verifiedTx; - try { - verifiedTx = trade.getXmrWalletService().verifyDepositTx( - offer.getId(), - tradeFee, - trade.getProcessModel().getTradeFeeAddress(), - sendTradeAmount, - securityDeposit, - depositAddress, - trader.getDepositTxHash(), - request.getDepositTxHex(), - request.getDepositTxKey(), - null); - } catch (Exception e) { - throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + trader.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); + boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && isFromTaker && trade.hasBuyerAsTakerWithoutDeposit(); + if (!isFromBuyerAsTakerWithoutDeposit) { + MoneroTx verifiedTx; + try { + verifiedTx = trade.getXmrWalletService().verifyDepositTx( + offer.getId(), + tradeFee, + trade.getProcessModel().getTradeFeeAddress(), + sendTradeAmount, + securityDeposit, + depositAddress, + sender.getDepositTxHash(), + request.getDepositTxHex(), + request.getDepositTxKey(), + null); + } catch (Exception e) { + throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + sender.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); + } + + // update trade state + sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit + sender.setDepositTxFee(verifiedTx.getFee()); + sender.setDepositTxHex(request.getDepositTxHex()); + sender.setDepositTxKey(request.getDepositTxKey()); } // update trade state - trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit - trader.setDepositTxFee(verifiedTx.getFee()); - trader.setDepositTxHex(request.getDepositTxHex()); - trader.setDepositTxKey(request.getDepositTxKey()); - if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey()); + if (request.getPaymentAccountKey() != null) sender.setPaymentAccountKey(request.getPaymentAccountKey()); processModel.getTradeManager().requestPersistence(); - // relay deposit txs when both available + // relay deposit txs when both requests received MoneroDaemon daemon = trade.getXmrWalletService().getDaemon(); - if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) { + if (processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) { // check timeout and extend just before relaying if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out before relaying deposit txs for {} {}" + trade.getClass().getSimpleName() + " " + trade.getShortId()); trade.addInitProgressStep(); + // relay deposit txs boolean depositTxsRelayed = false; + List txHashes = new ArrayList<>(); try { - // submit txs to pool but do not relay + // submit maker tx to pool but do not relay MoneroSubmitTxResult makerResult = daemon.submitTxHex(processModel.getMaker().getDepositTxHex(), true); - MoneroSubmitTxResult takerResult = daemon.submitTxHex(processModel.getTaker().getDepositTxHex(), true); if (!makerResult.isGood()) throw new RuntimeException("Error submitting maker deposit tx: " + JsonUtils.serialize(makerResult)); - if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult)); + txHashes.add(processModel.getMaker().getDepositTxHash()); + + // submit taker tx to pool but do not relay + if (!trade.hasBuyerAsTakerWithoutDeposit()) { + MoneroSubmitTxResult takerResult = daemon.submitTxHex(processModel.getTaker().getDepositTxHex(), true); + if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult)); + txHashes.add(processModel.getTaker().getDepositTxHash()); + } // relay txs - daemon.relayTxsByHash(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash())); + daemon.relayTxsByHash(txHashes); depositTxsRelayed = true; // update trade state @@ -160,7 +175,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { // flush txs from pool try { - daemon.flushTxPool(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()); + daemon.flushTxPool(txHashes); } catch (Exception e2) { log.warn("Error flushing deposit txs from pool for trade {}: {}\n", trade.getId(), e2.getMessage(), e2); } @@ -180,7 +195,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { }); if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId()); - if (processModel.getTaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId()); + if (processModel.getTaker().getDepositTxHex() == null && !trade.hasBuyerAsTakerWithoutDeposit()) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId()); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java index 10baac8567..18e97dd466 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java @@ -53,38 +53,44 @@ public class ArbitratorProcessReserveTx extends TradeTask { TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); boolean isFromMaker = sender == trade.getMaker(); boolean isFromBuyer = isFromMaker ? offer.getDirection() == OfferDirection.BUY : offer.getDirection() == OfferDirection.SELL; + sender = isFromMaker ? processModel.getMaker() : processModel.getTaker(); + BigInteger securityDeposit = isFromMaker ? isFromBuyer ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit() : isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); + sender.setSecurityDeposit(securityDeposit); // TODO (woodser): if signer online, should never be called by maker? - // process reserve tx with expected values - BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct()); - BigInteger tradeFee = isFromMaker ? offer.getMaxMakerFee() : trade.getTakerFee(); - BigInteger sendAmount = isFromBuyer ? BigInteger.ZERO : isFromMaker ? offer.getAmount() : trade.getAmount(); // maker reserve tx is for offer amount - BigInteger securityDeposit = isFromMaker ? isFromBuyer ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit() : isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); - MoneroTx verifiedTx; - try { - verifiedTx = trade.getXmrWalletService().verifyReserveTx( - offer.getId(), - penaltyFee, - tradeFee, - sendAmount, - securityDeposit, - request.getPayoutAddress(), - request.getReserveTxHash(), - request.getReserveTxHex(), - request.getReserveTxKey(), - null); - } catch (Exception e) { - log.error(ExceptionUtils.getStackTrace(e)); - throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); - } + // process reserve tx unless from buyer as taker without deposit + boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && !isFromMaker && trade.hasBuyerAsTakerWithoutDeposit(); + if (!isFromBuyerAsTakerWithoutDeposit) { - // save reserve tx to model - TradePeer trader = isFromMaker ? processModel.getMaker() : processModel.getTaker(); - trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit - trader.setReserveTxHash(request.getReserveTxHash()); - trader.setReserveTxHex(request.getReserveTxHex()); - trader.setReserveTxKey(request.getReserveTxKey()); + // process reserve tx with expected values + BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct()); + BigInteger tradeFee = isFromMaker ? offer.getMaxMakerFee() : trade.getTakerFee(); + BigInteger sendAmount = isFromBuyer ? BigInteger.ZERO : isFromMaker ? offer.getAmount() : trade.getAmount(); // maker reserve tx is for offer amount + MoneroTx verifiedTx; + try { + verifiedTx = trade.getXmrWalletService().verifyReserveTx( + offer.getId(), + penaltyFee, + tradeFee, + sendAmount, + securityDeposit, + request.getPayoutAddress(), + request.getReserveTxHash(), + request.getReserveTxHex(), + request.getReserveTxKey(), + null); + } catch (Exception e) { + log.error(ExceptionUtils.getStackTrace(e)); + throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); + } + + // save reserve tx to model + sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit + sender.setReserveTxHash(request.getReserveTxHash()); + sender.setReserveTxHex(request.getReserveTxHex()); + sender.setReserveTxKey(request.getReserveTxKey()); + } // persist trade processModel.getTradeManager().requestPersistence(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java index 9ec7aebe28..a846077db4 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java @@ -78,6 +78,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { null, null, null, + null, null); // send request to taker @@ -118,7 +119,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { // ensure arbitrator has reserve txs if (processModel.getMaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have maker's reserve tx after initializing trade"); - if (processModel.getTaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade"); + if (processModel.getTaker().getReserveTxHash() == null && !trade.hasBuyerAsTakerWithoutDeposit()) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade"); // create wallet for multisig MoneroWallet multisigWallet = trade.createWallet(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java index 8b739011d5..05fee1374a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java @@ -74,7 +74,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask { Preconditions.checkNotNull(trade.getSeller().getPaymentAccountPayload(), "Seller's payment account payload is null"); Preconditions.checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null"); Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null"); - Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null"); + if (!trade.hasBuyerAsTakerWithoutDeposit()) Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null"); checkNotNull(trade.getOffer(), "offer must not be null"); // create payout tx if we have seller's updated multisig hex diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java index 91398e82e1..9f191131ec 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java @@ -138,7 +138,8 @@ public class MakerSendInitTradeRequestToArbitrator extends TradeTask { trade.getSelf().getReserveTxHash(), trade.getSelf().getReserveTxHex(), trade.getSelf().getReserveTxKey(), - model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); + model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(), + trade.getChallenge()); // send request to arbitrator log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress()); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index 69d1620aea..e1c4cce5cc 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -83,7 +83,7 @@ public class MaybeSendSignContractRequest extends TradeTask { if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create deposit tx, tradeId=" + trade.getShortId()); trade.startProtocolTimeout(); - // collect relevant info + // collect info Integer subaddressIndex = null; boolean reserveExactAmount = false; if (trade instanceof MakerTrade) { @@ -97,53 +97,60 @@ public class MaybeSendSignContractRequest extends TradeTask { } // attempt creating deposit tx - try { - synchronized (HavenoUtils.getWalletFunctionLock()) { - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); - try { - depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); - } catch (Exception e) { - log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - trade.getXmrWalletService().handleWalletError(e, sourceConnection); + if (!trade.isBuyerAsTakerWithoutDeposit()) { + try { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); + try { + depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); + } catch (Exception e) { + log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + trade.getXmrWalletService().handleWalletError(e, sourceConnection); + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + + // check for timeout if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); - if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + if (depositTx != null) break; } - - // check for timeout - if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); - if (depositTx != null) break; } + } catch (Exception e) { + + // thaw deposit inputs + if (depositTx != null) { + trade.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(depositTx)); + trade.getSelf().setReserveTxKeyImages(null); + } + + // re-freeze maker offer inputs + if (trade instanceof MakerTrade) { + trade.getXmrWalletService().freezeOutputs(trade.getOffer().getOfferPayload().getReserveTxKeyImages()); + } + + throw e; } - } catch (Exception e) { - - // thaw deposit inputs - if (depositTx != null) { - trade.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(depositTx)); - trade.getSelf().setReserveTxKeyImages(null); - } - - // re-freeze maker offer inputs - if (trade instanceof MakerTrade) { - trade.getXmrWalletService().freezeOutputs(trade.getOffer().getOfferPayload().getReserveTxKeyImages()); - } - - throw e; } // reset protocol timeout trade.addInitProgressStep(); // update trade state - BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); - trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee())); - trade.getSelf().setDepositTx(depositTx); - trade.getSelf().setDepositTxHash(depositTx.getHash()); - trade.getSelf().setDepositTxFee(depositTx.getFee()); - trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx)); trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address? trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId())); + trade.getSelf().setPaymentAccountPayloadHash(trade.getSelf().getPaymentAccountPayload().getHash()); + BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); + if (depositTx == null) { + trade.getSelf().setSecurityDeposit(securityDeposit); + } else { + trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee())); + trade.getSelf().setDepositTx(depositTx); + trade.getSelf().setDepositTxHash(depositTx.getHash()); + trade.getSelf().setDepositTxFee(depositTx.getFee()); + trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx)); + } } // maker signs deposit hash nonce to avoid challenge protocol @@ -161,7 +168,7 @@ public class MaybeSendSignContractRequest extends TradeTask { trade.getProcessModel().getAccountId(), trade.getSelf().getPaymentAccountPayload().getHash(), trade.getSelf().getPayoutAddressString(), - depositTx.getHash(), + depositTx == null ? null : depositTx.getHash(), sig); // send request to trading peer diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java index 8fc93df9a9..1ce805aca6 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java @@ -63,20 +63,20 @@ public class ProcessSignContractRequest extends TradeTask { // extract fields from request // TODO (woodser): verify request and from maker or taker SignContractRequest request = (SignContractRequest) processModel.getTradeMessage(); - TradePeer trader = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); - trader.setDepositTxHash(request.getDepositTxHash()); - trader.setAccountId(request.getAccountId()); - trader.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash()); - trader.setPayoutAddressString(request.getPayoutAddress()); + TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); + sender.setDepositTxHash(request.getDepositTxHash()); + sender.setAccountId(request.getAccountId()); + sender.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash()); + sender.setPayoutAddressString(request.getPayoutAddress()); // maker sends witness signature of deposit tx hash - if (trader == trade.getMaker()) { - trader.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8)); - trader.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash()); + if (sender == trade.getMaker()) { + sender.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8)); + sender.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash()); } - // sign contract only when both deposit txs hashes known - if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null) { + // sign contract only when received from both peers + if (processModel.getMaker().getPaymentAccountPayloadHash() == null || processModel.getTaker().getPaymentAccountPayloadHash() == null) { complete(); return; } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java index dd43d6944f..7101c488a5 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java @@ -82,8 +82,8 @@ public class SendDepositRequest extends TradeTask { Version.getP2PMessageVersion(), new Date().getTime(), trade.getSelf().getContractSignature(), - trade.getSelf().getDepositTx().getFullHex(), - trade.getSelf().getDepositTx().getKey(), + trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getFullHex(), + trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getKey(), trade.getSelf().getPaymentAccountKey()); // update trade state diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java index 5ab91343aa..e6c71032f4 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -47,62 +47,63 @@ public class TakerReserveTradeFunds extends TradeTask { throw new RuntimeException("Expected taker trade but was " + trade.getClass().getSimpleName() + " " + trade.getShortId() + ". That should never happen."); } - // create reserve tx + // create reserve tx unless deposit not required from buyer as taker MoneroTxWallet reserveTx = null; - synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { + if (!trade.isBuyerAsTakerWithoutDeposit()) { + synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { - // check for timeout - if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId()); - trade.startProtocolTimeout(); + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId()); + trade.startProtocolTimeout(); - // collect relevant info - BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct()); - BigInteger takerFee = trade.getTakerFee(); - BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO; - BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee(); - String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + // collect relevant info + BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct()); + BigInteger takerFee = trade.getTakerFee(); + BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO; + BigInteger securityDeposit = trade.getSecurityDepositBeforeMiningFee(); + String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); - // attempt creating reserve tx - try { - synchronized (HavenoUtils.getWalletFunctionLock()) { - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); - try { - reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); - } catch (Exception e) { - log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - trade.getXmrWalletService().handleWalletError(e, sourceConnection); + // attempt creating reserve tx + try { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); + try { + reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); + } catch (Exception e) { + log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + trade.getXmrWalletService().handleWalletError(e, sourceConnection); + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + + // check for timeout if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); - if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + if (reserveTx != null) break; } - - // check for timeout - if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); - if (reserveTx != null) break; } - } - } catch (Exception e) { + } catch (Exception e) { - // reset state with wallet lock - model.getXmrWalletService().resetAddressEntriesForTrade(trade.getId()); - if (reserveTx != null) { - model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); - trade.getSelf().setReserveTxKeyImages(null); + // reset state with wallet lock + model.getXmrWalletService().resetAddressEntriesForTrade(trade.getId()); + if (reserveTx != null) { + model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); + trade.getSelf().setReserveTxKeyImages(null); + } + + throw e; } - throw e; + // reset protocol timeout + trade.startProtocolTimeout(); + + // update trade state + trade.getTaker().setReserveTxHash(reserveTx.getHash()); + trade.getTaker().setReserveTxHex(reserveTx.getFullHex()); + trade.getTaker().setReserveTxKey(reserveTx.getKey()); + trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); } - - - // reset protocol timeout - trade.startProtocolTimeout(); - - // update trade state - trade.getTaker().setReserveTxHash(reserveTx.getHash()); - trade.getTaker().setReserveTxHex(reserveTx.getFullHex()); - trade.getTaker().setReserveTxKey(reserveTx.getKey()); - trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); } // save process state diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java index b5a6e3624c..eb86f15109 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java @@ -48,7 +48,9 @@ public class TakerSendInitTradeRequestToArbitrator extends TradeTask { InitTradeRequest sourceRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to taker checkNotNull(sourceRequest); checkTradeId(processModel.getOfferId(), sourceRequest); - if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) { + throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + } // create request to arbitrator Offer offer = processModel.getOffer(); @@ -73,7 +75,8 @@ public class TakerSendInitTradeRequestToArbitrator extends TradeTask { trade.getSelf().getReserveTxHash(), trade.getSelf().getReserveTxHex(), trade.getSelf().getReserveTxKey(), - model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); + model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(), + trade.getChallenge()); // send request to arbitrator log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress()); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java index c6315eb174..1118cc34a7 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java @@ -47,7 +47,9 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask { runInterceptHook(); // verify trade state - if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) { + throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + } // collect fields Offer offer = model.getOffer(); @@ -55,6 +57,7 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask { P2PService p2PService = processModel.getP2PService(); XmrWalletService walletService = model.getXmrWalletService(); String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + String challenge = model.getChallenge(); // taker signs offer using offer id as nonce to avoid challenge protocol byte[] sig = HavenoUtils.sign(p2PService.getKeyRing(), offer.getId()); @@ -81,7 +84,8 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask { null, // reserve tx not sent from taker to maker null, null, - payoutAddress); + payoutAddress, + challenge); // send request to maker log.info("Sending {} with offerId {} and uid {} to maker {}", makerRequest.getClass().getSimpleName(), makerRequest.getOfferId(), makerRequest.getUid(), trade.getMaker().getNodeAddress()); diff --git a/core/src/main/java/haveno/core/user/Preferences.java b/core/src/main/java/haveno/core/user/Preferences.java index 3c09126c56..a3756041bf 100644 --- a/core/src/main/java/haveno/core/user/Preferences.java +++ b/core/src/main/java/haveno/core/user/Preferences.java @@ -616,14 +616,14 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid requestPersistence(); } - public void setBuyerSecurityDepositAsPercent(double buyerSecurityDepositAsPercent, PaymentAccount paymentAccount) { - double max = Restrictions.getMaxBuyerSecurityDepositAsPercent(); - double min = Restrictions.getMinBuyerSecurityDepositAsPercent(); + public void setSecurityDepositAsPercent(double securityDepositAsPercent, PaymentAccount paymentAccount) { + double max = Restrictions.getMaxSecurityDepositAsPercent(); + double min = Restrictions.getMinSecurityDepositAsPercent(); if (PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount)) - prefPayload.setBuyerSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent))); + prefPayload.setSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, securityDepositAsPercent))); else - prefPayload.setBuyerSecurityDepositAsPercent(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent))); + prefPayload.setSecurityDepositAsPercent(Math.min(max, Math.max(min, securityDepositAsPercent))); requestPersistence(); } @@ -755,6 +755,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid requestPersistence(); } + public void setShowPrivateOffers(boolean value) { + prefPayload.setShowPrivateOffers(value); + requestPersistence(); + } + public void setDenyApiTaker(boolean value) { prefPayload.setDenyApiTaker(value); requestPersistence(); @@ -838,16 +843,16 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid return prefPayload.isSplitOfferOutput(); } - public double getBuyerSecurityDepositAsPercent(PaymentAccount paymentAccount) { + public double getSecurityDepositAsPercent(PaymentAccount paymentAccount) { double value = PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount) ? - prefPayload.getBuyerSecurityDepositAsPercentForCrypto() : prefPayload.getBuyerSecurityDepositAsPercent(); + prefPayload.getSecurityDepositAsPercentForCrypto() : prefPayload.getSecurityDepositAsPercent(); - if (value < Restrictions.getMinBuyerSecurityDepositAsPercent()) { - value = Restrictions.getMinBuyerSecurityDepositAsPercent(); - setBuyerSecurityDepositAsPercent(value, paymentAccount); + if (value < Restrictions.getMinSecurityDepositAsPercent()) { + value = Restrictions.getMinSecurityDepositAsPercent(); + setSecurityDepositAsPercent(value, paymentAccount); } - return value == 0 ? Restrictions.getDefaultBuyerSecurityDepositAsPercent() : value; + return value == 0 ? Restrictions.getDefaultSecurityDepositAsPercent() : value; } @Override diff --git a/core/src/main/java/haveno/core/user/PreferencesPayload.java b/core/src/main/java/haveno/core/user/PreferencesPayload.java index 6d3d41f30f..44e6aef509 100644 --- a/core/src/main/java/haveno/core/user/PreferencesPayload.java +++ b/core/src/main/java/haveno/core/user/PreferencesPayload.java @@ -41,7 +41,7 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; -import static haveno.core.xmr.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent; @Slf4j @Data @@ -120,10 +120,10 @@ public final class PreferencesPayload implements PersistableEnvelope { private String rpcPw; @Nullable private String takeOfferSelectedPaymentAccountId; - private double buyerSecurityDepositAsPercent = getDefaultBuyerSecurityDepositAsPercent(); + private double securityDepositAsPercent = getDefaultSecurityDepositAsPercent(); private int ignoreDustThreshold = 600; private int clearDataAfterDays = Preferences.CLEAR_DATA_AFTER_DAYS_INITIAL; - private double buyerSecurityDepositAsPercentForCrypto = getDefaultBuyerSecurityDepositAsPercent(); + private double securityDepositAsPercentForCrypto = getDefaultSecurityDepositAsPercent(); private int blockNotifyPort; private boolean tacAcceptedV120; private double bsqAverageTrimThreshold = 0.05; @@ -134,6 +134,7 @@ public final class PreferencesPayload implements PersistableEnvelope { // Added in 1.5.5 private boolean hideNonAccountPaymentMethods; private boolean showOffersMatchingMyAccounts; + private boolean showPrivateOffers; private boolean denyApiTaker; private boolean notifyOnPreRelease; @@ -193,10 +194,10 @@ public final class PreferencesPayload implements PersistableEnvelope { .setUseStandbyMode(useStandbyMode) .setUseSoundForNotifications(useSoundForNotifications) .setUseSoundForNotificationsInitialized(useSoundForNotificationsInitialized) - .setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent) + .setSecurityDepositAsPercent(securityDepositAsPercent) .setIgnoreDustThreshold(ignoreDustThreshold) .setClearDataAfterDays(clearDataAfterDays) - .setBuyerSecurityDepositAsPercentForCrypto(buyerSecurityDepositAsPercentForCrypto) + .setSecurityDepositAsPercentForCrypto(securityDepositAsPercentForCrypto) .setBlockNotifyPort(blockNotifyPort) .setTacAcceptedV120(tacAcceptedV120) .setBsqAverageTrimThreshold(bsqAverageTrimThreshold) @@ -205,6 +206,7 @@ public final class PreferencesPayload implements PersistableEnvelope { .collect(Collectors.toList())) .setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods) .setShowOffersMatchingMyAccounts(showOffersMatchingMyAccounts) + .setShowPrivateOffers(showPrivateOffers) .setDenyApiTaker(denyApiTaker) .setNotifyOnPreRelease(notifyOnPreRelease); @@ -297,10 +299,10 @@ public final class PreferencesPayload implements PersistableEnvelope { proto.getRpcUser().isEmpty() ? null : proto.getRpcUser(), proto.getRpcPw().isEmpty() ? null : proto.getRpcPw(), proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(), - proto.getBuyerSecurityDepositAsPercent(), + proto.getSecurityDepositAsPercent(), proto.getIgnoreDustThreshold(), proto.getClearDataAfterDays(), - proto.getBuyerSecurityDepositAsPercentForCrypto(), + proto.getSecurityDepositAsPercentForCrypto(), proto.getBlockNotifyPort(), proto.getTacAcceptedV120(), proto.getBsqAverageTrimThreshold(), @@ -310,6 +312,7 @@ public final class PreferencesPayload implements PersistableEnvelope { .collect(Collectors.toList())), proto.getHideNonAccountPaymentMethods(), proto.getShowOffersMatchingMyAccounts(), + proto.getShowPrivateOffers(), proto.getDenyApiTaker(), proto.getNotifyOnPreRelease(), XmrNodeSettings.fromProto(proto.getXmrNodeSettings()) diff --git a/core/src/main/java/haveno/core/util/coin/CoinUtil.java b/core/src/main/java/haveno/core/util/coin/CoinUtil.java index ec7ff113e0..bf194b3ea0 100644 --- a/core/src/main/java/haveno/core/util/coin/CoinUtil.java +++ b/core/src/main/java/haveno/core/util/coin/CoinUtil.java @@ -47,35 +47,35 @@ public class CoinUtil { } /** - * @param value Btc amount to be converted to percent value. E.g. 0.01 BTC is 1% (of 1 BTC) + * @param value Xmr amount to be converted to percent value. E.g. 0.01 XMR is 1% (of 1 XMR) * @return The percentage value as double (e.g. 1% is 0.01) */ - public static double getAsPercentPerBtc(BigInteger value) { - return getAsPercentPerBtc(value, HavenoUtils.xmrToAtomicUnits(1.0)); + public static double getAsPercentPerXmr(BigInteger value) { + return getAsPercentPerXmr(value, HavenoUtils.xmrToAtomicUnits(1.0)); } /** - * @param part Btc amount to be converted to percent value, based on total value passed. - * E.g. 0.1 BTC is 25% (of 0.4 BTC) - * @param total Total Btc amount the percentage part is calculated from + * @param part Xmr amount to be converted to percent value, based on total value passed. + * E.g. 0.1 XMR is 25% (of 0.4 XMR) + * @param total Total Xmr amount the percentage part is calculated from * * @return The percentage value as double (e.g. 1% is 0.01) */ - public static double getAsPercentPerBtc(BigInteger part, BigInteger total) { + public static double getAsPercentPerXmr(BigInteger part, BigInteger total) { return MathUtils.roundDouble(HavenoUtils.divide(part == null ? BigInteger.ZERO : part, total == null ? BigInteger.valueOf(1) : total), 4); } /** * @param percent The percentage value as double (e.g. 1% is 0.01) * @param amount The amount as atomic units for the percentage calculation - * @return The percentage as atomic units (e.g. 1% of 1 BTC is 0.01 BTC) + * @return The percentage as atomic units (e.g. 1% of 1 XMR is 0.01 XMR) */ public static BigInteger getPercentOfAmount(double percent, BigInteger amount) { if (amount == null) amount = BigInteger.ZERO; return BigDecimal.valueOf(percent).multiply(new BigDecimal(amount)).setScale(8, RoundingMode.DOWN).toBigInteger(); } - public static BigInteger getRoundedAmount(BigInteger amount, Price price, long maxTradeLimit, String currencyCode, String paymentMethodId) { + public static BigInteger getRoundedAmount(BigInteger amount, Price price, Long maxTradeLimit, String currencyCode, String paymentMethodId) { if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) { return getRoundedAtmCashAmount(amount, price, maxTradeLimit); } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) { @@ -86,7 +86,7 @@ public class CoinUtil { return amount; } - public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, long maxTradeLimit) { + public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, Long maxTradeLimit) { return getAdjustedAmount(amount, price, maxTradeLimit, 10); } @@ -99,11 +99,11 @@ public class CoinUtil { * @param maxTradeLimit The max. trade limit of the users account, in atomic units. * @return The adjusted amount */ - public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, long maxTradeLimit) { + public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, Long maxTradeLimit) { return getAdjustedAmount(amount, price, maxTradeLimit, 1); } - public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Price price, long maxTradeLimit) { + public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Price price, Long maxTradeLimit) { DecimalFormat decimalFormat = new DecimalFormat("#.####"); double roundedXmrAmount = Double.parseDouble(decimalFormat.format(HavenoUtils.atomicUnitsToXmr(amount))); return HavenoUtils.xmrToAtomicUnits(roundedXmrAmount); @@ -121,7 +121,7 @@ public class CoinUtil { * @return The adjusted amount */ @VisibleForTesting - static BigInteger getAdjustedAmount(BigInteger amount, Price price, long maxTradeLimit, int factor) { + static BigInteger getAdjustedAmount(BigInteger amount, Price price, Long maxTradeLimit, int factor) { checkArgument( amount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(), "amount needs to be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr" @@ -163,11 +163,13 @@ public class CoinUtil { // If we are above our trade limit we reduce the amount by the smallestUnitForAmount BigInteger smallestUnitForAmountUnadjusted = price.getAmountByVolume(smallestUnitForVolume); - while (adjustedAmount > maxTradeLimit) { - adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact(); + if (maxTradeLimit != null) { + while (adjustedAmount > maxTradeLimit) { + adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact(); + } } adjustedAmount = Math.max(minTradeAmount, adjustedAmount); - adjustedAmount = Math.min(maxTradeLimit, adjustedAmount); + if (maxTradeLimit != null) adjustedAmount = Math.min(maxTradeLimit, adjustedAmount); return BigInteger.valueOf(adjustedAmount); } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java index a70d2cc1f3..b270762d3b 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java +++ b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java @@ -24,11 +24,13 @@ import org.bitcoinj.core.Coin; import java.math.BigInteger; public class Restrictions { + + // configure restrictions + public static final double MIN_SECURITY_DEPOSIT_PCT = 0.15; + public static final double MAX_SECURITY_DEPOSIT_PCT = 0.5; public static BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.1); - public static BigInteger MIN_BUYER_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1); - // For the seller we use a fixed one as there is no way the seller can cancel the trade - // To make it editable would just increase complexity. - public static BigInteger MIN_SELLER_SECURITY_DEPOSIT = MIN_BUYER_SECURITY_DEPOSIT; + public static BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1); + // At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the // mediated payout. For Refund agent cases we do not have that restriction. private static BigInteger MIN_REFUND_AT_MEDIATED_DISPUTE; @@ -53,31 +55,20 @@ public class Restrictions { return MIN_TRADE_AMOUNT; } - public static double getDefaultBuyerSecurityDepositAsPercent() { - return 0.15; // 15% of trade amount. + public static double getDefaultSecurityDepositAsPercent() { + return MIN_SECURITY_DEPOSIT_PCT; } - public static double getMinBuyerSecurityDepositAsPercent() { - return 0.15; // 15% of trade amount. + public static double getMinSecurityDepositAsPercent() { + return MIN_SECURITY_DEPOSIT_PCT; } - public static double getMaxBuyerSecurityDepositAsPercent() { - return 0.5; // 50% of trade amount. For a 1 BTC trade it is about 3500 USD @ 7000 USD/BTC + public static double getMaxSecurityDepositAsPercent() { + return MAX_SECURITY_DEPOSIT_PCT; } - // We use MIN_BUYER_SECURITY_DEPOSIT as well as lower bound in case of small trade amounts. - // So 0.0005 BTC is the min. buyer security deposit even with amount of 0.0001 BTC and 0.05% percentage value. - public static BigInteger getMinBuyerSecurityDeposit() { - return MIN_BUYER_SECURITY_DEPOSIT; - } - - - public static double getSellerSecurityDepositAsPercent() { - return 0.15; // 15% of trade amount. - } - - public static BigInteger getMinSellerSecurityDeposit() { - return MIN_SELLER_SECURITY_DEPOSIT; + public static BigInteger getMinSecurityDeposit() { + return MIN_SECURITY_DEPOSIT; } // This value must be lower than MIN_BUYER_SECURITY_DEPOSIT and SELLER_SECURITY_DEPOSIT diff --git a/core/src/main/resources/bip39_english.txt b/core/src/main/resources/bip39_english.txt new file mode 100644 index 0000000000..942040ed50 --- /dev/null +++ b/core/src/main/resources/bip39_english.txt @@ -0,0 +1,2048 @@ +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index c0d2d88fd8..86fd962d63 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -43,6 +43,8 @@ shared.buyMonero=Buy Monero shared.sellMonero=Sell Monero shared.buyCurrency=Buy {0} shared.sellCurrency=Sell {0} +shared.buyCurrencyLocked=Buy {0} 🔒 +shared.sellCurrencyLocked=Sell {0} 🔒 shared.buyingXMRWith=buying XMR with {0} shared.sellingXMRFor=selling XMR for {0} shared.buyingCurrency=buying {0} (selling XMR) @@ -349,6 +351,7 @@ market.trades.showVolumeInUSD=Show volume in USD offerbook.createOffer=Create offer offerbook.takeOffer=Take offer offerbook.takeOffer.createAccount=Create account and take offer +offerbook.takeOffer.enterChallenge=Enter the offer passphrase offerbook.trader=Trader offerbook.offerersBankId=Maker''s bank ID (BIC/SWIFT): {0} offerbook.offerersBankName=Maker''s bank name: {0} @@ -360,6 +363,8 @@ offerbook.availableOffersToSell=Sell {0} for {1} offerbook.filterByCurrency=Choose currency offerbook.filterByPaymentMethod=Choose payment method offerbook.matchingOffers=Offers matching my accounts +offerbook.filterNoDeposit=No deposit +offerbook.noDepositOffers=Offers with no deposit (passphrase required) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted @@ -527,7 +532,10 @@ createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Your buyer''s security deposit will be {0} createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} -createOffer.minSecurityDepositUsed=Min. buyer security deposit is used +createOffer.minSecurityDepositUsed=Minimum security deposit is used +createOffer.buyerAsTakerWithoutDeposit=No deposit required from buyer (passphrase protected) +createOffer.myDeposit=My security deposit (%) +createOffer.myDepositInfo=Your security deposit will be {0} #################################################################### @@ -553,6 +561,8 @@ takeOffer.fundsBox.networkFee=Total mining fees takeOffer.fundsBox.takeOfferSpinnerInfo=Taking offer: {0} takeOffer.fundsBox.paymentLabel=Haveno trade with ID {0} takeOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee) +takeOffer.fundsBox.noFundingRequiredTitle=No funding required +takeOffer.fundsBox.noFundingRequiredDescription=Get the offer passphrase from the seller outside Haveno to take this offer. takeOffer.success.headline=You have successfully taken an offer. takeOffer.success.info=You can see the status of your trade at \"Portfolio/Open trades\". takeOffer.error.message=An error occurred when taking the offer.\n\n{0} @@ -1968,6 +1978,7 @@ offerDetailsWindow.confirm.taker=Confirm: Take offer to {0} monero offerDetailsWindow.confirm.takerCrypto=Confirm: Take offer to {0} {1} offerDetailsWindow.creationDate=Creation date offerDetailsWindow.makersOnion=Maker's onion address +offerDetailsWindow.challenge=Offer passphrase qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -2269,6 +2280,12 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=No deposit required from buyer +popup.info.buyerAsTakerWithoutDeposit=\ + Your offer will not require a security deposit or fee from the XMR buyer.\n\n\ + To accept your offer, you must share a passphrase with your trade partner outside Haveno.\n\n\ + The passphrase is automatically generated and shown in the offer details after creation.\ + popup.info.torMigration.msg=Your Haveno node is probably using a deprecated Tor v2 address. \ Please switch your Haveno node to a Tor v3 address. \ Make sure to back up your data directory beforehand. @@ -2408,6 +2425,7 @@ navigation.support=\"Support\" formatter.formatVolumeLabel={0} amount{1} formatter.makerTaker=Maker as {0} {1} / Taker as {2} {3} +formatter.makerTakerLocked=Maker as {0} {1} / Taker as {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=You are {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 789eb761ca..85f021d4b3 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -40,6 +40,8 @@ shared.buyMonero=Koupit monero shared.sellMonero=Prodat monero shared.buyCurrency=Koupit {0} shared.sellCurrency=Prodat {0} +shared.buyCurrencyLocked=Koupit {0} 🔒 +shared.sellCurrencyLocked=Prodat {0} 🔒 shared.buyingXMRWith=nakoupit XMR za {0} shared.sellingXMRFor=prodat XMR za {0} shared.buyingCurrency=nakoupit {0} (prodat XMR) @@ -330,6 +332,7 @@ offerbook.createOffer=Vytvořit nabídku offerbook.takeOffer=Přijmout nabídku offerbook.takeOfferToBuy=Přijmout nabídku na nákup {0} offerbook.takeOfferToSell=Přijmout nabídku k prodeji {0} +offerbook.takeOffer.enterChallenge=Zadejte heslo nabídky offerbook.trader=Obchodník offerbook.offerersBankId=ID banky tvůrce (BIC/SWIFT): {0} offerbook.offerersBankName=Jméno banky tvůrce: {0} @@ -340,6 +343,8 @@ offerbook.availableOffers=Dostupné nabídky offerbook.filterByCurrency=Filtrovat podle měny offerbook.filterByPaymentMethod=Filtrovat podle platební metody offerbook.matchingOffers=Nabídky odpovídající mým účtům +offerbook.filterNoDeposit=Žádný vklad +offerbook.noDepositOffers=Nabídky bez zálohy (vyžaduje se heslo) offerbook.timeSinceSigning=Informace o účtu offerbook.timeSinceSigning.info=Tento účet byl ověřen a {0} offerbook.timeSinceSigning.info.arbitrator=podepsán rozhodcem a může podepisovat účty partnerů @@ -485,7 +490,10 @@ createOffer.setDepositAsBuyer=Nastavit mou kauci jako kupujícího (%) createOffer.setDepositForBothTraders=Nastavit kauci obou obchodníků (%) createOffer.securityDepositInfo=Kauce vašeho kupujícího bude {0} createOffer.securityDepositInfoAsBuyer=Vaše kauce jako kupující bude {0} -createOffer.minSecurityDepositUsed=Je použita min. záloha kupujícího +createOffer.minSecurityDepositUsed=Minimální bezpečnostní záloha je použita +createOffer.buyerAsTakerWithoutDeposit=Žádný vklad od kupujícího (chráněno heslem) +createOffer.myDeposit=Můj bezpečnostní vklad (%) +createOffer.myDepositInfo=Vaše záloha na bezpečnost bude {0} #################################################################### @@ -509,6 +517,8 @@ takeOffer.fundsBox.networkFee=Celkové poplatky za těžbu takeOffer.fundsBox.takeOfferSpinnerInfo=Přijímám nabídku: {0} takeOffer.fundsBox.paymentLabel=Haveno obchod s ID {0} takeOffer.fundsBox.fundsStructure=(kauce {0}, obchodní poplatek {1}, poplatek za těžbu {2}) +takeOffer.fundsBox.noFundingRequiredTitle=Žádné financování požadováno +takeOffer.fundsBox.noFundingRequiredDescription=Získejte passphrase nabídky od prodávajícího mimo Haveno, abyste tuto nabídku přijali. takeOffer.success.headline=Úspěšně jste přijali nabídku. takeOffer.success.info=Stav vašeho obchodu můžete vidět v \"Portfolio/Otevřené obchody\". takeOffer.error.message=Při převzetí nabídky došlo k chybě.\n\n{0} @@ -1464,6 +1474,7 @@ offerDetailsWindow.confirm.maker=Potvrďte: Umístit nabídku {0} monero offerDetailsWindow.confirm.taker=Potvrďte: Využít nabídku {0} monero offerDetailsWindow.creationDate=Datum vzniku offerDetailsWindow.makersOnion=Onion adresa tvůrce +offerDetailsWindow.challenge=Passphrase nabídky qRCodeWindow.headline=QR Kód qRCodeWindow.msg=Použijte tento QR kód k financování vaší peněženky Haveno z vaší externí peněženky. @@ -1689,6 +1700,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys byly podepsány popup.accountSigning.unsignedPubKeys.result.signed=Podepsané pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Nepodařilo se podepsat +popup.info.buyerAsTakerWithoutDeposit.headline=Žádný vklad není od kupujícího požadován +popup.info.buyerAsTakerWithoutDeposit=Vaše nabídka nebude vyžadovat bezpečnostní zálohu ani poplatek od kupujícího XMR.\n\nPro přijetí vaší nabídky musíte sdílet heslo se svým obchodním partnerem mimo Haveno.\n\nHeslo je automaticky vygenerováno a zobrazeno v detailech nabídky po jejím vytvoření. + #################################################################### # Notifications #################################################################### @@ -1814,6 +1828,7 @@ navigation.support=\"Podpora\" formatter.formatVolumeLabel={0} částka{1} formatter.makerTaker=Tvůrce jako {0} {1} / Příjemce jako {2} {3} +formatter.makerTakerLocked=Tvůrce jako {0} {1} / Příjemce jako {2} {3} 🔒 formatter.youAreAsMaker=Jste {1} {0} (jako tvůrce) / Příjemce je {3} {2} formatter.youAreAsTaker=Jste {1} {0} (jako příjemce) / Tvůrce je {3} {2} formatter.youAre={0}te {1} ({2}te {3}) diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 5d03c34b0d..e8b8584cd5 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -40,6 +40,8 @@ shared.buyMonero=Monero kaufen shared.sellMonero=Monero verkaufen shared.buyCurrency={0} kaufen shared.sellCurrency={0} verkaufen +shared.buyCurrencyLocked={0} kaufen 🔒 +shared.sellCurrencyLocked={0} verkaufen 🔒 shared.buyingXMRWith=kaufe XMR mit {0} shared.sellingXMRFor=verkaufe XMR für {0} shared.buyingCurrency=kaufe {0} (verkaufe XMR) @@ -330,6 +332,7 @@ offerbook.createOffer=Angebot erstellen offerbook.takeOffer=Angebot annehmen offerbook.takeOfferToBuy=Angebot annehmen {0} zu kaufen offerbook.takeOfferToSell=Angebot annehmen {0} zu verkaufen +offerbook.takeOffer.enterChallenge=Geben Sie das Angebots-Passphrase ein offerbook.trader=Händler offerbook.offerersBankId=Bankkennung des Erstellers (BIC/SWIFT): {0} offerbook.offerersBankName=Bankname des Erstellers: {0} @@ -340,6 +343,8 @@ offerbook.availableOffers=Verfügbare Angebote offerbook.filterByCurrency=Nach Währung filtern offerbook.filterByPaymentMethod=Nach Zahlungsmethode filtern offerbook.matchingOffers=Angebote die meinen Zahlungskonten entsprechen +offerbook.filterNoDeposit=Kein Deposit +offerbook.noDepositOffers=Angebote ohne Einzahlung (Passphrase erforderlich) offerbook.timeSinceSigning=Informationen zum Zahlungskonto offerbook.timeSinceSigning.info=Dieses Konto wurde verifiziert und {0} offerbook.timeSinceSigning.info.arbitrator=von einem Vermittler unterzeichnet und kann Partner-Konten unterzeichnen @@ -485,7 +490,10 @@ createOffer.setDepositAsBuyer=Meine Kaution als Käufer festlegen (%) createOffer.setDepositForBothTraders=Legen Sie die Kaution für beide Handelspartner fest (%) createOffer.securityDepositInfo=Die Kaution ihres Käufers wird {0} createOffer.securityDepositInfoAsBuyer=Ihre Kaution als Käufer wird {0} -createOffer.minSecurityDepositUsed=Min. Kaution des Käufers wird verwendet +createOffer.minSecurityDepositUsed=Der Mindest-Sicherheitsbetrag wird verwendet. +createOffer.buyerAsTakerWithoutDeposit=Kein Deposit erforderlich vom Käufer (Passphrase geschützt) +createOffer.myDeposit=Meine Sicherheitsleistung (%) +createOffer.myDepositInfo=Ihre Sicherheitsleistung beträgt {0} #################################################################### @@ -509,6 +517,8 @@ takeOffer.fundsBox.networkFee=Gesamte Mining-Gebühr takeOffer.fundsBox.takeOfferSpinnerInfo=Angebot annehmen: {0} takeOffer.fundsBox.paymentLabel=Haveno-Handel mit der ID {0} takeOffer.fundsBox.fundsStructure=({0} Kaution, {1} Handelsgebühr, {2} Mining-Gebühr) +takeOffer.fundsBox.noFundingRequiredTitle=Keine Finanzierung erforderlich +takeOffer.fundsBox.noFundingRequiredDescription=Holen Sie sich das Angebots-Passwort vom Verkäufer außerhalb von Haveno, um dieses Angebot anzunehmen. takeOffer.success.headline=Sie haben erfolgreich ein Angebot angenommen. takeOffer.success.info=Sie können den Status Ihres Trades unter \"Portfolio/Offene Trades\" einsehen. takeOffer.error.message=Bei der Angebotsannahme trat ein Fehler auf.\n\n{0} @@ -1464,6 +1474,7 @@ offerDetailsWindow.confirm.maker=Bestätigen: Anbieten monero zu {0} offerDetailsWindow.confirm.taker=Bestätigen: Angebot annehmen monero zu {0} offerDetailsWindow.creationDate=Erstellungsdatum offerDetailsWindow.makersOnion=Onion-Adresse des Erstellers +offerDetailsWindow.challenge=Angebots-Passphrase qRCodeWindow.headline=QR Code qRCodeWindow.msg=Bitte nutzen Sie diesen QR Code um Ihr Haveno Wallet von Ihrem externen Wallet aufzuladen. @@ -1690,6 +1701,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys wurden unterzeichnet popup.accountSigning.unsignedPubKeys.result.signed=Unterzeichnete Pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Unterzeichnung fehlgeschlagen +popup.info.buyerAsTakerWithoutDeposit.headline=Kein Depositum vom Käufer erforderlich +popup.info.buyerAsTakerWithoutDeposit=Ihr Angebot erfordert keine Sicherheitsleistung oder Gebühr vom XMR-Käufer.\n\nUm Ihr Angebot anzunehmen, müssen Sie ein Passwort mit Ihrem Handelspartner außerhalb von Haveno teilen.\n\nDas Passwort wird automatisch generiert und nach der Erstellung in den Angebotsdetails angezeigt. + #################################################################### # Notifications #################################################################### @@ -1815,6 +1829,7 @@ navigation.support=\"Support\" formatter.formatVolumeLabel={0} Betrag{1} formatter.makerTaker=Ersteller als {0} {1} / Abnehmer als {2} {3} +formatter.makerTakerLocked=Ersteller als {0} {1} / Abnehmer als {2} {3} 🔒 formatter.youAreAsMaker=Sie sind: {1} {0} (Ersteller) / Abnehmer ist: {3} {2} formatter.youAreAsTaker=Sie sind: {1} {0} (Abnehmer) / Ersteller ist: {3} {2} formatter.youAre=Sie {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 2ff762f847..d3f61ada16 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -40,6 +40,8 @@ shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} +shared.buyCurrencyLocked=Comprar {0} 🔒 +shared.sellCurrencyLocked=Vender {0} 🔒 shared.buyingXMRWith=Comprando XMR con {0} shared.sellingXMRFor=Vendiendo XMR por {0} shared.buyingCurrency=comprando {0} (Vendiendo XMR) @@ -330,6 +332,7 @@ offerbook.createOffer=Crear oferta offerbook.takeOffer=Tomar oferta offerbook.takeOfferToBuy=Tomar oferta de compra de {0} offerbook.takeOfferToSell=Tomar oferta de venta de {0} +offerbook.takeOffer.enterChallenge=Introduzca la frase secreta de la oferta offerbook.trader=Trader offerbook.offerersBankId=ID del banco del creador (BIC/SWIFT): {0} offerbook.offerersBankName=Nombre del banco del creador: {0} @@ -340,6 +343,8 @@ offerbook.availableOffers=Ofertas disponibles offerbook.filterByCurrency=Filtrar por moneda offerbook.filterByPaymentMethod=Filtrar por método de pago offerbook.matchingOffers=Ofertas que concuerden con mis cuentas +offerbook.filterNoDeposit=Sin depósito +offerbook.noDepositOffers=Ofertas sin depósito (se requiere frase de paso) offerbook.timeSinceSigning=Información de la cuenta offerbook.timeSinceSigning.info=Esta cuenta fue verificada y {0} offerbook.timeSinceSigning.info.arbitrator=firmada por un árbitro y puede firmar cuentas de pares @@ -485,7 +490,10 @@ createOffer.setDepositAsBuyer=Establecer mi depósito de seguridad como comprado createOffer.setDepositForBothTraders=Establecer el depósito de seguridad para los comerciantes (%) createOffer.securityDepositInfo=Su depósito de seguridad como comprador será {0} createOffer.securityDepositInfoAsBuyer=Su depósito de seguridad como comprador será {0} -createOffer.minSecurityDepositUsed=En uso el depósito de seguridad mínimo +createOffer.minSecurityDepositUsed=Se utiliza un depósito de seguridad mínimo +createOffer.buyerAsTakerWithoutDeposit=No se requiere depósito del comprador (protegido por passphrase) +createOffer.myDeposit=Mi depósito de seguridad (%) +createOffer.myDepositInfo=Tu depósito de seguridad será {0} #################################################################### @@ -509,6 +517,8 @@ takeOffer.fundsBox.networkFee=Comisiones de minado totales takeOffer.fundsBox.takeOfferSpinnerInfo=Aceptando oferta: {0} takeOffer.fundsBox.paymentLabel=Intercambio Haveno con ID {0} takeOffer.fundsBox.fundsStructure=({0} depósito de seguridad {1} tasa de intercambio, {2} tarifa de minado) +takeOffer.fundsBox.noFundingRequiredTitle=No se requiere financiamiento +takeOffer.fundsBox.noFundingRequiredDescription=Obtén la frase de acceso de la oferta del vendedor fuera de Haveno para aceptar esta oferta. takeOffer.success.headline=Ha aceptado la oferta con éxito. takeOffer.success.info=Puede ver el estado de su intercambio en \"Portafolio/Intercambios abiertos\". takeOffer.error.message=Un error ocurrió al tomar la oferta.\n\n{0} @@ -1465,6 +1475,7 @@ offerDetailsWindow.confirm.maker=Confirmar: Poner oferta para {0} monero offerDetailsWindow.confirm.taker=Confirmar: Tomar oferta {0} monero offerDetailsWindow.creationDate=Fecha de creación offerDetailsWindow.makersOnion=Dirección onion del creador +offerDetailsWindow.challenge=Frase de contraseña de la oferta qRCodeWindow.headline=Código QR qRCodeWindow.msg=Por favor, utilice este código QR para fondear su billetera Haveno desde su billetera externa. @@ -1691,6 +1702,9 @@ popup.accountSigning.unsignedPubKeys.signed=Las claves públicas se firmaron popup.accountSigning.unsignedPubKeys.result.signed=Claves públicas firmadas popup.accountSigning.unsignedPubKeys.result.failed=Error al firmar +popup.info.buyerAsTakerWithoutDeposit.headline=No se requiere depósito del comprador +popup.info.buyerAsTakerWithoutDeposit=Tu oferta no requerirá un depósito de seguridad ni una tarifa del comprador de XMR.\n\nPara aceptar tu oferta, debes compartir una frase de acceso con tu compañero de comercio fuera de Haveno.\n\nLa frase de acceso se genera automáticamente y se muestra en los detalles de la oferta después de la creación. + #################################################################### # Notifications #################################################################### @@ -1816,6 +1830,7 @@ navigation.support=\"Soporte\" formatter.formatVolumeLabel={0} cantidad{1} formatter.makerTaker=Creador como {0} {1} / Tomador como {2} {3} +formatter.makerTakerLocked=Creador como {0} {1} / Tomador como {2} {3} 🔒 formatter.youAreAsMaker=Usted es: {1} {0} (creador) / El tomador es: {3} {2} formatter.youAreAsTaker=Usted es: {1} {0} (tomador) / Creador es: {3} {2} formatter.youAre=Usted es {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index ed78e6ec4f..710bbb6b93 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -40,6 +40,8 @@ shared.buyMonero=خرید بیتکوین shared.sellMonero=بیتکوین بفروشید shared.buyCurrency=خرید {0} shared.sellCurrency=فروش {0} +shared.buyCurrencyLocked=بخر {0} 🔒 +shared.sellCurrencyLocked=فروش {0} 🔒 shared.buyingXMRWith=خرید بیتکوین با {0} shared.sellingXMRFor=فروش بیتکوین با {0} shared.buyingCurrency=خرید {0} ( فروش بیتکوین) @@ -330,6 +332,7 @@ offerbook.createOffer=ایجاد پیشنهاد offerbook.takeOffer=برداشتن پیشنهاد offerbook.takeOfferToBuy=پیشنهاد خرید {0} را بردار offerbook.takeOfferToSell=پیشنهاد فروش {0} را بردار +offerbook.takeOffer.enterChallenge=عبارت عبور پیشنهاد را وارد کنید offerbook.trader=معامله‌گر offerbook.offerersBankId=شناسه بانک سفارش‌گذار (BIC/SWIFT): {0} offerbook.offerersBankName= نام بانک سفارش‌گذار : {0} @@ -339,7 +342,9 @@ offerbook.offerersAcceptedBankSeats=بانک‌های کشورهای پذیرف offerbook.availableOffers=پیشنهادهای موجود offerbook.filterByCurrency=فیلتر بر اساس ارز offerbook.filterByPaymentMethod=فیلتر بر اساس روش پرداخت -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=پیشنهادات متناسب با حساب‌های من +offerbook.filterNoDeposit=هیچ سپرده‌ای +offerbook.noDepositOffers=پیشنهادهایی بدون ودیعه (نیاز به عبارت عبور) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts @@ -484,7 +489,10 @@ createOffer.setDepositAsBuyer=تنظیم سپرده‌ی اطمینان من ب createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=سپرده‌ی اطمینان خریدار شما {0} خواهد بود createOffer.securityDepositInfoAsBuyer=سپرده‌ی اطمینان شما به عنوان خریدار {0} خواهد بود -createOffer.minSecurityDepositUsed=Min. buyer security deposit is used +createOffer.minSecurityDepositUsed=حداقل سپرده امنیتی استفاده می‌شود +createOffer.buyerAsTakerWithoutDeposit=هیچ سپرده‌ای از خریدار مورد نیاز نیست (محافظت شده با پس‌عبارت) +createOffer.myDeposit=سپرده امنیتی من (%) +createOffer.myDepositInfo=ودیعه امنیتی شما {0} خواهد بود #################################################################### @@ -508,6 +516,8 @@ takeOffer.fundsBox.networkFee=کل کارمزد استخراج takeOffer.fundsBox.takeOfferSpinnerInfo=پذیرفتن پیشنهاد: {0} takeOffer.fundsBox.paymentLabel=معامله Haveno با شناسه‌ی {0} takeOffer.fundsBox.fundsStructure=({0} سپرده‌ی اطمینان، {1} هزینه‌ی معامله، {2} هزینه تراکنش شبکه) +takeOffer.fundsBox.noFundingRequiredTitle=نیاز به تأمین مالی نیست +takeOffer.fundsBox.noFundingRequiredDescription=برای پذیرش این پیشنهاد، رمزعبور آن را از فروشنده خارج از هاونئو دریافت کنید. takeOffer.success.headline=با موفقیت یک پیشنهاد را قبول کرده‌اید. takeOffer.success.info=شما می‌توانید وضعیت معامله‌ی خود را در \"سبد سهام /معاملات باز\" ببینید. takeOffer.error.message=هنگام قبول کردن پیشنهاد، اتفاقی رخ داده است.\n\n{0} @@ -1460,6 +1470,7 @@ offerDetailsWindow.confirm.maker=تأیید: پیشنهاد را به {0} بگذ offerDetailsWindow.confirm.taker=تأیید: پیشنهاد را به {0} بپذیرید offerDetailsWindow.creationDate=تاریخ ایجاد offerDetailsWindow.makersOnion=آدرس Onion سفارش گذار +offerDetailsWindow.challenge=Passphrase de l'offre qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1683,6 +1694,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=هیچ پیش‌پرداختی از خریدار مورد نیاز نیست +popup.info.buyerAsTakerWithoutDeposit=پیشنهاد شما نیاز به ودیعه امنیتی یا هزینه از خریدار XMR ندارد.\n\nبرای پذیرفتن پیشنهاد شما، باید یک پس‌عبارت را با شریک تجاری خود خارج از Haveno به اشتراک بگذارید.\n\nپس‌عبارت به‌طور خودکار تولید می‌شود و پس از ایجاد در جزئیات پیشنهاد نمایش داده می‌شود. + #################################################################### # Notifications #################################################################### @@ -1808,6 +1822,7 @@ navigation.support=\"پشتیبانی\" formatter.formatVolumeLabel={0} مبلغ {1} formatter.makerTaker=سفارش گذار به عنوان {0} {1} / پذیرنده به عنوان {2} {3} +formatter.makerTakerLocked=سفارش گذار به عنوان {0} {1} / پذیرنده به عنوان {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=شما {0} {1} ({2} {3}) هستید diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index e87a0caceb..5e734ac976 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -40,6 +40,8 @@ shared.buyMonero=Achat Monero shared.sellMonero=Vendre des Moneros shared.buyCurrency=Achat {0} shared.sellCurrency=Vendre {0} +shared.buyCurrencyLocked=Achat {0} 🔒 +shared.sellCurrencyLocked=Vendre {0} 🔒 shared.buyingXMRWith=achat XMR avec {0} shared.sellingXMRFor=vendre XMR pour {0} shared.buyingCurrency=achat {0} (vente XMR) @@ -330,6 +332,7 @@ offerbook.createOffer=Créer un ordre offerbook.takeOffer=Accepter un ordre offerbook.takeOfferToBuy=Accepter l''ordre d''achat {0} offerbook.takeOfferToSell=Accepter l''ordre de vente {0} +offerbook.takeOffer.enterChallenge=Entrez la phrase secrète de l'offre offerbook.trader=Échanger offerbook.offerersBankId=ID de la banque du maker (BIC/SWIFT): {0} offerbook.offerersBankName=Nom de la banque du maker: {0} @@ -340,6 +343,8 @@ offerbook.availableOffers=Ordres disponibles offerbook.filterByCurrency=Filtrer par devise offerbook.filterByPaymentMethod=Filtrer par mode de paiement offerbook.matchingOffers=Offres correspondants à mes comptes +offerbook.filterNoDeposit=Aucun dépôt +offerbook.noDepositOffers=Offres sans dépôt (passphrase requise) offerbook.timeSinceSigning=Informations du compte offerbook.timeSinceSigning.info=Ce compte a été vérifié et {0} offerbook.timeSinceSigning.info.arbitrator=signé par un arbitre et pouvant signer des comptes pairs @@ -485,7 +490,10 @@ createOffer.setDepositAsBuyer=Définir mon dépôt de garantie en tant qu'achete createOffer.setDepositForBothTraders=Établissez le dépôt de sécurité des deux traders (%) createOffer.securityDepositInfo=Le dépôt de garantie de votre acheteur sera de {0} createOffer.securityDepositInfoAsBuyer=Votre dépôt de garantie en tant qu''acheteur sera de {0} -createOffer.minSecurityDepositUsed=Le minimum de dépôt de garantie de l'acheteur est utilisé +createOffer.minSecurityDepositUsed=Le dépôt de sécurité minimum est utilisé +createOffer.buyerAsTakerWithoutDeposit=Aucun dépôt requis de la part de l'acheteur (protégé par un mot de passe) +createOffer.myDeposit=Mon dépôt de garantie (%) +createOffer.myDepositInfo=Votre dépôt de garantie sera de {0} #################################################################### @@ -509,6 +517,8 @@ takeOffer.fundsBox.networkFee=Total des frais de minage takeOffer.fundsBox.takeOfferSpinnerInfo=Acceptation de l'offre : {0} takeOffer.fundsBox.paymentLabel=Transaction Haveno avec l''ID {0} takeOffer.fundsBox.fundsStructure=({0} dépôt de garantie, {1} frais de transaction, {2} frais de minage) +takeOffer.fundsBox.noFundingRequiredTitle=Aucun financement requis +takeOffer.fundsBox.noFundingRequiredDescription=Obtenez la phrase secrète de l'offre auprès du vendeur en dehors de Haveno pour accepter cette offre. takeOffer.success.headline=Vous avez accepté un ordre avec succès. takeOffer.success.info=Vous pouvez voir vos transactions dans \"Portfolio/Échanges en cours\". takeOffer.error.message=Une erreur s''est produite pendant l’'acceptation de l''ordre.\n\n{0} @@ -1466,6 +1476,7 @@ offerDetailsWindow.confirm.maker=Confirmer: Placer un ordre de {0} monero offerDetailsWindow.confirm.taker=Confirmer: Acceptez l''ordre de {0} monero offerDetailsWindow.creationDate=Date de création offerDetailsWindow.makersOnion=Adresse onion du maker +offerDetailsWindow.challenge=Phrase secrète de l'offre qRCodeWindow.headline=QR Code qRCodeWindow.msg=Veuillez utiliser le code QR pour recharger du portefeuille externe au portefeuille Haveno. @@ -1692,6 +1703,9 @@ popup.accountSigning.unsignedPubKeys.signed=Les clés publiques ont été signé popup.accountSigning.unsignedPubKeys.result.signed=Clés publiques signées popup.accountSigning.unsignedPubKeys.result.failed=Échec de la signature +popup.info.buyerAsTakerWithoutDeposit.headline=Aucun dépôt requis de la part de l'acheteur +popup.info.buyerAsTakerWithoutDeposit=Votre offre ne nécessitera pas de dépôt de sécurité ni de frais de la part de l'acheteur XMR.\n\nPour accepter votre offre, vous devez partager un mot de passe avec votre partenaire commercial en dehors de Haveno.\n\nLe mot de passe est généré automatiquement et affiché dans les détails de l'offre après sa création. + #################################################################### # Notifications #################################################################### @@ -1817,6 +1831,7 @@ navigation.support=\"Assistance\" formatter.formatVolumeLabel={0} montant{1} formatter.makerTaker=Maker comme {0} {1} / Taker comme {2} {3} +formatter.makerTakerLocked=Maker comme {0} {1} / Taker comme {2} {3} 🔒 formatter.youAreAsMaker=Vous êtes {1} {0} (maker) / Le preneur est: {3} {2} formatter.youAreAsTaker=Vous êtes: {1} {0} (preneur) / Le maker est: {3} {2} formatter.youAre=Vous êtes {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 212c89f3b3..b968d83987 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -40,6 +40,8 @@ shared.buyMonero=Acquista monero shared.sellMonero=Vendi monero shared.buyCurrency=Acquista {0} shared.sellCurrency=Vendi {0} +shared.buyCurrencyLocked=Acquista {0} 🔒 +shared.sellCurrencyLocked=Vendi {0} 🔒 shared.buyingXMRWith=acquistando XMR con {0} shared.sellingXMRFor=vendendo XMR per {0} shared.buyingCurrency=comprando {0} (vendendo XMR) @@ -330,6 +332,7 @@ offerbook.createOffer=Crea offerta offerbook.takeOffer=Accetta offerta offerbook.takeOfferToBuy=Accetta l'offerta per acquistare {0} offerbook.takeOfferToSell=Accetta l'offerta per vendere {0} +offerbook.takeOffer.enterChallenge=Inserisci la passphrase dell'offerta offerbook.trader=Trader offerbook.offerersBankId=ID banca del Maker (BIC/SWIFT): {0} offerbook.offerersBankName=Nome della banca del Maker: {0} @@ -339,7 +342,9 @@ offerbook.offerersAcceptedBankSeats=Sede accettata dei paesi bancari (acquirente offerbook.availableOffers=Offerte disponibili offerbook.filterByCurrency=Filtra per valuta offerbook.filterByPaymentMethod=Filtra per metodo di pagamento -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=Offerte che corrispondono ai miei account +offerbook.filterNoDeposit=Nessun deposito +offerbook.noDepositOffers=Offerte senza deposito (passphrase richiesta) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=Questo account è stato verificato e {0} offerbook.timeSinceSigning.info.arbitrator=firmato da un arbitro e può firmare account peer @@ -484,7 +489,10 @@ createOffer.setDepositAsBuyer=Imposta il mio deposito cauzionale come acquirente createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Il deposito cauzionale dell'acquirente sarà {0} createOffer.securityDepositInfoAsBuyer=Il tuo deposito cauzionale come acquirente sarà {0} -createOffer.minSecurityDepositUsed=Viene utilizzato il minimo deposito cauzionale dell'acquirente +createOffer.minSecurityDepositUsed=Il deposito di sicurezza minimo è utilizzato +createOffer.buyerAsTakerWithoutDeposit=Nessun deposito richiesto dal compratore (protetto da passphrase) +createOffer.myDeposit=Il mio deposito di sicurezza (%) +createOffer.myDepositInfo=Il tuo deposito di sicurezza sarà {0} #################################################################### @@ -508,6 +516,8 @@ takeOffer.fundsBox.networkFee=Totale commissioni di mining takeOffer.fundsBox.takeOfferSpinnerInfo=Accettare l'offerta: {0} takeOffer.fundsBox.paymentLabel=Scambia Haveno con ID {0} takeOffer.fundsBox.fundsStructure=({0} deposito cauzionale, {1} commissione commerciale, {2} commissione mineraria) +takeOffer.fundsBox.noFundingRequiredTitle=Nessun finanziamento richiesto +takeOffer.fundsBox.noFundingRequiredDescription=Ottieni la passphrase dell'offerta dal venditore fuori da Haveno per accettare questa offerta. takeOffer.success.headline=Hai accettato con successo un'offerta. takeOffer.success.info=Puoi vedere lo stato del tuo scambio su \"Portafoglio/Scambi aperti\". takeOffer.error.message=Si è verificato un errore durante l'accettazione dell'offerta.\n\n{0} @@ -1463,6 +1473,7 @@ offerDetailsWindow.confirm.maker=Conferma: Piazza l'offerta a {0} monero offerDetailsWindow.confirm.taker=Conferma: Accetta l'offerta a {0} monero offerDetailsWindow.creationDate=Data di creazione offerDetailsWindow.makersOnion=Indirizzo .onion del maker +offerDetailsWindow.challenge=Passphrase dell'offerta qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1686,6 +1697,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=Nessun deposito richiesto dal compratore +popup.info.buyerAsTakerWithoutDeposit=La tua offerta non richiederà un deposito di sicurezza o una commissione da parte dell'acquirente XMR.\n\nPer accettare la tua offerta, devi condividere una passphrase con il tuo partner commerciale al di fuori di Haveno.\n\nLa passphrase viene generata automaticamente e mostrata nei dettagli dell'offerta dopo la creazione. + #################################################################### # Notifications #################################################################### @@ -1811,6 +1825,7 @@ navigation.support=\"Supporto\" formatter.formatVolumeLabel={0} importo{1} formatter.makerTaker=Maker come {0} {1} / Taker come {2} {3} +formatter.makerTakerLocked=Maker come {0} {1} / Taker come {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Sei {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 80542bc3e8..70a3536104 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -40,6 +40,8 @@ shared.buyMonero=ビットコインを買う shared.sellMonero=ビットコインを売る shared.buyCurrency={0}を買う shared.sellCurrency={0}を売る +shared.buyCurrencyLocked={0}を買う 🔒 +shared.sellCurrencyLocked={0}を売る 🔒 shared.buyingXMRWith=XMRを{0}で買う shared.sellingXMRFor=XMRを{0}で売る shared.buyingCurrency={0}を購入中 (XMRを売却中) @@ -330,6 +332,7 @@ offerbook.createOffer=オファーを作る offerbook.takeOffer=オファーを受ける offerbook.takeOfferToBuy={0}購入オファーを受ける offerbook.takeOfferToSell={0}売却オファーを受ける +offerbook.takeOffer.enterChallenge=オファーのパスフレーズを入力してください offerbook.trader=取引者 offerbook.offerersBankId=メイカーの銀行ID (BIC/SWIFT): {0} offerbook.offerersBankName=メーカーの銀行名: {0} @@ -340,6 +343,8 @@ offerbook.availableOffers=利用可能なオファー offerbook.filterByCurrency=通貨でフィルター offerbook.filterByPaymentMethod=支払い方法でフィルター offerbook.matchingOffers=アカウントと一致するオファー +offerbook.filterNoDeposit=デポジットなし +offerbook.noDepositOffers=預金不要のオファー(パスフレーズ必須) offerbook.timeSinceSigning=アカウント情報 offerbook.timeSinceSigning.info=このアカウントは認証されまして、{0} offerbook.timeSinceSigning.info.arbitrator=調停人に署名されました。ピアアカウントも署名できます @@ -485,7 +490,10 @@ createOffer.setDepositAsBuyer=購入時のセキュリティデポジット (%) createOffer.setDepositForBothTraders=両方の取引者の保証金を設定する(%) createOffer.securityDepositInfo=あなたの買い手のセキュリティデポジットは{0}です createOffer.securityDepositInfoAsBuyer=あなたの購入時のセキュリティデポジットは{0}です -createOffer.minSecurityDepositUsed=最小値の買い手の保証金は使用されます +createOffer.minSecurityDepositUsed=最低セキュリティデポジットが使用されます +createOffer.buyerAsTakerWithoutDeposit=購入者に保証金は不要(パスフレーズ保護) +createOffer.myDeposit=私の保証金(%) +createOffer.myDepositInfo=あなたのセキュリティデポジットは{0}です #################################################################### @@ -509,6 +517,8 @@ takeOffer.fundsBox.networkFee=合計マイニング手数料 takeOffer.fundsBox.takeOfferSpinnerInfo=オファーを受け入れる: {0} takeOffer.fundsBox.paymentLabel=次のIDとのHavenoトレード: {0} takeOffer.fundsBox.fundsStructure=({0} セキュリティデポジット, {1} 取引手数料, {2}マイニング手数料) +takeOffer.fundsBox.noFundingRequiredTitle=資金は必要ありません +takeOffer.fundsBox.noFundingRequiredDescription=このオファーを受けるには、Haveno外で売り手からオファーパスフレーズを取得してください。 takeOffer.success.headline=オファー受け入れに成功しました takeOffer.success.info=あなたのトレード状態は「ポートフォリオ/オープントレード」で見られます takeOffer.error.message=オファーの受け入れ時にエラーが発生しました。\n\n{0} @@ -1464,6 +1474,7 @@ offerDetailsWindow.confirm.maker=承認: ビットコインを{0}オファーを offerDetailsWindow.confirm.taker=承認: ビットコインを{0}オファーを受ける offerDetailsWindow.creationDate=作成日 offerDetailsWindow.makersOnion=メイカーのonionアドレス +offerDetailsWindow.challenge=オファーパスフレーズ qRCodeWindow.headline=QRコード qRCodeWindow.msg=外部ウォレットからHavenoウォレットへ送金するのに、このQRコードを利用して下さい。 @@ -1689,6 +1700,9 @@ popup.accountSigning.unsignedPubKeys.signed=パブリックキーは署名され popup.accountSigning.unsignedPubKeys.result.signed=署名されたパブリックキー popup.accountSigning.unsignedPubKeys.result.failed=署名が失敗しました +popup.info.buyerAsTakerWithoutDeposit.headline=購入者による保証金は不要 +popup.info.buyerAsTakerWithoutDeposit=あなたのオファーには、XMR購入者からのセキュリティデポジットや手数料は必要ありません。\n\nオファーを受け入れるには、Haveno外で取引相手とパスフレーズを共有する必要があります。\n\nパスフレーズは自動的に生成され、作成後にオファーの詳細に表示されます。 + #################################################################### # Notifications #################################################################### @@ -1814,6 +1828,7 @@ navigation.support=「サポート」 formatter.formatVolumeLabel={0} 額{1} formatter.makerTaker=メイカーは{0} {1} / テイカーは{2} {3} +formatter.makerTakerLocked=メイカーは{0} {1} / テイカーは{2} {3} 🔒 formatter.youAreAsMaker=あなたは:{1} {0}(メイカー) / テイカーは:{3} {2} formatter.youAreAsTaker=あなたは:{1} {0}(テイカー) / メイカーは{3} {2} formatter.youAre=あなたは{0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index f6659b16ef..93179908d6 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -40,6 +40,8 @@ shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} +shared.buyCurrencyLocked=Comprar {0} 🔒 +shared.sellCurrencyLocked=Vender {0} 🔒 shared.buyingXMRWith=comprando XMR com {0} shared.sellingXMRFor=vendendo XMR por {0} shared.buyingCurrency=comprando {0} (vendendo XMR) @@ -333,6 +335,7 @@ offerbook.createOffer=Criar oferta offerbook.takeOffer=Aceitar oferta offerbook.takeOfferToBuy=Comprar {0} offerbook.takeOfferToSell=Vender {0} +offerbook.takeOffer.enterChallenge=Digite a senha da oferta offerbook.trader=Trader offerbook.offerersBankId=ID do banco do ofertante (BIC/SWIFT): {0} offerbook.offerersBankName=Nome do banco do ofertante: {0} @@ -342,7 +345,9 @@ offerbook.offerersAcceptedBankSeats=Países aceitos como sede bancária (tomador offerbook.availableOffers=Ofertas disponíveis offerbook.filterByCurrency=Filtrar por moeda offerbook.filterByPaymentMethod=Filtrar por método de pagamento -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=Ofertas que correspondem às minhas contas +offerbook.filterNoDeposit=Sem depósito +offerbook.noDepositOffers=Ofertas sem depósito (senha necessária) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=Esta conta foi verificada e {0} offerbook.timeSinceSigning.info.arbitrator=assinada por um árbitro e pode assinar contas de pares @@ -487,7 +492,10 @@ createOffer.setDepositAsBuyer=Definir o meu depósito de segurança como comprad createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=O seu depósito de segurança do comprador será de {0} createOffer.securityDepositInfoAsBuyer=O seu depósito de segurança como comprador será de {0} -createOffer.minSecurityDepositUsed=Depósito de segurança mínimo para compradores foi usado +createOffer.minSecurityDepositUsed=O depósito de segurança mínimo é utilizado +createOffer.buyerAsTakerWithoutDeposit=Nenhum depósito necessário do comprador (protegido por senha) +createOffer.myDeposit=Meu depósito de segurança (%) +createOffer.myDepositInfo=Seu depósito de segurança será {0} #################################################################### @@ -511,6 +519,8 @@ takeOffer.fundsBox.networkFee=Total em taxas de mineração takeOffer.fundsBox.takeOfferSpinnerInfo=Aceitando a oferta: {0} takeOffer.fundsBox.paymentLabel=negociação Haveno com ID {0} takeOffer.fundsBox.fundsStructure=({0} depósito de segurança, {1} taxa de transação, {2} taxa de mineração) +takeOffer.fundsBox.noFundingRequiredTitle=Sem financiamento necessário +takeOffer.fundsBox.noFundingRequiredDescription=Obtenha a frase secreta da oferta com o vendedor fora do Haveno para aceitar esta oferta. takeOffer.success.headline=Você aceitou uma oferta com sucesso. takeOffer.success.info=Você pode ver o status de sua negociação em \"Portfolio/Negociações em aberto\". takeOffer.error.message=Ocorreu um erro ao aceitar a oferta.\n\n{0} @@ -1467,6 +1477,7 @@ offerDetailsWindow.confirm.maker=Criar oferta para {0} monero offerDetailsWindow.confirm.taker=Confirmar: Aceitar oferta de {0} monero offerDetailsWindow.creationDate=Criada em offerDetailsWindow.makersOnion=Endereço onion do ofertante +offerDetailsWindow.challenge=Passphrase da oferta qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1693,6 +1704,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=Nenhum depósito exigido do comprador +popup.info.buyerAsTakerWithoutDeposit=Sua oferta não exigirá um depósito de segurança ou taxa do comprador de XMR.\n\nPara aceitar sua oferta, você deve compartilhar uma senha com seu parceiro de negociação fora do Haveno.\n\nA senha é gerada automaticamente e exibida nos detalhes da oferta após a criação. + #################################################################### # Notifications #################################################################### @@ -1818,6 +1832,7 @@ navigation.support=\"Suporte\" formatter.formatVolumeLabel={0} quantia{1} formatter.makerTaker=Ofertante: {1} de {0} / Aceitador: {3} de {2} +formatter.makerTakerLocked=Ofertante: {1} de {0} / Aceitador: {3} de {2} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Você está {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index d7891bead6..1b1b5de6ec 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -40,6 +40,8 @@ shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} +shared.buyCurrencyLocked=Comprar {0} 🔒 +shared.sellCurrencyLocked=Vender {0} 🔒 shared.buyingXMRWith=comprando XMR com {0} shared.sellingXMRFor=vendendo XMR por {0} shared.buyingCurrency=comprando {0} (vendendo XMR) @@ -193,7 +195,7 @@ shared.iConfirm=Eu confirmo shared.openURL=Abrir {0} shared.fiat=Moeda fiduciária shared.crypto=Cripto -shared.preciousMetals=TODO +shared.preciousMetals=Metais Preciosos shared.all=Tudo shared.edit=Editar shared.advancedOptions=Opções avançadas @@ -330,6 +332,7 @@ offerbook.createOffer=Criar oferta offerbook.takeOffer=Aceitar oferta offerbook.takeOfferToBuy=Aceitar oferta para comprar {0} offerbook.takeOfferToSell=Aceitar oferta para vender {0} +offerbook.takeOffer.enterChallenge=Digite a senha da oferta offerbook.trader=Negociador offerbook.offerersBankId=ID do banco do ofertante (BIC/SWIFT): {0} offerbook.offerersBankName=Nome do banco do ofertante: {0} @@ -339,7 +342,9 @@ offerbook.offerersAcceptedBankSeats=Sede do banco aceite (aceitador):\n {0} offerbook.availableOffers=Ofertas disponíveis offerbook.filterByCurrency=Filtrar por moeda offerbook.filterByPaymentMethod=Filtrar por método de pagamento -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=Ofertas que correspondem às minhas contas +offerbook.filterNoDeposit=Sem depósito +offerbook.noDepositOffers=Ofertas sem depósito (senha necessária) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=Esta conta foi verificada e {0} offerbook.timeSinceSigning.info.arbitrator=assinada pelo árbitro e pode assinar contas de pares @@ -484,7 +489,10 @@ createOffer.setDepositAsBuyer=Definir o meu depósito de segurança enquanto com createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=O depósito de segurança do seu comprador será {0} createOffer.securityDepositInfoAsBuyer=O seu depósito de segurança enquanto comprador será {0} -createOffer.minSecurityDepositUsed=O mín. depósito de segurança para o comprador é utilizado +createOffer.minSecurityDepositUsed=O depósito de segurança mínimo é utilizado +createOffer.buyerAsTakerWithoutDeposit=Nenhum depósito exigido do comprador (protegido por frase secreta) +createOffer.myDeposit=Meu depósito de segurança (%) +createOffer.myDepositInfo=Seu depósito de segurança será {0} #################################################################### @@ -508,6 +516,8 @@ takeOffer.fundsBox.networkFee=Total de taxas de mineração takeOffer.fundsBox.takeOfferSpinnerInfo=Aceitando a oferta: {0} takeOffer.fundsBox.paymentLabel=negócio do Haveno com ID {0} takeOffer.fundsBox.fundsStructure=({0} depósito de segurança, {1} taxa de negócio, {2} taxa de mineração) +takeOffer.fundsBox.noFundingRequiredTitle=Nenhum financiamento necessário +takeOffer.fundsBox.noFundingRequiredDescription=Obtenha a senha da oferta com o vendedor fora do Haveno para aceitar esta oferta. takeOffer.success.headline=Você aceitou uma oferta com sucesso. takeOffer.success.info=Você pode ver o estado de seu negócio em \"Portefólio/Negócios abertos\". takeOffer.error.message=Ocorreu um erro ao aceitar a oferta .\n\n{0} @@ -1460,6 +1470,7 @@ offerDetailsWindow.confirm.maker=Confirmar: Criar oferta para {0} monero offerDetailsWindow.confirm.taker=Confirmar: Aceitar oferta de {0} monero offerDetailsWindow.creationDate=Data de criação offerDetailsWindow.makersOnion=Endereço onion do ofertante +offerDetailsWindow.challenge=Passphrase da oferta qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1683,6 +1694,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=Nenhum depósito exigido do comprador +popup.info.buyerAsTakerWithoutDeposit=Sua oferta não exigirá um depósito de segurança ou taxa do comprador de XMR.\n\nPara aceitar sua oferta, você deve compartilhar uma senha com seu parceiro comercial fora do Haveno.\n\nA senha é gerada automaticamente e exibida nos detalhes da oferta após a criação. + #################################################################### # Notifications #################################################################### @@ -1808,6 +1822,7 @@ navigation.support=\"Apoio\" formatter.formatVolumeLabel={0} quantia{1} formatter.makerTaker=Ofertante como {0} {1} / Aceitador como {2} {3} +formatter.makerTakerLocked=Ofertante como {0} {1} / Aceitador como {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Você é {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 6f1ba86d90..d40f9af7a9 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -40,6 +40,8 @@ shared.buyMonero=Купить биткойн shared.sellMonero=Продать биткойн shared.buyCurrency=Купить {0} shared.sellCurrency=Продать {0} +shared.buyCurrencyLocked=Купить {0} 🔒 +shared.sellCurrencyLocked=Продать {0} 🔒 shared.buyingXMRWith=покупка ВТС за {0} shared.sellingXMRFor=продажа ВТС за {0} shared.buyingCurrency=покупка {0} (продажа ВТС) @@ -330,6 +332,7 @@ offerbook.createOffer=Создать предложение offerbook.takeOffer=Принять предложение offerbook.takeOfferToBuy=Принять предложение купить {0} offerbook.takeOfferToSell=Принять предложение продать {0} +offerbook.takeOffer.enterChallenge=Введите фразу-пароль предложения offerbook.trader=Трейдер offerbook.offerersBankId=Идент. банка (BIC/SWIFT) мейкера: {0} offerbook.offerersBankName=Название банка мейкера: {0} @@ -339,7 +342,9 @@ offerbook.offerersAcceptedBankSeats=Допустимые страны банка offerbook.availableOffers=Доступные предложения offerbook.filterByCurrency=Фильтровать по валюте offerbook.filterByPaymentMethod=Фильтровать по способу оплаты -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=Предложения, соответствующие моим аккаунтам +offerbook.filterNoDeposit=Нет депозита +offerbook.noDepositOffers=Предложения без депозита (требуется пароль) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts @@ -484,7 +489,10 @@ createOffer.setDepositAsBuyer=Установить мой залог как по createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Сумма залога покупателя: {0} createOffer.securityDepositInfoAsBuyer=Сумма вашего залога: {0} -createOffer.minSecurityDepositUsed=Min. buyer security deposit is used +createOffer.minSecurityDepositUsed=Минимальный залог используется +createOffer.buyerAsTakerWithoutDeposit=Залог от покупателя не требуется (защищено паролем) +createOffer.myDeposit=Мой залог (%) +createOffer.myDepositInfo=Ваш залог составит {0} #################################################################### @@ -508,6 +516,8 @@ takeOffer.fundsBox.networkFee=Oбщая комиссия майнера takeOffer.fundsBox.takeOfferSpinnerInfo=Принятие предложения: {0} takeOffer.fundsBox.paymentLabel=Сделка в Haveno с идентификатором {0} takeOffer.fundsBox.fundsStructure=({0} — залог, {1} — комиссия за сделку, {2} — комиссия майнера) +takeOffer.fundsBox.noFundingRequiredTitle=Не требуется финансирование +takeOffer.fundsBox.noFundingRequiredDescription=Получите пароль предложения от продавца вне Haveno, чтобы принять это предложение. takeOffer.success.headline=Вы успешно приняли предложение. takeOffer.success.info=Статус вашей сделки отображается в разделе \«Папка/Текущие сделки\». takeOffer.error.message=Ошибка при принятии предложения:\n\n{0} @@ -1461,6 +1471,7 @@ offerDetailsWindow.confirm.maker=Подтвердите: разместить п offerDetailsWindow.confirm.taker=Подтвердите: принять предложение {0} биткойн offerDetailsWindow.creationDate=Дата создания offerDetailsWindow.makersOnion=Onion-адрес мейкера +offerDetailsWindow.challenge=Пароль предложения qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1684,6 +1695,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=Депозит от покупателя не требуется +popup.info.buyerAsTakerWithoutDeposit=Ваше предложение не потребует залога или комиссии от покупателя XMR.\n\nЧтобы принять ваше предложение, вы должны поделиться парольной фразой с вашим торговым партнером вне Haveno.\n\nПарольная фраза генерируется автоматически и отображается в деталях предложения после его создания. + #################################################################### # Notifications #################################################################### @@ -1809,6 +1823,7 @@ navigation.support=\«Поддержка\» formatter.formatVolumeLabel={0} сумма {1} formatter.makerTaker=Мейкер как {0} {1} / Тейкер как {2} {3} +formatter.makerTakerLocked=Мейкер как {0} {1} / Тейкер как {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Вы {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index db2855c570..81955b6f43 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -40,6 +40,8 @@ shared.buyMonero=ซื้อ monero (บิตคอยน์) shared.sellMonero=ขาย monero (บิตคอยน์) shared.buyCurrency=ซื้อ {0} shared.sellCurrency=ขาย {0} +shared.buyCurrencyLocked=ซื้อ {0} 🔒 +shared.sellCurrencyLocked=ขาย {0} 🔒 shared.buyingXMRWith=การซื้อ XMR กับ {0} shared.sellingXMRFor=การขาย XMR แก่ {0} shared.buyingCurrency=การซื้อ {0} (การขาย XMR) @@ -330,6 +332,7 @@ offerbook.createOffer=สร้างข้อเสนอ offerbook.takeOffer=รับข้อเสนอ offerbook.takeOfferToBuy=Take offer to buy {0} offerbook.takeOfferToSell=Take offer to sell {0} +offerbook.takeOffer.enterChallenge=กรอกพาสเฟรสข้อเสนอ offerbook.trader=Trader (เทรดเดอร์) offerbook.offerersBankId=รหัสธนาคารของผู้สร้าง (BIC / SWIFT): {0} offerbook.offerersBankName=ชื่อธนาคารของผู้สร้าง: {0} @@ -339,7 +342,9 @@ offerbook.offerersAcceptedBankSeats=ยอมรับตำแหน่งป offerbook.availableOffers=ข้อเสนอที่พร้อมใช้งาน offerbook.filterByCurrency=กรองตามสกุลเงิน offerbook.filterByPaymentMethod=ตัวกรองตามวิธีการชำระเงิน -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=ข้อเสนอที่ตรงกับบัญชีของฉัน +offerbook.filterNoDeposit=ไม่มีเงินมัดจำ +offerbook.noDepositOffers=ข้อเสนอที่ไม่มีเงินมัดจำ (ต้องการรหัสผ่าน) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts @@ -484,7 +489,10 @@ createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Your buyer''s security deposit will be {0} createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} -createOffer.minSecurityDepositUsed=Min. buyer security deposit is used +createOffer.minSecurityDepositUsed=เงินประกันความปลอดภัยขั้นต่ำถูกใช้ +createOffer.buyerAsTakerWithoutDeposit=ไม่ต้องวางมัดจำจากผู้ซื้อ (ป้องกันด้วยรหัสผ่าน) +createOffer.myDeposit=เงินประกันความปลอดภัยของฉัน (%) +createOffer.myDepositInfo=เงินประกันความปลอดภัยของคุณจะเป็น {0} #################################################################### @@ -508,6 +516,8 @@ takeOffer.fundsBox.networkFee=ยอดรวมค่าธรรมเนี takeOffer.fundsBox.takeOfferSpinnerInfo=ยอมรับข้อเสนอ: {0} takeOffer.fundsBox.paymentLabel=การซื้อขาย Haveno ด้วย ID {0} takeOffer.fundsBox.fundsStructure=({0} เงินประกัน {1} ค่าธรรมเนียมการซื้อขาย {2} ค่าธรรมเนียมการขุด) +takeOffer.fundsBox.noFundingRequiredTitle=ไม่ต้องใช้เงินทุน +takeOffer.fundsBox.noFundingRequiredDescription=รับรหัสผ่านข้อเสนอจากผู้ขายภายนอก Haveno เพื่อรับข้อเสนอนี้ takeOffer.success.headline=คุณได้รับข้อเสนอเป็นที่เรีบยร้อยแล้ว takeOffer.success.info=คุณสามารถดูสถานะการค้าของคุณได้ที่ \ "Portfolio (แฟ้มผลงาน) / เปิดการซื้อขาย \" takeOffer.error.message=เกิดข้อผิดพลาดขณะรับข้อเสนอ\n\n{0} @@ -1461,6 +1471,7 @@ offerDetailsWindow.confirm.maker=ยืนยัน: ยื่นข้อเส offerDetailsWindow.confirm.taker=ยืนยัน: รับข้อเสนอไปยัง {0} บิทคอยน์ offerDetailsWindow.creationDate=วันที่สร้าง offerDetailsWindow.makersOnion=ที่อยู่ onion ของผู้สร้าง +offerDetailsWindow.challenge=รหัสผ่านสำหรับข้อเสนอ qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1684,6 +1695,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=ไม่ต้องมีเงินมัดจำจากผู้ซื้อ +popup.info.buyerAsTakerWithoutDeposit=ข้อเสนอของคุณจะไม่ต้องการเงินมัดจำหรือค่าธรรมเนียมจากผู้ซื้อ XMR\n\nในการยอมรับข้อเสนอของคุณ คุณต้องแบ่งปันรหัสผ่านกับคู่ค้าการค้าของคุณภายนอก Haveno\n\nรหัสผ่านจะถูกสร้างโดยอัตโนมัติและแสดงในรายละเอียดข้อเสนอหลังจากการสร้าง + #################################################################### # Notifications #################################################################### @@ -1809,6 +1823,7 @@ navigation.support=\"ช่วยเหลือและสนับสนุ formatter.formatVolumeLabel={0} จำนวนยอด{1} formatter.makerTaker=ผู้สร้าง เป็น {0} {1} / ผู้รับเป็น {2} {3} +formatter.makerTakerLocked=ผู้สร้าง เป็น {0} {1} / ผู้รับเป็น {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=คุณคือ {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index d8436a0f41..275b95677f 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -43,6 +43,8 @@ shared.buyMonero=Monero Satın Al shared.sellMonero=Monero Sat shared.buyCurrency={0} satın al shared.sellCurrency={0} sat +shared.buyCurrencyLocked={0} satın al 🔒 +shared.sellCurrencyLocked={0} sat 🔒 shared.buyingXMRWith={0} ile XMR satın alınıyor shared.sellingXMRFor={0} karşılığında XMR satılıyor shared.buyingCurrency={0} satın alınıyor (XMR satılıyor) @@ -346,6 +348,7 @@ market.trades.showVolumeInUSD=Hacmi USD olarak göster offerbook.createOffer=Teklif oluştur offerbook.takeOffer=Teklif al offerbook.takeOffer.createAccount=Hesap oluştur ve teklifi al +offerbook.takeOffer.enterChallenge=Teklif şifresini girin offerbook.trader=Yatırımcı offerbook.offerersBankId=Yapıcının banka kimliği (BIC/SWIFT): {0} offerbook.offerersBankName=Yapıcının banka adı: {0} @@ -357,6 +360,8 @@ offerbook.availableOffersToSell={0} için {1} sat offerbook.filterByCurrency=Para birimini seç offerbook.filterByPaymentMethod=Ödeme yöntemini seç offerbook.matchingOffers=Uygun Teklif +offerbook.filterNoDeposit=Depozito yok +offerbook.noDepositOffers=Depozitosuz teklifler (şifre gereklidir) offerbook.timeSinceSigning=Hesap bilgisi offerbook.timeSinceSigning.info.arbitrator=bir hakem tarafından imzalandı ve eş hesaplarını imzalayabilir offerbook.timeSinceSigning.info.peer=bir eş tarafından imzalandı, limitlerin kaldırılması için %d gün bekleniyor @@ -524,7 +529,10 @@ createOffer.setDepositAsBuyer=Alıcı olarak benim güvenlik teminatımı ayarla createOffer.setDepositForBothTraders=Tüccarların güvenlik teminatı (%) createOffer.securityDepositInfo=Alıcının güvenlik teminatı {0} olacak createOffer.securityDepositInfoAsBuyer=Alıcı olarak güvenlik teminatınız {0} olacak -createOffer.minSecurityDepositUsed=Minimum alıcı güvenlik teminatı kullanıldı +createOffer.minSecurityDepositUsed=Minimum güvenlik depozitosu kullanılır +createOffer.buyerAsTakerWithoutDeposit=Alıcıdan depozito gerekmez (şifre korumalı) +createOffer.myDeposit=Güvenlik depozitam (%) +createOffer.myDepositInfo=Güvenlik depozitonuz {0} olacaktır. #################################################################### @@ -550,6 +558,8 @@ takeOffer.fundsBox.networkFee=Toplam madencilik ücretleri takeOffer.fundsBox.takeOfferSpinnerInfo=Teklif alınıyor: {0} takeOffer.fundsBox.paymentLabel=ID {0} ile Haveno işlemi takeOffer.fundsBox.fundsStructure=({0} güvenlik teminatı, {1} işlem ücreti) +takeOffer.fundsBox.noFundingRequiredTitle=Fonlama gerekmez +takeOffer.fundsBox.noFundingRequiredDescription=Bu teklifi almak için satıcıdan passphrase'i Haveno dışında alınız. takeOffer.success.headline=Teklifi başarıyla aldınız. takeOffer.success.info=İşleminizin durumunu \"Portföy/Açık işlemler\" kısmında görebilirsiniz. takeOffer.error.message=Teklif alımı sırasında bir hata oluştu.\n\n{0} @@ -1963,6 +1973,7 @@ offerDetailsWindow.confirm.taker=Onayla: {0} monero teklifi al offerDetailsWindow.confirm.takerCrypto=Onayla: {0} {1} teklifi al offerDetailsWindow.creationDate=Oluşturma tarihi offerDetailsWindow.makersOnion=Yapıcı'nın onion adresi +offerDetailsWindow.challenge=Teklif şifresi qRCodeWindow.headline=QR Kodu qRCodeWindow.msg=Harici cüzdanınızdan Haveno cüzdanınızı finanse etmek için bu QR kodunu kullanın. @@ -2260,6 +2271,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkey'ler imzalandı popup.accountSigning.unsignedPubKeys.result.signed=İmzalanmış pubkey'ler popup.accountSigning.unsignedPubKeys.result.failed=İmzalama başarısız oldu +popup.info.buyerAsTakerWithoutDeposit.headline=Alıcıdan depozito gerekmez +popup.info.buyerAsTakerWithoutDeposit=Teklifiniz, XMR alıcısından güvenlik depozitosu veya ücret talep etmeyecektir.\n\nTeklifinizi kabul etmek için, ticaret ortağınızla Haveno dışında bir şifre paylaşmalısınız.\n\nŞifre otomatik olarak oluşturulur ve oluşturulduktan sonra teklif detaylarında görüntülenir. + popup.info.torMigration.msg=Haveno düğümünüz muhtemelen eski bir Tor v2 adresi kullanıyor. \ Lütfen Haveno düğümünüzü bir Tor v3 adresine geçirin. \ Önceden veri dizininizi yedeklediğinizden emin olun. @@ -2399,6 +2413,7 @@ navigation.support="Destek" formatter.formatVolumeLabel={0} miktar{1} formatter.makerTaker=Yapan olarak {0} {1} / Alan olarak {2} {3} +formatter.makerTakerLocked=Yapıcı olarak {0} {1} / Alan olarak {2} {3} 🔒 formatter.youAreAsMaker=Yapan sizsiniz: {1} {0} (maker) / Alan: {3} {2} formatter.youAreAsTaker=Alan sizsiniz: {1} {0} (taker) / Yapan: {3} {2} formatter.youAre=Şu anda sizsiniz {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 828e81e4b1..8e541353b7 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -40,6 +40,8 @@ shared.buyMonero=Mua monero shared.sellMonero=Bán monero shared.buyCurrency=Mua {0} shared.sellCurrency=Bán {0} +shared.buyCurrencyLocked=Mua {0} 🔒 +shared.sellCurrencyLocked=Bán {0} 🔒 shared.buyingXMRWith=đang mua XMR với {0} shared.sellingXMRFor=đang bán XMR với {0} shared.buyingCurrency=đang mua {0} (đang bán XMR) @@ -330,6 +332,7 @@ offerbook.createOffer=Tạo chào giá offerbook.takeOffer=Nhận chào giá offerbook.takeOfferToBuy=Nhận chào giá mua {0} offerbook.takeOfferToSell=Nhận chào giá bán {0} +offerbook.takeOffer.enterChallenge=Nhập mật khẩu đề nghị offerbook.trader=Trader offerbook.offerersBankId=ID ngân hàng của người tạo (BIC/SWIFT): {0} offerbook.offerersBankName=Tên ngân hàng của người tạo: {0} @@ -339,7 +342,9 @@ offerbook.offerersAcceptedBankSeats=Các quốc gia có ngân hàng được ch offerbook.availableOffers=Các chào giá hiện có offerbook.filterByCurrency=Lọc theo tiền tệ offerbook.filterByPaymentMethod=Lọc theo phương thức thanh toán -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=Các ưu đãi phù hợp với tài khoản của tôi +offerbook.filterNoDeposit=Không đặt cọc +offerbook.noDepositOffers=Các ưu đãi không yêu cầu đặt cọc (cần mật khẩu) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts @@ -484,7 +489,10 @@ createOffer.setDepositAsBuyer=Cài đặt tiền đặt cọc của tôi với v createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Số tiền đặt cọc cho người mua của bạn sẽ là {0} createOffer.securityDepositInfoAsBuyer=Số tiền đặt cọc của bạn với vai trò người mua sẽ là {0} -createOffer.minSecurityDepositUsed=Min. buyer security deposit is used +createOffer.minSecurityDepositUsed=Khoản tiền đặt cọc bảo mật tối thiểu được sử dụng +createOffer.buyerAsTakerWithoutDeposit=Không cần đặt cọc từ người mua (được bảo vệ bằng mật khẩu) +createOffer.myDeposit=Tiền đặt cọc bảo mật của tôi (%) +createOffer.myDepositInfo=Khoản tiền đặt cọc của bạn sẽ là {0} #################################################################### @@ -508,6 +516,8 @@ takeOffer.fundsBox.networkFee=Tổng phí đào takeOffer.fundsBox.takeOfferSpinnerInfo=Chấp nhận đề xuất: {0} takeOffer.fundsBox.paymentLabel=giao dịch Haveno có ID {0} takeOffer.fundsBox.fundsStructure=({0} tiền gửi đại lý, {1} phí giao dịch, {2} phí đào) +takeOffer.fundsBox.noFundingRequiredTitle=Không cần tài trợ +takeOffer.fundsBox.noFundingRequiredDescription=Lấy mật khẩu giao dịch từ người bán ngoài Haveno để nhận đề nghị này. takeOffer.success.headline=Bạn đã nhận báo giá thành công. takeOffer.success.info=Bạn có thể xem trạng thái giao dịch của bạn tại \"Portfolio/Các giao dịch mở\". takeOffer.error.message=Có lỗi xảy ra khi nhận báo giá.\n\n{0} @@ -1463,6 +1473,7 @@ offerDetailsWindow.confirm.maker=Xác nhận: Đặt chào giá cho {0} monero offerDetailsWindow.confirm.taker=Xác nhận: Nhận chào giáo cho {0} monero offerDetailsWindow.creationDate=Ngày tạo offerDetailsWindow.makersOnion=Địa chỉ onion của người tạo +offerDetailsWindow.challenge=Mã bảo vệ giao dịch qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1686,6 +1697,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=Không cần đặt cọc từ người mua +popup.info.buyerAsTakerWithoutDeposit=Lời đề nghị của bạn sẽ không yêu cầu khoản đặt cọc bảo mật hoặc phí từ người mua XMR.\n\nĐể chấp nhận lời đề nghị của bạn, bạn phải chia sẻ một mật khẩu với đối tác giao dịch ngoài Haveno.\n\nMật khẩu được tạo tự động và hiển thị trong chi tiết lời đề nghị sau khi tạo. + #################################################################### # Notifications #################################################################### @@ -1811,6 +1825,7 @@ navigation.support=\"Hỗ trợ\" formatter.formatVolumeLabel={0} giá trị {1} formatter.makerTaker=Người tạo là {0} {1} / Người nhận là {2} {3} +formatter.makerTakerLocked=Người tạo là {0} {1} / Người nhận là {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Bạn là {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 684ba4c663..0f42566eef 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -40,6 +40,8 @@ shared.buyMonero=买入比特币 shared.sellMonero=卖出比特币 shared.buyCurrency=买入 {0} shared.sellCurrency=卖出 {0} +shared.buyCurrencyLocked=买入 {0} 🔒 +shared.sellCurrencyLocked=卖出 {0} 🔒 shared.buyingXMRWith=用 {0} 买入 XMR shared.sellingXMRFor=卖出 XMR 为 {0} shared.buyingCurrency=买入 {0}(卖出 XMR) @@ -330,6 +332,7 @@ offerbook.createOffer=创建报价 offerbook.takeOffer=接受报价 offerbook.takeOfferToBuy=接受报价来收购 {0} offerbook.takeOfferToSell=接受报价来出售 {0} +offerbook.takeOffer.enterChallenge=输入报价密码 offerbook.trader=商人 offerbook.offerersBankId=卖家的银行 ID(BIC/SWIFT):{0} offerbook.offerersBankName=卖家的银行名称:{0} @@ -339,7 +342,9 @@ offerbook.offerersAcceptedBankSeats=接受的银行所在国家(买家):\n offerbook.availableOffers=可用报价 offerbook.filterByCurrency=以货币筛选 offerbook.filterByPaymentMethod=以支付方式筛选 -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=匹配我的账户的报价 +offerbook.filterNoDeposit=无押金 +offerbook.noDepositOffers=无押金的报价(需要密码短语) offerbook.timeSinceSigning=账户信息 offerbook.timeSinceSigning.info=此账户已验证,{0} offerbook.timeSinceSigning.info.arbitrator=由仲裁员验证,并可以验证伙伴账户 @@ -485,7 +490,10 @@ createOffer.setDepositAsBuyer=设置自己作为买家的保证金(%) createOffer.setDepositForBothTraders=设置双方的保证金比例(%) createOffer.securityDepositInfo=您的买家的保证金将会是 {0} createOffer.securityDepositInfoAsBuyer=您作为买家的保证金将会是 {0} -createOffer.minSecurityDepositUsed=已使用最低买家保证金 +createOffer.minSecurityDepositUsed=最低安全押金已使用 +createOffer.buyerAsTakerWithoutDeposit=无需买家支付押金(使用口令保护) +createOffer.myDeposit=我的安全押金 (%) +createOffer.myDepositInfo=您的保证金为 {0} #################################################################### @@ -509,6 +517,8 @@ takeOffer.fundsBox.networkFee=总共挖矿手续费 takeOffer.fundsBox.takeOfferSpinnerInfo=接受报价:{0} takeOffer.fundsBox.paymentLabel=Haveno 交易 ID {0} takeOffer.fundsBox.fundsStructure=({0} 保证金,{1} 交易费,{2} 采矿费) +takeOffer.fundsBox.noFundingRequiredTitle=无需资金 +takeOffer.fundsBox.noFundingRequiredDescription=从卖方处获取交易密码(在Haveno之外)以接受此报价。 takeOffer.success.headline=你已成功下单一个报价。 takeOffer.success.info=你可以在“业务/未完成交易”页面内查看您的未完成交易。 takeOffer.error.message=下单时发生了一个错误。\n\n{0} @@ -1465,6 +1475,7 @@ offerDetailsWindow.confirm.maker=确定:发布报价 {0} 比特币 offerDetailsWindow.confirm.taker=确定:下单买入 {0} 比特币 offerDetailsWindow.creationDate=创建时间 offerDetailsWindow.makersOnion=卖家的匿名地址 +offerDetailsWindow.challenge=提供密码 qRCodeWindow.headline=二维码 qRCodeWindow.msg=请使用二维码从外部钱包充值至 Haveno 钱包 @@ -1693,6 +1704,9 @@ popup.accountSigning.unsignedPubKeys.signed=公钥已被验证 popup.accountSigning.unsignedPubKeys.result.signed=已验证公钥 popup.accountSigning.unsignedPubKeys.result.failed=未能验证公钥 +popup.info.buyerAsTakerWithoutDeposit.headline=买家无需支付保证金 +popup.info.buyerAsTakerWithoutDeposit=您的报价将不需要来自XMR买家的保证金或费用。\n\n要接受您的报价,您必须与交易伙伴在Haveno外共享一个密码短语。\n\n密码短语会自动生成,并在创建后显示在报价详情中。 + #################################################################### # Notifications #################################################################### @@ -1818,6 +1832,7 @@ navigation.support=“帮助” formatter.formatVolumeLabel={0} 数量 {1} formatter.makerTaker=卖家 {0} {1} / 买家 {2} {3} +formatter.makerTakerLocked=卖家 {0} {1} / 买家 {2} {3} 🔒 formatter.youAreAsMaker=您是 {1} {0} 卖家 / 买家是 {3} {2} formatter.youAreAsTaker=您是 {1} {0} 买家 / 卖家是 {3} {2} formatter.youAre=您是 {0} {1} ({2} {3}) diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index ebd7194be9..ccdbbcdf46 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -40,6 +40,8 @@ shared.buyMonero=買入比特幣 shared.sellMonero=賣出比特幣 shared.buyCurrency=買入 {0} shared.sellCurrency=賣出 {0} +shared.buyCurrencyLocked=買入 {0} 🔒 +shared.sellCurrencyLocked=賣出 {0} 🔒 shared.buyingXMRWith=用 {0} 買入 XMR shared.sellingXMRFor=賣出 XMR 為 {0} shared.buyingCurrency=買入 {0}(賣出 XMR) @@ -330,6 +332,7 @@ offerbook.createOffer=創建報價 offerbook.takeOffer=接受報價 offerbook.takeOfferToBuy=接受報價來收購 {0} offerbook.takeOfferToSell=接受報價來出售 {0} +offerbook.takeOffer.enterChallenge=輸入報價密碼 offerbook.trader=商人 offerbook.offerersBankId=賣家的銀行 ID(BIC/SWIFT):{0} offerbook.offerersBankName=賣家的銀行名稱:{0} @@ -339,7 +342,9 @@ offerbook.offerersAcceptedBankSeats=接受的銀行所在國家(買家):\n offerbook.availableOffers=可用報價 offerbook.filterByCurrency=以貨幣篩選 offerbook.filterByPaymentMethod=以支付方式篩選 -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=符合我的帳戶的報價 +offerbook.filterNoDeposit=無押金 +offerbook.noDepositOffers=無押金的報價(需要密碼短語) offerbook.timeSinceSigning=賬户信息 offerbook.timeSinceSigning.info=此賬户已驗證,{0} offerbook.timeSinceSigning.info.arbitrator=由仲裁員驗證,並可以驗證夥伴賬户 @@ -485,7 +490,10 @@ createOffer.setDepositAsBuyer=設置自己作為買家的保證金(%) createOffer.setDepositForBothTraders=設置雙方的保證金比例(%) createOffer.securityDepositInfo=您的買家的保證金將會是 {0} createOffer.securityDepositInfoAsBuyer=您作為買家的保證金將會是 {0} -createOffer.minSecurityDepositUsed=已使用最低買家保證金 +createOffer.minSecurityDepositUsed=最低保證金已使用 +createOffer.buyerAsTakerWithoutDeposit=買家無需支付保證金(通行密碼保護) +createOffer.myDeposit=我的保證金(%) +createOffer.myDepositInfo=您的保證金將為 {0} #################################################################### @@ -509,6 +517,8 @@ takeOffer.fundsBox.networkFee=總共挖礦手續費 takeOffer.fundsBox.takeOfferSpinnerInfo=接受報價:{0} takeOffer.fundsBox.paymentLabel=Haveno 交易 ID {0} takeOffer.fundsBox.fundsStructure=({0} 保證金,{1} 交易費,{2} 採礦費) +takeOffer.fundsBox.noFundingRequiredTitle=無需資金 +takeOffer.fundsBox.noFundingRequiredDescription=從賣家那裡在 Haveno 之外獲取優惠密碼以接受此優惠。 takeOffer.success.headline=你已成功下單一個報價。 takeOffer.success.info=你可以在“業務/未完成交易”頁面內查看您的未完成交易。 takeOffer.error.message=下單時發生了一個錯誤。\n\n{0} @@ -1465,6 +1475,7 @@ offerDetailsWindow.confirm.maker=確定:發佈報價 {0} 比特幣 offerDetailsWindow.confirm.taker=確定:下單買入 {0} 比特幣 offerDetailsWindow.creationDate=創建時間 offerDetailsWindow.makersOnion=賣家的匿名地址 +offerDetailsWindow.challenge=提供密碼 qRCodeWindow.headline=二維碼 qRCodeWindow.msg=請使用二維碼從外部錢包充值至 Haveno 錢包 @@ -1687,6 +1698,9 @@ popup.accountSigning.unsignedPubKeys.signed=公鑰已被驗證 popup.accountSigning.unsignedPubKeys.result.signed=已驗證公鑰 popup.accountSigning.unsignedPubKeys.result.failed=未能驗證公鑰 +popup.info.buyerAsTakerWithoutDeposit.headline=買家無需支付保證金 +popup.info.buyerAsTakerWithoutDeposit=您的報價不需要來自XMR買家的保證金或費用。\n\n要接受您的報價,您必須與您的交易夥伴在Haveno之外分享密碼短語。\n\n密碼短語會自動生成並在報價創建後顯示在報價詳情中。 + #################################################################### # Notifications #################################################################### @@ -1812,6 +1826,7 @@ navigation.support=“幫助” formatter.formatVolumeLabel={0} 數量 {1} formatter.makerTaker=賣家 {0} {1} / 買家 {2} {3} +formatter.makerTakerLocked=賣家 {0} {1} / 買家 {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=您是 {0} {1} ({2} {3}) diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index 081a0949f7..7768522b23 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -150,10 +150,12 @@ class GrpcOffersService extends OffersImplBase { req.getMarketPriceMarginPct(), req.getAmount(), req.getMinAmount(), - req.getBuyerSecurityDepositPct(), + req.getSecurityDepositPct(), req.getTriggerPrice(), req.getReserveExactAmount(), req.getPaymentAccountId(), + req.getIsPrivateOffer(), + req.getBuyerAsTakerWithoutDeposit(), offer -> { // This result handling consumer's accept operation will return // the new offer to the gRPC client after async placement is done. diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java index 123078b246..286fccf51a 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java @@ -138,6 +138,7 @@ class GrpcTradesService extends TradesImplBase { coreApi.takeOffer(req.getOfferId(), req.getPaymentAccountId(), req.getAmount(), + req.getChallenge(), trade -> { TradeInfo tradeInfo = toTradeInfo(trade); var reply = TakeOfferReply.newBuilder() diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java index 64d0066d5e..5abdee0d61 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java @@ -184,14 +184,14 @@ public abstract class PaymentMethodForm { Res.get("payment.maxPeriodAndLimitCrypto", getTimeText(hours), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY), true)) + paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY, false), true)) : Res.get("payment.maxPeriodAndLimit", getTimeText(hours), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY), true), + paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY, false), true), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferDirection.SELL), true), + paymentAccount, tradeCurrency.getCode(), OfferDirection.SELL, false), true), DisplayUtils.formatAccountAge(accountAge)); return limitationsText; } diff --git a/desktop/src/main/java/haveno/desktop/images.css b/desktop/src/main/java/haveno/desktop/images.css index 28887daaab..39f0c7e806 100644 --- a/desktop/src/main/java/haveno/desktop/images.css +++ b/desktop/src/main/java/haveno/desktop/images.css @@ -59,6 +59,10 @@ -fx-image: url("../../images/sell_red.png"); } +#image-lock2x { + -fx-image: url("../../images/lock@2x.png"); +} + #image-expand { -fx-image: url("../../images/expand.png"); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index 325e7f2930..bad2ee75f8 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -107,7 +107,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel { protected final ObjectProperty minVolume = new SimpleObjectProperty<>(); // Percentage value of buyer security deposit. E.g. 0.01 means 1% of trade amount - protected final DoubleProperty buyerSecurityDepositPct = new SimpleDoubleProperty(); + protected final DoubleProperty securityDepositPct = new SimpleDoubleProperty(); + protected final BooleanProperty buyerAsTakerWithoutDeposit = new SimpleBooleanProperty(); protected final ObservableList paymentAccounts = FXCollections.observableArrayList(); @@ -166,7 +167,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { reserveExactAmount = preferences.getSplitOfferOutput(); useMarketBasedPrice.set(preferences.isUsePercentageBasedPrice()); - buyerSecurityDepositPct.set(Restrictions.getMinBuyerSecurityDepositAsPercent()); + securityDepositPct.set(Restrictions.getMinSecurityDepositAsPercent()); paymentAccountsChangeListener = change -> fillPaymentAccounts(); } @@ -301,8 +302,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { useMarketBasedPrice.get() ? null : price.get(), useMarketBasedPrice.get(), useMarketBasedPrice.get() ? marketPriceMargin : 0, - buyerSecurityDepositPct.get(), - paymentAccount); + securityDepositPct.get(), + paymentAccount, + buyerAsTakerWithoutDeposit.get(), // private offer if buyer as taker without deposit + buyerAsTakerWithoutDeposit.get()); } void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -329,10 +332,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } private void setSuggestedSecurityDeposit(PaymentAccount paymentAccount) { - var minSecurityDeposit = Restrictions.getMinBuyerSecurityDepositAsPercent(); + var minSecurityDeposit = Restrictions.getMinSecurityDepositAsPercent(); try { if (getTradeCurrency() == null) { - setBuyerSecurityDeposit(minSecurityDeposit); + setSecurityDepositPct(minSecurityDeposit); return; } // Get average historic prices over for the prior trade period equaling the lock time @@ -355,16 +358,16 @@ public abstract class MutableOfferDataModel extends OfferDataModel { var min = extremes[0]; var max = extremes[1]; if (min == 0d || max == 0d) { - setBuyerSecurityDeposit(minSecurityDeposit); + setSecurityDepositPct(minSecurityDeposit); return; } // Suggested deposit is double the trade range over the previous lock time period, bounded by min/max deposit var suggestedSecurityDeposit = - Math.min(2 * (max - min) / max, Restrictions.getMaxBuyerSecurityDepositAsPercent()); - buyerSecurityDepositPct.set(Math.max(suggestedSecurityDeposit, minSecurityDeposit)); + Math.min(2 * (max - min) / max, Restrictions.getMaxSecurityDepositAsPercent()); + securityDepositPct.set(Math.max(suggestedSecurityDeposit, minSecurityDeposit)); } catch (Throwable t) { log.error(t.toString()); - buyerSecurityDepositPct.set(minSecurityDeposit); + securityDepositPct.set(minSecurityDeposit); } } @@ -455,6 +458,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { preferences.setUsePercentageBasedPrice(useMarketBasedPrice); } + protected void setBuyerAsTakerWithoutDeposit(boolean buyerAsTakerWithoutDeposit) { + this.buyerAsTakerWithoutDeposit.set(buyerAsTakerWithoutDeposit); + } + public ObservableList getPaymentAccounts() { return paymentAccounts; } @@ -467,11 +474,11 @@ public abstract class MutableOfferDataModel extends OfferDataModel { // disallow offers which no buyer can take due to trade limits on release if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), OfferDirection.BUY); + return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), OfferDirection.BUY, buyerAsTakerWithoutDeposit.get()); } if (paymentAccount != null) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction); + return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction, buyerAsTakerWithoutDeposit.get()); } else { return 0; } @@ -560,10 +567,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } } - BigInteger getSecurityDeposit() { - return isBuyOffer() ? getBuyerSecurityDeposit() : getSellerSecurityDeposit(); - } - void swapTradeToSavings() { xmrWalletService.resetAddressEntriesForOpenOffer(offerId); } @@ -588,8 +591,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel { this.volume.set(volume); } - protected void setBuyerSecurityDeposit(double value) { - this.buyerSecurityDepositPct.set(value); + protected void setSecurityDepositPct(double value) { + this.securityDepositPct.set(value); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -620,6 +623,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return minVolume; } + public ReadOnlyBooleanProperty getBuyerAsTakerWithoutDeposit() { + return buyerAsTakerWithoutDeposit; + } + protected void setMinAmount(BigInteger minAmount) { this.minAmount.set(minAmount); } @@ -644,35 +651,19 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return useMarketBasedPrice; } - ReadOnlyDoubleProperty getBuyerSecurityDepositPct() { - return buyerSecurityDepositPct; + ReadOnlyDoubleProperty getSecurityDepositPct() { + return securityDepositPct; } - protected BigInteger getBuyerSecurityDeposit() { - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(buyerSecurityDepositPct.get(), amount.get()); - return getBoundedBuyerSecurityDeposit(percentOfAmount); - } - - private BigInteger getSellerSecurityDeposit() { + protected BigInteger getSecurityDeposit() { BigInteger amount = this.amount.get(); - if (amount == null) - amount = BigInteger.ZERO; - - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount( - createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDepositPct.get()), amount); - return getBoundedSellerSecurityDeposit(percentOfAmount); + if (amount == null) amount = BigInteger.ZERO; + BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(securityDepositPct.get(), amount); + return getBoundedSecurityDeposit(percentOfAmount); } - protected BigInteger getBoundedBuyerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinBuyerSecurityDeposit from Restrictions. - return Restrictions.getMinBuyerSecurityDeposit().max(value); - } - - private BigInteger getBoundedSellerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinSellerSecurityDeposit from Restrictions. - return Restrictions.getMinSellerSecurityDeposit().max(value); + protected BigInteger getBoundedSecurityDeposit(BigInteger value) { + return Restrictions.getMinSecurityDeposit().max(value); } ReadOnlyObjectProperty totalToPayAsProperty() { @@ -684,7 +675,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } public BigInteger getMaxMakerFee() { - return HavenoUtils.multiply(amount.get(), HavenoUtils.MAKER_FEE_PCT); + return HavenoUtils.multiply(amount.get(), buyerAsTakerWithoutDeposit.get() ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT); } boolean canPlaceOffer() { @@ -692,8 +683,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel { GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation); } - public boolean isMinBuyerSecurityDeposit() { - return getBuyerSecurityDeposit().compareTo(Restrictions.getMinBuyerSecurityDeposit()) <= 0; + public boolean isMinSecurityDeposit() { + return getSecurityDeposit().compareTo(Restrictions.getMinSecurityDeposit()) <= 0; } public void setTriggerPrice(long triggerPrice) { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 3c6ed09788..9115c64491 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -77,6 +77,7 @@ import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -132,16 +133,17 @@ public abstract class MutableOfferView> exten private AutoTooltipButton nextButton, cancelButton1, cancelButton2, placeOfferButton, fundFromSavingsWalletButton; private Button priceTypeToggleButton; private InputTextField fixedPriceTextField, marketBasedPriceTextField, triggerPriceInputTextField; - protected InputTextField amountTextField, minAmountTextField, volumeTextField, buyerSecurityDepositInputTextField; + protected InputTextField amountTextField, minAmountTextField, volumeTextField, securityDepositInputTextField; private TextField currencyTextField; private AddressTextField addressTextField; private BalanceTextField balanceTextField; private CheckBox reserveExactAmountCheckbox; + private ToggleButton buyerAsTakerWithoutDepositSlider; private FundsTextField totalToPayTextField; private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel, waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescriptionLabel, tradeFeeDescriptionLabel, - resultLabel, tradeFeeInXmrLabel, xLabel, fakeXLabel, buyerSecurityDepositLabel, - buyerSecurityDepositPercentageLabel, triggerPriceCurrencyLabel, triggerPriceDescriptionLabel; + resultLabel, tradeFeeInXmrLabel, xLabel, fakeXLabel, securityDepositLabel, + securityDepositPercentageLabel, triggerPriceCurrencyLabel, triggerPriceDescriptionLabel; protected Label amountBtcLabel, volumeCurrencyLabel, minAmountBtcLabel; private ComboBox paymentAccountsComboBox; private ComboBox currencyComboBox; @@ -149,16 +151,16 @@ public abstract class MutableOfferView> exten private VBox currencySelection, fixedPriceBox, percentagePriceBox, currencyTextFieldBox, triggerPriceVBox; private HBox fundingHBox, firstRowHBox, secondRowHBox, placeOfferBox, amountValueCurrencyBox, priceAsPercentageValueCurrencyBox, volumeValueCurrencyBox, priceValueCurrencyBox, - minAmountValueCurrencyBox, advancedOptionsBox, triggerPriceHBox; + minAmountValueCurrencyBox, securityDepositAndFeeBox, triggerPriceHBox; private Subscription isWaitingForFundsSubscription, balanceSubscription; private ChangeListener amountFocusedListener, minAmountFocusedListener, volumeFocusedListener, - buyerSecurityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener, + securityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener, priceAsPercentageFocusedListener, getShowWalletFundedNotificationListener, - isMinBuyerSecurityDepositListener, triggerPriceFocusedListener; + isMinSecurityDepositListener, buyerAsTakerWithoutDepositListener, triggerPriceFocusedListener; private ChangeListener missingCoinListener; private ChangeListener tradeCurrencyCodeListener, errorMessageListener, - marketPriceMarginListener, volumeListener, buyerSecurityDepositInBTCListener; + marketPriceMarginListener, volumeListener, securityDepositInXMRListener; private ChangeListener marketPriceAvailableListener; private EventHandler currencyComboBoxSelectionHandler, paymentAccountsComboBoxSelectionHandler; private OfferView.CloseHandler closeHandler; @@ -168,7 +170,7 @@ public abstract class MutableOfferView> exten private final HashMap paymentAccountWarningDisplayed = new HashMap<>(); private boolean zelleWarningDisplayed, fasterPaymentsWarningDisplayed, isActivated; private InfoInputTextField marketBasedPriceInfoInputTextField, volumeInfoInputTextField, - buyerSecurityDepositInfoInputTextField, triggerPriceInfoInputTextField; + securityDepositInfoInputTextField, triggerPriceInfoInputTextField; private Text xIcon, fakeXIcon; @Setter @@ -252,6 +254,8 @@ public abstract class MutableOfferView> exten Label popOverLabel = OfferViewUtil.createPopOverLabel(Res.get("createOffer.triggerPrice.tooltip")); triggerPriceInfoInputTextField.setContentForPopOver(popOverLabel, AwesomeIcon.SHIELD); + + buyerAsTakerWithoutDepositSlider.setSelected(model.dataModel.getBuyerAsTakerWithoutDeposit().get()); } } @@ -323,6 +327,9 @@ public abstract class MutableOfferView> exten fundFromSavingsWalletButton.setId("sell-button"); } + buyerAsTakerWithoutDepositSlider.setVisible(model.isSellOffer()); + buyerAsTakerWithoutDepositSlider.setManaged(model.isSellOffer()); + placeOfferButton.updateText(placeOfferButtonLabel); updatePriceToggle(); } @@ -375,8 +382,11 @@ public abstract class MutableOfferView> exten setDepositTitledGroupBg.setVisible(false); setDepositTitledGroupBg.setManaged(false); - advancedOptionsBox.setVisible(false); - advancedOptionsBox.setManaged(false); + securityDepositAndFeeBox.setVisible(false); + securityDepositAndFeeBox.setManaged(false); + + buyerAsTakerWithoutDepositSlider.setVisible(false); + buyerAsTakerWithoutDepositSlider.setManaged(false); updateQrCode(); @@ -556,8 +566,8 @@ public abstract class MutableOfferView> exten volumeTextField.promptTextProperty().bind(model.volumePromptLabel); totalToPayTextField.textProperty().bind(model.totalToPay); addressTextField.amountAsProperty().bind(model.getDataModel().getMissingCoin()); - buyerSecurityDepositInputTextField.textProperty().bindBidirectional(model.buyerSecurityDeposit); - buyerSecurityDepositLabel.textProperty().bind(model.buyerSecurityDepositLabel); + securityDepositInputTextField.textProperty().bindBidirectional(model.securityDeposit); + securityDepositLabel.textProperty().bind(model.securityDepositLabel); tradeFeeInXmrLabel.textProperty().bind(model.tradeFeeInXmrWithFiat); tradeFeeDescriptionLabel.textProperty().bind(model.tradeFeeDescription); @@ -567,7 +577,7 @@ public abstract class MutableOfferView> exten fixedPriceTextField.validationResultProperty().bind(model.priceValidationResult); triggerPriceInputTextField.validationResultProperty().bind(model.triggerPriceValidationResult); volumeTextField.validationResultProperty().bind(model.volumeValidationResult); - buyerSecurityDepositInputTextField.validationResultProperty().bind(model.buyerSecurityDepositValidationResult); + securityDepositInputTextField.validationResultProperty().bind(model.securityDepositValidationResult); // funding fundingHBox.visibleProperty().bind(model.getDataModel().getIsXmrWalletFunded().not().and(model.showPayFundsScreenDisplayed)); @@ -604,8 +614,8 @@ public abstract class MutableOfferView> exten volumeTextField.promptTextProperty().unbindBidirectional(model.volume); totalToPayTextField.textProperty().unbind(); addressTextField.amountAsProperty().unbind(); - buyerSecurityDepositInputTextField.textProperty().unbindBidirectional(model.buyerSecurityDeposit); - buyerSecurityDepositLabel.textProperty().unbind(); + securityDepositInputTextField.textProperty().unbindBidirectional(model.securityDeposit); + securityDepositLabel.textProperty().unbind(); tradeFeeInXmrLabel.textProperty().unbind(); tradeFeeDescriptionLabel.textProperty().unbind(); tradeFeeInXmrLabel.visibleProperty().unbind(); @@ -617,7 +627,7 @@ public abstract class MutableOfferView> exten fixedPriceTextField.validationResultProperty().unbind(); triggerPriceInputTextField.validationResultProperty().unbind(); volumeTextField.validationResultProperty().unbind(); - buyerSecurityDepositInputTextField.validationResultProperty().unbind(); + securityDepositInputTextField.validationResultProperty().unbind(); // funding fundingHBox.visibleProperty().unbind(); @@ -679,9 +689,9 @@ public abstract class MutableOfferView> exten model.onFocusOutVolumeTextField(oldValue, newValue); volumeTextField.setText(model.volume.get()); }; - buyerSecurityDepositFocusedListener = (o, oldValue, newValue) -> { - model.onFocusOutBuyerSecurityDepositTextField(oldValue, newValue); - buyerSecurityDepositInputTextField.setText(model.buyerSecurityDeposit.get()); + securityDepositFocusedListener = (o, oldValue, newValue) -> { + model.onFocusOutSecurityDepositTextField(oldValue, newValue); + securityDepositInputTextField.setText(model.securityDeposit.get()); }; triggerPriceFocusedListener = (o, oldValue, newValue) -> { @@ -750,12 +760,11 @@ public abstract class MutableOfferView> exten } }; - buyerSecurityDepositInBTCListener = (observable, oldValue, newValue) -> { + securityDepositInXMRListener = (observable, oldValue, newValue) -> { if (!newValue.equals("")) { - Label depositInBTCInfo = OfferViewUtil.createPopOverLabel(model.getSecurityDepositPopOverLabel(newValue)); - buyerSecurityDepositInfoInputTextField.setContentForInfoPopOver(depositInBTCInfo); + updateSecurityDepositLabels(); } else { - buyerSecurityDepositInfoInputTextField.setContentForInfoPopOver(null); + securityDepositInfoInputTextField.setContentForInfoPopOver(null); } }; @@ -805,17 +814,29 @@ public abstract class MutableOfferView> exten } }; - isMinBuyerSecurityDepositListener = ((observable, oldValue, newValue) -> { - if (newValue) { - // show BTC - buyerSecurityDepositPercentageLabel.setText(Res.getBaseCurrencyCode()); - buyerSecurityDepositInputTextField.setDisable(true); - } else { - // show % - buyerSecurityDepositPercentageLabel.setText("%"); - buyerSecurityDepositInputTextField.setDisable(false); - } + isMinSecurityDepositListener = ((observable, oldValue, newValue) -> { + updateSecurityDepositLabels(); }); + + buyerAsTakerWithoutDepositListener = ((observable, oldValue, newValue) -> { + updateSecurityDepositLabels(); + }); + } + + private void updateSecurityDepositLabels() { + if (model.isMinSecurityDeposit.get()) { + // show XMR + securityDepositPercentageLabel.setText(Res.getBaseCurrencyCode()); + securityDepositInputTextField.setDisable(true); + } else { + // show % + securityDepositPercentageLabel.setText("%"); + securityDepositInputTextField.setDisable(model.getDataModel().buyerAsTakerWithoutDeposit.get()); + } + if (model.securityDepositInXMR.get() != null && !model.securityDepositInXMR.get().equals("")) { + Label depositInBTCInfo = OfferViewUtil.createPopOverLabel(model.getSecurityDepositPopOverLabel(model.securityDepositInXMR.get())); + securityDepositInfoInputTextField.setContentForInfoPopOver(depositInBTCInfo); + } } private void updateQrCode() { @@ -856,8 +877,9 @@ public abstract class MutableOfferView> exten model.marketPriceMargin.addListener(marketPriceMarginListener); model.volume.addListener(volumeListener); model.getDataModel().missingCoin.addListener(missingCoinListener); - model.buyerSecurityDepositInBTC.addListener(buyerSecurityDepositInBTCListener); - model.isMinBuyerSecurityDeposit.addListener(isMinBuyerSecurityDepositListener); + model.securityDepositInXMR.addListener(securityDepositInXMRListener); + model.isMinSecurityDeposit.addListener(isMinSecurityDepositListener); + model.getDataModel().buyerAsTakerWithoutDeposit.addListener(buyerAsTakerWithoutDepositListener); // focus out amountTextField.focusedProperty().addListener(amountFocusedListener); @@ -866,7 +888,7 @@ public abstract class MutableOfferView> exten triggerPriceInputTextField.focusedProperty().addListener(triggerPriceFocusedListener); marketBasedPriceTextField.focusedProperty().addListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().addListener(volumeFocusedListener); - buyerSecurityDepositInputTextField.focusedProperty().addListener(buyerSecurityDepositFocusedListener); + securityDepositInputTextField.focusedProperty().addListener(securityDepositFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().addListener(getShowWalletFundedNotificationListener); @@ -888,8 +910,9 @@ public abstract class MutableOfferView> exten model.marketPriceMargin.removeListener(marketPriceMarginListener); model.volume.removeListener(volumeListener); model.getDataModel().missingCoin.removeListener(missingCoinListener); - model.buyerSecurityDepositInBTC.removeListener(buyerSecurityDepositInBTCListener); - model.isMinBuyerSecurityDeposit.removeListener(isMinBuyerSecurityDepositListener); + model.securityDepositInXMR.removeListener(securityDepositInXMRListener); + model.isMinSecurityDeposit.removeListener(isMinSecurityDepositListener); + model.getDataModel().buyerAsTakerWithoutDeposit.removeListener(buyerAsTakerWithoutDepositListener); // focus out amountTextField.focusedProperty().removeListener(amountFocusedListener); @@ -898,7 +921,7 @@ public abstract class MutableOfferView> exten triggerPriceInputTextField.focusedProperty().removeListener(triggerPriceFocusedListener); marketBasedPriceTextField.focusedProperty().removeListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().removeListener(volumeFocusedListener); - buyerSecurityDepositInputTextField.focusedProperty().removeListener(buyerSecurityDepositFocusedListener); + securityDepositInputTextField.focusedProperty().removeListener(securityDepositFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().removeListener(getShowWalletFundedNotificationListener); @@ -997,22 +1020,46 @@ public abstract class MutableOfferView> exten } private void addOptionsGroup() { - setDepositTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, + setDepositTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, Res.get("shared.advancedOptions"), Layout.COMPACT_GROUP_DISTANCE); - advancedOptionsBox = new HBox(); - advancedOptionsBox.setSpacing(40); + securityDepositAndFeeBox = new HBox(); + securityDepositAndFeeBox.setSpacing(40); - GridPane.setRowIndex(advancedOptionsBox, gridRow); - GridPane.setColumnSpan(advancedOptionsBox, GridPane.REMAINING); - GridPane.setColumnIndex(advancedOptionsBox, 0); - GridPane.setHalignment(advancedOptionsBox, HPos.LEFT); - GridPane.setMargin(advancedOptionsBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); - gridPane.getChildren().add(advancedOptionsBox); + GridPane.setRowIndex(securityDepositAndFeeBox, gridRow); + GridPane.setColumnSpan(securityDepositAndFeeBox, GridPane.REMAINING); + GridPane.setColumnIndex(securityDepositAndFeeBox, 0); + GridPane.setHalignment(securityDepositAndFeeBox, HPos.LEFT); + GridPane.setMargin(securityDepositAndFeeBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); + gridPane.getChildren().add(securityDepositAndFeeBox); VBox tradeFeeFieldsBox = getTradeFeeFieldsBox(); tradeFeeFieldsBox.setMinWidth(240); - advancedOptionsBox.getChildren().addAll(getBuyerSecurityDepositBox(), tradeFeeFieldsBox); + securityDepositAndFeeBox.getChildren().addAll(getSecurityDepositBox(), tradeFeeFieldsBox); + + buyerAsTakerWithoutDepositSlider = FormBuilder.addSlideToggleButton(gridPane, ++gridRow, Res.get("createOffer.buyerAsTakerWithoutDeposit")); + buyerAsTakerWithoutDepositSlider.setOnAction(event -> { + + // popup info box + String key = "popup.info.buyerAsTakerWithoutDeposit"; + if (buyerAsTakerWithoutDepositSlider.isSelected() && DontShowAgainLookup.showAgain(key)) { + new Popup().headLine(Res.get(key + ".headline")) + .information(Res.get(key)) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.ok")) + .onAction(() -> model.dataModel.setBuyerAsTakerWithoutDeposit(true)) + .onClose(() -> { + buyerAsTakerWithoutDepositSlider.setSelected(false); + model.dataModel.setBuyerAsTakerWithoutDeposit(false); + }) + .dontShowAgainId(key) + .show(); + } else { + model.dataModel.setBuyerAsTakerWithoutDeposit(buyerAsTakerWithoutDepositSlider.isSelected()); + } + }); + GridPane.setHalignment(buyerAsTakerWithoutDepositSlider, HPos.LEFT); + GridPane.setMargin(buyerAsTakerWithoutDepositSlider, new Insets(0, 0, 0, 0)); Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++gridRow, Res.get("shared.nextStep"), Res.get("shared.cancel")); @@ -1060,26 +1107,28 @@ public abstract class MutableOfferView> exten nextButton.setManaged(false); cancelButton1.setVisible(false); cancelButton1.setManaged(false); - advancedOptionsBox.setVisible(false); - advancedOptionsBox.setManaged(false); + securityDepositAndFeeBox.setVisible(false); + securityDepositAndFeeBox.setManaged(false); + buyerAsTakerWithoutDepositSlider.setVisible(false); + buyerAsTakerWithoutDepositSlider.setManaged(false); } - private VBox getBuyerSecurityDepositBox() { + private VBox getSecurityDepositBox() { Tuple3 tuple = getEditableValueBoxWithInfo( Res.get("createOffer.securityDeposit.prompt")); - buyerSecurityDepositInfoInputTextField = tuple.second; - buyerSecurityDepositInputTextField = buyerSecurityDepositInfoInputTextField.getInputTextField(); - buyerSecurityDepositPercentageLabel = tuple.third; + securityDepositInfoInputTextField = tuple.second; + securityDepositInputTextField = securityDepositInfoInputTextField.getInputTextField(); + securityDepositPercentageLabel = tuple.third; // getEditableValueBox delivers BTC, so we overwrite it with % - buyerSecurityDepositPercentageLabel.setText("%"); + securityDepositPercentageLabel.setText("%"); Tuple2 tradeInputBoxTuple = getTradeInputBox(tuple.first, model.getSecurityDepositLabel()); VBox depositBox = tradeInputBoxTuple.second; - buyerSecurityDepositLabel = tradeInputBoxTuple.first; + securityDepositLabel = tradeInputBoxTuple.first; depositBox.setMaxWidth(310); - editOfferElements.add(buyerSecurityDepositInputTextField); - editOfferElements.add(buyerSecurityDepositPercentageLabel); + editOfferElements.add(securityDepositInputTextField); + editOfferElements.add(securityDepositPercentageLabel); return depositBox; } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index fabad8570f..8ededde35c 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -113,9 +113,9 @@ public abstract class MutableOfferViewModel ext public final StringProperty amount = new SimpleStringProperty(); public final StringProperty minAmount = new SimpleStringProperty(); - protected final StringProperty buyerSecurityDeposit = new SimpleStringProperty(); - final StringProperty buyerSecurityDepositInBTC = new SimpleStringProperty(); - final StringProperty buyerSecurityDepositLabel = new SimpleStringProperty(); + protected final StringProperty securityDeposit = new SimpleStringProperty(); + final StringProperty securityDepositInXMR = new SimpleStringProperty(); + final StringProperty securityDepositLabel = new SimpleStringProperty(); // Price in the viewModel is always dependent on fiat/crypto: Fiat Fiat/BTC, for cryptos we use inverted price. // The domain (dataModel) uses always the same price model (otherCurrencyBTC) @@ -151,14 +151,14 @@ public abstract class MutableOfferViewModel ext final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty(); private final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); final BooleanProperty isWaitingForFunds = new SimpleBooleanProperty(); - final BooleanProperty isMinBuyerSecurityDeposit = new SimpleBooleanProperty(); + final BooleanProperty isMinSecurityDeposit = new SimpleBooleanProperty(); final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty minAmountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty priceValidationResult = new SimpleObjectProperty<>(); final ObjectProperty triggerPriceValidationResult = new SimpleObjectProperty<>(new InputValidator.ValidationResult(true)); final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); - final ObjectProperty buyerSecurityDepositValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty securityDepositValidationResult = new SimpleObjectProperty<>(); private ChangeListener amountStringListener; private ChangeListener minAmountStringListener; @@ -171,6 +171,7 @@ public abstract class MutableOfferViewModel ext private ChangeListener priceListener; private ChangeListener volumeListener; private ChangeListener securityDepositAsDoubleListener; + private ChangeListener buyerAsTakerWithoutDepositListener; private ChangeListener isWalletFundedListener; private ChangeListener errorMessageListener; @@ -303,7 +304,7 @@ public abstract class MutableOfferViewModel ext dataModel.calculateVolume(); dataModel.calculateTotalToPay(); } - updateBuyerSecurityDeposit(); + updateSecurityDeposit(); updateButtonDisableState(); } }; @@ -419,34 +420,36 @@ public abstract class MutableOfferViewModel ext updateButtonDisableState(); } }; + securityDepositStringListener = (ov, oldValue, newValue) -> { if (!ignoreSecurityDepositStringListener) { if (securityDepositValidator.validate(newValue).isValid) { - setBuyerSecurityDepositToModel(); + setSecurityDepositToModel(); dataModel.calculateTotalToPay(); } updateButtonDisableState(); } }; - amountListener = (ov, oldValue, newValue) -> { if (newValue != null) { amount.set(HavenoUtils.formatXmr(newValue)); - buyerSecurityDepositInBTC.set(HavenoUtils.formatXmr(dataModel.getBuyerSecurityDeposit(), true)); + securityDepositInXMR.set(HavenoUtils.formatXmr(dataModel.getSecurityDeposit(), true)); } else { amount.set(""); - buyerSecurityDepositInBTC.set(""); + securityDepositInXMR.set(""); } applyMakerFee(); }; + minAmountListener = (ov, oldValue, newValue) -> { if (newValue != null) minAmount.set(HavenoUtils.formatXmr(newValue)); else minAmount.set(""); }; + priceListener = (ov, oldValue, newValue) -> { ignorePriceStringListener = true; if (newValue != null) @@ -457,6 +460,7 @@ public abstract class MutableOfferViewModel ext ignorePriceStringListener = false; applyMakerFee(); }; + volumeListener = (ov, oldValue, newValue) -> { ignoreVolumeStringListener = true; if (newValue != null) @@ -470,17 +474,26 @@ public abstract class MutableOfferViewModel ext securityDepositAsDoubleListener = (ov, oldValue, newValue) -> { if (newValue != null) { - buyerSecurityDeposit.set(FormattingUtils.formatToPercent((double) newValue)); + securityDeposit.set(FormattingUtils.formatToPercent((double) newValue)); if (dataModel.getAmount().get() != null) { - buyerSecurityDepositInBTC.set(HavenoUtils.formatXmr(dataModel.getBuyerSecurityDeposit(), true)); + securityDepositInXMR.set(HavenoUtils.formatXmr(dataModel.getSecurityDeposit(), true)); } - updateBuyerSecurityDeposit(); + updateSecurityDeposit(); } else { - buyerSecurityDeposit.set(""); - buyerSecurityDepositInBTC.set(""); + securityDeposit.set(""); + securityDepositInXMR.set(""); } }; + buyerAsTakerWithoutDepositListener = (ov, oldValue, newValue) -> { + if (dataModel.paymentAccount != null) xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); + xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + if (amount.get() != null) amountValidationResult.set(isXmrInputValid(amount.get())); + updateSecurityDeposit(); + applyMakerFee(); + dataModel.calculateTotalToPay(); + updateButtonDisableState(); + }; isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); /* feeFromFundingTxListener = (ov, oldValue, newValue) -> { @@ -525,14 +538,15 @@ public abstract class MutableOfferViewModel ext marketPriceMargin.addListener(marketPriceMarginStringListener); dataModel.getUseMarketBasedPrice().addListener(useMarketBasedPriceListener); volume.addListener(volumeStringListener); - buyerSecurityDeposit.addListener(securityDepositStringListener); + securityDeposit.addListener(securityDepositStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().addListener(amountListener); dataModel.getMinAmount().addListener(minAmountListener); dataModel.getPrice().addListener(priceListener); dataModel.getVolume().addListener(volumeListener); - dataModel.getBuyerSecurityDepositPct().addListener(securityDepositAsDoubleListener); + dataModel.getSecurityDepositPct().addListener(securityDepositAsDoubleListener); + dataModel.getBuyerAsTakerWithoutDeposit().addListener(buyerAsTakerWithoutDepositListener); // dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().addListener(isWalletFundedListener); @@ -547,14 +561,15 @@ public abstract class MutableOfferViewModel ext marketPriceMargin.removeListener(marketPriceMarginStringListener); dataModel.getUseMarketBasedPrice().removeListener(useMarketBasedPriceListener); volume.removeListener(volumeStringListener); - buyerSecurityDeposit.removeListener(securityDepositStringListener); + securityDeposit.removeListener(securityDepositStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().removeListener(amountListener); dataModel.getMinAmount().removeListener(minAmountListener); dataModel.getPrice().removeListener(priceListener); dataModel.getVolume().removeListener(volumeListener); - dataModel.getBuyerSecurityDepositPct().removeListener(securityDepositAsDoubleListener); + dataModel.getSecurityDepositPct().removeListener(securityDepositAsDoubleListener); + dataModel.getBuyerAsTakerWithoutDeposit().removeListener(buyerAsTakerWithoutDepositListener); //dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().removeListener(isWalletFundedListener); @@ -593,9 +608,9 @@ public abstract class MutableOfferViewModel ext } securityDepositValidator.setPaymentAccount(dataModel.paymentAccount); - validateAndSetBuyerSecurityDepositToModel(); - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); - buyerSecurityDepositLabel.set(getSecurityDepositLabel()); + validateAndSetSecurityDepositToModel(); + securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); + securityDepositLabel.set(getSecurityDepositLabel()); applyMakerFee(); return result; @@ -932,14 +947,14 @@ public abstract class MutableOfferViewModel ext } } - void onFocusOutBuyerSecurityDepositTextField(boolean oldValue, boolean newValue) { + void onFocusOutSecurityDepositTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { - InputValidator.ValidationResult result = securityDepositValidator.validate(buyerSecurityDeposit.get()); - buyerSecurityDepositValidationResult.set(result); + InputValidator.ValidationResult result = securityDepositValidator.validate(securityDeposit.get()); + securityDepositValidationResult.set(result); if (result.isValid) { - double defaultSecurityDeposit = Restrictions.getDefaultBuyerSecurityDepositAsPercent(); + double defaultSecurityDeposit = Restrictions.getDefaultSecurityDepositAsPercent(); String key = "buyerSecurityDepositIsLowerAsDefault"; - double depositAsDouble = ParsingUtils.parsePercentStringToDouble(buyerSecurityDeposit.get()); + double depositAsDouble = ParsingUtils.parsePercentStringToDouble(securityDeposit.get()); if (preferences.showAgain(key) && depositAsDouble < defaultSecurityDeposit) { String postfix = dataModel.isBuyOffer() ? Res.get("createOffer.tooLowSecDeposit.makerIsBuyer") : @@ -950,26 +965,26 @@ public abstract class MutableOfferViewModel ext .width(800) .actionButtonText(Res.get("createOffer.resetToDefault")) .onAction(() -> { - dataModel.setBuyerSecurityDeposit(defaultSecurityDeposit); + dataModel.setSecurityDepositPct(defaultSecurityDeposit); ignoreSecurityDepositStringListener = true; - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); + securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); ignoreSecurityDepositStringListener = false; }) .closeButtonText(Res.get("createOffer.useLowerValue")) - .onClose(this::applyBuyerSecurityDepositOnFocusOut) + .onClose(this::applySecurityDepositOnFocusOut) .dontShowAgainId(key) .show(); } else { - applyBuyerSecurityDepositOnFocusOut(); + applySecurityDepositOnFocusOut(); } } } } - private void applyBuyerSecurityDepositOnFocusOut() { - setBuyerSecurityDepositToModel(); + private void applySecurityDepositOnFocusOut() { + setSecurityDepositToModel(); ignoreSecurityDepositStringListener = true; - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); + securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); ignoreSecurityDepositStringListener = false; } @@ -1024,13 +1039,15 @@ public abstract class MutableOfferViewModel ext } public String getSecurityDepositLabel() { - return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? Res.get("createOffer.setDepositForBothTraders") : + return dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer() ? Res.get("createOffer.myDeposit") : + Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? Res.get("createOffer.setDepositForBothTraders") : dataModel.isBuyOffer() ? Res.get("createOffer.setDepositAsBuyer") : Res.get("createOffer.setDeposit"); } - public String getSecurityDepositPopOverLabel(String depositInBTC) { - return dataModel.isBuyOffer() ? Res.get("createOffer.securityDepositInfoAsBuyer", depositInBTC) : - Res.get("createOffer.securityDepositInfo", depositInBTC); + public String getSecurityDepositPopOverLabel(String depositInXMR) { + return dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer() ? Res.get("createOffer.myDepositInfo", depositInXMR) : + dataModel.isBuyOffer() ? Res.get("createOffer.securityDepositInfoAsBuyer", depositInXMR) : + Res.get("createOffer.securityDepositInfo", depositInXMR); } public String getSecurityDepositInfo() { @@ -1193,19 +1210,19 @@ public abstract class MutableOfferViewModel ext } } - private void setBuyerSecurityDepositToModel() { - if (buyerSecurityDeposit.get() != null && !buyerSecurityDeposit.get().isEmpty()) { - dataModel.setBuyerSecurityDeposit(ParsingUtils.parsePercentStringToDouble(buyerSecurityDeposit.get())); + private void setSecurityDepositToModel() { + if (!(dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer()) && securityDeposit.get() != null && !securityDeposit.get().isEmpty()) { + dataModel.setSecurityDepositPct(ParsingUtils.parsePercentStringToDouble(securityDeposit.get())); } else { - dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()); + dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositAsPercent()); } } - private void validateAndSetBuyerSecurityDepositToModel() { + private void validateAndSetSecurityDepositToModel() { // If the security deposit in the model is not valid percent - String value = FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get()); + String value = FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get()); if (!securityDepositValidator.validate(value).isValid) { - dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()); + dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositAsPercent()); } } @@ -1263,15 +1280,17 @@ public abstract class MutableOfferViewModel ext isWaitingForFunds.set(!waitingForFundsText.get().isEmpty()); } - private void updateBuyerSecurityDeposit() { - isMinBuyerSecurityDeposit.set(dataModel.isMinBuyerSecurityDeposit()); - - if (dataModel.isMinBuyerSecurityDeposit()) { - buyerSecurityDepositLabel.set(Res.get("createOffer.minSecurityDepositUsed")); - buyerSecurityDeposit.set(HavenoUtils.formatXmr(Restrictions.getMinBuyerSecurityDeposit())); + private void updateSecurityDeposit() { + isMinSecurityDeposit.set(dataModel.isMinSecurityDeposit()); + if (dataModel.isMinSecurityDeposit()) { + securityDepositLabel.set(Res.get("createOffer.minSecurityDepositUsed")); + securityDeposit.set(HavenoUtils.formatXmr(Restrictions.getMinSecurityDeposit())); } else { - buyerSecurityDepositLabel.set(getSecurityDepositLabel()); - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); + securityDepositLabel.set(getSecurityDepositLabel()); + boolean hasBuyerAsTakerWithoutDeposit = dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer(); + securityDeposit.set(FormattingUtils.formatToPercent(hasBuyerAsTakerWithoutDeposit ? + Restrictions.getDefaultSecurityDepositAsPercent() : // use default percent if no deposit from buyer + dataModel.getSecurityDepositPct().get())); } } @@ -1293,8 +1312,8 @@ public abstract class MutableOfferViewModel ext } // validating the percentage deposit value only makes sense if it is actually used - if (!dataModel.isMinBuyerSecurityDeposit()) { - inputDataValid = inputDataValid && securityDepositValidator.validate(buyerSecurityDeposit.get()).isValid; + if (!dataModel.isMinSecurityDeposit()) { + inputDataValid = inputDataValid && securityDepositValidator.validate(securityDeposit.get()).isValid; } isNextButtonDisabled.set(!inputDataValid); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java index 8b92477e2e..71a827ffa3 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java @@ -87,4 +87,8 @@ public abstract class OfferDataModel extends ActivatableDataModel { }); } + + public boolean hasTotalToPay() { + return totalToPay.get() != null && totalToPay.get().compareTo(BigInteger.ZERO) > 0; + } } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index 6d9575b0d3..9b3571458f 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -17,6 +17,7 @@ package haveno.desktop.main.offer.offerbook; +import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.UserThread; @@ -44,7 +45,6 @@ import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.components.AccountStatusTooltipLabel; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; -import haveno.desktop.components.AutoTooltipSlideToggleButton; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.AutoTooltipTextField; import haveno.desktop.components.AutocompleteComboBox; @@ -83,6 +83,7 @@ import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; +import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; @@ -120,7 +121,8 @@ abstract public class OfferBookView paymentMethodComboBox; private AutoTooltipButton createOfferButton; private AutoTooltipTextField filterInputField; - private AutoTooltipSlideToggleButton matchingOffersToggle; + private ToggleButton matchingOffersToggleButton; + private ToggleButton noDepositOffersToggleButton; private AutoTooltipTableColumn amountColumn; private AutoTooltipTableColumn volumeColumn; private AutoTooltipTableColumn marketColumn; @@ -183,9 +185,17 @@ abstract public class OfferBookView model.onShowOffersMatchingMyAccounts(matchingOffersToggle.isSelected())); + matchingOffersToggleButton.setSelected(model.useOffersMatchingMyAccountsFilter); + matchingOffersToggleButton.disableProperty().bind(model.disableMatchToggle); + matchingOffersToggleButton.setOnAction(e -> model.onShowOffersMatchingMyAccounts(matchingOffersToggleButton.isSelected())); + + noDepositOffersToggleButton.setSelected(model.showPrivateOffers); + noDepositOffersToggleButton.setOnAction(e -> model.onShowPrivateOffers(noDepositOffersToggleButton.isSelected())); model.getOfferList().comparatorProperty().bind(tableView.comparatorProperty()); @@ -452,8 +465,10 @@ abstract public class OfferBookView account = model.getMostMaturePaymentAccountForOffer(offer); if (account.isPresent()) { long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), - offer.getCurrencyCode(), offer.getMirroredDirection()); + offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); new Popup() .warning(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", HavenoUtils.formatXmr(tradeLimit, true), @@ -1123,7 +1142,10 @@ abstract public class OfferBookView offerBookListItem.getOffer().isPrivateOffer() == showPrivateOffers); + if (!filterText.isEmpty()) { // filter node address diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java index beae7c11e3..0f6c5db8fb 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -341,7 +341,7 @@ class TakeOfferDataModel extends OfferDataModel { long getMaxTradeLimit() { if (paymentAccount != null) { return accountAgeWitnessService.getMyTradeLimit(paymentAccount, getCurrencyCode(), - offer.getMirroredDirection()); + offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); } else { return 0; } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index 6f9991331d..1d662aebc7 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -123,6 +123,8 @@ public class TakeOfferView extends ActivatableViewAndModel { String key = "CreateOfferCancelAndFunded"; - if (model.dataModel.getIsXmrWalletFunded().get() && + if (model.dataModel.getIsXmrWalletFunded().get() && model.dataModel.hasTotalToPay() && model.dataModel.preferences.showAgain(key)) { new Popup().backgroundInfo(Res.get("takeOffer.alreadyFunded.askCancel")) .closeButtonText(Res.get("shared.no")) diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java b/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java new file mode 100644 index 0000000000..bd169b3a4b --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java @@ -0,0 +1,245 @@ +/* + * This file is part of Haveno. + * + * Haveno 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. + * + * Haveno 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 Haveno. If not, see . + */ + +package haveno.desktop.main.overlays.editor; + +import haveno.common.util.Utilities; +import haveno.core.locale.GlobalSettings; +import haveno.desktop.components.InputTextField; +import haveno.desktop.main.overlays.Overlay; +import haveno.desktop.util.FormBuilder; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.value.ChangeListener; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.scene.Camera; +import javafx.scene.PerspectiveCamera; +import javafx.scene.Scene; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.GridPane; +import javafx.scene.transform.Rotate; +import javafx.stage.Modality; +import javafx.util.Duration; +import lombok.extern.slf4j.Slf4j; + +import java.util.function.Consumer; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import static haveno.desktop.util.FormBuilder.addInputTextField; + +@Slf4j +public class PasswordPopup extends Overlay { + private InputTextField inputTextField; + private static PasswordPopup INSTANCE; + private Consumer actionHandler; + private ChangeListener focusListener; + private EventHandler keyEventEventHandler; + + public PasswordPopup() { + width = 600; + type = Type.Confirmation; + if (INSTANCE != null) + INSTANCE.hide(); + INSTANCE = this; + } + + public PasswordPopup onAction(Consumer confirmHandler) { + this.actionHandler = confirmHandler; + return this; + } + + @Override + public void show() { + actionButtonText("CONFIRM"); + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + applyStyles(); + onShow(); + } + + @Override + protected void onShow() { + super.display(); + + if (stage != null) { + focusListener = (observable, oldValue, newValue) -> { + if (!newValue) + hide(); + }; + stage.focusedProperty().addListener(focusListener); + + Scene scene = stage.getScene(); + if (scene != null) + scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + } + + @Override + public void hide() { + animateHide(); + } + + @Override + protected void onHidden() { + INSTANCE = null; + + if (stage != null) { + if (focusListener != null) + stage.focusedProperty().removeListener(focusListener); + + Scene scene = stage.getScene(); + if (scene != null) + scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + } + + private void addContent() { + gridPane.setPadding(new Insets(64)); + + inputTextField = addInputTextField(gridPane, ++rowIndex, null, -10d); + GridPane.setColumnSpan(inputTextField, 2); + inputTextField.requestFocus(); + + keyEventEventHandler = event -> { + if (Utilities.isAltOrCtrlPressed(KeyCode.R, event)) { + doClose(); + } + }; + } + + @Override + protected void addHeadLine() { + super.addHeadLine(); + GridPane.setHalignment(headLineLabel, HPos.CENTER); + } + + protected void setupKeyHandler(Scene scene) { + scene.setOnKeyPressed(e -> { + if (e.getCode() == KeyCode.ESCAPE) { + e.consume(); + doClose(); + } + if (e.getCode() == KeyCode.ENTER) { + e.consume(); + apply(); + } + }); + } + + @Override + protected void animateHide(Runnable onFinishedHandler) { + if (GlobalSettings.getUseAnimations()) { + double duration = getDuration(300); + Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); + + gridPane.setRotationAxis(Rotate.X_AXIS); + Camera camera = gridPane.getScene().getCamera(); + gridPane.getScene().setCamera(new PerspectiveCamera()); + + Timeline timeline = new Timeline(); + ObservableList keyFrames = timeline.getKeyFrames(); + keyFrames.add(new KeyFrame(Duration.millis(0), + new KeyValue(gridPane.rotateProperty(), 0, interpolator), + new KeyValue(gridPane.opacityProperty(), 1, interpolator) + )); + keyFrames.add(new KeyFrame(Duration.millis(duration), + new KeyValue(gridPane.rotateProperty(), -90, interpolator), + new KeyValue(gridPane.opacityProperty(), 0, interpolator) + )); + timeline.setOnFinished(event -> { + gridPane.setRotate(0); + gridPane.setRotationAxis(Rotate.Z_AXIS); + gridPane.getScene().setCamera(camera); + onFinishedHandler.run(); + }); + timeline.play(); + } else { + onFinishedHandler.run(); + } + } + + @Override + protected void animateDisplay() { + if (GlobalSettings.getUseAnimations()) { + double startY = -160; + double duration = getDuration(400); + Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); + Timeline timeline = new Timeline(); + ObservableList keyFrames = timeline.getKeyFrames(); + keyFrames.add(new KeyFrame(Duration.millis(0), + new KeyValue(gridPane.opacityProperty(), 0, interpolator), + new KeyValue(gridPane.translateYProperty(), startY, interpolator) + )); + + keyFrames.add(new KeyFrame(Duration.millis(duration), + new KeyValue(gridPane.opacityProperty(), 1, interpolator), + new KeyValue(gridPane.translateYProperty(), 0, interpolator) + )); + + timeline.play(); + } + } + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(15, 15, 30, 30)); + } + + @Override + protected void addButtons() { + buttonDistance = 10; + super.addButtons(); + + actionButton.setOnAction(event -> apply()); + } + + private void apply() { + hide(); + if (actionHandler != null && inputTextField != null) + actionHandler.accept(inputTextField.getText()); + } + + @Override + protected void applyStyles() { + super.applyStyles(); + FormBuilder.getIconForLabel(AwesomeIcon.LOCK, headlineIcon, "1.5em"); + } + + @Override + protected void setModality() { + stage.initOwner(owner.getScene().getWindow()); + stage.initModality(Modality.NONE); + } + + @Override + protected void addEffectToBackground() { + } + + @Override + protected void removeEffectFromBackground() { + } +} diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java index 262ec5efea..4cefcbde26 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java @@ -141,7 +141,7 @@ public class ContractWindow extends Overlay { DisplayUtils.formatDateTime(offer.getDate()) + " / " + DisplayUtils.formatDateTime(dispute.getTradeDate())); String currencyCode = offer.getCurrencyCode(); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.offerType"), - DisplayUtils.getDirectionBothSides(offer.getDirection())); + DisplayUtils.getDirectionBothSides(offer.getDirection(), offer.isPrivateOffer())); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradePrice"), FormattingUtils.formatPrice(contract.getPrice())); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradeAmount"), diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java index 997a724224..ebf1c25309 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -99,6 +99,7 @@ public class DisputeSummaryWindow extends Overlay { reasonWasOtherRadioButton, reasonWasBankRadioButton, reasonWasOptionTradeRadioButton, reasonWasSellerNotRespondingRadioButton, reasonWasWrongSenderAccountRadioButton, reasonWasPeerWasLateRadioButton, reasonWasTradeAlreadySettledRadioButton; + private CoreDisputesService.PayoutSuggestion payoutSuggestion; // Dispute object of other trade peer. The dispute field is the one from which we opened the close dispute window. private Optional peersDisputeOptional; @@ -700,33 +701,28 @@ public class DisputeSummaryWindow extends Overlay { } private void applyPayoutAmountsToDisputeResult(Toggle selectedTradeAmountToggle) { - CoreDisputesService.DisputePayout payout; if (selectedTradeAmountToggle == buyerGetsTradeAmountRadioButton) { - payout = CoreDisputesService.DisputePayout.BUYER_GETS_TRADE_AMOUNT; + payoutSuggestion = CoreDisputesService.PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT; disputeResult.setWinner(DisputeResult.Winner.BUYER); } else if (selectedTradeAmountToggle == buyerGetsAllRadioButton) { - payout = CoreDisputesService.DisputePayout.BUYER_GETS_ALL; + payoutSuggestion = CoreDisputesService.PayoutSuggestion.BUYER_GETS_ALL; disputeResult.setWinner(DisputeResult.Winner.BUYER); } else if (selectedTradeAmountToggle == sellerGetsTradeAmountRadioButton) { - payout = CoreDisputesService.DisputePayout.SELLER_GETS_TRADE_AMOUNT; + payoutSuggestion = CoreDisputesService.PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT; disputeResult.setWinner(DisputeResult.Winner.SELLER); } else if (selectedTradeAmountToggle == sellerGetsAllRadioButton) { - payout = CoreDisputesService.DisputePayout.SELLER_GETS_ALL; + payoutSuggestion = CoreDisputesService.PayoutSuggestion.SELLER_GETS_ALL; disputeResult.setWinner(DisputeResult.Winner.SELLER); } else { // should not happen throw new IllegalStateException("Unknown radio button"); } - disputesService.applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, -1); + disputesService.applyPayoutAmountsToDisputeResult(payoutSuggestion, dispute, disputeResult, -1); buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost())); sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost())); } private void applyTradeAmountRadioButtonStates() { - Contract contract = dispute.getContract(); - BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit(); - BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit(); - BigInteger tradeAmount = contract.getTradeAmount(); BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmountBeforeCost(); BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmountBeforeCost(); @@ -734,20 +730,22 @@ public class DisputeSummaryWindow extends Overlay { buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(buyerPayoutAmount)); sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(sellerPayoutAmount)); - if (buyerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit)) && - sellerPayoutAmount.equals(sellerSecurityDeposit)) { - buyerGetsTradeAmountRadioButton.setSelected(true); - } else if (buyerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)) && - sellerPayoutAmount.equals(BigInteger.ZERO)) { - buyerGetsAllRadioButton.setSelected(true); - } else if (sellerPayoutAmount.equals(tradeAmount.add(sellerSecurityDeposit)) - && buyerPayoutAmount.equals(buyerSecurityDeposit)) { - sellerGetsTradeAmountRadioButton.setSelected(true); - } else if (sellerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)) - && buyerPayoutAmount.equals(BigInteger.ZERO)) { - sellerGetsAllRadioButton.setSelected(true); - } else { - customRadioButton.setSelected(true); + switch (payoutSuggestion) { + case BUYER_GETS_TRADE_AMOUNT: + buyerGetsTradeAmountRadioButton.setSelected(true); + break; + case BUYER_GETS_ALL: + buyerGetsAllRadioButton.setSelected(true); + break; + case SELLER_GETS_TRADE_AMOUNT: + sellerGetsTradeAmountRadioButton.setSelected(true); + break; + case SELLER_GETS_ALL: + sellerGetsAllRadioButton.setSelected(true); + break; + case CUSTOM: + customRadioButton.setSelected(true); + break; } } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index e93dc647a3..7825c7a610 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -29,6 +29,7 @@ import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; +import haveno.core.offer.OpenOffer; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; @@ -42,6 +43,8 @@ import haveno.desktop.Navigation; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.BusyAnimation; import haveno.desktop.main.overlays.Overlay; +import haveno.desktop.main.overlays.editor.PasswordPopup; +import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.CssTheme; import haveno.desktop.util.DisplayUtils; import static haveno.desktop.util.FormBuilder.addButtonAfterGroup; @@ -195,7 +198,7 @@ public class OfferDetailsWindow extends Overlay { rows++; } - addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.Offer")); + addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get(offer.isPrivateOffer() ? "shared.Offer" : "shared.Offer")); String counterCurrencyDirectionInfo = ""; String xmrDirectionInfo = ""; @@ -217,7 +220,7 @@ public class OfferDetailsWindow extends Overlay { xmrDirectionInfo = direction == OfferDirection.BUY ? toReceive : toSpend; } else { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, - DisplayUtils.getDirectionBothSides(direction), firstRowDistance); + DisplayUtils.getDirectionBothSides(direction, offer.isPrivateOffer()), firstRowDistance); } String amount = Res.get("shared.xmrAmount"); if (takeOfferHandlerOptional.isPresent()) { @@ -342,6 +345,10 @@ public class OfferDetailsWindow extends Overlay { // get amount reserved for the offer BigInteger reservedAmount = isMyOffer ? offer.getReservedAmount() : null; + // get offer challenge + OpenOffer myOpenOffer = HavenoUtils.openOfferManager.getOpenOfferById(offer.getId()).orElse(null); + String offerChallenge = myOpenOffer == null ? null : myOpenOffer.getChallenge(); + rows = 3; if (countryCode != null) rows++; @@ -349,6 +356,8 @@ public class OfferDetailsWindow extends Overlay { rows++; if (reservedAmount != null) rows++; + if (offerChallenge != null) + rows++; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE); addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(), @@ -365,6 +374,7 @@ public class OfferDetailsWindow extends Overlay { " " + HavenoUtils.formatXmr(offer.getOfferPayload().getMaxSellerSecurityDeposit(), true); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), value); + if (reservedAmount != null) { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.reservedAmount"), HavenoUtils.formatXmr(reservedAmount, true)); } @@ -373,6 +383,9 @@ public class OfferDetailsWindow extends Overlay { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.countryBank"), CountryUtil.getNameAndCode(countryCode)); + if (offerChallenge != null) + addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.challenge"), offerChallenge); + if (placeOfferHandlerOptional.isPresent()) { addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("offerDetailsWindow.commitment"), Layout.GROUP_DISTANCE); final Tuple2 labelLabelTuple2 = addConfirmationLabelLabel(gridPane, rowIndex, Res.get("offerDetailsWindow.agree"), Res.get("createOffer.tac"), @@ -416,13 +429,13 @@ public class OfferDetailsWindow extends Overlay { ++rowIndex, 1, isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); - AutoTooltipButton button = (AutoTooltipButton) placeOfferTuple.first; - button.setMinHeight(40); - button.setPadding(new Insets(0, 20, 0, 20)); - button.setGraphic(iconView); - button.setGraphicTextGap(10); - button.setId(isBuyerRole ? "buy-button-big" : "sell-button-big"); - button.updateText(isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); + AutoTooltipButton confirmButton = (AutoTooltipButton) placeOfferTuple.first; + confirmButton.setMinHeight(40); + confirmButton.setPadding(new Insets(0, 20, 0, 20)); + confirmButton.setGraphic(iconView); + confirmButton.setGraphicTextGap(10); + confirmButton.setId(isBuyerRole ? "buy-button-big" : "sell-button-big"); + confirmButton.updateText(isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); busyAnimation = placeOfferTuple.second; Label spinnerInfoLabel = placeOfferTuple.third; @@ -436,29 +449,48 @@ public class OfferDetailsWindow extends Overlay { placeOfferTuple.fourth.getChildren().add(cancelButton); - button.setOnAction(e -> { + confirmButton.setOnAction(e -> { if (GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation)) { - button.setDisable(true); - cancelButton.setDisable(isPlaceOffer ? false : true); // TODO: enable cancel button for taking an offer until messages sent - // temporarily disabled due to high CPU usage (per issue #4649) - // busyAnimation.play(); - if (isPlaceOffer) { - spinnerInfoLabel.setText(Res.get("createOffer.fundsBox.placeOfferSpinnerInfo")); - placeOfferHandlerOptional.ifPresent(Runnable::run); + if (!isPlaceOffer && offer.isPrivateOffer()) { + new PasswordPopup() + .headLine(Res.get("offerbook.takeOffer.enterChallenge")) + .onAction(password -> { + if (offer.getChallengeHash().equals(HavenoUtils.getChallengeHash(password))) { + offer.setChallenge(password); + confirmTakeOfferAux(confirmButton, cancelButton, spinnerInfoLabel, isPlaceOffer); + } else { + new Popup().warning(Res.get("password.wrongPw")).show(); + } + }) + .closeButtonText(Res.get("shared.cancel")) + .show(); } else { - - // subscribe to trade progress - spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo", "0%")); - numTradesSubscription = EasyBind.subscribe(tradeManager.getNumPendingTrades(), newNum -> { - subscribeToProgress(spinnerInfoLabel); - }); - - takeOfferHandlerOptional.ifPresent(Runnable::run); + confirmTakeOfferAux(confirmButton, cancelButton, spinnerInfoLabel, isPlaceOffer); } } }); } + private void confirmTakeOfferAux(Button button, Button cancelButton, Label spinnerInfoLabel, boolean isPlaceOffer) { + button.setDisable(true); + cancelButton.setDisable(isPlaceOffer ? false : true); // TODO: enable cancel button for taking an offer until messages sent + // temporarily disabled due to high CPU usage (per issue #4649) + // busyAnimation.play(); + if (isPlaceOffer) { + spinnerInfoLabel.setText(Res.get("createOffer.fundsBox.placeOfferSpinnerInfo")); + placeOfferHandlerOptional.ifPresent(Runnable::run); + } else { + + // subscribe to trade progress + spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo", "0%")); + numTradesSubscription = EasyBind.subscribe(tradeManager.getNumPendingTrades(), newNum -> { + subscribeToProgress(spinnerInfoLabel); + }); + + takeOfferHandlerOptional.ifPresent(Runnable::run); + } + } + private void subscribeToProgress(Label spinnerInfoLabel) { Trade trade = tradeManager.getTrade(offer.getId()); if (trade == null || initProgressSubscription != null) return; diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java index 75514e4300..e3fd44c0ba 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java @@ -52,7 +52,7 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { DuplicateOfferDataModel(CreateOfferService createOfferService, OpenOfferManager openOfferManager, OfferUtil offerUtil, - XmrWalletService btcWalletService, + XmrWalletService xmrWalletService, Preferences preferences, User user, P2PService p2PService, @@ -65,7 +65,7 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { super(createOfferService, openOfferManager, offerUtil, - btcWalletService, + xmrWalletService, preferences, user, p2PService, @@ -85,20 +85,21 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); + setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit()); - setBuyerSecurityDeposit(getBuyerSecurityAsPercent(offer)); + setSecurityDepositPct(getSecurityAsPercent(offer)); if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } } - private double getBuyerSecurityAsPercent(Offer offer) { - BigInteger offerBuyerSecurityDeposit = getBoundedBuyerSecurityDeposit(offer.getMaxBuyerSecurityDeposit()); - double offerBuyerSecurityDepositAsPercent = CoinUtil.getAsPercentPerBtc(offerBuyerSecurityDeposit, + private double getSecurityAsPercent(Offer offer) { + BigInteger offerSellerSecurityDeposit = getBoundedSecurityDeposit(offer.getMaxSellerSecurityDeposit()); + double offerSellerSecurityDepositAsPercent = CoinUtil.getAsPercentPerXmr(offerSellerSecurityDeposit, offer.getAmount()); - return Math.min(offerBuyerSecurityDepositAsPercent, - Restrictions.getMaxBuyerSecurityDepositAsPercent()); + return Math.min(offerSellerSecurityDepositAsPercent, + Restrictions.getMaxSecurityDepositAsPercent()); } @Override diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java index be2b811f07..d7ab366b75 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -95,7 +95,7 @@ class EditOfferDataModel extends MutableOfferDataModel { price.set(null); volume.set(null); minVolume.set(null); - buyerSecurityDepositPct.set(0); + securityDepositPct.set(0); paymentAccounts.clear(); paymentAccount = null; marketPriceMargin = 0; @@ -127,12 +127,12 @@ class EditOfferDataModel extends MutableOfferDataModel { // If the security deposit got bounded because it was below the coin amount limit, it can be bigger // by percentage than the restriction. We can't determine the percentage originally entered at offer // creation, so just use the default value as it doesn't matter anyway. - double buyerSecurityDepositPercent = CoinUtil.getAsPercentPerBtc(offer.getMaxBuyerSecurityDeposit(), offer.getAmount()); - if (buyerSecurityDepositPercent > Restrictions.getMaxBuyerSecurityDepositAsPercent() - && offer.getMaxBuyerSecurityDeposit().equals(Restrictions.getMinBuyerSecurityDeposit())) - buyerSecurityDepositPct.set(Restrictions.getDefaultBuyerSecurityDepositAsPercent()); + double securityDepositPercent = CoinUtil.getAsPercentPerXmr(offer.getMaxSellerSecurityDeposit(), offer.getAmount()); + if (securityDepositPercent > Restrictions.getMaxSecurityDepositAsPercent() + && offer.getMaxSellerSecurityDeposit().equals(Restrictions.getMinSecurityDeposit())) + securityDepositPct.set(Restrictions.getDefaultSecurityDepositAsPercent()); else - buyerSecurityDepositPct.set(buyerSecurityDepositPercent); + securityDepositPct.set(securityDepositPercent); allowAmountUpdate = false; } @@ -211,7 +211,7 @@ class EditOfferDataModel extends MutableOfferDataModel { offerPayload.getLowerClosePrice(), offerPayload.getUpperClosePrice(), offerPayload.isPrivateOffer(), - offerPayload.getHashOfChallenge(), + offerPayload.getChallengeHash(), offerPayload.getExtraDataMap(), offerPayload.getProtocolVersion(), offerPayload.getArbitratorSigner(), diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java index 721f21bbfc..34b78be683 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java @@ -111,7 +111,7 @@ class EditOfferViewModel extends MutableOfferViewModel { } public boolean isSecurityDepositValid() { - return securityDepositValidator.validate(buyerSecurityDeposit.get()).isValid; + return securityDepositValidator.validate(securityDeposit.get()).isValid; } @Override diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java index 9c351e6272..b95aad8806 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java @@ -357,7 +357,7 @@ public class FailedTradesView extends ActivatableViewAndModel if ((item == null)) return ""; - return DisplayUtils.getDirectionWithCode(dataModel.getDirection(item.getOffer()), item.getOffer().getCurrencyCode()); + return DisplayUtils.getDirectionWithCode(dataModel.getDirection(item.getOffer()), item.getOffer().getCurrencyCode(), item.getOffer().isPrivateOffer()); } String getMarketLabel(OpenOfferListItem item) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 8119166996..e4fc8b1bdc 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -380,14 +380,11 @@ public class PendingTradesDataModel extends ActivatableDataModel { tradeStateChangeListener = (observable, oldValue, newValue) -> { String makerDepositTxHash = selectedTrade.getMaker().getDepositTxHash(); String takerDepositTxHash = selectedTrade.getTaker().getDepositTxHash(); - if (makerDepositTxHash != null && takerDepositTxHash != null) { // TODO (woodser): this treats separate deposit ids as one unit, being both available or unavailable - makerTxId.set(makerDepositTxHash); - takerTxId.set(takerDepositTxHash); + makerTxId.set(nullToEmptyString(makerDepositTxHash)); + takerTxId.set(nullToEmptyString(takerDepositTxHash)); + if (makerDepositTxHash != null || takerDepositTxHash != null) { notificationCenter.setSelectedTradeId(tradeId); selectedTrade.stateProperty().removeListener(tradeStateChangeListener); - } else { - makerTxId.set(""); - takerTxId.set(""); } }; selectedTrade.stateProperty().addListener(tradeStateChangeListener); @@ -401,13 +398,8 @@ public class PendingTradesDataModel extends ActivatableDataModel { isMaker = tradeManager.isMyOffer(offer); String makerDepositTxHash = selectedTrade.getMaker().getDepositTxHash(); String takerDepositTxHash = selectedTrade.getTaker().getDepositTxHash(); - if (makerDepositTxHash != null && takerDepositTxHash != null) { - makerTxId.set(makerDepositTxHash); - takerTxId.set(takerDepositTxHash); - } else { - makerTxId.set(""); - takerTxId.set(""); - } + makerTxId.set(nullToEmptyString(makerDepositTxHash)); + takerTxId.set(nullToEmptyString(takerDepositTxHash)); notificationCenter.setSelectedTradeId(tradeId); } else { selectedTrade = null; @@ -419,6 +411,10 @@ public class PendingTradesDataModel extends ActivatableDataModel { }); } + private String nullToEmptyString(String str) { + return str == null ? "" : str; + } + private void tryOpenDispute(boolean isSupportTicket) { Trade trade = getTrade(); if (trade == null) { @@ -446,7 +442,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { } depositTxId = trade.getMaker().getDepositTxHash(); } else { - if (trade.getTaker().getDepositTxHash() == null) { + if (trade.getTaker().getDepositTxHash() == null && !trade.hasBuyerAsTakerWithoutDeposit()) { log.error("Deposit tx must not be null"); new Popup().instruction(Res.get("portfolio.pending.error.depositTxNull")).show(); return; diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 73160484f2..8c31e3d8e6 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -403,7 +403,7 @@ public class PendingTradesView extends ActivatableViewAndModel labelSelfTxIdTextFieldVBoxTuple3 = - addTopLabelTxIdTextField(gridPane, gridRow, Res.get("shared.yourDepositTransactionId"), - Layout.COMPACT_FIRST_ROW_DISTANCE); + boolean showSelfTxId = model.dataModel.isMaker() || !trade.hasBuyerAsTakerWithoutDeposit(); + if (showSelfTxId) { + final Tuple3 labelSelfTxIdTextFieldVBoxTuple3 = + addTopLabelTxIdTextField(gridPane, gridRow, Res.get("shared.yourDepositTransactionId"), + Layout.COMPACT_FIRST_ROW_DISTANCE); - GridPane.setColumnSpan(labelSelfTxIdTextFieldVBoxTuple3.third, 2); - selfTxIdTextField = labelSelfTxIdTextFieldVBoxTuple3.second; + GridPane.setColumnSpan(labelSelfTxIdTextFieldVBoxTuple3.third, 2); + selfTxIdTextField = labelSelfTxIdTextFieldVBoxTuple3.second; - String selfTxId = model.dataModel.isMaker() ? model.dataModel.makerTxId.get() : model.dataModel.takerTxId.get(); - if (!selfTxId.isEmpty()) - selfTxIdTextField.setup(selfTxId, trade); - else - selfTxIdTextField.cleanup(); + String selfTxId = model.dataModel.isMaker() ? model.dataModel.makerTxId.get() : model.dataModel.takerTxId.get(); + if (!selfTxId.isEmpty()) + selfTxIdTextField.setup(selfTxId, trade); + else + selfTxIdTextField.cleanup(); + } // peer's deposit tx id - final Tuple3 labelPeerTxIdTextFieldVBoxTuple3 = - addTopLabelTxIdTextField(gridPane, ++gridRow, Res.get("shared.peerDepositTransactionId"), - -Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR); + boolean showPeerTxId = !model.dataModel.isMaker() || !trade.hasBuyerAsTakerWithoutDeposit(); + if (showPeerTxId) { + final Tuple3 labelPeerTxIdTextFieldVBoxTuple3 = + addTopLabelTxIdTextField(gridPane, showSelfTxId ? ++gridRow : gridRow, Res.get("shared.peerDepositTransactionId"), + showSelfTxId ? -Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR : Layout.COMPACT_FIRST_ROW_DISTANCE); - GridPane.setColumnSpan(labelPeerTxIdTextFieldVBoxTuple3.third, 2); - peerTxIdTextField = labelPeerTxIdTextFieldVBoxTuple3.second; + GridPane.setColumnSpan(labelPeerTxIdTextFieldVBoxTuple3.third, 2); + peerTxIdTextField = labelPeerTxIdTextFieldVBoxTuple3.second; - String peerTxId = model.dataModel.isMaker() ? model.dataModel.takerTxId.get() : model.dataModel.makerTxId.get(); - if (!peerTxId.isEmpty()) - peerTxIdTextField.setup(peerTxId, trade); - else - peerTxIdTextField.cleanup(); + String peerTxId = model.dataModel.isMaker() ? model.dataModel.takerTxId.get() : model.dataModel.makerTxId.get(); + if (!peerTxId.isEmpty()) + peerTxIdTextField.setup(peerTxId, trade); + else + peerTxIdTextField.cleanup(); + } if (model.dataModel.getTrade() != null) { checkNotNull(model.dataModel.getTrade().getOffer(), "Offer must not be null in TradeStepView"); @@ -648,7 +654,7 @@ public abstract class TradeStepView extends AnchorPane { model.dataModel.onMoveInvalidTradeToFailedTrades(trade); new Popup().warning(Res.get("portfolio.pending.mediationResult.error.depositTxNull")).show(); // TODO (woodser): separate error messages for maker/taker return; - } else if (trade instanceof TakerTrade && trade.getTakerDepositTx() == null) { + } else if (trade instanceof TakerTrade && trade.getTakerDepositTx() == null && !trade.hasBuyerAsTakerWithoutDeposit()) { log.error("trade.getTakerDepositTx() was null at openMediationResultPopup. " + "We add the trade to failed trades. TradeId={}", trade.getId()); //model.dataModel.addTradeToFailedTrades(); diff --git a/desktop/src/main/java/haveno/desktop/theme-dark.css b/desktop/src/main/java/haveno/desktop/theme-dark.css index 8e7345b385..37903692ce 100644 --- a/desktop/src/main/java/haveno/desktop/theme-dark.css +++ b/desktop/src/main/java/haveno/desktop/theme-dark.css @@ -557,3 +557,12 @@ -fx-text-fill: -bs-text-color; -fx-fill: -bs-text-color; } + +.toggle-button-no-slider { + -fx-focus-color: transparent; + -fx-faint-focus-color: transparent; +} + +.toggle-button-no-slider:selected { + -fx-background-color: -bs-color-gray-ddd; +} diff --git a/desktop/src/main/java/haveno/desktop/theme-light.css b/desktop/src/main/java/haveno/desktop/theme-light.css index b4ab888f87..7605eb1819 100644 --- a/desktop/src/main/java/haveno/desktop/theme-light.css +++ b/desktop/src/main/java/haveno/desktop/theme-light.css @@ -125,3 +125,8 @@ .progress-bar > .secondary-bar { -fx-background-color: -bs-color-gray-3; } + +.toggle-button-no-slider { + -fx-focus-color: transparent; + -fx-faint-focus-color: transparent; +} diff --git a/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java b/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java index 2a38895421..250c8ad339 100644 --- a/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java +++ b/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java @@ -117,17 +117,21 @@ public class DisplayUtils { /////////////////////////////////////////////////////////////////////////////////////////// public static String getDirectionWithCode(OfferDirection direction, String currencyCode) { - if (CurrencyUtil.isTraditionalCurrency(currencyCode)) - return (direction == OfferDirection.BUY) ? Res.get("shared.buyCurrency", Res.getBaseCurrencyCode()) : Res.get("shared.sellCurrency", Res.getBaseCurrencyCode()); - else - return (direction == OfferDirection.SELL) ? Res.get("shared.buyCurrency", currencyCode) : Res.get("shared.sellCurrency", currencyCode); + return getDirectionWithCode(direction, currencyCode, false); } - public static String getDirectionBothSides(OfferDirection direction) { + public static String getDirectionWithCode(OfferDirection direction, String currencyCode, boolean isPrivate) { + if (CurrencyUtil.isTraditionalCurrency(currencyCode)) + return (direction == OfferDirection.BUY) ? Res.get(isPrivate ? "shared.buyCurrencyLocked" : "shared.buyCurrency", Res.getBaseCurrencyCode()) : Res.get(isPrivate ? "shared.sellCurrencyLocked" : "shared.sellCurrency", Res.getBaseCurrencyCode()); + else + return (direction == OfferDirection.SELL) ? Res.get(isPrivate ? "shared.buyCurrencyLocked" : "shared.buyCurrency", currencyCode) : Res.get(isPrivate ? "shared.sellCurrencyLocked" : "shared.sellCurrency", currencyCode); + } + + public static String getDirectionBothSides(OfferDirection direction, boolean isLocked) { String currencyCode = Res.getBaseCurrencyCode(); return direction == OfferDirection.BUY ? - Res.get("formatter.makerTaker", currencyCode, Res.get("shared.buyer"), currencyCode, Res.get("shared.seller")) : - Res.get("formatter.makerTaker", currencyCode, Res.get("shared.seller"), currencyCode, Res.get("shared.buyer")); + Res.get(isLocked ? "formatter.makerTakerLocked" : "formatter.makerTaker", currencyCode, Res.get("shared.buyer"), currencyCode, Res.get("shared.seller")) : + Res.get(isLocked ? "formatter.makerTakerLocked" : "formatter.makerTaker", currencyCode, Res.get("shared.seller"), currencyCode, Res.get("shared.buyer")); } public static String getDirectionForBuyer(boolean isMyOffer, String currencyCode) { diff --git a/desktop/src/main/resources/images/lock.png b/desktop/src/main/resources/images/lock.png deleted file mode 100644 index 3a4bba5d91e1919cd73c6b5830508436da6a946b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22292 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 D;@nZw diff --git a/desktop/src/main/resources/images/lock@2x.png b/desktop/src/main/resources/images/lock@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..371f6aeb5d8845ffaaef57e46036d90f05797826 GIT binary patch literal 721 zcmV;?0xtcDP)0EtA|W9mp&}N)5*Y$lmAxlAT8HZsXL znJ`mJ6uamTu!0LO)JzJcsC=Gtn8>J=If*k!z`(ni^S$r;e91}3p^1AbigL8AuFteI zeI6@gX`VbIXPy;Hz1}goW8B1LpIe#%2T9h99l4|5AhRxSy?#>G^qy4$)n_fpNi|q0 zi&R{-PM^s?VDwEzF;2Nu{^B?e+$O;_S%DN1;8Bh{P)Q`v84A%c-pGbA^;hKpa2Ri; zwydpVT%Hy!7!DYa4(H_0=6MrO#Y1)(b>V{Cx7YFs2iO{EKq`2sD^ z6wBM3D4zIrHeXqr#%FKw44I`K>u$`B4S=_QPC^Q`1}#qu#g7P~BY&DFXUjtA6P~4> z;kz(a`I@eWaKP6_TZJWS`&CIg>*V9=PiL%;35XXj@kP_L;K%@cy>TT|Dnrb=UeIQ_mZrU0b;q_C40!j9EVuFWY+E_ zSbguJRp;qKx+NM~=s zgTx&utX^XUAhX!BK)f{L<&)^}3^@aI{ky}U4KWIQ_9`^_bMQfpumaH8X5l>bZdA|M z&in%gfB<+YX8<(-CLe@BIs}81E23E@3PN9H0KnzM_ zAUJRYz(ayp0A@18I|7(-Uzk7;TKCFn7Bb_%|0VklVx(?C;s@SE00000NkvXXu0mjf Dtl2{# literal 0 HcmV?d00001 diff --git a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java index 98c933ed4a..33d43b7bc5 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java @@ -53,7 +53,7 @@ public class CreateOfferDataModelTest { when(xmrWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry); when(preferences.isUsePercentageBasedPrice()).thenReturn(true); - when(preferences.getBuyerSecurityDepositAsPercent(null)).thenReturn(0.01); + when(preferences.getSecurityDepositAsPercent(null)).thenReturn(0.01); when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet()); diff --git a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java index 4b33c8bab7..a8c6ede578 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java @@ -55,6 +55,7 @@ import static haveno.desktop.maker.PreferenceMakers.empty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -99,7 +100,7 @@ public class CreateOfferViewModelTest { when(paymentAccount.getPaymentMethod()).thenReturn(PaymentMethod.ZELLE); when(user.getPaymentAccountsAsObservable()).thenReturn(FXCollections.observableSet()); when(securityDepositValidator.validate(any())).thenReturn(new InputValidator.ValidationResult(false)); - when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any())).thenReturn(100000000L); + when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any(), anyBoolean())).thenReturn(100000000L); when(preferences.getUserCountry()).thenReturn(new Country("ES", "Spain", null)); when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet()); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index cb6ba81860..c9b8a75445 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -521,10 +521,12 @@ message PostOfferRequest { double market_price_margin_pct = 5; uint64 amount = 6 [jstype = JS_STRING]; uint64 min_amount = 7 [jstype = JS_STRING]; - double buyer_security_deposit_pct = 8; + double security_deposit_pct = 8; string trigger_price = 9; bool reserve_exact_amount = 10; string payment_account_id = 11; + bool is_private_offer = 12; + bool buyer_as_taker_without_deposit = 13; } message PostOfferReply { @@ -570,6 +572,8 @@ message OfferInfo { string arbitrator_signer = 29; string split_output_tx_hash = 30; uint64 split_output_tx_fee = 31 [jstype = JS_STRING]; + bool is_private_offer = 32; + string challenge = 33; } message AvailabilityResultWithDescription { @@ -785,6 +789,7 @@ message TakeOfferRequest { string offer_id = 1; string payment_account_id = 2; uint64 amount = 3 [jstype = JS_STRING]; + string challenge = 4; } message TakeOfferReply { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index db5aa49d7d..b52274ae57 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -250,6 +250,7 @@ message InitTradeRequest { string reserve_tx_hex = 18; string reserve_tx_key = 19; string payout_address = 20; + string challenge = 21; } message InitMultisigRequest { @@ -650,7 +651,7 @@ message OfferPayload { int64 lower_close_price = 30; int64 upper_close_price = 31; bool is_private_offer = 32; - string hash_of_challenge = 33; + string challenge_hash = 33; map extra_data = 34; int32 protocol_version = 35; NodeAddress arbitrator_signer = 36; @@ -1412,6 +1413,7 @@ message OpenOffer { string reserve_tx_hash = 9; string reserve_tx_hex = 10; string reserve_tx_key = 11; + string challenge = 12; } message Tradable { @@ -1528,6 +1530,7 @@ message Trade { string counter_currency_extra_data = 26; string uid = 27; bool is_completed = 28; + string challenge = 29; } message BuyerAsMakerTrade { @@ -1723,9 +1726,9 @@ message PreferencesPayload { string rpc_user = 43; string rpc_pw = 44; string take_offer_selected_payment_account_id = 45; - double buyer_security_deposit_as_percent = 46; + double security_deposit_as_percent = 46; int32 ignore_dust_threshold = 47; - double buyer_security_deposit_as_percent_for_crypto = 48; + double security_deposit_as_percent_for_crypto = 48; int32 block_notify_port = 49; int32 css_theme = 50; bool tac_accepted_v120 = 51; @@ -1744,6 +1747,7 @@ message PreferencesPayload { bool use_sound_for_notifications_initialized = 64; string buy_screen_other_currency_code = 65; string sell_screen_other_currency_code = 66; + bool show_private_offers = 67; } message AutoConfirmSettings { From bd5accb5a5c19acb4075d9d942acb85d1de0a036 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 16 Dec 2024 10:57:41 -0500 Subject: [PATCH 034/371] update translations for reserving only necessary funds --- core/src/main/resources/i18n/displayStrings.properties | 2 +- core/src/main/resources/i18n/displayStrings_cs.properties | 2 +- core/src/main/resources/i18n/displayStrings_de.properties | 2 +- core/src/main/resources/i18n/displayStrings_es.properties | 2 +- core/src/main/resources/i18n/displayStrings_fa.properties | 2 +- core/src/main/resources/i18n/displayStrings_fr.properties | 2 +- core/src/main/resources/i18n/displayStrings_it.properties | 2 +- core/src/main/resources/i18n/displayStrings_ja.properties | 2 +- core/src/main/resources/i18n/displayStrings_pt-br.properties | 2 +- core/src/main/resources/i18n/displayStrings_pt.properties | 2 +- core/src/main/resources/i18n/displayStrings_ru.properties | 2 +- core/src/main/resources/i18n/displayStrings_th.properties | 2 +- core/src/main/resources/i18n/displayStrings_tr.properties | 2 +- core/src/main/resources/i18n/displayStrings_vi.properties | 2 +- core/src/main/resources/i18n/displayStrings_zh-hans.properties | 2 +- core/src/main/resources/i18n/displayStrings_zh-hant.properties | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 86fd962d63..71ed6e3164 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -200,7 +200,7 @@ shared.total=Total shared.totalsNeeded=Funds needed shared.tradeWalletAddress=Trade wallet address shared.tradeWalletBalance=Trade wallet balance -shared.reserveExactAmount=Reserve only the funds needed. May require a mining fee and 10 confirmations (~20 minutes) before your offer is live. +shared.reserveExactAmount=Reserve only the necessary funds. Requires a mining fee and ~20 minutes before your offer goes live. shared.makerTxFee=Maker: {0} shared.takerTxFee=Taker: {0} shared.iConfirm=I confirm diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 85f021d4b3..e2e692c42b 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -188,7 +188,7 @@ shared.total=Celkem shared.totalsNeeded=Potřebné prostředky shared.tradeWalletAddress=Adresa obchodní peněženky shared.tradeWalletBalance=Zůstatek obchodní peněženky -shared.reserveExactAmount=Rezervujte pouze potřebné finanční prostředky. Před aktivací vaší nabídky může být vyžadována těžební poplatek a 10 potvrzení (~20 minut). +shared.reserveExactAmount=Rezervujte pouze nezbytné prostředky. Vyžaduje poplatek za těžbu a přibližně 20 minut, než vaše nabídka půjde živě. shared.makerTxFee=Tvůrce: {0} shared.takerTxFee=Příjemce: {0} shared.iConfirm=Potvrzuji diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index e8b8584cd5..d1bfa9f0b7 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -188,7 +188,7 @@ shared.total=Insgesamt shared.totalsNeeded=Benötigte Gelder shared.tradeWalletAddress=Adresse der Handels-Wallet shared.tradeWalletBalance=Guthaben der Handels-Wallet -shared.reserveExactAmount=Reservieren Sie nur die benötigten Mittel. Es kann erforderlich sein, eine Mining-Gebühr zu zahlen und 10 Bestätigungen (~20 Minuten) abzuwarten, bevor Ihr Angebot aktiv ist. +shared.reserveExactAmount=Reserviere nur die notwendigen Mittel. Erfordert eine Mining-Gebühr und ca. 20 Minuten, bevor dein Angebot live geht. shared.makerTxFee=Ersteller: {0} shared.takerTxFee=Abnehmer: {0} shared.iConfirm=Ich bestätige diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index d3f61ada16..af7539e7fd 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -188,7 +188,7 @@ shared.total=Total shared.totalsNeeded=Fondos necesarios shared.tradeWalletAddress=Dirección de la cartera para intercambio shared.tradeWalletBalance=Saldo de la cartera de intercambio -shared.reserveExactAmount=Reserve solo los fondos necesarios. Podría requerir una tarifa de minería y 10 confirmaciones (~20 minutos) antes de que su oferta esté activa. +shared.reserveExactAmount=Reserve solo los fondos necesarios. Requiere una tarifa de minería y aproximadamente 20 minutos antes de que tu oferta se haga pública. shared.makerTxFee=Creador: {0} shared.takerTxFee=Tomador: {0} shared.iConfirm=Confirmo diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 710bbb6b93..6c5286402f 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -188,7 +188,7 @@ shared.total=مجموع shared.totalsNeeded=وجه مورد نیاز shared.tradeWalletAddress=آدرس کیف‌پول معاملات shared.tradeWalletBalance=موجودی کیف‌پول معاملات -shared.reserveExactAmount=رزرو فقط مقدار مورد نیاز پول. قبل از فعال شدن پیشنهاد شما، ممکن است نیاز به هزینه استخراج و 10 تایید (~20 دقیقه) باشد. +shared.reserveExactAmount=فقط وجوه مورد نیاز را رزرو کنید. نیاز به هزینه استخراج و حدود ۲۰ دقیقه زمان قبل از فعال شدن پیشنهاد شما دارد. shared.makerTxFee=سفارش گذار: {0} shared.takerTxFee=پذیرنده سفارش: {0} shared.iConfirm=تایید می‌کنم diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 5e734ac976..2712ff4163 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -188,7 +188,7 @@ shared.total=Total shared.totalsNeeded=Fonds nécessaires shared.tradeWalletAddress=Adresse du portefeuille de trading shared.tradeWalletBalance=Solde du portefeuille de trading -shared.reserveExactAmount=Réservez uniquement les fonds nécessaires. Il peut être nécessaire de payer des frais de minage et d'attendre 10 confirmations (~20 minutes) avant que votre offre ne soit active. +shared.reserveExactAmount=Réservez uniquement les fonds nécessaires. Nécessite des frais de minage et environ 20 minutes avant que votre offre ne soit mise en ligne. shared.makerTxFee=Maker: {0} shared.takerTxFee=Taker: {0} shared.iConfirm=Je confirme diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index b968d83987..9e52fd8976 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -188,7 +188,7 @@ shared.total=Totale shared.totalsNeeded=Fondi richiesti shared.tradeWalletAddress=Indirizzo del portafoglio per gli scambi shared.tradeWalletBalance=Saldo del portafogli per gli scambi -shared.reserveExactAmount=Riserva solo i fondi necessari. Potrebbe essere richiesta una tassa di mining e 10 conferme (~20 minuti) prima che la tua offerta sia attiva. +shared.reserveExactAmount=Riserva solo i fondi necessari. Richiede una tassa di mining e circa 20 minuti prima che la tua offerta diventi attiva. shared.makerTxFee=Maker: {0} shared.takerTxFee=Taker: {0} shared.iConfirm=Confermo diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 70a3536104..c1331ac5b2 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -188,7 +188,7 @@ shared.total=合計 shared.totalsNeeded=必要な資金 shared.tradeWalletAddress=トレードウォレットアドレス shared.tradeWalletBalance=トレードウォレット残高 -shared.reserveExactAmount=必要な資金のみを予約してください。提供が有効になる前に、マイニング手数料と10回の確認(約20分)が必要な場合があります。 +shared.reserveExactAmount=必要な資金のみを予約してください。オファーが公開されるまでにマイニング手数料と約20分が必要です。 shared.makerTxFee=メイカー: {0} shared.takerTxFee=テイカー: {0} shared.iConfirm=確認します diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 93179908d6..d7006d4659 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -188,7 +188,7 @@ shared.total=Total shared.totalsNeeded=Fundos necessária shared.tradeWalletAddress=Endereço da carteira de negociação shared.tradeWalletBalance=Saldo da carteira de negociação -shared.reserveExactAmount=Reserve apenas os fundos necessários. Pode ser necessário uma taxa de mineração e 10 confirmações (~20 minutos) antes que sua oferta esteja ativa. +shared.reserveExactAmount=Reserve apenas os fundos necessários. Requer uma taxa de mineração e cerca de 20 minutos antes que sua oferta seja publicada. shared.makerTxFee=Ofertante: {0} shared.takerTxFee=Aceitador: {0} shared.iConfirm=Eu confirmo diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 1b1b5de6ec..cd016e3aff 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -188,7 +188,7 @@ shared.total=Total shared.totalsNeeded=Fundos necessários shared.tradeWalletAddress=Endereço da carteira do negócio shared.tradeWalletBalance=Saldo da carteira de negócio -shared.reserveExactAmount=Reserve apenas os fundos necessários. Pode ser necessário uma taxa de mineração e 10 confirmações (~20 minutos) antes que sua oferta seja ativada. +shared.reserveExactAmount=Reserve apenas os fundos necessários. Requer uma taxa de mineração e ~20 minutos antes que sua oferta seja publicada. shared.makerTxFee=Ofertante: {0} shared.takerTxFee=Aceitador: {0} shared.iConfirm=Eu confirmo diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index d40f9af7a9..664aebdaf9 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -188,7 +188,7 @@ shared.total=Всего shared.totalsNeeded=Требуемая сумма shared.tradeWalletAddress=Адрес кошелька сделки shared.tradeWalletBalance=Баланс кошелька сделки -shared.reserveExactAmount=Зарезервируйте только необходимые средства. Может потребоваться комиссия за майнинг и 10 подтверждений (~20 минут), прежде чем ваше предложение станет активным. +shared.reserveExactAmount=Резервируйте только необходимые средства. Требуется комиссия за майнинг и ~20 минут, прежде чем ваше предложение станет активным. shared.makerTxFee=Мейкер: {0} shared.takerTxFee=Тейкер: {0} shared.iConfirm=Подтверждаю diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 81955b6f43..2688b08d91 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -188,7 +188,7 @@ shared.total=ยอดทั้งหมด shared.totalsNeeded=เงินที่จำเป็น shared.tradeWalletAddress=ที่อยู่ Trade wallet shared.tradeWalletBalance=ยอดคงเหลือของ Trade wallet -shared.reserveExactAmount=สงวนเฉพาะเงินทุนที่จำเป็นเท่านั้น อาจจะต้องเสียค่าขุดแร่และรับรอง 10 ครั้ง (~20 นาที) ก่อนที่ข้อเสนอของคุณจะเป็นสถานะออนไลน์ +shared.reserveExactAmount=สำรองเฉพาะเงินที่จำเป็น ต้องใช้ค่าธรรมเนียมการขุดและเวลาประมาณ 20 นาทีก่อนที่ข้อเสนอของคุณจะเผยแพร่ shared.makerTxFee=ผู้ทำ: {0} shared.takerTxFee=ผู้รับ: {0} shared.iConfirm=ฉันยืนยัน diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 275b95677f..f3863027c7 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -200,7 +200,7 @@ shared.total=Toplam shared.totalsNeeded=Gereken fonlar shared.tradeWalletAddress=İşlem cüzdan adresi shared.tradeWalletBalance=İşlem cüzdan bakiyesi -shared.reserveExactAmount=Sadece gerekli fonları ayırın. Teklifinizin aktif olması için madencilik ücreti ve 10 onay (yaklaşık 20 dakika) gerekebilir. +shared.reserveExactAmount=Yalnızca gerekli fonları ayırın. Teklifinizin aktif hale gelmesi için bir madencilik ücreti ve yaklaşık 20 dakika gereklidir. shared.makerTxFee=Yapıcı: {0} shared.takerTxFee=Alıcı: {0} shared.iConfirm=Onaylıyorum diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 8e541353b7..40b2b67940 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -188,7 +188,7 @@ shared.total=Tổng shared.totalsNeeded=Số tiền cần shared.tradeWalletAddress=Địa chỉ ví giao dịch shared.tradeWalletBalance=Số dư ví giao dịch -shared.reserveExactAmount=Dự trữ chỉ số tiền cần thiết. Có thể yêu cầu một phí đào và 10 xác nhận (~20 phút) trước khi giao dịch của bạn trở nên hiệu lực. +shared.reserveExactAmount=Chỉ giữ lại số tiền cần thiết. Yêu cầu phí khai thác và khoảng 20 phút trước khi đề nghị của bạn được công khai. shared.makerTxFee=Người tạo: {0} shared.takerTxFee=Người nhận: {0} shared.iConfirm=Tôi xác nhận diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 0f42566eef..b495893a29 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -188,7 +188,7 @@ shared.total=合计 shared.totalsNeeded=需要资金 shared.tradeWalletAddress=交易钱包地址 shared.tradeWalletBalance=交易钱包余额 -shared.reserveExactAmount=仅保留所需的资金。在您的交易生效前可能需要支付挖矿费和10次确认(约20分钟)。 +shared.reserveExactAmount=仅保留必要的资金。需要支付矿工费用,并且大约需要 20 分钟后您的报价才会生效。 shared.makerTxFee=卖家:{0} shared.takerTxFee=买家:{0} shared.iConfirm=我确认 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index ccdbbcdf46..9132242ee7 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -188,7 +188,7 @@ shared.total=合計 shared.totalsNeeded=需要資金 shared.tradeWalletAddress=交易錢包地址 shared.tradeWalletBalance=交易錢包餘額 -shared.reserveExactAmount=僅保留所需的資金。在您的交易生效前可能需要支付礦工費和 10 次確認(約20分鐘)。 +shared.reserveExactAmount=僅保留必要的資金。需支付礦工費,約 20 分鐘後您的報價才會上線。 shared.makerTxFee=賣家:{0} shared.takerTxFee=買家:{0} shared.iConfirm=我確認 From c75e3aa4559cd1d233b18dc55ee1415033ccdb6c Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 16 Dec 2024 11:30:08 -0500 Subject: [PATCH 035/371] replace checkbox to reserve necessary funds with slider --- .../desktop/main/offer/MutableOfferView.java | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 9115c64491..7339e5b055 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -71,7 +71,6 @@ import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.control.Button; -import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; @@ -137,7 +136,7 @@ public abstract class MutableOfferView> exten private TextField currencyTextField; private AddressTextField addressTextField; private BalanceTextField balanceTextField; - private CheckBox reserveExactAmountCheckbox; + private ToggleButton reserveExactAmountSlider; private ToggleButton buyerAsTakerWithoutDepositSlider; private FundsTextField totalToPayTextField; private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel, @@ -176,6 +175,8 @@ public abstract class MutableOfferView> exten @Setter private OfferView.OfferActionHandler offerActionHandler; + private int heightAdjustment = -5; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -436,7 +437,7 @@ public abstract class MutableOfferView> exten qrCodeImageView.setVisible(true); balanceTextField.setVisible(true); cancelButton2.setVisible(true); - reserveExactAmountCheckbox.setVisible(true); + reserveExactAmountSlider.setVisible(true); } private void updateOfferElementsStyle() { @@ -958,7 +959,7 @@ public abstract class MutableOfferView> exten } private void addPaymentGroup() { - paymentTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 1, Res.get("offerbook.createOffer")); + paymentTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 1, Res.get("offerbook.createOffer"), heightAdjustment); GridPane.setColumnSpan(paymentTitledGroupBg, 2); HBox paymentGroupBox = new HBox(); @@ -976,7 +977,7 @@ public abstract class MutableOfferView> exten GridPane.setRowIndex(paymentGroupBox, gridRow); GridPane.setColumnSpan(paymentGroupBox, 2); - GridPane.setMargin(paymentGroupBox, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 0)); + GridPane.setMargin(paymentGroupBox, new Insets(Layout.FIRST_ROW_DISTANCE + heightAdjustment, 0, 0, 0)); gridPane.getChildren().add(paymentGroupBox); tradingAccountBoxTuple.first.setMinWidth(800); @@ -1012,7 +1013,7 @@ public abstract class MutableOfferView> exten private void addAmountPriceGroup() { amountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, - Res.get("createOffer.setAmountPrice"), 25); + Res.get("createOffer.setAmountPrice"), 25 + heightAdjustment); GridPane.setColumnSpan(amountTitledGroupBg, 2); addAmountPriceFields(); @@ -1021,7 +1022,7 @@ public abstract class MutableOfferView> exten private void addOptionsGroup() { setDepositTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, - Res.get("shared.advancedOptions"), Layout.COMPACT_GROUP_DISTANCE); + Res.get("shared.advancedOptions"), 25 + heightAdjustment); securityDepositAndFeeBox = new HBox(); securityDepositAndFeeBox.setSpacing(40); @@ -1136,7 +1137,7 @@ public abstract class MutableOfferView> exten private void addFundingGroup() { // don't increase gridRow as we removed button when this gets visible payFundsTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 3, - Res.get("createOffer.fundsBox.title"), 25); + Res.get("createOffer.fundsBox.title"), 20 + heightAdjustment); payFundsTitledGroupBg.getStyleClass().add("last"); GridPane.setColumnSpan(payFundsTitledGroupBg, 2); payFundsTitledGroupBg.setVisible(false); @@ -1144,7 +1145,7 @@ public abstract class MutableOfferView> exten totalToPayTextField = addFundsTextfield(gridPane, gridRow, Res.get("shared.totalsNeeded"), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); totalToPayTextField.setVisible(false); - GridPane.setMargin(totalToPayTextField, new Insets(65, 10, 0, 0)); + GridPane.setMargin(totalToPayTextField, new Insets(60 + heightAdjustment, 10, 0, 0)); qrCodeImageView = new ImageView(); qrCodeImageView.setVisible(false); @@ -1170,15 +1171,15 @@ public abstract class MutableOfferView> exten Res.get("shared.tradeWalletBalance")); balanceTextField.setVisible(false); - reserveExactAmountCheckbox = FormBuilder.addLabelCheckBox(gridPane, ++gridRow, - Res.get("shared.reserveExactAmount")); - - GridPane.setHalignment(reserveExactAmountCheckbox, HPos.LEFT); - - reserveExactAmountCheckbox.setVisible(false); - reserveExactAmountCheckbox.setSelected(preferences.getSplitOfferOutput()); - reserveExactAmountCheckbox.setOnAction(event -> { - boolean selected = reserveExactAmountCheckbox.isSelected(); + + reserveExactAmountSlider = FormBuilder.addSlideToggleButton(gridPane, ++gridRow, Res.get("shared.reserveExactAmount"), heightAdjustment); + GridPane.setHalignment(reserveExactAmountSlider, HPos.LEFT); + GridPane.setMargin(reserveExactAmountSlider, new Insets(-5, 0, -5, 0)); + reserveExactAmountSlider.setPadding(new Insets(0, 0, 0, 0)); + reserveExactAmountSlider.setVisible(false); + reserveExactAmountSlider.setSelected(preferences.getSplitOfferOutput()); + reserveExactAmountSlider.setOnAction(event -> { + boolean selected = reserveExactAmountSlider.isSelected(); if (selected != preferences.getSplitOfferOutput()) { preferences.setSplitOfferOutput(selected); model.dataModel.setReserveExactAmount(selected); @@ -1338,7 +1339,7 @@ public abstract class MutableOfferView> exten firstRowHBox.getChildren().addAll(amountBox, xLabel, percentagePriceBox, resultLabel, volumeBox); GridPane.setColumnSpan(firstRowHBox, 2); GridPane.setRowIndex(firstRowHBox, gridRow); - GridPane.setMargin(firstRowHBox, new Insets(40, 10, 0, 0)); + GridPane.setMargin(firstRowHBox, new Insets(40 + heightAdjustment, 10, 0, 0)); gridPane.getChildren().add(firstRowHBox); } From 544d69827a2929a109acdcde5fb4b34a0d5f54b5 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 17 Dec 2024 09:12:35 -0500 Subject: [PATCH 036/371] show locked symbol for private offers in trade history --- .../main/portfolio/closedtrades/ClosedTradesListItem.java | 2 +- .../main/portfolio/failedtrades/FailedTradesViewModel.java | 2 +- desktop/src/main/java/haveno/desktop/util/DisplayUtils.java | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java index 6c1e30b828..7b6ca92a19 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java @@ -105,7 +105,7 @@ public class ClosedTradesListItem implements FilterableListItem { ? offer.getDirection() : offer.getMirroredDirection(); String currencyCode = tradable.getOffer().getCurrencyCode(); - return DisplayUtils.getDirectionWithCode(direction, currencyCode); + return DisplayUtils.getDirectionWithCode(direction, currencyCode, offer.isPrivateOffer()); } public Date getDate() { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesViewModel.java index eef2ffa444..b6dd35d3a5 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesViewModel.java @@ -68,7 +68,7 @@ class FailedTradesViewModel extends ActivatableWithDataModel Date: Tue, 17 Dec 2024 10:36:14 -0500 Subject: [PATCH 037/371] fix scheduling offers by computing spendable amount from txs --- .../haveno/core/offer/CreateOfferService.java | 1 - .../haveno/core/offer/OpenOfferManager.java | 66 +++++++++++-------- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index 407953b05f..e135bfa123 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -127,7 +127,6 @@ public class CreateOfferService { isPrivateOffer, buyerAsTakerWithoutDeposit); - // verify buyer as taker security deposit boolean isBuyerMaker = offerUtil.isBuyOffer(direction); if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) { diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 86f45566dd..25aebaecb1 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -948,7 +948,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (openOffer.getScheduledTxHashes() != null) { boolean scheduledTxsAvailable = true; for (MoneroTxWallet tx : xmrWalletService.getTxs(openOffer.getScheduledTxHashes())) { - if (!tx.isLocked() && !isOutputsAvailable(tx)) { + if (!tx.isLocked() && !hasSpendableAmount(tx)) { scheduledTxsAvailable = false; break; } @@ -1165,31 +1165,21 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe throw new RuntimeException("Not enough money in Haveno wallet"); } - // get earliest available or pending txs with sufficient incoming amount + // get earliest available or pending txs with sufficient spendable amount BigInteger scheduledAmount = BigInteger.ZERO; Set scheduledTxs = new HashSet(); for (MoneroTxWallet tx : xmrWalletService.getTxs()) { - // skip if no funds available - BigInteger sentToSelfAmount = xmrWalletService.getAmountSentToSelf(tx); // amount sent to self always shows 0, so compute from destinations manually - if (sentToSelfAmount.equals(BigInteger.ZERO) && (tx.getIncomingTransfers() == null || tx.getIncomingTransfers().isEmpty())) continue; - if (!isOutputsAvailable(tx)) continue; + // get spendable amount + BigInteger spendableAmount = getSpendableAmount(tx); + + // skip if no spendable amount or already scheduled + if (spendableAmount.equals(BigInteger.ZERO)) continue; if (isTxScheduledByOtherOffer(openOffers, openOffer, tx.getHash())) continue; - // schedule transaction if funds sent to self, because they are not included in incoming transfers // TODO: fix in libraries? - if (sentToSelfAmount.compareTo(BigInteger.ZERO) > 0) { - scheduledAmount = scheduledAmount.add(sentToSelfAmount); - scheduledTxs.add(tx); - } else if (tx.getIncomingTransfers() != null) { - - // schedule transaction if incoming tranfers to account 0 - for (MoneroIncomingTransfer transfer : tx.getIncomingTransfers()) { - if (transfer.getAccountIndex() == 0) { - scheduledAmount = scheduledAmount.add(transfer.getAmount()); - scheduledTxs.add(tx); - } - } - } + // schedule tx + scheduledAmount = scheduledAmount.add(spendableAmount); + scheduledTxs.add(tx); // break if sufficient funds if (scheduledAmount.compareTo(offerReserveAmount) >= 0) break; @@ -1202,6 +1192,34 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe openOffer.setState(OpenOffer.State.PENDING); } + private BigInteger getSpendableAmount(MoneroTxWallet tx) { + + // compute spendable amount from outputs if confirmed + if (tx.isConfirmed()) { + BigInteger spendableAmount = BigInteger.ZERO; + if (tx.getOutputsWallet() != null) { + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (!output.isSpent() && !output.isFrozen() && output.getAccountIndex() == 0) { + spendableAmount = spendableAmount.add(output.getAmount()); + } + } + } + return spendableAmount; + } + + // funds sent to self always show 0 incoming amount, so compute from destinations manually + // TODO: this excludes change output, so change is missing from spendable amount until confirmed + BigInteger sentToSelfAmount = xmrWalletService.getAmountSentToSelf(tx); + if (sentToSelfAmount.compareTo(BigInteger.ZERO) > 0) return sentToSelfAmount; + + // if not confirmed and not sent to self, return incoming amount + return tx.getIncomingAmount() == null ? BigInteger.ZERO : tx.getIncomingAmount(); + } + + private boolean hasSpendableAmount(MoneroTxWallet tx) { + return getSpendableAmount(tx).compareTo(BigInteger.ZERO) > 0; + } + private BigInteger getScheduledAmount(List openOffers) { BigInteger scheduledAmount = BigInteger.ZERO; for (OpenOffer openOffer : openOffers) { @@ -1233,14 +1251,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return false; } - private boolean isOutputsAvailable(MoneroTxWallet tx) { - if (tx.getOutputsWallet() == null) return false; - for (MoneroOutputWallet output : tx.getOutputsWallet()) { - if (output.isSpent() || output.isFrozen()) return false; - } - return true; - } - private void signAndPostOffer(OpenOffer openOffer, boolean useSavingsWallet, // TODO: remove this? TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { From af3c7059a9100619d4453b04ead6755fb0b1bdfe Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 18 Dec 2024 10:43:12 -0500 Subject: [PATCH 038/371] play chime when buyer can send payment --- core/src/main/java/haveno/core/trade/Trade.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 95b067b28a..e88f70bda9 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -657,6 +657,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ThreadUtils.submitToPool(() -> { if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling(); if (newValue == Trade.Phase.DEPOSITS_PUBLISHED) onDepositsPublished(); + if (newValue == Trade.Phase.DEPOSITS_CONFIRMED) onDepositsConfirmed(); + if (newValue == Trade.Phase.DEPOSITS_UNLOCKED) onDepositsUnlocked(); if (newValue == Trade.Phase.PAYMENT_SENT) onPaymentSent(); if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod(); if (isPaymentReceived()) { @@ -2892,10 +2894,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ThreadUtils.submitToPool(() -> xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages())); } + private void onDepositsConfirmed() { + HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_CONFIRMED, "Trade Deposits Confirmed", "The deposit transactions have confirmed"); + } + + private void onDepositsUnlocked() { + HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_UNLOCKED, "Trade Deposits Unlocked", "The deposit transactions have unlocked"); + } + private void onPaymentSent() { - if (this instanceof SellerTrade) { - HavenoUtils.notificationService.sendTradeNotification(this, Phase.PAYMENT_SENT, "Payment Sent", "The buyer has sent the payment"); // TODO (woodser): use language translation - } + HavenoUtils.notificationService.sendTradeNotification(this, Phase.PAYMENT_SENT, "Payment Sent", "The buyer has sent the payment"); } /////////////////////////////////////////////////////////////////////////////////////////// From 5c79380e638a304b42ceb4a15f34f9eebd9ec43b Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 18 Dec 2024 12:14:16 -0500 Subject: [PATCH 039/371] remove padding from no deposit slider --- .../main/java/haveno/desktop/main/offer/MutableOfferView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 7339e5b055..0bcfa79050 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -1039,6 +1039,7 @@ public abstract class MutableOfferView> exten securityDepositAndFeeBox.getChildren().addAll(getSecurityDepositBox(), tradeFeeFieldsBox); buyerAsTakerWithoutDepositSlider = FormBuilder.addSlideToggleButton(gridPane, ++gridRow, Res.get("createOffer.buyerAsTakerWithoutDeposit")); + buyerAsTakerWithoutDepositSlider.setPadding(new Insets(0, 0, 0, 0)); buyerAsTakerWithoutDepositSlider.setOnAction(event -> { // popup info box From 323d14feb0387e881690c5cda6401447430d2e74 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 18 Dec 2024 11:11:58 -0500 Subject: [PATCH 040/371] bump version to 1.0.15 --- build.gradle | 2 +- common/src/main/java/haveno/common/app/Version.java | 2 +- desktop/package/linux/exchange.haveno.Haveno.metainfo.xml | 2 +- desktop/package/macosx/Info.plist | 4 ++-- seednode/src/main/java/haveno/seednode/SeedNodeMain.java | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index d35c6bd05c..5024c62c97 100644 --- a/build.gradle +++ b/build.gradle @@ -610,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.14-SNAPSHOT' + version = '1.0.15-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 2b4b01b036..a263b65755 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.0.14"; + public static final String VERSION = "1.0.15"; /** * Holds a list of the tagged resource files for optimizing the getData requests. diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index 6805134589..6ff3b23dbc 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -60,6 +60,6 @@ - + diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index 31325683fd..62cc8f887a 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.14 + 1.0.15 CFBundleShortVersionString - 1.0.14 + 1.0.15 CFBundleExecutable Haveno diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index e0f5ad7c3e..03f0cc51a7 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.0.14"; + private static final String VERSION = "1.0.15"; private SeedNode seedNode; private Timer checkConnectionLossTime; From 7e4e9507105c687f63b006d644a7dac72e02b778 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 19 Dec 2024 06:57:49 -0500 Subject: [PATCH 041/371] update flatpak release date --- desktop/package/linux/exchange.haveno.Haveno.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index 6ff3b23dbc..64730a4596 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -60,6 +60,6 @@ - + From a557d90e5d040f6305f9926f10c8a6acdbefa3d5 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 19 Dec 2024 16:15:23 -0500 Subject: [PATCH 042/371] fix password prompt on startup by referencing lock@2x.png --- desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java index a1aee58c4f..b64c5c0d51 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java @@ -200,7 +200,7 @@ public class HavenoAppMain extends HavenoExecutable { // Add an icon to the dialog Stage stage = (Stage) getDialogPane().getScene().getWindow(); - stage.getIcons().add(ImageUtil.getImageByPath("lock.png")); + stage.getIcons().add(ImageUtil.getImageByPath("lock@2x.png")); // Create the password field PasswordField passwordField = new PasswordField(); From 1a51b171a079f36909f41ee4b7e6f7186bf02f44 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 19 Dec 2024 16:21:03 -0500 Subject: [PATCH 043/371] bump version to 1.0.16 --- build.gradle | 2 +- common/src/main/java/haveno/common/app/Version.java | 2 +- desktop/package/linux/exchange.haveno.Haveno.metainfo.xml | 2 +- desktop/package/macosx/Info.plist | 4 ++-- seednode/src/main/java/haveno/seednode/SeedNodeMain.java | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 5024c62c97..617ee2bd7b 100644 --- a/build.gradle +++ b/build.gradle @@ -610,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.15-SNAPSHOT' + version = '1.0.16-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index a263b65755..4bad358ec5 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.0.15"; + public static final String VERSION = "1.0.16"; /** * Holds a list of the tagged resource files for optimizing the getData requests. diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index 64730a4596..e4544f485a 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -60,6 +60,6 @@ - + diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index 62cc8f887a..1cd1baf927 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.15 + 1.0.16 CFBundleShortVersionString - 1.0.15 + 1.0.16 CFBundleExecutable Haveno diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index 03f0cc51a7..75839255e4 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.0.15"; + private static final String VERSION = "1.0.16"; private SeedNode seedNode; private Timer checkConnectionLossTime; From aab4d0207ec43ff3d4c1b6ad221046189592c6cd Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 20 Dec 2024 06:41:42 -0500 Subject: [PATCH 044/371] update links to typescript client and tests --- docs/developer-guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 337d44c615..565ee4886e 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -31,8 +31,8 @@ Follow [instructions](https://github.com/haveno-dex/haveno-ts#run-tests) to run For example, the gRPC function to get offers is implemented by [`GrpcServer`](https://github.com/haveno-dex/haveno/blob/master/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java) > [`GrpcOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java#L104) > [`CoreApi.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/api/CoreApi.java#L128) > [`CoreOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/api/CoreOffersService.java#L126) > [`OfferBookService.getOffers()`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/offer/OfferBookService.java#L193). 5. Build Haveno: `make` 6. Update the gRPC client in haveno-ts: `npm install` -7. Add the corresponding typescript method(s) to [haveno.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/haveno.ts) with clear and concise documentation. -8. Add clean and comprehensive tests to [haveno.test.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/haveno.test.ts), following existing patterns. +7. Add the corresponding typescript method(s) to [HavenoClient.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/HavenoClient.ts) with clear and concise documentation. +8. Add clean and comprehensive tests to [HavenoClient.test.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/HavenoClient.test.ts), following existing patterns. 9. Run the tests with `npm run test -- -t 'my test'` to run tests by name and `npm test` to run all tests together. Ensure all tests pass and there are no exception stacktraces in the terminals of Alice, Bob, or the arbitrator. 10. Open pull requests to the haveno and haveno-ts projects for the backend and frontend implementations. From 34e0c4b71fff7ff4ff038b9493e85261bfd78785 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 20 Dec 2024 09:36:25 -0500 Subject: [PATCH 045/371] remove bitcoin donation address from readme --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 46fb3528ca..5c439f9a55 100644 --- a/README.md +++ b/README.md @@ -73,18 +73,9 @@ To incentivize development and reward contributors, we adopt a simple bounty sys To bring Haveno to life, we need resources. If you have the possibility, please consider [becoming a sponsor](https://haveno.exchange/sponsors/) or donating to the project: -### Monero -

    Donate Monero
    42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F

    If you are using a wallet that supports OpenAlias (like the 'official' CLI and GUI wallets), you can simply put `fund@haveno.exchange` as the "receiver" address. - -### Bitcoin - -

    - Donate Bitcoin
    - 1AKq3CE1yBAnxGmHXbNFfNYStcByNDc5gQ -

    From 7240b5f2223f7596fc58d2572c0deeeb889fc9c0 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 21 Dec 2024 06:12:21 -0500 Subject: [PATCH 046/371] document changing download url for network deployment --- docs/create-mainnet.md | 4 ++++ docs/deployment-guide.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/create-mainnet.md b/docs/create-mainnet.md index 38c211d4cd..4b220e162e 100644 --- a/docs/create-mainnet.md +++ b/docs/create-mainnet.md @@ -118,6 +118,10 @@ The price node is separated from Haveno and is run as a standalone service. To d After the price node is built and deployed, add the price node to `DEFAULT_NODES` in [ProvidersRepository.java](https://github.com/haveno-dex/haveno/blob/3cdd88b56915c7f8afd4f1a39e6c1197c2665d63/core/src/main/java/haveno/core/provider/ProvidersRepository.java#L50). +### Update the download URL + +Change every instance of `https://haveno.exchange/downloads` to your download URL. For example, `https://havenoexample.com/downloads`. + ## Review all local changes For comparison, placeholders to run on mainnet are marked [here on this branch](https://github.com/haveno-dex/haveno/tree/mainnet_placeholders). diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index 2f7590d0ec..e9e013c166 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -243,6 +243,10 @@ Set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `true` for the arbitrator to assig Otherwise set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `false` and set the XMR address in `getGlobalTradeFeeAddress()` to collect all trade fees to a single address (e.g. a multisig wallet shared among network administrators). +## Update the download URL + +Change every instance of `https://haveno.exchange/downloads` to your download URL. For example, `https://havenoexample.com/downloads`. + ## Start users for testing Start user1 on Monero's mainnet using `make user1-desktop-mainnet` or Monero's stagenet using `make user1-desktop-stagenet`. From 389c5dddac40aa36a53821081426a5701aef80a7 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 20 Dec 2024 05:23:12 -0500 Subject: [PATCH 047/371] fix no deposit filter applied to sell tab --- .../desktop/main/offer/offerbook/OfferBookViewModel.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index 55fb0946c8..d0c9317274 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -579,7 +579,10 @@ abstract class OfferBookViewModel extends ActivatableViewModel { getCurrencyAndMethodPredicate(direction, selectedTradeCurrency).and(getOffersMatchingMyAccountsPredicate()) : getCurrencyAndMethodPredicate(direction, selectedTradeCurrency); - predicate = predicate.and(offerBookListItem -> offerBookListItem.getOffer().isPrivateOffer() == showPrivateOffers); + // filter private offers + if (direction == OfferDirection.BUY) { + predicate = predicate.and(offerBookListItem -> offerBookListItem.getOffer().isPrivateOffer() == showPrivateOffers); + } if (!filterText.isEmpty()) { From c5ef60ce5c43a8446cc8771678995ef2fb64483e Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 20 Dec 2024 11:45:42 -0500 Subject: [PATCH 048/371] fix ui to set security deposit pct w/o deposit --- .../main/offer/MutableOfferViewModel.java | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index 8ededde35c..d92a0322d2 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -56,6 +56,7 @@ import haveno.core.util.coin.CoinUtil; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; import haveno.core.util.validation.InputValidator; +import haveno.core.util.validation.InputValidator.ValidationResult; import haveno.core.util.validation.MonetaryValidator; import haveno.core.xmr.wallet.Restrictions; import haveno.desktop.Navigation; @@ -490,6 +491,8 @@ public abstract class MutableOfferViewModel ext xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); if (amount.get() != null) amountValidationResult.set(isXmrInputValid(amount.get())); updateSecurityDeposit(); + setSecurityDepositToModel(); + onFocusOutSecurityDepositTextField(true, false); // refresh security deposit field applyMakerFee(); dataModel.calculateTotalToPay(); updateButtonDisableState(); @@ -769,7 +772,8 @@ public abstract class MutableOfferViewModel ext } } } - // We want to trigger a recalculation of the volume + + // trigger recalculation of the volume UserThread.execute(() -> { onFocusOutVolumeTextField(true, false); onFocusOutMinAmountTextField(true, false); @@ -815,6 +819,11 @@ public abstract class MutableOfferViewModel ext } maybeShowMakeOfferToUnsignedAccountWarning(); + + // trigger recalculation of the security deposit + UserThread.execute(() -> { + onFocusOutSecurityDepositTextField(true, false); + }); } } @@ -944,11 +953,16 @@ public abstract class MutableOfferViewModel ext if (marketPriceMargin.get() == null && amount.get() != null && volume.get() != null) { updateMarketPriceToManual(); } + + // trigger recalculation of security deposit + UserThread.execute(() -> { + onFocusOutSecurityDepositTextField(true, false); + }); } } void onFocusOutSecurityDepositTextField(boolean oldValue, boolean newValue) { - if (oldValue && !newValue) { + if (oldValue && !newValue && !isMinSecurityDeposit.get()) { InputValidator.ValidationResult result = securityDepositValidator.validate(securityDeposit.get()); securityDepositValidationResult.set(result); if (result.isValid) { @@ -1040,6 +1054,7 @@ public abstract class MutableOfferViewModel ext public String getSecurityDepositLabel() { return dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer() ? Res.get("createOffer.myDeposit") : + dataModel.isMinSecurityDeposit() ? Res.get("createOffer.minSecurityDepositUsed") : Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? Res.get("createOffer.setDepositForBothTraders") : dataModel.isBuyOffer() ? Res.get("createOffer.setDepositAsBuyer") : Res.get("createOffer.setDeposit"); } @@ -1211,7 +1226,7 @@ public abstract class MutableOfferViewModel ext } private void setSecurityDepositToModel() { - if (!(dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer()) && securityDeposit.get() != null && !securityDeposit.get().isEmpty()) { + if (securityDeposit.get() != null && !securityDeposit.get().isEmpty() && !isMinSecurityDeposit.get()) { dataModel.setSecurityDepositPct(ParsingUtils.parsePercentStringToDouble(securityDeposit.get())); } else { dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositAsPercent()); @@ -1282,11 +1297,11 @@ public abstract class MutableOfferViewModel ext private void updateSecurityDeposit() { isMinSecurityDeposit.set(dataModel.isMinSecurityDeposit()); + securityDepositLabel.set(getSecurityDepositLabel()); if (dataModel.isMinSecurityDeposit()) { - securityDepositLabel.set(Res.get("createOffer.minSecurityDepositUsed")); securityDeposit.set(HavenoUtils.formatXmr(Restrictions.getMinSecurityDeposit())); + securityDepositValidationResult.set(new ValidationResult(true)); } else { - securityDepositLabel.set(getSecurityDepositLabel()); boolean hasBuyerAsTakerWithoutDeposit = dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer(); securityDeposit.set(FormattingUtils.formatToPercent(hasBuyerAsTakerWithoutDeposit ? Restrictions.getDefaultSecurityDepositAsPercent() : // use default percent if no deposit from buyer From 542441d9d24a976fc1139b2686cf4ccbc3bedad1 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 20 Dec 2024 13:44:00 -0500 Subject: [PATCH 049/371] increase contrast of filter toggles and remove bottom highlight --- .../haveno/desktop/main/offer/offerbook/OfferBookView.java | 2 +- desktop/src/main/java/haveno/desktop/theme-dark.css | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index 9b3571458f..b37aa340bb 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -185,7 +185,7 @@ abstract public class OfferBookView Date: Sat, 21 Dec 2024 08:06:56 -0500 Subject: [PATCH 050/371] allow scheduling funds from split output tx --- .../haveno/core/offer/OpenOfferManager.java | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 25aebaecb1..b7cb7255f8 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -115,7 +115,6 @@ import lombok.Getter; import monero.common.MoneroRpcConnection; import monero.daemon.model.MoneroKeyImageSpentStatus; import monero.daemon.model.MoneroTx; -import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroTransferQuery; @@ -1159,23 +1158,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private void scheduleWithEarliestTxs(List openOffers, OpenOffer openOffer) { - // check for sufficient balance - scheduled offers amount - BigInteger offerReserveAmount = openOffer.getOffer().getAmountNeeded(); - if (xmrWalletService.getBalance().subtract(getScheduledAmount(openOffers)).compareTo(offerReserveAmount) < 0) { - throw new RuntimeException("Not enough money in Haveno wallet"); - } - // get earliest available or pending txs with sufficient spendable amount + BigInteger offerReserveAmount = openOffer.getOffer().getAmountNeeded(); BigInteger scheduledAmount = BigInteger.ZERO; Set scheduledTxs = new HashSet(); for (MoneroTxWallet tx : xmrWalletService.getTxs()) { - // get spendable amount - BigInteger spendableAmount = getSpendableAmount(tx); + // get unscheduled spendable amount + BigInteger spendableAmount = getUnscheduledSpendableAmount(tx, openOffers); - // skip if no spendable amount or already scheduled + // skip if no spendable amount if (spendableAmount.equals(BigInteger.ZERO)) continue; - if (isTxScheduledByOtherOffer(openOffers, openOffer, tx.getHash())) continue; // schedule tx scheduledAmount = scheduledAmount.add(spendableAmount); @@ -1184,7 +1177,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // break if sufficient funds if (scheduledAmount.compareTo(offerReserveAmount) >= 0) break; } - if (scheduledAmount.compareTo(offerReserveAmount) < 0) throw new RuntimeException("Not enough funds to schedule offer"); + if (scheduledAmount.compareTo(offerReserveAmount) < 0) throw new RuntimeException("Not enough funds to create offer"); // schedule txs openOffer.setScheduledTxHashes(scheduledTxs.stream().map(tx -> tx.getHash()).collect(Collectors.toList())); @@ -1192,6 +1185,30 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe openOffer.setState(OpenOffer.State.PENDING); } + private BigInteger getUnscheduledSpendableAmount(MoneroTxWallet tx, List openOffers) { + if (isScheduledWithUnknownAmount(tx, openOffers)) return BigInteger.ZERO; + return getSpendableAmount(tx).subtract(getSplitAmount(tx, openOffers)).max(BigInteger.ZERO); + } + + private boolean isScheduledWithUnknownAmount(MoneroTxWallet tx, List openOffers) { + for (OpenOffer openOffer : openOffers) { + if (openOffer.getScheduledTxHashes() == null) continue; + if (openOffer.getScheduledTxHashes().contains(tx.getHash()) && !tx.getHash().equals(openOffer.getSplitOutputTxHash())) { + return true; + } + } + return false; + } + + private BigInteger getSplitAmount(MoneroTxWallet tx, List openOffers) { + for (OpenOffer openOffer : openOffers) { + if (openOffer.getSplitOutputTxHash() == null) continue; + if (!openOffer.getSplitOutputTxHash().equals(tx.getHash())) continue; + return openOffer.getOffer().getAmountNeeded(); + } + return BigInteger.ZERO; + } + private BigInteger getSpendableAmount(MoneroTxWallet tx) { // compute spendable amount from outputs if confirmed @@ -1220,23 +1237,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return getSpendableAmount(tx).compareTo(BigInteger.ZERO) > 0; } - private BigInteger getScheduledAmount(List openOffers) { - BigInteger scheduledAmount = BigInteger.ZERO; - for (OpenOffer openOffer : openOffers) { - if (openOffer.getState() != OpenOffer.State.PENDING) continue; - if (openOffer.getScheduledTxHashes() == null) continue; - List fundingTxs = xmrWalletService.getTxs(openOffer.getScheduledTxHashes()); - for (MoneroTxWallet fundingTx : fundingTxs) { - if (fundingTx.getIncomingTransfers() != null) { - for (MoneroIncomingTransfer transfer : fundingTx.getIncomingTransfers()) { - if (transfer.getAccountIndex() == 0) scheduledAmount = scheduledAmount.add(transfer.getAmount()); - } - } - } - } - return scheduledAmount; - } - private boolean isTxScheduledByOtherOffer(List openOffers, OpenOffer openOffer, String txHash) { for (OpenOffer otherOffer : openOffers) { if (otherOffer == openOffer) continue; From 5444d96832dd18281e134c562e2ce53682f6aafe Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 21 Dec 2024 08:40:48 -0500 Subject: [PATCH 051/371] reverse order of funds > confirmations and memo columns --- .../main/funds/transactions/TransactionsView.fxml | 2 +- .../main/funds/transactions/TransactionsView.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml index 7c5da97808..24d8f121d7 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml @@ -38,8 +38,8 @@ + -
    diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java index 9544f50748..b882782212 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java @@ -70,7 +70,7 @@ public class TransactionsView extends ActivatableView { @FXML TableView tableView; @FXML - TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, memoColumn, confidenceColumn, revertTxColumn; + TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, confidenceColumn, memoColumn, revertTxColumn; @FXML Label numItems; @FXML @@ -133,8 +133,8 @@ public class TransactionsView extends ActivatableView { transactionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txId", Res.getBaseCurrencyCode()))); amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode()))); txFeeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txFee", Res.getBaseCurrencyCode()))); - memoColumn.setGraphic(new AutoTooltipLabel(Res.get("funds.tx.memo"))); confidenceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations", Res.getBaseCurrencyCode()))); + memoColumn.setGraphic(new AutoTooltipLabel(Res.get("funds.tx.memo"))); revertTxColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.revert", Res.getBaseCurrencyCode()))); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); @@ -146,8 +146,8 @@ public class TransactionsView extends ActivatableView { setTransactionColumnCellFactory(); setAmountColumnCellFactory(); setTxFeeColumnCellFactory(); - setMemoColumnCellFactory(); setConfidenceColumnCellFactory(); + setMemoColumnCellFactory(); setRevertTxColumnCellFactory(); dateColumn.setComparator(Comparator.comparing(TransactionsListItem::getDate)); @@ -221,8 +221,8 @@ public class TransactionsView extends ActivatableView { columns[3] = item.getTxId(); columns[4] = item.getAmountStr(); columns[5] = item.getTxFeeStr(); - columns[6] = item.getMemo() == null ? "" : item.getMemo(); - columns[7] = String.valueOf(item.getNumConfirmations()); + columns[6] = String.valueOf(item.getNumConfirmations()); + columns[7] = item.getMemo() == null ? "" : item.getMemo(); return columns; }; From 42ede83ca208e9f4786916d2eb1e07a53149753e Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 21 Dec 2024 08:43:14 -0500 Subject: [PATCH 052/371] 'show all' resets default currency to create new offer --- .../desktop/main/offer/offerbook/OfferBookViewModel.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index d0c9317274..967e29b6e3 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -260,7 +260,10 @@ abstract class OfferBookViewModel extends ActivatableViewModel { showAllTradeCurrenciesProperty.set(showAllEntry); if (isEditEntry(code)) navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class); - else if (!showAllEntry) { + else if (showAllEntry) { + this.selectedTradeCurrency = getDefaultTradeCurrency(); + tradeCurrencyCode.set(selectedTradeCurrency.getCode()); + } else { this.selectedTradeCurrency = tradeCurrency; tradeCurrencyCode.set(code); } From fdee0440235133612148677f82789d3ca4eaa9b9 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 21 Dec 2024 08:44:54 -0500 Subject: [PATCH 053/371] fix occasional miscolored buttons to remove or edit my offer --- .../main/offer/offerbook/OfferBookView.java | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index b37aa340bb..3bcb3cb03f 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -1069,39 +1069,39 @@ abstract public class OfferBookView() { OfferFilterService.Result canTakeOfferResult = null; - final ImageView iconView = new ImageView(); - final AutoTooltipButton button = new AutoTooltipButton(); - - { - button.setGraphic(iconView); - button.setGraphicTextGap(10); - button.setPrefWidth(10000); - } - - final ImageView iconView2 = new ImageView(); - final AutoTooltipButton button2 = new AutoTooltipButton(); - - { - button2.setGraphic(iconView2); - button2.setGraphicTextGap(10); - button2.setPrefWidth(10000); - } - - final HBox hbox = new HBox(); - - { - hbox.setSpacing(8); - hbox.setAlignment(Pos.CENTER); - hbox.getChildren().add(button); - hbox.getChildren().add(button2); - HBox.setHgrow(button, Priority.ALWAYS); - HBox.setHgrow(button2, Priority.ALWAYS); - } - @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); + final ImageView iconView = new ImageView(); + final AutoTooltipButton button = new AutoTooltipButton(); + + { + button.setGraphic(iconView); + button.setGraphicTextGap(10); + button.setPrefWidth(10000); + } + + final ImageView iconView2 = new ImageView(); + final AutoTooltipButton button2 = new AutoTooltipButton(); + + { + button2.setGraphic(iconView2); + button2.setGraphicTextGap(10); + button2.setPrefWidth(10000); + } + + final HBox hbox = new HBox(); + + { + hbox.setSpacing(8); + hbox.setAlignment(Pos.CENTER); + hbox.getChildren().add(button); + hbox.getChildren().add(button2); + HBox.setHgrow(button, Priority.ALWAYS); + HBox.setHgrow(button2, Priority.ALWAYS); + } + TableRow tableRow = getTableRow(); if (item != null && !empty) { Offer offer = item.getOffer(); From f053a274a4335c4b0d4fb0d99a07cf423c18f902 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 21 Dec 2024 09:18:34 -0500 Subject: [PATCH 054/371] bump version to 1.0.17 --- build.gradle | 2 +- common/src/main/java/haveno/common/app/Version.java | 2 +- desktop/package/linux/exchange.haveno.Haveno.metainfo.xml | 2 +- desktop/package/macosx/Info.plist | 4 ++-- docs/deployment-guide.md | 2 +- seednode/src/main/java/haveno/seednode/SeedNodeMain.java | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 617ee2bd7b..67a6a60fab 100644 --- a/build.gradle +++ b/build.gradle @@ -610,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.16-SNAPSHOT' + version = '1.0.17-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 4bad358ec5..8f2c2e135e 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.0.16"; + public static final String VERSION = "1.0.17"; /** * Holds a list of the tagged resource files for optimizing the getData requests. diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index e4544f485a..6e4605f4e5 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -60,6 +60,6 @@ - + diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index 1cd1baf927..88fa15378e 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.16 + 1.0.17 CFBundleShortVersionString - 1.0.16 + 1.0.17 CFBundleExecutable Haveno diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index e9e013c166..e4496fcc37 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -270,7 +270,7 @@ Then follow these instructions: https://github.com/haveno-dex/haveno/blob/master Set the mandatory minimum version for trading (optional) -If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.16) in the field labeled "Min. version required for trading". +If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.17) in the field labeled "Min. version required for trading". Send update alert diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index 75839255e4..7482cdbc7a 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.0.16"; + private static final String VERSION = "1.0.17"; private SeedNode seedNode; private Timer checkConnectionLossTime; From adcf158a904b7f36e2ddbeba9bf13d2c4ec7989c Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 23 Dec 2024 06:28:31 -0500 Subject: [PATCH 055/371] show security deposit from trade amount in take offer and trade details --- .../desktop/main/overlays/windows/OfferDetailsWindow.java | 4 ++-- .../desktop/main/overlays/windows/TradeDetailsWindow.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index 7825c7a610..adf841a147 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -368,11 +368,11 @@ public class OfferDetailsWindow extends Overlay { DisplayUtils.formatDateTime(offer.getDate())); String value = Res.getWithColAndCap("shared.buyer") + " " + - HavenoUtils.formatXmr(offer.getOfferPayload().getMaxBuyerSecurityDeposit(), true) + + HavenoUtils.formatXmr(takeOfferHandlerOptional.isPresent() ? offer.getOfferPayload().getBuyerSecurityDepositForTradeAmount(tradeAmount) : offer.getOfferPayload().getMaxBuyerSecurityDeposit(), true) + " / " + Res.getWithColAndCap("shared.seller") + " " + - HavenoUtils.formatXmr(offer.getOfferPayload().getMaxSellerSecurityDeposit(), true); + HavenoUtils.formatXmr(takeOfferHandlerOptional.isPresent() ? offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(tradeAmount) : offer.getOfferPayload().getMaxSellerSecurityDeposit(), true); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), value); if (reservedAmount != null) { diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java index 0484305792..0e3397ca54 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java @@ -201,11 +201,11 @@ public class TradeDetailsWindow extends Overlay { DisplayUtils.formatDateTime(trade.getDate())); String securityDeposit = Res.getWithColAndCap("shared.buyer") + " " + - HavenoUtils.formatXmr(offer.getMaxBuyerSecurityDeposit(), true) + + HavenoUtils.formatXmr(trade.getBuyerSecurityDepositBeforeMiningFee(), true) + " / " + Res.getWithColAndCap("shared.seller") + " " + - HavenoUtils.formatXmr(offer.getMaxSellerSecurityDeposit(), true); + HavenoUtils.formatXmr(trade.getSellerSecurityDepositBeforeMiningFee(), true); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); NodeAddress arbitratorNodeAddress = trade.getArbitratorNodeAddress(); From ed87b36a763195f97471af327af4f3f448921da8 Mon Sep 17 00:00:00 2001 From: phytohydra <144396848+phytohydra@users.noreply.github.com> Date: Thu, 26 Dec 2024 08:56:53 +0000 Subject: [PATCH 056/371] Add Haveno version to password dialog --- .../src/main/java/haveno/desktop/app/HavenoAppMain.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java index b64c5c0d51..0656476a01 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java @@ -29,6 +29,7 @@ import haveno.desktop.setup.DesktopPersistedDataHost; import haveno.desktop.util.ImageUtil; import javafx.application.Application; import javafx.application.Platform; +import javafx.geometry.Pos; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; import javafx.scene.control.Dialog; @@ -210,9 +211,13 @@ public class HavenoAppMain extends HavenoExecutable { Label errorMessageField = new Label(errorMessage); errorMessageField.setTextFill(Color.color(1, 0, 0)); + // Create the version field + Label versionField = new Label(Version.VERSION); + // Set the dialog content VBox vbox = new VBox(10); - vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), passwordField, errorMessageField); + vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), versionField, passwordField, errorMessageField); + vbox.setAlignment(Pos.TOP_CENTER); getDialogPane().setContent(vbox); // Add OK and Cancel buttons From 018ac61054a9f286de24c1407ab57b96458719dc Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 26 Dec 2024 09:07:02 -0500 Subject: [PATCH 057/371] show reserved balance for offer funding subaddresses and reset if unused --- .../tasks/MakerReserveOfferFunds.java | 14 ++++++++- .../core/xmr/wallet/XmrWalletService.java | 31 +++++++++++++------ .../main/funds/deposit/DepositListItem.java | 2 +- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index 4b44a8102d..a33b9b836d 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -33,6 +33,7 @@ import haveno.core.xmr.model.XmrAddressEntry; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; import monero.daemon.model.MoneroOutput; +import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroTxWallet; @Slf4j @@ -62,7 +63,6 @@ public class MakerReserveOfferFunds extends Task { model.getXmrWalletService().getXmrConnectionService().verifyConnection(); // create reserve tx - MoneroTxWallet reserveTx = null; synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { // reset protocol timeout @@ -79,6 +79,7 @@ public class MakerReserveOfferFunds extends Task { Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex(); // attempt creating reserve tx + MoneroTxWallet reserveTx = null; try { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { @@ -121,6 +122,17 @@ public class MakerReserveOfferFunds extends Task { openOffer.setReserveTxHex(reserveTx.getFullHex()); openOffer.setReserveTxKey(reserveTx.getKey()); offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); + + // reset offer funding address entry if unused + List inputs = model.getXmrWalletService().getOutputs(reservedKeyImages); + boolean usesFundingEntry = false; + for (MoneroOutputWallet input : inputs) { + if (input.getAccountIndex() == 0 && input.getSubaddressIndex() == fundingEntry.getSubaddressIndex()) { + usesFundingEntry = true; + break; + } + } + if (!usesFundingEntry) model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); } complete(); } catch (Throwable t) { diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 79e9248c64..ccd8d491ce 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -578,15 +578,6 @@ public class XmrWalletService extends XmrWalletBase { } } - public BigInteger getOutputsAmount(Collection keyImages) { - BigInteger sum = BigInteger.ZERO; - for (String keyImage : keyImages) { - List outputs = getOutputs(new MoneroOutputQuery().setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage))); - if (!outputs.isEmpty()) sum = sum.add(outputs.get(0).getAmount()); - } - return sum; - } - private List getSubaddressesWithExactInput(BigInteger amount) { // fetch unspent, unfrozen, unlocked outputs @@ -1125,6 +1116,15 @@ public class XmrWalletService extends XmrWalletBase { return subaddress == null ? BigInteger.ZERO : subaddress.getBalance(); } + public BigInteger getBalanceForSubaddress(int subaddressIndex, boolean includeFrozen) { + return getBalanceForSubaddress(subaddressIndex).add(includeFrozen ? getFrozenBalanceForSubaddress(subaddressIndex) : BigInteger.ZERO); + } + + public BigInteger getFrozenBalanceForSubaddress(int subaddressIndex) { + List outputs = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setAccountIndex(0).setSubaddressIndex(subaddressIndex)); + return outputs.stream().map(output -> output.getAmount()).reduce(BigInteger.ZERO, BigInteger::add); + } + public BigInteger getAvailableBalanceForSubaddress(int subaddressIndex) { MoneroSubaddress subaddress = getSubaddress(subaddressIndex); return subaddress == null ? BigInteger.ZERO : subaddress.getUnlockedBalance(); @@ -1250,6 +1250,19 @@ public class XmrWalletService extends XmrWalletBase { return filteredOutputs; } + public List getOutputs(Collection keyImages) { + List outputs = new ArrayList(); + for (String keyImage : keyImages) { + List outputList = getOutputs(new MoneroOutputQuery().setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage))); + if (!outputList.isEmpty()) outputs.add(outputList.get(0)); + } + return outputs; + } + + public BigInteger getOutputsAmount(Collection keyImages) { + return getOutputs(keyImages).stream().map(output -> output.getAmount()).reduce(BigInteger.ZERO, BigInteger::add); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Util /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java index 180b883d61..9e21f19b5f 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java @@ -60,7 +60,7 @@ class DepositListItem { this.xmrWalletService = xmrWalletService; this.addressEntry = addressEntry; - balanceAsBI = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); + balanceAsBI = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex(), true); balance.set(HavenoUtils.formatXmr(balanceAsBI)); updateUsage(addressEntry.getSubaddressIndex()); From cccd9cf094fe5bb8406dfa1358246de6de34ac26 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 23 Dec 2024 08:35:39 -0500 Subject: [PATCH 058/371] fix null wallet on error handling --- .../main/java/haveno/core/trade/Trade.java | 11 ++------ .../haveno/core/xmr/wallet/XmrWalletBase.java | 16 ++++++++++- .../core/xmr/wallet/XmrWalletService.java | 27 +++++++++---------- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index e88f70bda9..c5698a61c3 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -852,14 +852,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) { - if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) { - onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread - return true; - } - return false; - } - public boolean isIdling() { return this instanceof ArbitratorTrade && isDepositsConfirmed() && walletExists() && pollNormalStartTimeMs == null; // arbitrator idles trade after deposits confirm unless overriden } @@ -2386,7 +2378,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return tradeVolumeProperty; } - private void onConnectionChanged(MoneroRpcConnection connection) { + @Override + protected void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { // use current connection diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java index 81eada3295..06f2e17112 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java @@ -17,6 +17,7 @@ import javafx.beans.property.LongProperty; import javafx.beans.property.SimpleLongProperty; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroRpcConnection; import monero.common.TaskLooper; import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; @@ -24,7 +25,7 @@ import monero.wallet.MoneroWalletFull; import monero.wallet.model.MoneroWalletListener; @Slf4j -public class XmrWalletBase { +public abstract class XmrWalletBase { // constants public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 120; @@ -137,6 +138,19 @@ public class XmrWalletBase { } } + public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) { + if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) { + onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread + return true; + } + return false; + } + + protected abstract void onConnectionChanged(MoneroRpcConnection connection); + + + // ------------------------------ PRIVATE HELPERS ------------------------- + private void updateSyncProgress(long height, long targetHeight) { resetSyncProgressTimeout(); UserThread.execute(() -> { diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index ccd8d491ce..fe085de1bb 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1334,7 +1334,7 @@ public class XmrWalletService extends XmrWalletBase { maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); } - private void maybeInitMainWallet(boolean sync, int numAttempts) { + private void maybeInitMainWallet(boolean sync, int numSyncAttempts) { ThreadUtils.execute(() -> { try { doMaybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); @@ -1346,7 +1346,7 @@ public class XmrWalletService extends XmrWalletBase { }, THREAD_ID); } - private void doMaybeInitMainWallet(boolean sync, int numAttempts) { + private void doMaybeInitMainWallet(boolean sync, int numSyncAttempts) { synchronized (walletLock) { if (isShutDownStarted) return; @@ -1374,7 +1374,7 @@ public class XmrWalletService extends XmrWalletBase { // sync main wallet if applicable // TODO: error handling and re-initialization is jenky, refactor - if (sync && numAttempts > 0) { + if (sync && numSyncAttempts > 0) { try { // switch connection if disconnected @@ -1393,7 +1393,7 @@ public class XmrWalletService extends XmrWalletBase { log.warn("Error syncing wallet with progress on startup: " + e.getMessage()); forceCloseMainWallet(); requestSwitchToNextBestConnection(sourceConnection); - maybeInitMainWallet(true, numAttempts - 1); // re-initialize wallet and sync again + maybeInitMainWallet(true, numSyncAttempts - 1); // re-initialize wallet and sync again return; } log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); @@ -1428,8 +1428,8 @@ public class XmrWalletService extends XmrWalletBase { } catch (Exception e) { if (isClosingWallet || isShutDownStarted || HavenoUtils.havenoSetup.getWalletInitialized().get()) return; // ignore if wallet closing, shut down started, or app already initialized log.warn("Error initially syncing main wallet: {}", e.getMessage()); - if (numAttempts <= 1) { - log.warn("Failed to sync main wallet. Opening app without syncing", numAttempts); + if (numSyncAttempts <= 1) { + log.warn("Failed to sync main wallet. Opening app without syncing", numSyncAttempts); HavenoUtils.havenoSetup.getWalletInitialized().set(true); saveMainWallet(false); @@ -1440,7 +1440,7 @@ public class XmrWalletService extends XmrWalletBase { } else { log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000); UserThread.runAfter(() -> { - maybeInitMainWallet(true, numAttempts - 1); + maybeInitMainWallet(true, numSyncAttempts - 1); }, xmrConnectionService.getRefreshPeriodMs() / 1000); } } @@ -1754,7 +1754,8 @@ public class XmrWalletService extends XmrWalletBase { return MONERO_WALLET_RPC_MANAGER.startInstance(cmd); } - private void onConnectionChanged(MoneroRpcConnection connection) { + @Override + protected void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { // use current connection @@ -1858,13 +1859,13 @@ public class XmrWalletService extends XmrWalletBase { log.warn("Force restarting main wallet"); if (isClosingWallet) return; forceCloseMainWallet(); - maybeInitMainWallet(true); + doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); } public void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) { if (HavenoUtils.isUnresponsive(e)) forceCloseMainWallet(); // wallet can be stuck a while - if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection); - getWallet(); // re-open wallet + requestSwitchToNextBestConnection(sourceConnection); + if (wallet == null) doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); } private void startPolling() { @@ -2033,10 +2034,6 @@ public class XmrWalletService extends XmrWalletBase { return requestSwitchToNextBestConnection(null); } - public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) { - return xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection); - } - private void onNewBlock(long height) { UserThread.execute(() -> { walletHeight.set(height); From fc1388d2f4d4ce3d95080e43085ee8b4d4178685 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 27 Dec 2024 11:57:47 -0500 Subject: [PATCH 059/371] fix npe accessing funding address entry from api --- .../placeoffer/tasks/MakerReserveOfferFunds.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index a33b9b836d..0d9271a41e 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -124,15 +124,17 @@ public class MakerReserveOfferFunds extends Task { offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); // reset offer funding address entry if unused - List inputs = model.getXmrWalletService().getOutputs(reservedKeyImages); - boolean usesFundingEntry = false; - for (MoneroOutputWallet input : inputs) { - if (input.getAccountIndex() == 0 && input.getSubaddressIndex() == fundingEntry.getSubaddressIndex()) { - usesFundingEntry = true; - break; + if (fundingEntry != null) { + List inputs = model.getXmrWalletService().getOutputs(reservedKeyImages); + boolean usesFundingEntry = false; + for (MoneroOutputWallet input : inputs) { + if (input.getAccountIndex() == 0 && input.getSubaddressIndex() == fundingEntry.getSubaddressIndex()) { + usesFundingEntry = true; + break; + } } + if (!usesFundingEntry) model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); } - if (!usesFundingEntry) model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); } complete(); } catch (Throwable t) { From 6a798312fef2b8f8e622ad4b92b4caa4de4c354a Mon Sep 17 00:00:00 2001 From: phytohydra <144396848+phytohydra@users.noreply.github.com> Date: Fri, 27 Dec 2024 16:56:44 -0800 Subject: [PATCH 060/371] Add version number to splash screen, update version in pw dialog to have a leading "v" --- desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java | 4 ++-- desktop/src/main/java/haveno/desktop/main/MainView.java | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java index 0656476a01..99cd7a17d6 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java @@ -212,8 +212,8 @@ public class HavenoAppMain extends HavenoExecutable { errorMessageField.setTextFill(Color.color(1, 0, 0)); // Create the version field - Label versionField = new Label(Version.VERSION); - + Label versionField = new Label("v" + Version.VERSION); + // Set the dialog content VBox vbox = new VBox(10); vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), versionField, passwordField, errorMessageField); diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index 9bf5f378e7..7d170c873b 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -20,6 +20,7 @@ package haveno.desktop.main; import com.google.inject.Inject; import com.jfoenix.controls.JFXBadge; import com.jfoenix.controls.JFXComboBox; +import haveno.common.app.Version; import haveno.common.HavenoException; import haveno.common.Timer; import haveno.common.UserThread; @@ -510,6 +511,8 @@ public class MainView extends InitializableView { ImageView logo = new ImageView(); logo.setId(Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_MAINNET ? "image-splash-logo" : "image-splash-testnet-logo"); + Label versionLabel = new Label("v" + Version.VERSION); + // createBitcoinInfoBox xmrSplashInfo = new AutoTooltipLabel(); xmrSplashInfo.textProperty().bind(model.getXmrInfo()); @@ -621,7 +624,7 @@ public class MainView extends InitializableView { splashP2PNetworkBox.setPrefHeight(40); splashP2PNetworkBox.getChildren().addAll(splashP2PNetworkLabel, splashP2PNetworkBusyAnimation, splashP2PNetworkIcon, showTorNetworkSettingsButton); - vBox.getChildren().addAll(logo, blockchainSyncBox, xmrSyncIndicator, splashP2PNetworkBox); + vBox.getChildren().addAll(logo, versionLabel, blockchainSyncBox, xmrSyncIndicator, splashP2PNetworkBox); return vBox; } From 2dc7405f8225195279ea3ddd9a85c14512bcdeb7 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 28 Dec 2024 08:31:30 -0500 Subject: [PATCH 061/371] log connection read timeouts at info level --- .../network/p2p/network/Connection.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/p2p/src/main/java/haveno/network/p2p/network/Connection.java b/p2p/src/main/java/haveno/network/p2p/network/Connection.java index 8165ecd0b3..79df171470 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/Connection.java +++ b/p2p/src/main/java/haveno/network/p2p/network/Connection.java @@ -178,6 +178,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { private static int numThrottledInvalidRequestReports = 0; private static long lastLoggedWarningTs = 0; private static int numThrottledWarnings = 0; + private static long lastLoggedInfoTs = 0; + private static int numThrottledInfos = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -676,7 +678,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { throttleWarn("SocketException (expected if connection lost). closeConnectionReason=" + closeConnectionReason + "; connection=" + this); } else if (e instanceof SocketTimeoutException || e instanceof TimeoutException) { closeConnectionReason = CloseConnectionReason.SOCKET_TIMEOUT; - throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this); + throttleInfo("Shut down caused by exception " + e.getMessage() + " on connection=" + this); } else if (e instanceof EOFException) { closeConnectionReason = CloseConnectionReason.TERMINATED; throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this); @@ -937,8 +939,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { } private synchronized void throttleWarn(String msg) { - boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; - if (logWarning) { + boolean doLog = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; + if (doLog) { log.warn(msg); if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings); numThrottledWarnings = 0; @@ -947,4 +949,16 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { numThrottledWarnings++; } } + + private synchronized void throttleInfo(String msg) { + boolean doLog = System.currentTimeMillis() - lastLoggedInfoTs > LOG_THROTTLE_INTERVAL_MS; + if (doLog) { + log.info(msg); + if (numThrottledInfos > 0) log.info("{} info logs were throttled since the last log entry", numThrottledInfos); + numThrottledInfos = 0; + lastLoggedInfoTs = System.currentTimeMillis(); + } else { + numThrottledInfos++; + } + } } From 89007c496e0b3f723ab6dd359baf246d4fab7f41 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 27 Dec 2024 14:55:54 -0500 Subject: [PATCH 062/371] fix connected status in network settings for current connection --- .../desktop/main/settings/network/NetworkSettingsView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java index 546a55505f..399f050826 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java @@ -521,7 +521,7 @@ public class NetworkSettingsView extends ActivatableView { if (connectionService.isShutDownStarted()) return; // ignore if shutting down moneroNetworkListItems.clear(); moneroNetworkListItems.setAll(connectionService.getConnections().stream() - .map(connection -> new MoneroNetworkListItem(connection, Boolean.TRUE.equals(connection.isConnected()) && connection == connectionService.getConnection())) + .map(connection -> new MoneroNetworkListItem(connection, connection == connectionService.getConnection() && Boolean.TRUE.equals(connectionService.isConnected()))) .collect(Collectors.toList())); updateChainHeightTextField(connectionService.chainHeightProperty().get()); }); From c1b17cf61257a492da3137d9f23e496dbea729bc Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 27 Dec 2024 14:56:32 -0500 Subject: [PATCH 063/371] update p2p table on user thread to fix null scene --- .../main/settings/network/NetworkSettingsView.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java index 399f050826..0773217cd1 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java @@ -508,12 +508,14 @@ public class NetworkSettingsView extends ActivatableView { } private void updateP2PTable() { - if (connectionService.isShutDownStarted()) return; // ignore if shutting down - p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup); - p2pNetworkListItems.clear(); - p2pNetworkListItems.setAll(p2PService.getNetworkNode().getAllConnections().stream() - .map(connection -> new P2pNetworkListItem(connection, clockWatcher)) - .collect(Collectors.toList())); + UserThread.execute(() -> { + if (connectionService.isShutDownStarted()) return; // ignore if shutting down + p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup); + p2pNetworkListItems.clear(); + p2pNetworkListItems.setAll(p2PService.getNetworkNode().getAllConnections().stream() + .map(connection -> new P2pNetworkListItem(connection, clockWatcher)) + .collect(Collectors.toList())); + }); } private void updateMoneroConnectionsTable() { From 0462ddc2731a7e64aa9cd1285d8161975d601322 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 27 Dec 2024 08:09:54 -0500 Subject: [PATCH 064/371] backup wallet files before saving --- .../main/java/haveno/core/xmr/wallet/XmrWalletService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index fe085de1bb..89b744ccb3 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -376,8 +376,8 @@ public class XmrWalletService extends XmrWalletBase { } public void saveWallet(MoneroWallet wallet, boolean backup) { - wallet.save(); if (backup) backupWallet(getWalletName(wallet.getPath())); + wallet.save(); } public void closeWallet(MoneroWallet wallet, boolean save) { @@ -385,8 +385,8 @@ public class XmrWalletService extends XmrWalletBase { MoneroError err = null; String path = wallet.getPath(); try { - wallet.close(save); - if (save) backupWallet(getWalletName(path)); + if (save) saveWallet(wallet, true); + wallet.close(); } catch (MoneroError e) { err = e; } From 9e95de2d7e808052c68ad0a8a6be2fe80e644c2b Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 27 Dec 2024 10:17:18 -0500 Subject: [PATCH 065/371] save and backup wallet files once per 5 minutes on polling --- .../main/java/haveno/core/trade/Trade.java | 4 ++- .../haveno/core/xmr/wallet/XmrWalletBase.java | 19 ++++++++++- .../core/xmr/wallet/XmrWalletService.java | 32 +++++++++++-------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index c5698a61c3..069b257dd5 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -901,6 +901,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + @Override public void requestSaveWallet() { // save wallet off main thread @@ -911,6 +912,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { }, getId()); } + @Override public void saveWallet() { synchronized (walletLock) { if (!walletExists()) { @@ -2675,7 +2677,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { pollInProgress = false; } } - requestSaveWallet(); + saveWalletWithDelay(); } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java index 06f2e17112..3f4ba3aa03 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java @@ -30,11 +30,13 @@ public abstract class XmrWalletBase { // constants public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 120; public static final int DIRECT_SYNC_WITHIN_BLOCKS = 100; + public static final int SAVE_WALLET_DELAY_SECONDS = 300; // inherited protected MoneroWallet wallet; @Getter protected final Object walletLock = new Object(); + protected Timer saveWalletDelayTimer; @Getter protected XmrConnectionService xmrConnectionService; protected boolean wasWalletSynced; @@ -146,8 +148,23 @@ public abstract class XmrWalletBase { return false; } + public void saveWalletWithDelay() { + // delay writing to disk to avoid frequent write operations + if (saveWalletDelayTimer == null) { + saveWalletDelayTimer = UserThread.runAfter(() -> { + requestSaveWallet(); + UserThread.execute(() -> saveWalletDelayTimer = null); + }, SAVE_WALLET_DELAY_SECONDS, TimeUnit.SECONDS); + } + } + + // --------------------------------- ABSTRACT ----------------------------- + + public abstract void saveWallet(); + + public abstract void requestSaveWallet(); + protected abstract void onConnectionChanged(MoneroRpcConnection connection); - // ------------------------------ PRIVATE HELPERS ------------------------- diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 89b744ccb3..3bf8cfdeb1 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -245,16 +245,20 @@ public class XmrWalletService extends XmrWalletBase { return user.getWalletCreationDate(); } - public void saveMainWallet() { - saveMainWallet(!(Utilities.isWindows() && wallet != null)); + @Override + public void saveWallet() { + saveWallet(!(Utilities.isWindows() && wallet != null)); } - public void saveMainWallet(boolean backup) { - saveWallet(getWallet(), backup); + public void saveWallet(boolean backup) { + synchronized (walletLock) { + saveWallet(getWallet(), backup); + } } - public void requestSaveMainWallet() { - ThreadUtils.submitToPool(() -> saveMainWallet()); // save wallet off main thread + @Override + public void requestSaveWallet() { + ThreadUtils.submitToPool(() -> saveWallet()); // save wallet off main thread } public boolean isWalletAvailable() { @@ -443,7 +447,7 @@ public class XmrWalletService extends XmrWalletBase { if (Boolean.TRUE.equals(txConfig.getRelay())) { cachedTxs.addFirst(tx); cacheWalletInfo(); - requestSaveMainWallet(); + requestSaveWallet(); } return tx; } @@ -453,7 +457,7 @@ public class XmrWalletService extends XmrWalletBase { public String relayTx(String metadata) { synchronized (walletLock) { String txId = wallet.relayTx(metadata); - requestSaveMainWallet(); + requestSaveWallet(); return txId; } } @@ -552,7 +556,7 @@ public class XmrWalletService extends XmrWalletBase { // freeze outputs for (String keyImage : unfrozenKeyImages) wallet.freezeOutput(keyImage); cacheWalletInfo(); - requestSaveMainWallet(); + requestSaveWallet(); } } @@ -574,7 +578,7 @@ public class XmrWalletService extends XmrWalletBase { // thaw outputs for (String keyImage : frozenKeyImages) wallet.thawOutput(keyImage); cacheWalletInfo(); - requestSaveMainWallet(); + requestSaveWallet(); } } @@ -1424,14 +1428,14 @@ public class XmrWalletService extends XmrWalletBase { HavenoUtils.havenoSetup.getWalletInitialized().set(true); // save but skip backup on initialization - saveMainWallet(false); + saveWallet(false); } catch (Exception e) { if (isClosingWallet || isShutDownStarted || HavenoUtils.havenoSetup.getWalletInitialized().get()) return; // ignore if wallet closing, shut down started, or app already initialized log.warn("Error initially syncing main wallet: {}", e.getMessage()); if (numSyncAttempts <= 1) { log.warn("Failed to sync main wallet. Opening app without syncing", numSyncAttempts); HavenoUtils.havenoSetup.getWalletInitialized().set(true); - saveMainWallet(false); + saveWallet(false); // reschedule to init main wallet UserThread.runAfter(() -> { @@ -1809,7 +1813,7 @@ public class XmrWalletService extends XmrWalletBase { tasks.add(() -> { try { wallet.changePassword(oldPassword, newPassword); - saveMainWallet(); + saveWallet(); } catch (Exception e) { log.warn("Error changing main wallet password: " + e.getMessage() + "\n", e); throw e; @@ -2002,7 +2006,7 @@ public class XmrWalletService extends XmrWalletBase { if (wallet != null && !isShutDownStarted) { try { cacheWalletInfo(); - requestSaveMainWallet(); + saveWalletWithDelay(); } catch (Exception e) { log.warn("Error caching wallet info: " + e.getMessage() + "\n", e); } From a9325356c489c4bd0aa31e718eabacdfaa857948 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 5 Jan 2025 07:06:46 -0500 Subject: [PATCH 066/371] update translations for higher chargeback warnings --- core/src/main/resources/i18n/displayStrings.properties | 6 +++--- core/src/main/resources/i18n/displayStrings_cs.properties | 4 ++++ core/src/main/resources/i18n/displayStrings_de.properties | 4 ++++ core/src/main/resources/i18n/displayStrings_es.properties | 4 ++++ core/src/main/resources/i18n/displayStrings_fa.properties | 4 ++++ core/src/main/resources/i18n/displayStrings_fr.properties | 4 ++++ core/src/main/resources/i18n/displayStrings_it.properties | 4 ++++ core/src/main/resources/i18n/displayStrings_ja.properties | 4 ++++ .../src/main/resources/i18n/displayStrings_pt-br.properties | 4 ++++ core/src/main/resources/i18n/displayStrings_pt.properties | 4 ++++ core/src/main/resources/i18n/displayStrings_ru.properties | 4 ++++ core/src/main/resources/i18n/displayStrings_th.properties | 4 ++++ core/src/main/resources/i18n/displayStrings_tr.properties | 6 +++--- core/src/main/resources/i18n/displayStrings_vi.properties | 4 ++++ .../main/resources/i18n/displayStrings_zh-hans.properties | 4 ++++ .../main/resources/i18n/displayStrings_zh-hant.properties | 4 ++++ 16 files changed, 62 insertions(+), 6 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 71ed6e3164..ecb66ca760 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2702,9 +2702,9 @@ payment.account.revolut.addUserNameInfo={0}\n\ This will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.cashapp.info=Cash App has higher chargeback risk than most bank transfers. Please be aware of this when trading with Cash App. -payment.venmo.info=Venmo has higher chargeback risk than most bank transfers. Please be aware of this when trading with Venmo. -payment.paypal.info=PayPal has higher chargeback risk than most bank transfers. Please be aware of this when trading with PayPal. +payment.cashapp.info=Please be aware that Cash App has higher chargeback risk than most bank transfers. +payment.venmo.info=Please be aware that Venmo has higher chargeback risk than most bank transfers. +payment.paypal.info=Please be aware that PayPal has higher chargeback risk than most bank transfers. payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\n\ diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index e2e692c42b..375cc2bf37 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -1994,6 +1994,10 @@ payment.revolut.info=Revolut vyžaduje „uživatelské jméno“ jako ID účtu payment.account.revolut.addUserNameInfo={0}\nVáš stávající účet Revolut ({1}) nemá "Uživatelské jméno".\nChcete-li aktualizovat údaje o svém účtu, zadejte své "Uživatelské jméno" Revolut.\nTo neovlivní stav podepisování věku vašeho účtu. payment.revolut.addUserNameInfo.headLine=Aktualizujte účet Revolut +payment.cashapp.info=Upozorňujeme, že Cash App má vyšší riziko zpětného strhu než většina bankovních převodů. +payment.venmo.info=Upozorňujeme, že Venmo má vyšší riziko zpětného strhu než většina bankovních převodů. +payment.paypal.info=Upozorňujeme, že PayPal má vyšší riziko zpětného strhu než většina bankovních převodů. + payment.amazonGiftCard.upgrade=Platba kartou Amazon eGift nyní vyžaduje také nastavení země. payment.account.amazonGiftCard.addCountryInfo={0}\nVáš stávající účet pro platbu kartou Amazon eGift ({1}) nemá nastavenou zemi.\nVyberte prosím zemi, ve které je možné vaše karty Amazon eGift uplatnit.\nTato aktualizace vašeho účtu nebude mít vliv na stáří tohoto účtu. payment.amazonGiftCard.upgrade.headLine=Aktualizace účtu pro platbu kartou Amazon eGift diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index d1bfa9f0b7..2c9683be7d 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1995,6 +1995,10 @@ payment.revolut.info=Revolut benötigt den "Benutzernamen" als Account ID und ni payment.account.revolut.addUserNameInfo={0}\nDein existierendes Revolut Konto ({1}) hat keinen "Benutzernamen".\nBitte geben Sie Ihren Revolut "Benutzernamen" ein um Ihre Kontodaten zu aktualisieren.\nDas wird Ihr Kontoalter und die Verifizierung nicht beeinflussen. payment.revolut.addUserNameInfo.headLine=Revolut Account updaten +payment.cashapp.info=Bitte beachten Sie, dass Cash App ein höheres Rückbuchungsrisiko hat als die meisten Banküberweisungen. +payment.venmo.info=Bitte beachten Sie, dass Venmo ein höheres Rückbuchungsrisiko hat als die meisten Banküberweisungen. +payment.paypal.info=Bitte beachten Sie, dass PayPal ein höheres Rückbuchungsrisiko hat als die meisten Banküberweisungen. + payment.amazonGiftCard.upgrade=Bei der Zahlungsmethode Amazon Geschenkkarten muss das Land angegeben werden. payment.account.amazonGiftCard.addCountryInfo={0}\nDein bestehendes Amazon Geschenkkarten Konto ({1}) wurde keinem Land zugeteilt.\nBitte geben Sie das Amazon Geschenkkarten Land ein um Ihre Kontodaten zu aktualisieren.\nDas wird ihr Kontoalter nicht beeinflussen. payment.amazonGiftCard.upgrade.headLine=Amazon Geschenkkarte Konto updaten diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index af7539e7fd..45b3cfeb84 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -1996,6 +1996,10 @@ payment.revolut.info=Revolut requiere el 'nombre de usuario' como ID de cuenta y payment.account.revolut.addUserNameInfo={0}\nSu cuenta de Revolut ({1}) no tiene un "nombre de usuario".\nPor favor introduzca su "nombre de usuario" en Revolut para actualizar sus datos de cuenta.\nEsto no afectará a su estado de edad de firmado de cuenta. payment.revolut.addUserNameInfo.headLine=Actualizar cuenta Revolut +payment.cashapp.info=Tenga en cuenta que Cash App tiene un mayor riesgo de contracargos que la mayoría de las transferencias bancarias. +payment.venmo.info=Tenga en cuenta que Venmo tiene un mayor riesgo de contracargos que la mayoría de las transferencias bancarias. +payment.paypal.info=Tenga en cuenta que PayPal tiene un mayor riesgo de contracargos que la mayoría de las transferencias bancarias. + payment.amazonGiftCard.upgrade=El método de pago Tarjetas regalo Amazon requiere que se especifique el país payment.account.amazonGiftCard.addCountryInfo={0}\nSu cuenta actual de Tarjeta regalo Amazon ({1}) no tiene un País especificado.\nPor favor introduzca el país de su Tarjeta regalo Amazon para actualizar sus datos de cuenta.\nEsto no afectará el estatus de edad de su cuenta. payment.amazonGiftCard.upgrade.headLine=Actualizar cuenta Tarjeta regalo Amazon diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 6c5286402f..ed81083c42 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1988,6 +1988,10 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account +payment.cashapp.info=لطفاً توجه داشته باشید که Cash App ریسک بازپرداخت بالاتری نسبت به بیشتر انتقالات بانکی دارد. +payment.venmo.info=لطفاً توجه داشته باشید که Venmo ریسک بازپرداخت بالاتری نسبت به بیشتر انتقالات بانکی دارد. +payment.paypal.info=لطفاً توجه داشته باشید که PayPal ریسک بازپرداخت بالاتری نسبت به بیشتر انتقالات بانکی دارد. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 2712ff4163..b7a76ab299 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -1997,6 +1997,10 @@ payment.revolut.info=Revolut nécessite le 'Nom d'utilisateur' en tant qu'ID de payment.account.revolut.addUserNameInfo={0}\nVotre compte Revolut existant ({1}) n'a pas de "Nom d'utilisateur".\nVeuillez entrer votre "Nom d'utilisateur" Revolut pour mettre à jour les données de votre compte.\nCeci n'affectera pas l'âge du compte. payment.revolut.addUserNameInfo.headLine=Mettre à jour le compte Revolut +payment.cashapp.info=Veuillez noter que Cash App présente un risque de rétrofacturation plus élevé que la plupart des virements bancaires. +payment.venmo.info=Veuillez noter que Venmo présente un risque de rétrofacturation plus élevé que la plupart des virements bancaires. +payment.paypal.info=Veuillez noter que PayPal présente un risque de rétrofacturation plus élevé que la plupart des virements bancaires. + payment.amazonGiftCard.upgrade=La méthode de paiement via carte cadeaux Amazon nécessite que le pays soit spécifié. payment.account.amazonGiftCard.addCountryInfo={0}\nVotre compte carte cadeau Amazon existant ({1}) n'a pas de pays spécifé.\nVeuillez entrer le pays de votre compte carte cadeau Amazon pour mettre à jour les données de votre compte.\nCeci n'affectera pas le statut de l'âge du compte. payment.amazonGiftCard.upgrade.headLine=Mettre à jour le compte des cartes cadeaux Amazon diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 9e52fd8976..367dd03974 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1991,6 +1991,10 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account +payment.cashapp.info=Si prega di notare che Cash App ha un rischio di chargeback più elevato rispetto alla maggior parte dei bonifici bancari. +payment.venmo.info=Si prega di notare che Venmo ha un rischio di chargeback più elevato rispetto alla maggior parte dei bonifici bancari. +payment.paypal.info=Si prega di notare che PayPal ha un rischio di chargeback più elevato rispetto alla maggior parte dei bonifici bancari. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index c1331ac5b2..d2a32da7f5 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -1994,6 +1994,10 @@ payment.revolut.info=以前の場合と違って、Revolutは電話番号やメ payment.account.revolut.addUserNameInfo={0}\n現在の「Revolut」アカウント({1})には「ユーザ名」がありません。 \nアカウントデータを更新するのにRevolutの「ユーザ名」を入力して下さい。\nアカウント年齢署名状況に影響を及ぼしません。 payment.revolut.addUserNameInfo.headLine=Revolutアカウントをアップデートする +payment.cashapp.info=Cash App はほとんどの銀行振込よりもチャージバックリスクが高いことにご注意ください。 +payment.venmo.info=Venmo はほとんどの銀行振込よりもチャージバックリスクが高いことにご注意ください。 +payment.paypal.info=PayPal はほとんどの銀行振込よりもチャージバックリスクが高いことにご注意ください。 + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index d7006d4659..d6c907975b 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -1998,6 +1998,10 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account +payment.cashapp.info=Por favor, esteja ciente de que o Cash App tem um risco maior de estorno do que a maioria das transferências bancárias. +payment.venmo.info=Por favor, esteja ciente de que o Venmo tem um risco maior de estorno do que a maioria das transferências bancárias. +payment.paypal.info=Por favor, esteja ciente de que o PayPal tem um risco maior de estorno do que a maioria das transferências bancárias. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index cd016e3aff..f97d482965 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1988,6 +1988,10 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account +payment.cashapp.info=Esteja ciente de que o Cash App tem um risco de estorno maior do que a maioria das transferências bancárias. +payment.venmo.info=Esteja ciente de que o Venmo tem um risco de estorno maior do que a maioria das transferências bancárias. +payment.paypal.info=Esteja ciente de que o PayPal tem um risco de estorno maior do que a maioria das transferências bancárias. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 664aebdaf9..2562c75e5f 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1989,6 +1989,10 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account +payment.cashapp.info=Обратите внимание, что Cash App имеет более высокий риск возврата платежей, чем большинство банковских переводов. +payment.venmo.info=Обратите внимание, что Venmo имеет более высокий риск возврата платежей, чем большинство банковских переводов. +payment.paypal.info=Обратите внимание, что PayPal имеет более высокий риск возврата платежей, чем большинство банковских переводов. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 2688b08d91..6d739c8e05 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1989,6 +1989,10 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account +payment.cashapp.info=โปรดทราบว่า Cash App มีความเสี่ยงในการเรียกเงินคืนสูงกว่าการโอนเงินผ่านธนาคารส่วนใหญ่ +payment.venmo.info=โปรดทราบว่า Venmo มีความเสี่ยงในการเรียกเงินคืนสูงกว่าการโอนเงินผ่านธนาคารส่วนใหญ่ +payment.paypal.info=โปรดทราบว่า PayPal มีความเสี่ยงในการเรียกเงินคืนสูงกว่าการโอนเงินผ่านธนาคารส่วนใหญ่ + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index f3863027c7..1f2d2c536b 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -2689,9 +2689,9 @@ payment.account.revolut.addUserNameInfo={0}\n\ Bu, hesap yaş imza durumunuzu etkilemeyecektir. payment.revolut.addUserNameInfo.headLine=Revolut hesabını güncelle -payment.cashapp.info=Cash App, çoğu banka transferine göre daha yüksek geri ödeme riskine sahiptir. Cash App ile ticaret yaparken bunun farkında olun. -payment.venmo.info=Venmo, çoğu banka transferine göre daha yüksek geri ödeme riskine sahiptir. Venmo ile ticaret yaparken bunun farkında olun. -payment.paypal.info=PayPal, çoğu banka transferine göre daha yüksek geri ödeme riskine sahiptir. PayPal ile ticaret yaparken bunun farkında olun. +payment.cashapp.info=Lütfen Cash App'in çoğu banka transferinden daha yüksek geri ödeme riski taşıdığını unutmayın. +payment.venmo.info=Lütfen Venmo'nun çoğu banka transferinden daha yüksek geri ödeme riski taşıdığını unutmayın. +payment.paypal.info=Lütfen PayPal'in çoğu banka transferinden daha yüksek geri ödeme riski taşıdığını unutmayın. payment.amazonGiftCard.upgrade=Amazon hediye kartları ödeme yöntemi için ülkenin belirtilmesi gerekmektedir. payment.account.amazonGiftCard.addCountryInfo={0}\n\ diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 40b2b67940..78db6a6a8c 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1991,6 +1991,10 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account +payment.cashapp.info=Vui lòng lưu ý rằng Cash App có rủi ro bồi hoàn cao hơn so với hầu hết các chuyển khoản ngân hàng. +payment.venmo.info=Vui lòng lưu ý rằng Venmo có rủi ro bồi hoàn cao hơn so với hầu hết các chuyển khoản ngân hàng. +payment.paypal.info=Vui lòng lưu ý rằng PayPal có rủi ro bồi hoàn cao hơn so với hầu hết các chuyển khoản ngân hàng. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index b495893a29..809d61f235 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -1998,6 +1998,10 @@ payment.revolut.info=Revolut 要求使用“用户名”作为帐户 ID,而不 payment.account.revolut.addUserNameInfo={0}\n您现有的 Revolut 帐户({1})尚未设置“用户名”。\n请输入您的 Revolut ``用户名''以更新您的帐户数据。\n这不会影响您的账龄验证状态。 payment.revolut.addUserNameInfo.headLine=更新 Revolut 账户 +payment.cashapp.info=请注意,Cash App 的退款风险高于大多数银行转账。 +payment.venmo.info=请注意,Venmo 的退款风险高于大多数银行转账。 +payment.paypal.info=请注意,PayPal 的退款风险高于大多数银行转账。 + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 9132242ee7..c6239bee95 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -1992,6 +1992,10 @@ payment.revolut.info=Revolut 要求使用“用户名”作為帳户 ID,而不 payment.account.revolut.addUserNameInfo={0}\n您現有的 Revolut 帳户({1})尚未設置“用户名”。\n請輸入您的 Revolut ``用户名''以更新您的帳户數據。\n這不會影響您的賬齡驗證狀態。 payment.revolut.addUserNameInfo.headLine=更新 Revolut 賬户 +payment.cashapp.info=請注意,Cash App 的退款風險高於大多數銀行轉帳。 +payment.venmo.info=請注意,Venmo 的退款風險高於大多數銀行轉帳。 +payment.paypal.info=請注意,PayPal 的退款風險高於大多數銀行轉帳。 + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account From 25f85f9f8d5a87551ad9282ce765798706101e19 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 6 Jan 2025 09:20:07 -0500 Subject: [PATCH 067/371] update translations to register filter object --- core/src/main/resources/i18n/displayStrings_cs.properties | 1 + core/src/main/resources/i18n/displayStrings_de.properties | 2 +- core/src/main/resources/i18n/displayStrings_es.properties | 2 +- core/src/main/resources/i18n/displayStrings_fa.properties | 1 + core/src/main/resources/i18n/displayStrings_fr.properties | 2 +- core/src/main/resources/i18n/displayStrings_it.properties | 1 + core/src/main/resources/i18n/displayStrings_ja.properties | 1 + core/src/main/resources/i18n/displayStrings_pt-br.properties | 1 + core/src/main/resources/i18n/displayStrings_pt.properties | 1 + core/src/main/resources/i18n/displayStrings_ru.properties | 1 + core/src/main/resources/i18n/displayStrings_th.properties | 1 + core/src/main/resources/i18n/displayStrings_tr.properties | 2 +- core/src/main/resources/i18n/displayStrings_vi.properties | 1 + core/src/main/resources/i18n/displayStrings_zh-hans.properties | 1 + core/src/main/resources/i18n/displayStrings_zh-hant.properties | 1 + 15 files changed, 15 insertions(+), 4 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 375cc2bf37..59d940a0eb 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -1631,6 +1631,7 @@ popup.warning.nodeBanned=Jeden z {0} uzlů byl zabanován. popup.warning.priceRelay=cenové relé popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Aktualizujte prosím na nejnovější verzi Haveno. Byla vydána povinná aktualizace, která zakazuje obchodování se starými verzemi. Další informace naleznete na fóru Haveno. +popup.warning.noFilter=Nepřijali jsme objekt filtru od seedových uzlů. Prosím informujte správce sítě, aby zaregistrovali objekt filtru. popup.warning.burnXMR=Tato transakce není možná, protože poplatky za těžbu {0} by přesáhly částku převodu {1}. Počkejte prosím, dokud nebudou poplatky za těžbu opět nízké nebo dokud nenahromadíte více XMR k převodu. popup.warning.openOffer.makerFeeTxRejected=Transakční poplatek tvůrce za nabídku s ID {0} byl odmítnut sítí Monero.\nID transakce = {1}.\nNabídka byla odstraněna, aby se předešlo dalším problémům.\nPřejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 2c9683be7d..c6002f049a 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1631,7 +1631,7 @@ popup.warning.nodeBanned=Einer der {0} Nodes wurde gebannt. popup.warning.priceRelay=Preisrelais popup.warning.seed=Seed popup.warning.mandatoryUpdate.trading=Bitte aktualisieren Sie auf die neueste Haveno-Version. Es wurde ein obligatorisches Update veröffentlicht, das den Handel mit alten Versionen deaktiviert. Bitte besuchen Sie das Haveno-Forum für weitere Informationen. -popup.warning.noFilter=Wir haben kein Filterobjekt von den Seed Nodes erhalten. Diese Situation ist unerwartet. Bitte informieren Sie die Haveno Entwickler. +popup.warning.noFilter=Wir haben kein Filterobjekt von den Seed-Knoten erhalten. Bitte informieren Sie die Netzwerkadministratoren, ein Filterobjekt zu registrieren. popup.warning.burnXMR=Die Transaktion ist nicht möglich, da die Mininggebühren von {0} den übertragenen Betrag von {1} überschreiten würden. Bitte warten Sie, bis die Gebühren wieder niedrig sind, oder Sie mehr XMR zum übertragen angesammelt haben. popup.warning.openOffer.makerFeeTxRejected=Die Verkäufergebühren-Transaktion für das Angebot mit der ID {0} wurde vom Monero-Netzwerk abgelehnt.\nTransaktions-ID={1}.\nDas Angebot wurde entfernt, um weitere Probleme zu vermeiden.\nBitte gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie eine SPV-Resynchronisierung durch.\nFür weitere Hilfe wenden Sie sich bitte an den Haveno-Support-Kanal des Haveno Keybase Teams. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 45b3cfeb84..303330e489 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -1632,7 +1632,7 @@ popup.warning.nodeBanned=Uno de los nodos {0} ha sido baneado. popup.warning.priceRelay=retransmisión de precio popup.warning.seed=semilla popup.warning.mandatoryUpdate.trading=Por favor, actualice a la última versión de Haveno. Se lanzó una actualización obligatoria que inhabilita intercambios con versiones anteriores. Por favor, lea el Foro de Haveno para más información\n -popup.warning.noFilter=No hemos recibido un objeto de filtro desde los nodos semilla. Esta situación no se esperaba. Por favor, informe a los desarrolladores Haveno. +popup.warning.noFilter=No recibimos un objeto de filtro de los nodos semilla. Por favor, informe a los administradores de la red que registren un objeto de filtro. popup.warning.burnXMR=Esta transacción no es posible, ya que las comisiones de minado de {0} excederían la cantidad a transferir de {1}. Por favor, espere a que las comisiones de minado bajen o hasta que haya acumulado más XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=La tasa de transacción para la oferta con ID {0} se rechazó por la red Monero.\nID de transacción={1}\nLa oferta se ha eliminado para evitar futuros problemas.\nPor favor vaya a \"Configuración/Información de red\" y haga una resincronización SPV.\nPara más ayuda por favor contacte con el equipo de soporte de Haveno en el canal de Haveno en Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index ed81083c42..647b9dc735 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1627,6 +1627,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=رله قیمت popup.warning.seed=دانه popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. +popup.warning.noFilter=ما شیء فیلتر را از گره‌های اولیه دریافت نکردیم. لطفاً به مدیران شبکه اطلاع دهید که یک شیء فیلتر ثبت کنند. popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index b7a76ab299..7ee0f6a420 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -1633,7 +1633,7 @@ popup.warning.nodeBanned=Un des noeuds {0} a été banni. popup.warning.priceRelay=Relais de prix popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Veuillez faire une mise à jour vers la dernière version de Haveno. Une mise à jour obligatoire a été publiée, laquelle désactive le trading sur les anciennes versions. Veuillez consulter le Forum Haveno pour obtenir plus d'informations. -popup.warning.noFilter=Nous n'avons pas reçu d'object de filtre de la part des noeuds source. Ceci n'est pas une situation attendue. Veuillez informer les développeurs de Haveno +popup.warning.noFilter=Nous n'avons pas reçu d'objet de filtre des nœuds de seed. Veuillez informer les administrateurs du réseau d'enregistrer un objet de filtre. popup.warning.burnXMR=Cette transaction n''est pas possible, car les frais de minage de {0} dépasseraient le montant à transférer de {1}. Veuillez patienter jusqu''à ce que les frais de minage soient de nouveau bas ou jusqu''à ce que vous ayez accumulé plus de XMR à transférer. popup.warning.openOffer.makerFeeTxRejected=La transaction de frais de maker pour l''offre avec ID {0} a été rejetée par le réseau Monero.\nID de transaction={1}.\nL''offre a été retirée pour éviter d''autres problèmes.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l''aide, le canal support de l''équipe Haveno disposible sur Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 367dd03974..495c2380f8 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1630,6 +1630,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=ripetitore di prezzo popup.warning.seed=seme popup.warning.mandatoryUpdate.trading=Si prega di aggiornare Haveno all'ultima versione. È stato rilasciato un aggiornamento obbligatorio che disabilita il trading per le vecchie versioni. Per ulteriori informazioni, consultare il forum Haveno. +popup.warning.noFilter=Non abbiamo ricevuto un oggetto filtro dai nodi seme. Si prega di informare gli amministratori di rete di registrare un oggetto filtro. popup.warning.burnXMR=Questa transazione non è possibile, poiché le commissioni di mining di {0} supererebbero l'importo da trasferire di {1}. Attendi fino a quando le commissioni di mining non saranno nuovamente basse o fino a quando non avrai accumulato più XMR da trasferire. popup.warning.openOffer.makerFeeTxRejected=La commissione della transazione del creatore dell'offerta con ID {0} è stata rifiutata dalla rete Monero.\nTransazione ID={1}.\nL'offerta è stata rimossa per evitare ulteriori problemi.\nVai su \"Impostazioni/Informazioni di rete\" ed esegui una risincronizzazione SPV.\nPer ulteriore assistenza, contattare il canale di supporto Haveno nel team di Haveno Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index d2a32da7f5..b31773063e 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -1631,6 +1631,7 @@ popup.warning.nodeBanned={0}ノードの1つが禁止されました。 popup.warning.priceRelay=価格中継 popup.warning.seed=シード popup.warning.mandatoryUpdate.trading=最新のHavenoバージョンに更新してください。古いバージョンのトレードを無効にする必須の更新プログラムがリリースされました。詳細については、Havenoフォーラムをご覧ください。 +popup.warning.noFilter=シードノードからフィルターオブジェクトを受け取っていません。ネットワーク管理者にフィルターオブジェクトを登録するように通知してください。 popup.warning.burnXMR={0}のマイニング手数料が{1}の送金額を超えるため、このトランザクションは利用不可です。マイニング手数料が再び低くなるか、送金するXMRがさらに蓄積されるまでお待ちください。 popup.warning.openOffer.makerFeeTxRejected=ID{0}で識別されるオファーのためのメイカー手数料トランザクションがビットコインネットワークに拒否されました。\nトランザクションID= {1} 。\n更なる問題を避けるため、そのオファーは削除されました。\n\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\nさらにサポートを受けるため、Haveno Keybaseチームのサポートチャンネルに連絡して下さい。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index d6c907975b..6ac9da556c 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -1637,6 +1637,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=transmissão de preço popup.warning.seed=semente popup.warning.mandatoryUpdate.trading=Faça o update para a última versão do Haveno. Um update obrigatório foi lançado e desabilita negociações em versões antigas. Por favor, veja o Fórum do Haveno para mais informações. +popup.warning.noFilter=Não recebemos um objeto de filtro dos nós seed. Por favor, informe aos administradores da rede para registrar um objeto de filtro. popup.warning.burnXMR=Esta transação não é possível, pois as taxas de mineração de {0} excederiam o montante a transferir de {1}. Aguarde até que as taxas de mineração estejam novamente baixas ou até você ter acumulado mais XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=A transação de taxa de ofertante para a oferta com ID {0} foi rejeitada pela rede Monero.\nID da transação: {1}.\nA oferta foi removida para evitar problemas adicionais.\nPor favor, vá até "Configurações/Informações da rede" e ressincronize o arquivo SPV.\nPara mais informações, por favor acesse o canal #support do time da Haveno na Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index f97d482965..45cd1170dc 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1627,6 +1627,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=transmissão de preço popup.warning.seed=semente popup.warning.mandatoryUpdate.trading=Por favor, atualize para a versão mais recente do Haveno. Uma atualização obrigatória que desativa negociação para versões antigas foi lançada. Por favor, confira o Fórum Haveno para mais informações. +popup.warning.noFilter=Não recebemos um objeto de filtro dos nós sementes. Por favor, informe os administradores da rede para registrar um objeto de filtro. popup.warning.burnXMR=Esta transação não é possível, pois as taxas de mineração de {0} excederia o montante a transferir de {1}. Aguarde até que as taxas de mineração estejam novamente baixas ou até você ter acumulado mais XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=A transação da taxa de ofertante para a oferta com o ID {0} foi rejeitada pela rede do Monero.\nID da transação={1}.\nA oferta foi removida para evitar futuros problemas.\nPor favor vá à \"Definições/Informação da Rede\" e re-sincronize o ficheiro SPV.\nPara mais ajuda por favor contacte o canal de apoio do Haveno na equipa Keybase do Haveno. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 2562c75e5f..ac05e10b83 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1628,6 +1628,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=ретранслятор курса popup.warning.seed=мнемоническая фраза popup.warning.mandatoryUpdate.trading=Обновите Haveno до последней версии. Вышло обязательное обновление, которое делает невозможной торговлю в старых версиях приложения. Посетите форум Haveno, чтобы узнать подробности. +popup.warning.noFilter=Мы не получили объект фильтра от узлов-источников. Пожалуйста, сообщите администраторам сети, чтобы они зарегистрировали объект фильтра. popup.warning.burnXMR=Данную транзакцию невозможно завершить, так как плата за нее ({0}) превышает сумму перевода ({1}). Подождите, пока плата за транзакцию не снизится или пока у вас не появится больше XMR для завершения перевода. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 6d739c8e05..5e25ee8149 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1628,6 +1628,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=ราคาผลัดเปลี่ยน popup.warning.seed=รหัสลับเพื่อกู้ข้อมูล popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. +popup.warning.noFilter=เราไม่ได้รับวัตถุกรองจากโหนดต้นทาง กรุณาแจ้งผู้ดูแลระบบเครือข่ายให้ลงทะเบียนวัตถุกรอง popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 1f2d2c536b..cdba715870 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -2160,7 +2160,7 @@ popup.warning.seed=anahtar kelime popup.warning.mandatoryUpdate.trading=Lütfen en son Haveno sürümüne güncelleyin. \ Eski sürümler için ticareti devre dışı bırakan zorunlu bir güncelleme yayınlandı. \ Daha fazla bilgi için lütfen Haveno Forumunu kontrol edin. -popup.warning.noFilter=Tohum düğümlerinden bir filtre nesnesi almadık. Lütfen Haveno ağ yöneticilerini bir filtre nesnesi kaydetmek için ctrl + f ile bilgilendirin. +popup.warning.noFilter=Tohum düğümlerinden bir filtre nesnesi almadık. Lütfen ağ yöneticilerine bir filtre nesnesi kaydetmeleri için bilgi verin. popup.warning.burnXMR=Bu işlem mümkün değil, çünkü {0} tutarındaki madencilik ücretleri, transfer edilecek {1} tutarını aşacaktır. \ Lütfen madencilik ücretleri tekrar düşük olana kadar bekleyin veya transfer etmek için daha fazla XMR biriktirin. diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 78db6a6a8c..7ea2eb9d27 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1630,6 +1630,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=rơle giá popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. +popup.warning.noFilter=Chúng tôi không nhận được đối tượng bộ lọc từ các nút hạt giống. Vui lòng thông báo cho quản trị viên mạng để đăng ký một đối tượng bộ lọc. popup.warning.burnXMR=Không thể thực hiện giao dịch, vì phí đào {0} vượt quá số lượng {1} cần chuyển. Vui lòng chờ tới khi phí đào thấp xuống hoặc khi bạn tích lũy đủ XMR để chuyển. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 809d61f235..9f3e495ba7 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -1635,6 +1635,7 @@ popup.warning.nodeBanned=其中一个 {0} 节点已被禁用 popup.warning.priceRelay=价格传递 popup.warning.seed=种子 popup.warning.mandatoryUpdate.trading=请更新到最新的 Haveno 版本。强制更新禁止了旧版本进行交易。更多信息请访问 Haveno 论坛。 +popup.warning.noFilter=我们没有从种子节点接收到过滤器对象。请通知网络管理员注册一个过滤器对象。 popup.warning.burnXMR=这笔交易是无法实现,因为 {0} 的挖矿手续费用会超过 {1} 的转账金额。请等到挖矿手续费再次降低或您积累了更多的 XMR 来转账。 popup.warning.openOffer.makerFeeTxRejected=交易 ID 为 {0} 的挂单费交易被比特币网络拒绝。\n交易 ID = {1}\n交易已被移至失败交易。\n请到“设置/网络信息”进行 SPV 重新同步。\n如需更多帮助,请联系 Haveno Keybase 团队的 Support 频道 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index c6239bee95..c802eba7b8 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -1631,6 +1631,7 @@ popup.warning.nodeBanned=其中一個 {0} 節點已被禁用 popup.warning.priceRelay=價格傳遞 popup.warning.seed=種子 popup.warning.mandatoryUpdate.trading=請更新到最新的 Haveno 版本。強制更新禁止了舊版本進行交易。更多信息請訪問 Haveno 論壇。 +popup.warning.noFilter=我們未從種子節點收到過濾器物件。請通知網路管理員註冊過濾器物件。 popup.warning.burnXMR=這筆交易是無法實現,因為 {0} 的挖礦手續費用會超過 {1} 的轉賬金額。請等到挖礦手續費再次降低或您積累了更多的 XMR 來轉賬。 popup.warning.openOffer.makerFeeTxRejected=交易 ID 為 {0} 的掛單費交易被比特幣網絡拒絕。\n交易 ID = {1}\n交易已被移至失敗交易。\n請到“設置/網絡信息”進行 SPV 重新同步。\n如需更多幫助,請聯繫 Haveno Keybase 團隊的 Support 頻道 From e4f3d136606d70603d2e956138632485c17b6e04 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 6 Jan 2025 09:47:54 -0500 Subject: [PATCH 068/371] penalize menu only appears for arbitrator in failed trades view --- .../failedtrades/FailedTradesView.java | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java index b95aad8806..35fe31dc22 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java @@ -27,6 +27,7 @@ import haveno.core.offer.Offer; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; +import haveno.core.user.User; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; @@ -98,15 +99,18 @@ public class FailedTradesView extends ActivatableViewAndModel filterTextFieldListener; private Scene scene; private XmrWalletService xmrWalletService; + private User user; private ContextMenu contextMenu; @Inject public FailedTradesView(FailedTradesViewModel model, TradeDetailsWindow tradeDetailsWindow, - XmrWalletService xmrWalletService) { + XmrWalletService xmrWalletService, + User user) { super(model); this.tradeDetailsWindow = tradeDetailsWindow; this.xmrWalletService = xmrWalletService; + this.user = user; } @Override @@ -190,9 +194,33 @@ public class FailedTradesView extends ActivatableViewAndModel { + Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); + handleContextMenu("portfolio.failed.penalty.msg", + Res.get(selectedFailedTrade.getMaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), + Res.get("shared.maker"), + selectedFailedTrade.getMaker().getSecurityDeposit(), + selectedFailedTrade.getMaker().getReserveTxHash(), + selectedFailedTrade.getMaker().getReserveTxHex()); + }); + + item2.setOnAction(event -> { + Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); + handleContextMenu("portfolio.failed.penalty.msg", + Res.get(selectedFailedTrade.getTaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), + Res.get("shared.taker"), + selectedFailedTrade.getTaker().getSecurityDeposit(), + selectedFailedTrade.getTaker().getReserveTxHash(), + selectedFailedTrade.getTaker().getReserveTxHex()); + }); + + contextMenu.getItems().addAll(item1, item2); + } tableView.setRowFactory(tv -> { TableRow row = new TableRow<>(); @@ -202,26 +230,6 @@ public class FailedTradesView extends ActivatableViewAndModel { - Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); - handleContextMenu("portfolio.failed.penalty.msg", - Res.get(selectedFailedTrade.getMaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), - Res.get("shared.maker"), - selectedFailedTrade.getMaker().getSecurityDeposit(), - selectedFailedTrade.getMaker().getReserveTxHash(), - selectedFailedTrade.getMaker().getReserveTxHex()); - }); - - item2.setOnAction(event -> { - Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); - handleContextMenu("portfolio.failed.penalty.msg", - Res.get(selectedFailedTrade.getTaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), - Res.get("shared.taker"), - selectedFailedTrade.getTaker().getSecurityDeposit(), - selectedFailedTrade.getTaker().getReserveTxHash(), - selectedFailedTrade.getTaker().getReserveTxHex()); - }); - numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { ObservableList> tableColumns = tableView.getColumns(); From a1a7f9ccc9cf928c08c4d9cbf44cd7ca078abb2a Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 7 Jan 2025 06:33:34 -0500 Subject: [PATCH 069/371] Revert "update translations to register filter object" This reverts commit 3860ce942c9e05318d0b809e166c345ad187f284. --- core/src/main/resources/i18n/displayStrings_cs.properties | 1 - core/src/main/resources/i18n/displayStrings_de.properties | 2 +- core/src/main/resources/i18n/displayStrings_es.properties | 2 +- core/src/main/resources/i18n/displayStrings_fa.properties | 1 - core/src/main/resources/i18n/displayStrings_fr.properties | 2 +- core/src/main/resources/i18n/displayStrings_it.properties | 1 - core/src/main/resources/i18n/displayStrings_ja.properties | 1 - core/src/main/resources/i18n/displayStrings_pt-br.properties | 1 - core/src/main/resources/i18n/displayStrings_pt.properties | 1 - core/src/main/resources/i18n/displayStrings_ru.properties | 1 - core/src/main/resources/i18n/displayStrings_th.properties | 1 - core/src/main/resources/i18n/displayStrings_tr.properties | 2 +- core/src/main/resources/i18n/displayStrings_vi.properties | 1 - core/src/main/resources/i18n/displayStrings_zh-hans.properties | 1 - core/src/main/resources/i18n/displayStrings_zh-hant.properties | 1 - 15 files changed, 4 insertions(+), 15 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 59d940a0eb..375cc2bf37 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -1631,7 +1631,6 @@ popup.warning.nodeBanned=Jeden z {0} uzlů byl zabanován. popup.warning.priceRelay=cenové relé popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Aktualizujte prosím na nejnovější verzi Haveno. Byla vydána povinná aktualizace, která zakazuje obchodování se starými verzemi. Další informace naleznete na fóru Haveno. -popup.warning.noFilter=Nepřijali jsme objekt filtru od seedových uzlů. Prosím informujte správce sítě, aby zaregistrovali objekt filtru. popup.warning.burnXMR=Tato transakce není možná, protože poplatky za těžbu {0} by přesáhly částku převodu {1}. Počkejte prosím, dokud nebudou poplatky za těžbu opět nízké nebo dokud nenahromadíte více XMR k převodu. popup.warning.openOffer.makerFeeTxRejected=Transakční poplatek tvůrce za nabídku s ID {0} byl odmítnut sítí Monero.\nID transakce = {1}.\nNabídka byla odstraněna, aby se předešlo dalším problémům.\nPřejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index c6002f049a..2c9683be7d 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1631,7 +1631,7 @@ popup.warning.nodeBanned=Einer der {0} Nodes wurde gebannt. popup.warning.priceRelay=Preisrelais popup.warning.seed=Seed popup.warning.mandatoryUpdate.trading=Bitte aktualisieren Sie auf die neueste Haveno-Version. Es wurde ein obligatorisches Update veröffentlicht, das den Handel mit alten Versionen deaktiviert. Bitte besuchen Sie das Haveno-Forum für weitere Informationen. -popup.warning.noFilter=Wir haben kein Filterobjekt von den Seed-Knoten erhalten. Bitte informieren Sie die Netzwerkadministratoren, ein Filterobjekt zu registrieren. +popup.warning.noFilter=Wir haben kein Filterobjekt von den Seed Nodes erhalten. Diese Situation ist unerwartet. Bitte informieren Sie die Haveno Entwickler. popup.warning.burnXMR=Die Transaktion ist nicht möglich, da die Mininggebühren von {0} den übertragenen Betrag von {1} überschreiten würden. Bitte warten Sie, bis die Gebühren wieder niedrig sind, oder Sie mehr XMR zum übertragen angesammelt haben. popup.warning.openOffer.makerFeeTxRejected=Die Verkäufergebühren-Transaktion für das Angebot mit der ID {0} wurde vom Monero-Netzwerk abgelehnt.\nTransaktions-ID={1}.\nDas Angebot wurde entfernt, um weitere Probleme zu vermeiden.\nBitte gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie eine SPV-Resynchronisierung durch.\nFür weitere Hilfe wenden Sie sich bitte an den Haveno-Support-Kanal des Haveno Keybase Teams. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 303330e489..45b3cfeb84 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -1632,7 +1632,7 @@ popup.warning.nodeBanned=Uno de los nodos {0} ha sido baneado. popup.warning.priceRelay=retransmisión de precio popup.warning.seed=semilla popup.warning.mandatoryUpdate.trading=Por favor, actualice a la última versión de Haveno. Se lanzó una actualización obligatoria que inhabilita intercambios con versiones anteriores. Por favor, lea el Foro de Haveno para más información\n -popup.warning.noFilter=No recibimos un objeto de filtro de los nodos semilla. Por favor, informe a los administradores de la red que registren un objeto de filtro. +popup.warning.noFilter=No hemos recibido un objeto de filtro desde los nodos semilla. Esta situación no se esperaba. Por favor, informe a los desarrolladores Haveno. popup.warning.burnXMR=Esta transacción no es posible, ya que las comisiones de minado de {0} excederían la cantidad a transferir de {1}. Por favor, espere a que las comisiones de minado bajen o hasta que haya acumulado más XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=La tasa de transacción para la oferta con ID {0} se rechazó por la red Monero.\nID de transacción={1}\nLa oferta se ha eliminado para evitar futuros problemas.\nPor favor vaya a \"Configuración/Información de red\" y haga una resincronización SPV.\nPara más ayuda por favor contacte con el equipo de soporte de Haveno en el canal de Haveno en Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 647b9dc735..ed81083c42 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1627,7 +1627,6 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=رله قیمت popup.warning.seed=دانه popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. -popup.warning.noFilter=ما شیء فیلتر را از گره‌های اولیه دریافت نکردیم. لطفاً به مدیران شبکه اطلاع دهید که یک شیء فیلتر ثبت کنند. popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 7ee0f6a420..b7a76ab299 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -1633,7 +1633,7 @@ popup.warning.nodeBanned=Un des noeuds {0} a été banni. popup.warning.priceRelay=Relais de prix popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Veuillez faire une mise à jour vers la dernière version de Haveno. Une mise à jour obligatoire a été publiée, laquelle désactive le trading sur les anciennes versions. Veuillez consulter le Forum Haveno pour obtenir plus d'informations. -popup.warning.noFilter=Nous n'avons pas reçu d'objet de filtre des nœuds de seed. Veuillez informer les administrateurs du réseau d'enregistrer un objet de filtre. +popup.warning.noFilter=Nous n'avons pas reçu d'object de filtre de la part des noeuds source. Ceci n'est pas une situation attendue. Veuillez informer les développeurs de Haveno popup.warning.burnXMR=Cette transaction n''est pas possible, car les frais de minage de {0} dépasseraient le montant à transférer de {1}. Veuillez patienter jusqu''à ce que les frais de minage soient de nouveau bas ou jusqu''à ce que vous ayez accumulé plus de XMR à transférer. popup.warning.openOffer.makerFeeTxRejected=La transaction de frais de maker pour l''offre avec ID {0} a été rejetée par le réseau Monero.\nID de transaction={1}.\nL''offre a été retirée pour éviter d''autres problèmes.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l''aide, le canal support de l''équipe Haveno disposible sur Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 495c2380f8..367dd03974 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1630,7 +1630,6 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=ripetitore di prezzo popup.warning.seed=seme popup.warning.mandatoryUpdate.trading=Si prega di aggiornare Haveno all'ultima versione. È stato rilasciato un aggiornamento obbligatorio che disabilita il trading per le vecchie versioni. Per ulteriori informazioni, consultare il forum Haveno. -popup.warning.noFilter=Non abbiamo ricevuto un oggetto filtro dai nodi seme. Si prega di informare gli amministratori di rete di registrare un oggetto filtro. popup.warning.burnXMR=Questa transazione non è possibile, poiché le commissioni di mining di {0} supererebbero l'importo da trasferire di {1}. Attendi fino a quando le commissioni di mining non saranno nuovamente basse o fino a quando non avrai accumulato più XMR da trasferire. popup.warning.openOffer.makerFeeTxRejected=La commissione della transazione del creatore dell'offerta con ID {0} è stata rifiutata dalla rete Monero.\nTransazione ID={1}.\nL'offerta è stata rimossa per evitare ulteriori problemi.\nVai su \"Impostazioni/Informazioni di rete\" ed esegui una risincronizzazione SPV.\nPer ulteriore assistenza, contattare il canale di supporto Haveno nel team di Haveno Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index b31773063e..d2a32da7f5 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -1631,7 +1631,6 @@ popup.warning.nodeBanned={0}ノードの1つが禁止されました。 popup.warning.priceRelay=価格中継 popup.warning.seed=シード popup.warning.mandatoryUpdate.trading=最新のHavenoバージョンに更新してください。古いバージョンのトレードを無効にする必須の更新プログラムがリリースされました。詳細については、Havenoフォーラムをご覧ください。 -popup.warning.noFilter=シードノードからフィルターオブジェクトを受け取っていません。ネットワーク管理者にフィルターオブジェクトを登録するように通知してください。 popup.warning.burnXMR={0}のマイニング手数料が{1}の送金額を超えるため、このトランザクションは利用不可です。マイニング手数料が再び低くなるか、送金するXMRがさらに蓄積されるまでお待ちください。 popup.warning.openOffer.makerFeeTxRejected=ID{0}で識別されるオファーのためのメイカー手数料トランザクションがビットコインネットワークに拒否されました。\nトランザクションID= {1} 。\n更なる問題を避けるため、そのオファーは削除されました。\n\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\nさらにサポートを受けるため、Haveno Keybaseチームのサポートチャンネルに連絡して下さい。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 6ac9da556c..d6c907975b 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -1637,7 +1637,6 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=transmissão de preço popup.warning.seed=semente popup.warning.mandatoryUpdate.trading=Faça o update para a última versão do Haveno. Um update obrigatório foi lançado e desabilita negociações em versões antigas. Por favor, veja o Fórum do Haveno para mais informações. -popup.warning.noFilter=Não recebemos um objeto de filtro dos nós seed. Por favor, informe aos administradores da rede para registrar um objeto de filtro. popup.warning.burnXMR=Esta transação não é possível, pois as taxas de mineração de {0} excederiam o montante a transferir de {1}. Aguarde até que as taxas de mineração estejam novamente baixas ou até você ter acumulado mais XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=A transação de taxa de ofertante para a oferta com ID {0} foi rejeitada pela rede Monero.\nID da transação: {1}.\nA oferta foi removida para evitar problemas adicionais.\nPor favor, vá até "Configurações/Informações da rede" e ressincronize o arquivo SPV.\nPara mais informações, por favor acesse o canal #support do time da Haveno na Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 45cd1170dc..f97d482965 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1627,7 +1627,6 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=transmissão de preço popup.warning.seed=semente popup.warning.mandatoryUpdate.trading=Por favor, atualize para a versão mais recente do Haveno. Uma atualização obrigatória que desativa negociação para versões antigas foi lançada. Por favor, confira o Fórum Haveno para mais informações. -popup.warning.noFilter=Não recebemos um objeto de filtro dos nós sementes. Por favor, informe os administradores da rede para registrar um objeto de filtro. popup.warning.burnXMR=Esta transação não é possível, pois as taxas de mineração de {0} excederia o montante a transferir de {1}. Aguarde até que as taxas de mineração estejam novamente baixas ou até você ter acumulado mais XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=A transação da taxa de ofertante para a oferta com o ID {0} foi rejeitada pela rede do Monero.\nID da transação={1}.\nA oferta foi removida para evitar futuros problemas.\nPor favor vá à \"Definições/Informação da Rede\" e re-sincronize o ficheiro SPV.\nPara mais ajuda por favor contacte o canal de apoio do Haveno na equipa Keybase do Haveno. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index ac05e10b83..2562c75e5f 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1628,7 +1628,6 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=ретранслятор курса popup.warning.seed=мнемоническая фраза popup.warning.mandatoryUpdate.trading=Обновите Haveno до последней версии. Вышло обязательное обновление, которое делает невозможной торговлю в старых версиях приложения. Посетите форум Haveno, чтобы узнать подробности. -popup.warning.noFilter=Мы не получили объект фильтра от узлов-источников. Пожалуйста, сообщите администраторам сети, чтобы они зарегистрировали объект фильтра. popup.warning.burnXMR=Данную транзакцию невозможно завершить, так как плата за нее ({0}) превышает сумму перевода ({1}). Подождите, пока плата за транзакцию не снизится или пока у вас не появится больше XMR для завершения перевода. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 5e25ee8149..6d739c8e05 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1628,7 +1628,6 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=ราคาผลัดเปลี่ยน popup.warning.seed=รหัสลับเพื่อกู้ข้อมูล popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. -popup.warning.noFilter=เราไม่ได้รับวัตถุกรองจากโหนดต้นทาง กรุณาแจ้งผู้ดูแลระบบเครือข่ายให้ลงทะเบียนวัตถุกรอง popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index cdba715870..1f2d2c536b 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -2160,7 +2160,7 @@ popup.warning.seed=anahtar kelime popup.warning.mandatoryUpdate.trading=Lütfen en son Haveno sürümüne güncelleyin. \ Eski sürümler için ticareti devre dışı bırakan zorunlu bir güncelleme yayınlandı. \ Daha fazla bilgi için lütfen Haveno Forumunu kontrol edin. -popup.warning.noFilter=Tohum düğümlerinden bir filtre nesnesi almadık. Lütfen ağ yöneticilerine bir filtre nesnesi kaydetmeleri için bilgi verin. +popup.warning.noFilter=Tohum düğümlerinden bir filtre nesnesi almadık. Lütfen Haveno ağ yöneticilerini bir filtre nesnesi kaydetmek için ctrl + f ile bilgilendirin. popup.warning.burnXMR=Bu işlem mümkün değil, çünkü {0} tutarındaki madencilik ücretleri, transfer edilecek {1} tutarını aşacaktır. \ Lütfen madencilik ücretleri tekrar düşük olana kadar bekleyin veya transfer etmek için daha fazla XMR biriktirin. diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 7ea2eb9d27..78db6a6a8c 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1630,7 +1630,6 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=rơle giá popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. -popup.warning.noFilter=Chúng tôi không nhận được đối tượng bộ lọc từ các nút hạt giống. Vui lòng thông báo cho quản trị viên mạng để đăng ký một đối tượng bộ lọc. popup.warning.burnXMR=Không thể thực hiện giao dịch, vì phí đào {0} vượt quá số lượng {1} cần chuyển. Vui lòng chờ tới khi phí đào thấp xuống hoặc khi bạn tích lũy đủ XMR để chuyển. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 9f3e495ba7..809d61f235 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -1635,7 +1635,6 @@ popup.warning.nodeBanned=其中一个 {0} 节点已被禁用 popup.warning.priceRelay=价格传递 popup.warning.seed=种子 popup.warning.mandatoryUpdate.trading=请更新到最新的 Haveno 版本。强制更新禁止了旧版本进行交易。更多信息请访问 Haveno 论坛。 -popup.warning.noFilter=我们没有从种子节点接收到过滤器对象。请通知网络管理员注册一个过滤器对象。 popup.warning.burnXMR=这笔交易是无法实现,因为 {0} 的挖矿手续费用会超过 {1} 的转账金额。请等到挖矿手续费再次降低或您积累了更多的 XMR 来转账。 popup.warning.openOffer.makerFeeTxRejected=交易 ID 为 {0} 的挂单费交易被比特币网络拒绝。\n交易 ID = {1}\n交易已被移至失败交易。\n请到“设置/网络信息”进行 SPV 重新同步。\n如需更多帮助,请联系 Haveno Keybase 团队的 Support 频道 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index c802eba7b8..c6239bee95 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -1631,7 +1631,6 @@ popup.warning.nodeBanned=其中一個 {0} 節點已被禁用 popup.warning.priceRelay=價格傳遞 popup.warning.seed=種子 popup.warning.mandatoryUpdate.trading=請更新到最新的 Haveno 版本。強制更新禁止了舊版本進行交易。更多信息請訪問 Haveno 論壇。 -popup.warning.noFilter=我們未從種子節點收到過濾器物件。請通知網路管理員註冊過濾器物件。 popup.warning.burnXMR=這筆交易是無法實現,因為 {0} 的挖礦手續費用會超過 {1} 的轉賬金額。請等到挖礦手續費再次降低或您積累了更多的 XMR 來轉賬。 popup.warning.openOffer.makerFeeTxRejected=交易 ID 為 {0} 的掛單費交易被比特幣網絡拒絕。\n交易 ID = {1}\n交易已被移至失敗交易。\n請到“設置/網絡信息”進行 SPV 重新同步。\n如需更多幫助,請聯繫 Haveno Keybase 團隊的 Support 頻道 From 21ea08a68d873f0e896034f15e72585c2a78fa66 Mon Sep 17 00:00:00 2001 From: slrslr <6596726+slrslr@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:47:54 +0100 Subject: [PATCH 070/371] Update displayStrings_cs.properties --- .../i18n/displayStrings_cs.properties | 1731 ++++++++++++++--- 1 file changed, 1431 insertions(+), 300 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 375cc2bf37..7c313343ba 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -28,11 +28,14 @@ shared.readMore=Přečíst více shared.openHelp=Otevřít nápovědu shared.warning=Varování shared.close=Zavřít +shared.closeAnywayDanger=Přesto vypnout (NEBEZPEČNÉ!) +shared.okWait=Dobře, počkám shared.cancel=Zrušit shared.ok=OK shared.yes=Ano shared.no=Ne shared.iUnderstand=Rozumím +shared.continueAnyway=Přesto pokračovat shared.na=N/A shared.shutDown=Vypnout shared.reportBug=Nahlásit chybu na GitHubu @@ -67,8 +70,8 @@ shared.fixedPriceInCurForCur=Pevná cena v {0} za 1 {1} shared.amount=Množství shared.txFee=Transakční poplatek shared.tradeFee=Obchodní poplatek -shared.buyerSecurityDeposit=Vklad kupujícího -shared.sellerSecurityDeposit=Vklad prodejce +shared.buyerSecurityDeposit=Vklad kauce kupujícího +shared.sellerSecurityDeposit=Vklad kauce prodejce shared.amountWithCur=Množství v {0} shared.volumeWithCur=Objem v {0} shared.currency=Měna @@ -83,12 +86,13 @@ shared.balanceWithCur=Zůstatek v {0} shared.utxo=Nevyčerpaný transakční výstup shared.txId=ID transakce shared.confirmations=Potvrzení -shared.revert=Návratová Tx +shared.revert=Vzít zpět Tx shared.select=Vybrat shared.usage=Použití shared.state=Stav shared.tradeId=ID obchodu shared.offerId=ID nabídky +shared.traderId=ID obchodníka shared.bankName=Jméno banky shared.acceptedBanks=Přijímané banky shared.amountMinMax=Množství (min - max) @@ -99,12 +103,12 @@ shared.XMRMinMax=XMR (min - max) shared.removeOffer=Odstranit nabídku shared.dontRemoveOffer=Neodstraňovat nabídku shared.editOffer=Upravit nabídku +shared.duplicateOffer=Duplikovat nabídku shared.openLargeQRWindow=Otevřít velké okno s QR kódem -shared.tradingAccount=Obchodní účet +shared.chooseTradingAccount=Vyberte obchodní účet shared.faq=Navštívit stránku FAQ shared.yesCancel=Ano, zrušit shared.nextStep=Další krok -shared.selectTradingAccount=Vyberte obchodní účet shared.fundFromSavingsWalletButton=Použít prostředky z peněženky Haveno shared.fundFromExternalWalletButton=Otevřít vaši externí peněženku pro financování shared.openDefaultWalletFailed=Nepodařilo se otevřít aplikaci peněženky Monero. Jste si jisti, že máte nějakou nainstalovanou? @@ -113,10 +117,15 @@ shared.aboveInPercent=% nad tržní cenou shared.enterPercentageValue=Zadejte % hodnotu shared.OR=NEBO shared.notEnoughFunds=Ve své peněžence Haveno nemáte pro tuto transakci dostatek prostředků — je potřeba {0}, ale k dispozici je pouze {1}.\n\nPřidejte prostředky z externí peněženky nebo financujte svou peněženku Haveno v části Prostředky > Přijmout prostředky. -shared.waitingForFunds=Čekání na finance... +shared.waitingForFunds=Čekání na finanční prostředky... +shared.yourDepositTransactionId=ID vaší vkladové transakce +shared.peerDepositTransactionId=ID vkladové transakce peera +shared.makerDepositTransactionId=ID vkladové transakce tvůrce +shared.takerDepositTransactionId=ID vkladové transakce příjemce shared.TheXMRBuyer=XMR kupující shared.You=Vy -shared.sendingConfirmation=Posílám potvrzení... +shared.preparingConfirmation=Příprava potvrzení... +shared.sendingConfirmation=Odesílání potvrzení... shared.sendingConfirmationAgain=Prosím pošlete potvrzení znovu shared.exportCSV=Exportovat do CSV shared.exportJSON=Exportovat do JSON @@ -125,9 +134,11 @@ shared.noDateAvailable=Žádné datum není k dispozici shared.noDetailsAvailable=Detaily nejsou k dispozici shared.notUsedYet=Ještě nepoužito shared.date=Datum +shared.sendFundsDetailsWithFee=Odesílání: {0}nNa přijímací adresu: {1}.nPožadován těžební poplatek: {2}\n\nPříjemce dostane: {3}\n\nJste si jisti, že chcete vyplatit tuto částku? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno zjistil, že tato transakce by vytvořila drobné mince, které jsou pod limitem drobných mincí (a není to povoleno pravidly pro konsenzus Monero). Místo toho budou tyto drobné mince ({0} satoshi {1}) přidány k poplatku za těžbu.\n\n\n shared.copyToClipboard=Kopírovat do schránky +shared.copiedToClipboard=Zkopírováno do schránky! shared.language=Jazyk shared.country=Země shared.applyAndShutDown=Potvrdit a ukončit @@ -165,6 +176,7 @@ shared.acceptedTakerCountries=Země příjemce akceptovány shared.tradePrice=Tržní cena shared.tradeAmount=Výše obchodu shared.tradeVolume=Objem obchodu +shared.reservedAmount=Rezervovaná částka shared.invalidKey=Vložený klíč není správný shared.enterPrivKey=Pro odemknutí vložte privátní klíč shared.payoutTxId=ID platební transakce @@ -192,9 +204,12 @@ shared.reserveExactAmount=Rezervujte pouze nezbytné prostředky. Vyžaduje popl shared.makerTxFee=Tvůrce: {0} shared.takerTxFee=Příjemce: {0} shared.iConfirm=Potvrzuji -shared.openURL=Otevřené {0} +shared.openURL=Otevřít {0} shared.fiat=Fiat shared.crypto=Krypto +shared.traditional=Tradiční +shared.otherAssets=jiná aktiva +shared.other=Jiné shared.preciousMetals=Drahé kovy shared.all=Vše shared.edit=Upravit @@ -213,12 +228,16 @@ shared.mediator=Mediátor shared.arbitrator=Rozhodce shared.refundAgent=Rozhodce shared.refundAgentForSupportStaff=Rozhodce pro vrácení peněz -shared.delayedPayoutTxId=ID odložené platební transakce -shared.delayedPayoutTxReceiverAddress=Odložená výplatní transakce odeslána na +shared.delayedPayoutTxId=ID zpožděné platební transakce +shared.delayedPayoutTxReceiverAddress=Zpožděná výplatní transakce odeslána na shared.unconfirmedTransactionsLimitReached=Momentálně máte příliš mnoho nepotvrzených transakcí. Prosím zkuste to znovu později. shared.numItemsLabel=Počet položek: {0} shared.filter=Filtr shared.enabled=Aktivní +shared.pending=Otevřené +shared.me=Já +shared.maker=Tvůrce +shared.taker=Příjemce #################################################################### @@ -233,14 +252,15 @@ mainView.menu.market=Trh mainView.menu.buyXmr=Koupit XMR mainView.menu.sellXmr=Prodat XMR mainView.menu.portfolio=Portfolio -mainView.menu.funds=Finance +mainView.menu.funds=Prostředky mainView.menu.support=Podpora mainView.menu.settings=Nastavení mainView.menu.account=Účet mainView.marketPriceWithProvider.label=Tržní cena {0} mainView.marketPrice.havenoInternalPrice=Cena posledního Haveno obchodu -mainView.marketPrice.tooltip.havenoInternalPrice=Neexistují tržní ceny od externích poskytovatelů cenových feedů.\nZobrazená cena je nejnovější obchodní cena Haveno pro tuto měnu. +mainView.marketPrice.tooltip.havenoInternalPrice=Neexistují tržní ceny od externích poskytovatelů cenových feedů.\n\ +Zobrazená cena je nejnovější obchodní cena Haveno pro tuto měnu. mainView.marketPrice.tooltip=Tržní cena je poskytována {0}{1}\nPoslední aktualizace: {2}\nURL uzlu poskytovatele: {3} mainView.balance.available=Dostupný zůstatek mainView.balance.reserved=Rezervováno v nabídkách @@ -252,7 +272,7 @@ mainView.footer.usingTor=(přes Tor) mainView.footer.localhostMoneroNode=(localhost) mainView.footer.clearnet=(přes clearnet) mainView.footer.xmrInfo={0} {1} -mainView.footer.xmrFeeRate=/ Aktuální poplatek: {0} sat/vB +mainView.footer.xmrFeeRate=/ Míra poplatku: {0} sat/vB mainView.footer.xmrInfo.initializing=Připojování k síti Haveno mainView.footer.xmrInfo.synchronizingWith=Synchronizace s {0} na bloku: {1} / {2} mainView.footer.xmrInfo.connectedTo=Připojeno k {0} v bloku {1} @@ -265,7 +285,7 @@ mainView.footer.p2pPeers=Haveno síťové uzly: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Připojování do sítě Tor... mainView.bootstrapState.torNodeCreated=(2/4) Tor uzel vytvořen -mainView.bootstrapState.hiddenServicePublished=(3/4) Skrytá služba publikována +mainView.bootstrapState.hiddenServicePublished=(3/4) Skrytá služba zveřejněna mainView.bootstrapState.initialDataReceived=(4/4) Iniciační data přijata mainView.bootstrapWarning.noSeedNodesAvailable=Žádné seed uzly nejsou k dispozici @@ -283,6 +303,7 @@ mainView.walletServiceErrorMsg.rejectedTxException=Transakce byla ze sítě zam mainView.networkWarning.allConnectionsLost=Ztratili jste připojení ke všem {0} síťovým peer uzlům.\nMožná jste ztratili připojení k internetu nebo byl váš počítač v pohotovostním režimu. mainView.networkWarning.localhostMoneroLost=Ztratili jste připojení k localhost uzlu Monero.\nRestartujte aplikaci Haveno a připojte se k jiným uzlům Monero nebo restartujte localhost Monero uzel. mainView.version.update=(Dostupná aktualizace) +mainView.status.connections=Příchozí připojení: {0}nOdchozí připojení: {1} #################################################################### @@ -294,11 +315,10 @@ market.tabs.spreadCurrency=Nabídky podle měn market.tabs.spreadPayment=Nabídky podle způsobů platby market.tabs.trades=Obchody +# OfferBookView +market.offerBook.filterPrompt=Filtr + # OfferBookChartView -market.offerBook.buyCrypto=Koupit {0} (prodat {1}) -market.offerBook.sellCrypto=Prodat {0} (koupit {1}) -market.offerBook.buyWithTraditional=Koupit {0} -market.offerBook.sellWithTraditional=Prodat {0} market.offerBook.sellOffersHeaderLabel=Prodat {0} kupujícímu market.offerBook.buyOffersHeaderLabel=Koupit {0} od prodejce market.offerBook.buy=Chci koupit monero @@ -330,8 +350,7 @@ market.trades.showVolumeInUSD=Zobrazit objem v USD offerbook.createOffer=Vytvořit nabídku offerbook.takeOffer=Přijmout nabídku -offerbook.takeOfferToBuy=Přijmout nabídku na nákup {0} -offerbook.takeOfferToSell=Přijmout nabídku k prodeji {0} +offerbook.takeOffer.createAccount=Vytvořit účet a přijmout nabídku offerbook.takeOffer.enterChallenge=Zadejte heslo nabídky offerbook.trader=Obchodník offerbook.offerersBankId=ID banky tvůrce (BIC/SWIFT): {0} @@ -339,14 +358,14 @@ offerbook.offerersBankName=Jméno banky tvůrce: {0} offerbook.offerersBankSeat=Sídlo banky tvůrce: {0} offerbook.offerersAcceptedBankSeatsEuro=Přijatá sídla bank (příjemce): Všechny země Eura offerbook.offerersAcceptedBankSeats=Přijatá sídla bank (příjemce):\n {0} -offerbook.availableOffers=Dostupné nabídky +offerbook.availableOffersToBuy=Koupit {0} pomocí {1} +offerbook.availableOffersToSell=Prodat {0} za {1} offerbook.filterByCurrency=Filtrovat podle měny offerbook.filterByPaymentMethod=Filtrovat podle platební metody offerbook.matchingOffers=Nabídky odpovídající mým účtům offerbook.filterNoDeposit=Žádný vklad offerbook.noDepositOffers=Nabídky bez zálohy (vyžaduje se heslo) offerbook.timeSinceSigning=Informace o účtu -offerbook.timeSinceSigning.info=Tento účet byl ověřen a {0} offerbook.timeSinceSigning.info.arbitrator=podepsán rozhodcem a může podepisovat účty partnerů offerbook.timeSinceSigning.info.peer=podepsáno partnerem, nyní čeká ještě %d dnů na zrušení limitů offerbook.timeSinceSigning.info.peerLimitLifted=podepsán partnerem a limity byly zrušeny @@ -354,16 +373,23 @@ offerbook.timeSinceSigning.info.signer=podepsán partnerem a může podepsat ú offerbook.timeSinceSigning.info.banned=účet byl zablokován offerbook.timeSinceSigning.daysSinceSigning={0} dní offerbook.timeSinceSigning.daysSinceSigning.long={0} od podpisu +offerbook.timeSinceSigning.tooltip.accountLimit=Limit účtu: {0} +offerbook.timeSinceSigning.tooltip.accountLimitLifted=Limit účtu odebrán +offerbook.timeSinceSigning.tooltip.info.unsigned=Tento účet ještě nebyl podepsán +offerbook.timeSinceSigning.tooltip.info.signed=Tento účet byl podepsán +offerbook.timeSinceSigning.tooltip.info.signedAndLifted=Tento účet byl podepsán a může podepisovat účty peerů +offerbook.timeSinceSigning.tooltip.checkmark.buyXmr=nakoupeno XMR od podepsaného účtu +offerbook.timeSinceSigning.tooltip.checkmark.wait=čekáno alespoň {0} dnů +offerbook.timeSinceSigning.tooltip.learnMore=Více informací offerbook.xmrAutoConf=Je automatické potvrzení povoleno -offerbook.buyXmrWith=Kupte XMR za: +offerbook.buyXmrWith=Koupit XMR za: offerbook.sellXmrFor=Prodat XMR za: -offerbook.timeSinceSigning.help=Když úspěšně dokončíte obchod s uživatelem, který má podepsaný platební účet, je váš platební účet podepsán.\n{0} dní později se počáteční limit {1} zruší a váš účet může podepisovat platební účty ostatních uživatelů. +offerbook.timeSinceSigning.help=Když úspěšně dokončíte obchod s uživatelem, který má podepsaný platební účet, je váš platební účet podepsán.\n\ + {0} dní později se počáteční limit {1} zruší a váš účet může podepisovat platební účty ostatních uživatelů. offerbook.timeSinceSigning.notSigned=Dosud nepodepsáno offerbook.timeSinceSigning.notSigned.ageDays={0} dní offerbook.timeSinceSigning.notSigned.noNeed=N/A -shared.notSigned=Tento účet ještě nebyl podepsán a byl vytvořen před {0} dny -shared.notSigned.noNeed=Tento typ účtu nevyžaduje podepisování shared.notSigned.noNeedDays=Tento typ účtu nevyžaduje podepisování a byl vytvořen před {0} dny shared.notSigned.noNeedAlts=Kryptoměnové účty neprocházejí kontrolou podpisu a stáří @@ -371,23 +397,18 @@ offerbook.nrOffers=Počet nabídek: {0} offerbook.volume={0} (min - max) offerbook.deposit=Kauce XMR (%) offerbook.deposit.help=Kauce zaplacená každým obchodníkem k zajištění obchodu. Bude vrácena po dokončení obchodu. + offerbook.createNewOffer=Vytvořit nabídku pro {0} {1} +offerbook.createOfferDisabled.tooltip=Můžete vytvořit zároveň jen jednu nabídku -offerbook.createOfferToBuy=Vytvořit novou nabídku k nákupu {0} -offerbook.createOfferToSell=Vytvořit novou nabídku k prodeji {0} -offerbook.createOfferToBuy.withTraditional=Vytvořit novou nabídku k nákupu {0} za {1} -offerbook.createOfferToSell.forTraditional=Vytvořit novou nabídku k prodeji {0} za {1} -offerbook.createOfferToBuy.withCrypto=Vytvořit novou nabídku k prodeji {0} (koupit {1}) -offerbook.createOfferToSell.forCrypto=Vytvořit novou nabídku na nákup {0} (prodat {1}) - -offerbook.takeOfferButton.tooltip=Využijte nabídku {0} -offerbook.yesCreateOffer=Ano, vytvořit nabídku +offerbook.takeOfferButton.tooltip=Využít nabídku {0} offerbook.setupNewAccount=Založit nový obchodní účet offerbook.removeOffer.success=Odebrání nabídky bylo úspěšné. offerbook.removeOffer.failed=Odebrání nabídky selhalo:\n{0} offerbook.deactivateOffer.failed=Deaktivace nabídky se nezdařila:\n{0} offerbook.activateOffer.failed=Zveřejnění nabídky se nezdařilo:\n{0} -offerbook.withdrawFundsHint=Prostředky, které jste zaplatili, můžete vybrat z obrazovky {0}. +offerbook.withdrawFundsHint=Nabídka byla odebrána. Prostředky již nejsou pro tuto nabídku rezervovány. \n + Dostupné prostředky můžete odeslat do své externí peněženky prostřednictvím {0}. offerbook.warning.noTradingAccountForCurrency.headline=Žádný platební účet pro vybranou měnu offerbook.warning.noTradingAccountForCurrency.msg=Pro vybranou měnu nemáte nastavený platební účet.\n\nChcete místo toho vytvořit nabídku pro jinou měnu? @@ -396,10 +417,18 @@ offerbook.warning.noMatchingAccount.msg=Tato nabídka používá platební metod offerbook.warning.counterpartyTradeRestrictions=Tuto nabídku nelze přijmout z důvodu obchodních omezení protistrany -offerbook.warning.newVersionAnnouncement=S touto verzí softwaru mohou obchodní partneři navzájem ověřovat a podepisovat platební účty ostatních a vytvářet tak síť důvěryhodných platebních účtů.\n\nPo úspěšném obchodování s partnerským účtem s ověřeným platebním účtem bude váš platební účet podepsán a obchodní limity budou zrušeny po určitém časovém intervalu (délka tohoto intervalu závisí na způsobu ověření).\n\nDalší informace o podepsání účtu naleznete v dokumentaci na adrese [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. +offerbook.warning.newVersionAnnouncement=S touto verzí softwaru mohou obchodní partneři navzájem ověřovat a podepisovat platební účty ostatních a vytvářet tak síť důvěryhodných platebních účtů.\n\n +Po úspěšném obchodování s partnerským účtem s ověřeným platebním účtem bude váš platební účet podepsán a obchodní limity budou zrušeny po určitém časovém intervalu (délka tohoto intervalu závisí na způsobu ověření).\n\n +Další informace o podepsání účtu naleznete v dokumentaci na adrese [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits/#account-signing]. -popup.warning.tradeLimitDueAccountAgeRestriction.seller=Povolená částka obchodu je omezena na {0} z důvodu bezpečnostních omezení na základě následujících kritérií:\n- Účet kupujícího nebyl podepsán rozhodcem ani obchodním partnerem\n- Doba od podpisu účtu kupujícího není alespoň 30 dní\n- Způsob platby této nabídky je považován za riskantní pro bankovní zpětné zúčtování\n\n{1} -popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Povolená částka obchodu je omezena na {0} z důvodu bezpečnostních omezení na základě následujících kritérií:\n- Váš účet nebyl podepsán rozhodcem ani obchodním partnerem\n- Čas od podpisu vašeho účtu není alespoň 30 dní\n- Způsob platby této nabídky je považován za riskantní pro bankovní zpětné zúčtování\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=Povolená částka obchodu je omezena na {0} z důvodu bezpečnostních omezení na základě následujících kritérií:\n\ +- Účet kupujícího nebyl podepsán rozhodcem ani obchodním partnerem\n\ +- Doba od podpisu účtu kupujícího není alespoň 30 dní\n\ +- Způsob platby této nabídky je považován za riskantní pro bankovní zpětné zúčtování\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Povolená částka obchodu je omezena na {0} z důvodu bezpečnostních omezení na základě následujících kritérií:\n\ +- Váš účet nebyl podepsán rozhodcem ani obchodním partnerem\n\ +- Čas od podpisu vašeho účtu není alespoň 30 dní\n\ +- Způsob platby této nabídky je považován za riskantní pro bankovní zpětné zúčtování\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Tento platební metoda je dočasně omezena na {0} do {1}, protože všichni kupující mají nové účty.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Vaše nabídka bude omezena na kupující s podepsanými a starými účty, protože překračuje {0}.\n\n{1} @@ -409,8 +438,13 @@ offerbook.warning.offerBlocked=Tato nabídka byla blokována vývojáři Haveno. offerbook.warning.currencyBanned=Měna použitá v této nabídce byla blokována vývojáři Haveno.\nDalší informace naleznete na fóru Haveno. offerbook.warning.paymentMethodBanned=Vývojáři Haveno zablokovali způsob platby použitý v této nabídce.\nDalší informace naleznete na fóru Haveno. offerbook.warning.nodeBlocked=Onion adresa tohoto obchodníka byla zablokována vývojáři Haveno.\nPravděpodobně existuje neošetřená chyba způsobující problémy při přijímání nabídek od tohoto obchodníka. -offerbook.warning.requireUpdateToNewVersion=Vaše verze Haveno již není kompatibilní pro obchodování. Aktualizujte prosím na nejnovější verzi Haveno na adrese [HYPERLINK:https://haveno.exchange/downloads]. -offerbook.warning.offerWasAlreadyUsedInTrade=Tuto nabídku nemůžete přijmout, protože jste ji již dříve využili. Je možné, že váš předchozí pokus o přijetí nabídky vyústil v neúspěšný obchod. +offerbook.warning.requireUpdateToNewVersion=Vaše verze Haveno již není kompatibilní pro obchodování. + Aktualizujte prosím na nejnovější verzi Haveno. +offerbook.warning.offerWasAlreadyUsedInTrade=Tuto nabídku nemůžete přijmout, protože jste ji již dříve využili. \ +Je možné, že váš předchozí pokus o přijetí nabídky vyústil v neúspěšný obchod. + +offerbook.warning.arbitratorNotValidated=Tuto nabídku nelze přijmout, protože rozhodce je neplatný +offerbook.warning.signatureNotValidated=Tuto nabídku nelze přijmout, protože rozhodce má neplatný podpis offerbook.info.sellAtMarketPrice=Budete prodávat za tržní cenu (aktualizováno každou minutu). offerbook.info.buyAtMarketPrice=Budete nakupovat za tržní cenu (aktualizováno každou minutu). @@ -420,25 +454,28 @@ offerbook.info.sellAboveMarketPrice=Získáte o {0} více, než je aktuální tr offerbook.info.buyBelowMarketPrice=Platíte o {0} méně, než je aktuální tržní cena (aktualizováno každou minutu). offerbook.info.buyAtFixedPrice=Budete nakupovat za tuto pevnou cenu. offerbook.info.sellAtFixedPrice=Budete prodávat za tuto pevnou cenu. -offerbook.info.noArbitrationInUserLanguage=V případě sporu mějte na paměti, že arbitráž pro tuto nabídku bude řešit {0}. Jazyk je aktuálně nastaven na {1}. offerbook.info.roundedFiatVolume=Částka byla zaokrouhlena, aby se zvýšilo soukromí vašeho obchodu. #################################################################### # Offerbook / Create offer #################################################################### -createOffer.amount.prompt=Vložte množství v XMR +createOffer.amount.prompt=Zadejte množství v XMR createOffer.price.prompt=Zadejte cenu -createOffer.volume.prompt=Vložte množství v {0} +createOffer.volume.prompt=Zadejte množství v {0} createOffer.amountPriceBox.amountDescription=Množství XMR, které chcete {0} -createOffer.amountPriceBox.buy.volumeDescription=Částka v {0}, kterou utratíte -createOffer.amountPriceBox.sell.volumeDescription=Částka v {0}, kterou přijmete +createOffer.amountPriceBox.buy.amountDescriptionCrypto=Množství XMR k prodeji +createOffer.amountPriceBox.sell.amountDescriptionCrypto=Množství XMR k nákupu +createOffer.amountPriceBox.buy.volumeDescription=Množství {0}, které odešlete +createOffer.amountPriceBox.sell.volumeDescription=Množství {0}, které přijmete +createOffer.amountPriceBox.buy.volumeDescriptionCrypto=Množství {0} k prodání +createOffer.amountPriceBox.sell.volumeDescriptionCrypto=Množství {0} k nakoupení createOffer.amountPriceBox.minAmountDescription=Minimální množství XMR createOffer.securityDeposit.prompt=Kauce -createOffer.fundsBox.title=Financujte svou nabídku +createOffer.fundsBox.title=Financovat nabídku createOffer.fundsBox.offerFee=Obchodní poplatek createOffer.fundsBox.networkFee=Poplatek za těžbu -createOffer.fundsBox.placeOfferSpinnerInfo=Probíhá publikování nabídky ... +createOffer.fundsBox.placeOfferSpinnerInfo=Probíhá zveřejnění nabídky ... createOffer.fundsBox.paymentLabel=Haveno obchod s ID {0} createOffer.fundsBox.fundsStructure=(kauce {0}, obchodní poplatek {1}, poplatek za těžbu {2}) createOffer.success.headline=Vaše nabídka byla vytvořena @@ -450,45 +487,50 @@ createOffer.info.buyBelowMarketPrice=Vždy zaplatíte o {0} % méně, než je ak createOffer.warning.sellBelowMarketPrice=Vždy získáte o {0} % méně, než je aktuální tržní cena, protože cena vaší nabídky bude průběžně aktualizována. createOffer.warning.buyAboveMarketPrice=Vždy zaplatíte o {0} % více, než je aktuální tržní cena, protože cena vaší nabídky bude průběžně aktualizována. createOffer.tradeFee.descriptionXMROnly=Obchodní poplatek -createOffer.tradeFee.descriptionBSQEnabled=Zvolte měnu obchodního poplatku +createOffer.tradeFee.description=Obchodní poplatek createOffer.triggerPrice.prompt=Nepovinná limitní cena createOffer.triggerPrice.label=Deaktivovat nabídku, pokud tržní cena dosáhne {0} -createOffer.triggerPrice.tooltip=Abyste se ochránili před prudkými výkyvy tržních cen, můžete nastavit limitní cenu, po jejímž dosažení bude vaše nabídka stažena. +createOffer.triggerPrice.tooltip=Abyste se ochránili před prudkými výkyvy tržních cen, můžete nastavit limitní cenu, \ +po jejímž dosažení bude vaše nabídka stažena. createOffer.triggerPrice.invalid.tooLow=Hodnota musí být vyšší než {0} createOffer.triggerPrice.invalid.tooHigh=Hodnota musí být nižší než {0} # new entries -createOffer.placeOfferButton=Přehled: Umístěte nabídku {0} monero -createOffer.createOfferFundWalletInfo.headline=Financujte svou nabídku +createOffer.placeOfferButton=Zkontrolovat vytvoření nabídky {0} monero +createOffer.placeOfferButtonCrypto=Zkontrolovat vytvoření nabídky {0} {1} +createOffer.createOfferFundWalletInfo.headline=Financovat nabídku # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Výše obchodu: {0}\n createOffer.createOfferFundWalletInfo.msg=Potřebujete vložit {0} do této nabídky.\n\n\ - Tyto prostředky jsou rezervovány ve vaší místní peněžence a budou zablokovány v multisig peněžence, jakmile někdo přijme vaši nabídku.\n\n\ - Částka je součtem:\n\ - {1}\ - - Vaše záloha: {2}\n\ - - Poplatek za obchodování: {3} + Tyto prostředky jsou rezervovány ve vaší místní peněžence a budou zablokovány v multisig peněžence, jakmile někdo přijme vaši nabídku.\n\n\ + Částka je součtem:\n\ + {1}\ + - Vaše záloha: {2}\n\ + - Poplatek za obchodování: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) -createOffer.amountPriceBox.error.message=Při zadávání nabídky došlo k chybě:\n\n{0}\n\nPeněženku ještě neopustily žádné finanční prostředky.\nRestartujte aplikaci a zkontrolujte síťové připojení. +createOffer.amountPriceBox.error.message=Při zadávání nabídky došlo k chybě:\n\n{0}\n\n +Peněženku ještě neopustily žádné finanční prostředky.\n\ +Restartujte aplikaci a zkontrolujte síťové připojení. createOffer.setAmountPrice=Nastavit množství a cenu -createOffer.warnCancelOffer=Tuto nabídku jste již financovali. Pokud ji nyní zrušíte, zůstanou vaše prostředky v místní peněžence Haveno a budou k dispozici pro výběr na obrazovce "Prostředky/Odeslat prostředky". Opravdu si přejete zrušit? +createOffer.warnCancelOffer=Tuto nabídku jste již financovali. Pokud ji nyní zrušíte, zůstanou vaše prostředky v místní peněžence Haveno a budou k dispozici pro výběr na obrazovce \"Prostředky/Odeslat prostředky\". Opravdu si přejete zrušit? createOffer.timeoutAtPublishing=Při zveřejnění nabídky došlo k vypršení časového limitu. createOffer.errorInfo=\n\nTvůrčí poplatek je již zaplacen. V nejhorším případě jste tento poplatek ztratili.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit. -createOffer.tooLowSecDeposit.warning=Nastavili jste kauci na nižší hodnotu, než je doporučená výchozí hodnota {0}.\nOpravdu chcete použít nižší kauci? +createOffer.tooLowSecDeposit.warning=Nastavili jste kauci na nižší hodnotu, než je doporučená výchozí hodnota {0}.\n + Opravdu chcete použít nižší kauci? createOffer.tooLowSecDeposit.makerIsSeller=Poskytuje vám to menší ochranu v případě, že obchodní partner nedodrží obchodní protokol. -createOffer.tooLowSecDeposit.makerIsBuyer=Obchodní partner bude mít menší jistotu, že dodržíte obchodní protokol, protože uložená kauce bude příliš nízká. Ostatní uživatelé mohou raději využít jiné nabídky než té vaší. +createOffer.tooLowSecDeposit.makerIsBuyer=Obchodní partner bude mít menší jistotu, že dodržíte obchodní protokol, protože uložená kauce bude příliš nízká. \ + Ostatní uživatelé mohou raději využít jiné nabídky než té vaší. createOffer.resetToDefault=Ne, restartovat na výchozí hodnotu createOffer.useLowerValue=Ano, použijte moji nižší hodnotu createOffer.priceOutSideOfDeviation=Cena, kterou jste zadali, je mimo max. povolenou odchylku od tržní ceny.\nMax. povolená odchylka je {0} a lze ji upravit v preferencích. createOffer.changePrice=Změnit cenu -createOffer.tac=Publikováním této nabídky souhlasím s obchodováním s jakýmkoli obchodníkem, který splňuje podmínky definované na této obrazovce. -createOffer.currencyForFee=Obchodní poplatek +createOffer.tac=Zveřejněním této nabídky souhlasím s obchodováním s jakýmkoli obchodníkem, který splňuje podmínky definované na této obrazovce. createOffer.setDeposit=Nastavit kauci kupujícího (%) createOffer.setDepositAsBuyer=Nastavit mou kauci jako kupujícího (%) createOffer.setDepositForBothTraders=Nastavit kauci obou obchodníků (%) -createOffer.securityDepositInfo=Kauce vašeho kupujícího bude {0} +createOffer.securityDepositInfo=Vaše kauce kupujícího bude {0} createOffer.securityDepositInfoAsBuyer=Vaše kauce jako kupující bude {0} createOffer.minSecurityDepositUsed=Minimální bezpečnostní záloha je použita createOffer.buyerAsTakerWithoutDeposit=Žádný vklad od kupujícího (chráněno heslem) @@ -502,14 +544,16 @@ createOffer.myDepositInfo=Vaše záloha na bezpečnost bude {0} takeOffer.amount.prompt=Vložte množství v XMR takeOffer.amountPriceBox.buy.amountDescription=Množství XMR na prodej -takeOffer.amountPriceBox.sell.amountDescription=Množství XMR k nákupu +takeOffer.amountPriceBox.sell.amountDescription=Množství XMR k nakoupení +takeOffer.amountPriceBox.buy.amountDescriptionCrypto=Množství XMR na prodej +takeOffer.amountPriceBox.sell.amountDescriptionCrypto=Množství XMR k nakoupení takeOffer.amountPriceBox.priceDescription=Cena za monero v {0} takeOffer.amountPriceBox.amountRangeDescription=Možný rozsah množství takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=Částka, kterou jste zadali, přesahuje počet povolených desetinných míst.\nČástka byla upravena na 4 desetinná místa. takeOffer.validation.amountSmallerThanMinAmount=Částka nesmí být menší než minimální částka stanovená v nabídce. takeOffer.validation.amountLargerThanOfferAmount=Vstupní částka nesmí být vyšší než částka stanovená v nabídce. takeOffer.validation.amountLargerThanOfferAmountMinusFee=Toto vstupní množství by vytvořilo zanedbatelné drobné pro prodejce XMR. -takeOffer.fundsBox.title=Financujte svůj obchod +takeOffer.fundsBox.title=Financovat obchod takeOffer.fundsBox.isOfferAvailable=Kontroluje se, zda je nabídka k dispozici ... takeOffer.fundsBox.tradeAmount=Částka k prodeji takeOffer.fundsBox.offerFee=Obchodní poplatek @@ -524,18 +568,19 @@ takeOffer.success.info=Stav vašeho obchodu můžete vidět v \"Portfolio/Otevř takeOffer.error.message=Při převzetí nabídky došlo k chybě.\n\n{0} # new entries -takeOffer.takeOfferButton=Přehled: Využijte nabídku {0} monero +takeOffer.takeOfferButton=Zkontrolovat přijetí nabídky {0} monero +takeOffer.takeOfferButtonCrypto=Zkontrolovat přijetí nabídky {0} {1} takeOffer.noPriceFeedAvailable=Tuto nabídku nemůžete vzít, protože používá procentuální cenu založenou na tržní ceně, ale není k dispozici žádný zdroj cen. -takeOffer.takeOfferFundWalletInfo.headline=Financujte svůj obchod +takeOffer.takeOfferFundWalletInfo.headline=Financovat obchod # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Výše obchodu: {0} \n takeOffer.takeOfferFundWalletInfo.msg=Abyste mohli tuto nabídku využít, musíte vložit {0}.\n\nČástka je součtem:\n{1} - Vaší kauce: {2}\n- Obchodního poplatku: {3}\n- Celkového poplatku za těžbu: {4}\n\nPři financování obchodu si můžete vybrat ze dvou možností:\n- Použijte svou peněženku Haveno (pohodlné, ale transakce mohou být propojitelné) NEBO\n- Platba z externí peněženky (potenciálně více soukromé)\n\nPo uzavření tohoto vyskakovacího okna se zobrazí všechny možnosti a podrobnosti financování. takeOffer.alreadyPaidInFunds=Pokud jste již prostředky zaplatili, můžete je vybrat na obrazovce \"Prostředky/Odeslat prostředky\". -takeOffer.paymentInfo=Informace o platbě takeOffer.setAmountPrice=Nastavit částku -takeOffer.alreadyFunded.askCancel=Tuto nabídku jste již financovali. Pokud ji nyní zrušíte, zůstanou vaše prostředky v místní peněžence Haveno a budou k dispozici pro výběr na obrazovce "Prostředky/Odeslat prostředky". Opravdu si přejete zrušit? +takeOffer.alreadyFunded.askCancel=Tuto nabídku jste již financovali. Pokud ji nyní zrušíte, zůstanou vaše prostředky v místní peněžence Haveno a budou k dispozici pro výběr na obrazovce \"Prostředky/Odeslat prostředky\". Opravdu si přejete zrušit? takeOffer.failed.offerNotAvailable=Žádost o nabídku se nezdařila, protože nabídka již není k dispozici. Možná, že mezitím nabídku přijal jiný obchodník. takeOffer.failed.offerTaken=Tuto nabídku nemůžete přijmout, protože ji již přijal jiný obchodník. +takeOffer.failed.offerInvalid=Tuto nabídku nemůžete přijmout, protože podpis tvůrce je neplatný. takeOffer.failed.offerRemoved=Tuto nabídku nemůžete přijmout, protože mezitím byla nabídka odstraněna. takeOffer.failed.offererNotOnline=Přijetí nabídky se nezdařilo, protože tvůrce již není online. takeOffer.failed.offererOffline=Tuto nabídku nemůžete přijmout, protože je tvůrce offline. @@ -543,7 +588,7 @@ takeOffer.warning.connectionToPeerLost=Ztratili jste spojení s tvůrcem.\nMohli takeOffer.error.noFundsLost=\n\nPeněženku ještě neopustily žádné finanční prostředky.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit. # suppress inspection "TrailingSpacesInProperty" -takeOffer.error.feePaid=.\n\n +takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nVkladová transakce je již zveřejněna.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit.\nPokud problém přetrvává, kontaktujte vývojáře a požádejte je o podporu. takeOffer.error.payoutPublished=\n\nVyplacená transakce je již zveřejněna.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit.\nPokud problém přetrvává, kontaktujte vývojáře a požádejte je o podporu. takeOffer.tac=Přijetím této nabídky souhlasím s obchodními podmínkami definovanými na této obrazovce. @@ -555,11 +600,12 @@ takeOffer.tac=Přijetím této nabídky souhlasím s obchodními podmínkami def openOffer.header.triggerPrice=Limitní cena openOffer.triggerPrice=Limitní cena {0} -openOffer.triggered=Nabídka byla deaktivována, protože tržní cena dosáhla vámi stanovené limitní ceny.\nProsím nastavte novou limitní cenu ve vaší nabídce +openOffer.triggered=Nabídka byla deaktivována, protože tržní cena dosáhla vámi stanovené limitní ceny.\n\ + Prosím nastavte novou limitní cenu ve vaší nabídce editOffer.setPrice=Nastavit cenu editOffer.confirmEdit=Potvrdit: Upravit nabídku -editOffer.publishOffer=Publikování vaší nabídky. +editOffer.publishOffer=Zveřejnění vaší nabídky. editOffer.failed=Úprava nabídky se nezdařila:\n{0} editOffer.success=Vaše nabídka byla úspěšně upravena. editOffer.invalidDeposit=Kauce kupujícího není v rámci omezení definovaných Haveno DAO a nemůže být dále upravována. @@ -573,10 +619,21 @@ portfolio.tab.pendingTrades=Otevřené obchody portfolio.tab.history=Historie portfolio.tab.failed=Selhalo portfolio.tab.editOpenOffer=Upravit nabídku +portfolio.tab.duplicateOffer=Duplicitní nabídka +portfolio.context.offerLikeThis=Vytvořit novou nabídku jako je tato... +portfolio.context.notYourOffer=Duplikovat můžete pouze nabídky, u kterých jste byli tvůrcem. portfolio.closedTrades.deviation.help=Procentuální odchylka od tržní ceny -portfolio.pending.invalidTx=Došlo k problému s chybějící nebo neplatnou transakcí.\n\nProsím neposílejte fiat nebo crypto platby.\n\nOtevřete úkol pro podporu, některý z mediátorů vám pomůže.\n\nChybová zpráva: {0} +portfolio.pending.invalidTx=Došlo k problému s chybějící nebo neplatnou transakcí.\n\n\ + Prosím neposílejte fiat nebo crypto platby.\n\n\ + Otevřete úkol pro podporu, některý z mediátorů vám pomůže.\n\n\ + Chybová zpráva: {0} + +portfolio.pending.unconfirmedTooLong=Vkladové transakce obchodu {0} jsou stále nepotvrzené po {1} hodinách. \ + Zkontrolujte transakce vkladu pomocí průzkumníka blockchainu; pokud jsou potvrzené, ale nezobrazují se jako \ + potvrzené v Haveno, zkuste Haveno restartovat.\n\n\ + Pokud problém přetrvává, kontaktujte podporu Haveno [HYPERLINK:https://matrix.to/#/#haveno:monero.social]. portfolio.pending.step1.waitForConf=Počkejte na potvrzení na blockchainu portfolio.pending.step2_buyer.startPayment=Zahajte platbu @@ -617,30 +674,40 @@ portfolio.pending.autoConf.state.FAILED=Služba se vrátila se selháním. Není portfolio.pending.step1.info=Vkladová transakce byla zveřejněna.\n{0} před zahájením platby musíte počkat na alespoň jedno potvrzení na blockchainu. portfolio.pending.step1.warn=Vkladová transakce není stále potvrzena. K tomu někdy dochází ve vzácných případech, kdy byl poplatek za financování jednoho obchodníka z externí peněženky příliš nízký. -portfolio.pending.step1.openForDispute=Vkladová transakce není stále potvrzena. Můžete počkat déle nebo požádat o pomoc mediátora. +portfolio.pending.step1.openForDispute=Vkladová transakce není stále potvrzena. \ + Pokud jste čekali mnohem déle než 20 minut, můžete poádat o pomoc podporu Haveno. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Váš obchod má alespoň jedno potvrzení blockchainu.\n\n -portfolio.pending.step2_buyer.refTextWarn=Důležité: když vyplňujete platební informace, nechte pole \"důvod platby\" prázdné. NEPOUŽÍVEJTE ID obchodu ani jiné poznámky jako např. 'monero', 'XMR' nebo 'Haveno'. Můžete se se svým obchodním partnerem domluvit pomocí chatu na identifikaci platby, která bude vyhovovat oběma. +portfolio.pending.step2_buyer.refTextWarn=Důležité: když vyplňujete platební informace, nechte pole \"důvod platby\" \ + prázdné. NEPOUŽÍVEJTE ID obchodu ani jiné poznámky jako např. 'monero', 'XMR' nebo 'Haveno'. \ + Můžete se se svým obchodním partnerem domluvit pomocí chatu na \"důvod platby\", který bude vyhovovat oběma. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=Pokud vaše banka účtuje poplatky za převod, musíte tyto poplatky uhradit vy. # suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.fees.swift=Ujistěte se, že k odeslání platby SWIFT používáte model SHA (model sdílených poplatků). \ + Více detailů [HYPERLINK:https://haveno.exchange/wiki/SWIFT#Use_the_correct_fee_option]. +# suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Převeďte prosím z vaší externí {0} peněženky\n{1} prodejci XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Přejděte do banky a zaplaťte {0} prodejci XMR.\n\n portfolio.pending.step2_buyer.cash.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zapište na papírový doklad: NO REFUNDS - bez náhrady.\nPoté ji roztrhněte na 2 části, vytvořte fotografii a odešlete ji na e-mailovou adresu prodejce XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Zaplaťte prosím {0} prodejci XMR pomocí MoneyGram.\n\n -portfolio.pending.step2_buyer.moneyGram.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zašlete autorizační číslo a fotografii s potvrzením e-mailem prodejci XMR.\nPotvrzení musí jasně uvádět celé jméno, zemi, stát a částku prodávajícího. E-mail prodejce je: {0}. +portfolio.pending.step2_buyer.moneyGram.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zašlete autorizační číslo a fotografii s potvrzením e-mailem prodejci XMR.\n\ +Potvrzení musí jasně uvádět celé jméno, zemi, stát a částku prodávajícího. E-mail prodejce je: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Zaplaťte prosím {0} prodejci XMR pomocí Western Union.\n\n -portfolio.pending.step2_buyer.westernUnion.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zašlete prodejci XMR e-mail s MTCN (sledovací číslo) a fotografii s potvrzením o přijetí.\nPotvrzení musí jasně uvádět celé jméno prodávajícího, město, zemi a částku. E-mail prodejce je: {0}. +portfolio.pending.step2_buyer.westernUnion.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zašlete prodejci XMR e-mail s MTCN (sledovací číslo) a fotografii s potvrzením o přijetí.\n\ + Potvrzení musí jasně uvádět celé jméno prodávajícího, město, zemi a částku. E-mail prodejce je: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Zašlete prosím {0} prodejci XMR pomocí \"US Postal Money Order\".\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Zašlete prosím {0} prodejci XMR v poštovní zásilce (\"Pay by Mail\"). Konkrétní instrukce naleznete v obchodní smlouvě. V případě pochybností se můžete zeptat protistrany pomocí obchodního chatu. Více informací naleznete na Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Zašlete prosím {0} prodejci XMR v poštovní zásilce (\"Hotovost poštou\"). \ + Konkrétní instrukce naleznete v obchodní smlouvě. V případě pochybností se můžete zeptat protistrany pomocí obchodního chatu. \ + Více informací naleznete na Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Prosím uhraďte {0} pomocí zvolené platební metody prodejci XMR. V dalším kroku naleznete detaily o účtu prodejce.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -652,21 +719,35 @@ portfolio.pending.step2_buyer.sellersAddress={0} adresa prodejce portfolio.pending.step2_buyer.buyerAccount=Použijte svůj platební účet portfolio.pending.step2_buyer.paymentSent=Platba zahájena portfolio.pending.step2_buyer.warn=Platbu {0} jste ještě neprovedli!\nVezměte prosím na vědomí, že obchod musí být dokončen do {1}. -portfolio.pending.step2_buyer.openForDispute=Neukončili jste platbu!\nMax. doba obchodu uplynula. Obraťte se na mediátora a požádejte o pomoc. +portfolio.pending.step2_buyer.openForDispute=Neukončili jste platbu!\nMax. doba obchodu uplynula, ale platbu stále ještě můžete dokončit.\n\ + Obraťte se na mediátora a požádejte o pomoc. portfolio.pending.step2_buyer.paperReceipt.headline=Odeslali jste papírový doklad prodejci XMR? -portfolio.pending.step2_buyer.paperReceipt.msg=Zapamatujte si:\nMusíte napsat na papírový doklad: NO REFUNDS - bez náhrady.\nPoté ho roztrhněte na 2 části, vytvořte fotografii a odešlete ji na e-mailovou adresu prodejce XMR. +portfolio.pending.step2_buyer.paperReceipt.msg=Zapamatujte si:\n\ + Musíte napsat na papírový doklad: NO REFUNDS - bez náhrady.\n\ + Poté ho roztrhněte na 2 části, vytvořte fotografii a odešlete ji na e-mailovou adresu prodejce XMR. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Odeslat autorizační číslo a účtenku -portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Musíte zaslat autorizační číslo a fotografii dokladu e-mailem prodejci XMR.\nDoklad musí jasně uvádět celé jméno prodávajícího, zemi, stát a částku. E-mail prodejce je: {0}.\n\nOdeslali jste autorizační číslo a smlouvu prodejci? +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Musíte zaslat autorizační číslo a fotografii dokladu e-mailem prodejci XMR.\n\ + Doklad musí jasně uvádět celé jméno prodávajícího, zemi, stát a částku. E-mail prodejce je: {0}.\n\n + Odeslali jste autorizační číslo a smlouvu prodejci? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Pošlete MTCN a účtenku -portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Musíte odeslat MTCN (sledovací číslo) a fotografii dokladu e-mailem prodejci XMR.\nDoklad musí jasně uvádět celé jméno prodávajícího, město, zemi a částku. E-mail prodejce je: {0}.\n\nOdeslali jste MTCN a smlouvu prodejci? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Musíte odeslat MTCN (sledovací číslo) a fotografii dokladu e-mailem prodejci XMR.\n\ + Doklad musí jasně uvádět celé jméno prodávajícího, město, zemi a částku. E-mail prodejce je: {0}.\n\n + Odeslali jste MTCN a smlouvu prodejci? portfolio.pending.step2_buyer.halCashInfo.headline=Pošlete HalCash kód -portfolio.pending.step2_buyer.halCashInfo.msg=Musíte odeslat jak textovou zprávu s kódem HalCash tak i obchodní ID ({0}) prodejci XMR.\nMobilní číslo prodejce je {1}.\n\nPoslali jste kód prodejci? -portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Některé banky mohou ověřovat jméno příjemce. Účty Faster Payments vytvořené u starých klientů Haveno neposkytují jméno příjemce, proto si jej (v případě potřeby) vyžádejte pomocí obchodního chatu. +portfolio.pending.step2_buyer.halCashInfo.msg=Musíte odeslat jak textovou zprávu s kódem HalCash tak i \ + obchodní ID ({0}) prodejci XMR.\nMobilní číslo prodejce je {1}.\n\n + Poslali jste kód prodejci? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Některé banky mohou ověřovat jméno příjemce. \ + Účty Faster Payments vytvořené u starých klientů Haveno neposkytují jméno příjemce, \ + proto si jej (v případě potřeby) vyžádejte pomocí obchodního chatu. portfolio.pending.step2_buyer.confirmStart.headline=Potvrďte, že jste zahájili platbu portfolio.pending.step2_buyer.confirmStart.msg=Zahájili jste platbu {0} vašemu obchodnímu partnerovi? portfolio.pending.step2_buyer.confirmStart.yes=Ano, zahájil jsem platbu portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=Neposkytli jste doklad o platbě -portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=Nezadali jste ID transakce a klíč transakce.\n\nNeposkytnutím těchto údajů nemůže peer použít funkci automatického potvrzení k uvolnění XMR, jakmile bude přijat XMR.\nKromě toho Haveno vyžaduje, aby odesílatel transakce XMR mohl tyto informace poskytnout mediátorovi nebo rozhodci v případě sporu.\nDalší podrobnosti na wiki Haveno: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=Nezadali jste ID transakce a klíč transakce.\n\n\ + Neposkytnutím těchto údajů nemůže peer použít funkci automatického potvrzení k uvolnění XMR, jakmile bude přijat XMR.\n\ + Kromě toho Haveno vyžaduje, aby odesílatel transakce XMR mohl tyto informace poskytnout mediátorovi nebo rozhodci v případě sporu.\n\ + Další podrobnosti na wiki Haveno: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Vstup není 32 bajtová hexadecimální hodnota portfolio.pending.step2_buyer.confirmStart.warningButton=Ignorovat a přesto pokračovat portfolio.pending.step2_seller.waitPayment.headline=Počkejte na platbu @@ -674,9 +755,20 @@ portfolio.pending.step2_seller.f2fInfo.headline=Kontaktní informace kupujícíh portfolio.pending.step2_seller.waitPayment.msg=Vkladová transakce má alespoň jedno potvrzení na blockchainu.\nMusíte počkat, než kupující XMR zahájí platbu {0}. portfolio.pending.step2_seller.warn=Kupující XMR dosud neprovedl platbu {0}.\nMusíte počkat, než zahájí platbu.\nPokud obchod nebyl dokončen dne {1}, bude rozhodce vyšetřovat. portfolio.pending.step2_seller.openForDispute=Kupující XMR ještě nezačal s platbou!\nMax. povolené období pro obchod vypršelo.\nMůžete počkat déle a dát obchodnímu partnerovi více času nebo požádat o pomoc mediátora. +disputeChat.chatWindowTitle=Okno chatu sporu pro obchod s ID ''{0}'' tradeChat.chatWindowTitle=Okno chatu pro obchod s ID ''{0}'' tradeChat.openChat=Otevřít chatovací okno -tradeChat.rules=Můžete komunikovat se svým obchodním partnerem a vyřešit případné problémy s tímto obchodem.\nOdpovídat v chatu není povinné.\nPokud obchodník poruší některé z níže uvedených pravidel, zahajte spor a nahlaste jej mediátorovi nebo rozhodci.\n\nPravidla chatu:\n\t● Neposílejte žádné odkazy (riziko malwaru). Můžete odeslat ID transakce a jméno block exploreru.\n\t● Neposílejte seed slova, soukromé klíče, hesla nebo jiné citlivé informace!\n\t● Nepodporujte obchodování mimo Haveno (bez zabezpečení).\n\t● Nezapojujte se do žádných forem podvodů v oblasti sociálního inženýrství.\n\t● Pokud partner nereaguje a dává přednost nekomunikovat prostřednictvím chatu, respektujte jeho rozhodnutí.\n\t● Soustřeďte konverzaci pouze na obchod. Tento chat není náhradou messengeru.\n\t● Udržujte konverzaci přátelskou a uctivou. +tradeChat.rules=Můžete komunikovat se svým obchodním partnerem a vyřešit případné problémy s tímto obchodem.\n\ + Odpovídat v chatu není povinné.\n\ + Pokud obchodník poruší některé z níže uvedených pravidel, zahajte spor a nahlaste jej mediátorovi nebo rozhodci.\n\n\ + Pravidla chatu:\n\ + \t● Neposílejte žádné odkazy (riziko malwaru). Můžete odeslat ID transakce a jméno block exploreru.\n\ + \t● Neposílejte seed slova, soukromé klíče, hesla nebo jiné citlivé informace!\n\ + \t● Nepodporujte obchodování mimo Haveno (bez zabezpečení).\n\ + \t● Nezapojujte se do žádných forem podvodů v oblasti sociálního inženýrství.\n\ + \t● Pokud partner nereaguje a dává přednost nekomunikovat prostřednictvím chatu, respektujte jeho rozhodnutí.\n\ + \t● Soustřeďte konverzaci pouze na obchod. Tento chat není náhradou messengeru.\n\ + \t● Udržujte konverzaci přátelskou a uctivou. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Nedefinováno @@ -696,25 +788,43 @@ portfolio.pending.step3_buyer.wait.info=Čekání na potvrzení prodejce XMR na portfolio.pending.step3_buyer.wait.msgStateInfo.label=Stav zprávy o zahájení platby portfolio.pending.step3_buyer.warn.part1a=na {0} blockchainu portfolio.pending.step3_buyer.warn.part1b=u vašeho poskytovatele plateb (např. banky) -portfolio.pending.step3_buyer.warn.part2=Prodejce XMR vaši platbu stále nepotvrdil. Zkontrolujte {0}, zda bylo odeslání platby úspěšné. -portfolio.pending.step3_buyer.openForDispute=Prodejce XMR nepotvrdil vaši platbu! Max. období pro uskutečnění obchodu uplynulo. Můžete počkat déle a dát obchodnímu partnerovi více času nebo požádat o pomoc mediátora. +portfolio.pending.step3_buyer.warn.part2=Prodejce XMR vaši platbu stále nepotvrdil. Zkontrolujte {0}, zda \ + bylo odeslání platby úspěšné. +portfolio.pending.step3_buyer.openForDispute=Prodejce XMR nepotvrdil vaši platbu! Max. období pro uskutečnění obchodu uplynulo. \ + Můžete počkat déle a dát obchodnímu partnerovi více času nebo požádat o pomoc mediátora. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Váš obchodní partner potvrdil, že zahájil platbu {0}.\n\n -portfolio.pending.step3_seller.crypto.explorer=ve vašem oblíbeném {0} blockchain exploreru +portfolio.pending.step3_seller.crypto.explorer=ve vašem oblíbeném {0} průzkumníku blockchainu portfolio.pending.step3_seller.crypto.wallet=na vaší {0} peněžence -portfolio.pending.step3_seller.crypto={0}Zkontrolujte prosím {1}, zda transakce na vaši přijímací adresu\n{2}\nmá již dostatečné potvrzení na blockchainu.\nČástka platby musí být {3}\n\nPo zavření vyskakovacího okna můžete zkopírovat a vložit svou {4} adresu z hlavní obrazovky. +portfolio.pending.step3_seller.crypto={0}Zkontrolujte prosím {1}, zda transakce na vaši přijímací adresu\n\ +{2}\n\ +má již dostatečné potvrzení na blockchainu.\nČástka platby musí být {3}\n\n\ +Po zavření vyskakovacího okna můžete zkopírovat a vložit svou {4} adresu z hlavní obrazovky. portfolio.pending.step3_seller.postal={0}Zkontrolujte, zda jste od kupujícího XMR obdrželi {1} přes \"US Postal Money Order\". # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.payByMail={0}Zkontrolujte, zda jste od kupujícího XMR obdrželi {1} přes \"Pay by Mail\". +portfolio.pending.step3_seller.payByMail={0}Zkontrolujte, zda jste od kupujícího XMR obdrželi {1} přes \"Hotovost poštou\". # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.bank=Váš obchodní partner potvrdil, že zahájil platbu {0}.\n\nPřejděte na webovou stránku online bankovnictví a zkontrolujte, zda jste od kupujícího XMR obdrželi {1}. -portfolio.pending.step3_seller.cash=Vzhledem k tomu, že se platba provádí prostřednictvím hotovostního vkladu, musí kupující XMR napsat na papírový doklad \"NO REFUND\", roztrhat ho na 2 části a odeslat vám e-mailem fotografii.\n\nAbyste se vyhnuli riziku zpětného zúčtování, potvrďte pouze, zda jste obdrželi e-mail a zda si jste jisti, že papírový doklad je platný.\nPokud si nejste jisti, {0} -portfolio.pending.step3_seller.moneyGram=Kupující vám musí zaslat e-mailem autorizační číslo a fotografii s potvrzením.\nPotvrzení musí jasně uvádět vaše celé jméno, zemi, stát a částku. Zkontrolujte si prosím váš e-mail, pokud jste obdrželi autorizační číslo.\n\nPo uzavření tohoto vyskakovacího okna se zobrazí jméno a adresa kupujícího XMR pro vyzvednutí peněz z MoneyGram.\n\nPotvrďte příjem až po úspěšném vyzvednutí peněz! -portfolio.pending.step3_seller.westernUnion=Kupující vám musí zaslat MTCN (sledovací číslo) a fotografii s potvrzením e-mailem.\nPotvrzení musí jasně uvádět vaše celé jméno, město, zemi a částku. Zkontrolujte svůj e-mail, pokud jste obdrželi MTCN.\n\nPo zavření tohoto vyskakovacího okna uvidíte jméno a adresu kupujícího XMR pro vyzvednutí peněz z Western Union.\n\nPotvrďte příjem až po úspěšném vyzvednutí peněz! -portfolio.pending.step3_seller.halCash=Kupující vám musí poslat kód HalCash jako textovou zprávu. Kromě toho obdržíte zprávu od HalCash s požadovanými informacemi pro výběr EUR z bankomatu podporujícího HalCash.\n\nPoté, co jste vyzvedli peníze z bankomatu, potvrďte zde přijetí platby! -portfolio.pending.step3_seller.amazonGiftCard=Kupující vám poslal e-mailovou kartu Amazon eGift e-mailem nebo textovou zprávou na váš mobilní telefon. Uplatněte nyní kartu Amazon eGift ve svém účtu Amazon a po přijetí potvrďte potvrzení o platbě. +portfolio.pending.step3_seller.bank=Váš obchodní partner potvrdil, že zahájil platbu {0}.\n\n\ + Přejděte na webovou stránku online bankovnictví a zkontrolujte, zda jste od kupujícího XMR obdrželi {1}. +portfolio.pending.step3_seller.cash=Vzhledem k tomu, že se platba provádí prostřednictvím hotovostního vkladu, musí kupující XMR napsat na papírový doklad \"NO REFUND\", roztrhat ho na 2 části a odeslat vám e-mailem fotografii.\n\n\ +Abyste se vyhnuli riziku zpětného zúčtování, potvrďte pouze, zda jste obdrželi e-mail a zda si jste jisti, že papírový doklad je platný.\n\ +Pokud si nejste jisti, {0} +portfolio.pending.step3_seller.moneyGram=Kupující vám musí zaslat e-mailem autorizační číslo a fotografii s potvrzením.\n\ + Potvrzení musí jasně uvádět vaše celé jméno, zemi, stát a částku. Zkontrolujte si prosím váš e-mail, pokud jste obdrželi autorizační číslo.\n\n\ + Po uzavření tohoto vyskakovacího okna se zobrazí jméno a adresa kupujícího XMR pro vyzvednutí peněz z MoneyGram.\n\n\ + Potvrďte příjem až po úspěšném vyzvednutí peněz! +portfolio.pending.step3_seller.westernUnion=Kupující vám musí zaslat MTCN (sledovací číslo) a fotografii s potvrzením e-mailem.\n\ + Potvrzení musí jasně uvádět vaše celé jméno, město, zemi a částku. Zkontrolujte svůj e-mail, pokud jste obdrželi MTCN.\n\n\ + Po zavření tohoto vyskakovacího okna uvidíte jméno a adresu kupujícího XMR pro vyzvednutí peněz z Western Union.\n\n\ + Potvrďte příjem až po úspěšném vyzvednutí peněz! +portfolio.pending.step3_seller.halCash=Kupující vám musí poslat kód HalCash jako textovou zprávu. Kromě toho obdržíte zprávu od HalCash s požadovanými informacemi pro výběr EUR z bankomatu podporujícího HalCash.\n\n\ + Poté, co jste vyzvedli peníze z bankomatu, potvrďte zde přijetí platby! +portfolio.pending.step3_seller.amazonGiftCard=Kupující vám poslal e-mailovou kartu Amazon eGift e-mailem nebo textovou zprávou \ + na váš mobilní telefon. Uplatněte nyní kartu Amazon eGift ve svém účtu Amazon \ + a po přijetí potvrďte potvrzení o platbě. -portfolio.pending.step3_seller.bankCheck=\n\nOvěřte také, zda se jméno odesílatele uvedené v obchodní smlouvě shoduje s jménem uvedeným na výpisu z účtu:\nJméno odesílatele podle obchodní smlouvy: {0}\n\nPokud jména nejsou úplně stejná, {1} +portfolio.pending.step3_seller.bankCheck=\n\nOvěřte také, zda se jméno odesílatele uvedené v obchodní smlouvě shoduje s jménem uvedeným na výpisu z účtu:\nJméno odesílatele podle obchodní smlouvy: {0}\n\n\ + Pokud jména nejsou úplně stejná, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=nepotvrzujte příjem platby. Místo toho otevřete spor stisknutím \"alt + o\" nebo \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Potvrďte příjem platby @@ -731,8 +841,10 @@ portfolio.pending.step3_seller.buyerStartedPayment.crypto=Podívejte se na potvr portfolio.pending.step3_seller.buyerStartedPayment.traditional=Zkontrolujte na svém obchodním účtu (např. Bankovní účet) a potvrďte, kdy jste platbu obdrželi. portfolio.pending.step3_seller.warn.part1a=na {0} blockchainu portfolio.pending.step3_seller.warn.part1b=u vašeho poskytovatele plateb (např. banky) -portfolio.pending.step3_seller.warn.part2=Stále jste nepotvrdili přijetí platby. Zkontrolujte {0}, zda jste obdrželi platbu. -portfolio.pending.step3_seller.openForDispute=Nepotvrdili jste příjem platby!\nUplynulo max. období obchodu.\nPotvrďte nebo požádejte o pomoc mediátora. +portfolio.pending.step3_seller.warn.part2=Stále jste nepotvrdili přijetí platby. \ + Zkontrolujte {0}, zda jste obdrželi platbu. +portfolio.pending.step3_seller.openForDispute=Nepotvrdili jste příjem platby!\n\ + Uplynulo max. období obchodu.\nPotvrďte nebo požádejte o pomoc mediátora. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Obdrželi jste od svého obchodního partnera platbu v měně {0}?\n\n # suppress inspection "TrailingSpacesInProperty" @@ -741,23 +853,20 @@ portfolio.pending.step3_seller.onPaymentReceived.name=Ověřte také, zda se jm portfolio.pending.step3_seller.onPaymentReceived.note=Vezměte prosím na vědomí, že jakmile potvrdíte příjem, dosud uzamčený obchodovaný XMR bude uvolněn kupujícímu a kauce bude vrácena.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Potvrďte, že jste obdržel(a) platbu portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Ano, obdržel(a) jsem platbu -portfolio.pending.step3_seller.onPaymentReceived.signer=DŮLEŽITÉ: Potvrzením přijetí platby ověřujete také účet protistrany a odpovídajícím způsobem jej podepisujete. Protože účet protistrany dosud nebyl podepsán, měli byste odložit potvrzení platby co nejdéle, abyste snížili riziko zpětného zúčtování. +portfolio.pending.step3_seller.onPaymentReceived.signer=DŮLEŽITÉ: Potvrzením přijetí platby ověřujete také \ + účet protistrany a odpovídajícím způsobem jej podepisujete. Protože účet protistrany dosud nebyl podepsán, \ + měli byste odložit potvrzení platby co nejdéle, abyste snížili riziko zpětného zúčtování. portfolio.pending.step5_buyer.groupTitle=Shrnutí dokončeného obchodu +portfolio.pending.step5_buyer.groupTitle.mediated=Tento obchod byl vyřešen pomocí mediátora +portfolio.pending.step5_buyer.groupTitle.arbitrated=Tento obchod byl vyřešen pomocí rozhodce portfolio.pending.step5_buyer.tradeFee=Obchodní poplatek portfolio.pending.step5_buyer.makersMiningFee=Poplatek za těžbu portfolio.pending.step5_buyer.takersMiningFee=Celkové poplatky za těžbu portfolio.pending.step5_buyer.refunded=Vrácená kauce -portfolio.pending.step5_buyer.withdrawXMR=Vyberte své moneroy -portfolio.pending.step5_buyer.amount=Částka k výběru -portfolio.pending.step5_buyer.withdrawToAddress=Adresa výběru -portfolio.pending.step5_buyer.moveToHavenoWallet=Uchovat prostředky v peněžence Haveno -portfolio.pending.step5_buyer.withdrawExternal=Vybrat do externí peněženky -portfolio.pending.step5_buyer.alreadyWithdrawn=Vaše finanční prostředky již byly vybrány.\nZkontrolujte historii transakcí. -portfolio.pending.step5_buyer.confirmWithdrawal=Potvrďte žádost o výběr portfolio.pending.step5_buyer.amountTooLow=Částka k převodu je nižší než transakční poplatek a min. možná hodnota tx (drobné). -portfolio.pending.step5_buyer.withdrawalCompleted.headline=Výběr byl dokončen -portfolio.pending.step5_buyer.withdrawalCompleted.msg=Vaše dokončené obchody jsou uloženy na \"Portfolio/Historie\".\nVšechny své transakce Monero si můžete prohlédnout v sekci \"Finance/Transakce\" +portfolio.pending.step5_buyer.tradeCompleted.headline=Obchod dokončen +portfolio.pending.step5_buyer.tradeCompleted.msg=Vaše dokončené obchody jsou uchovávány pod \"Portfolio/Historie\".\nVšechny své monero transakce najdete pod \"Prostředky/Transakce\" portfolio.pending.step5_buyer.bought=Koupili jste portfolio.pending.step5_buyer.paid=Zaplatili jste @@ -777,25 +886,35 @@ portfolio.pending.tradePeriodInfo=Po prvním potvrzení na blockchainu začíná portfolio.pending.tradePeriodWarning=Pokud je tato lhůta překročena, mohou oba obchodníci zahájit spor. portfolio.pending.tradeNotCompleted=Obchod nebyl dokončen včas (do {0}) portfolio.pending.tradeProcess=Obchodní proces +portfolio.pending.stillNotResolved=Pokud váš problém zůstává nevyřešen, můžete požádat o podporu v naší [Matrix místnosti](https://matrix.to/#/#haveno:monero.social). + portfolio.pending.openAgainDispute.msg=Pokud si nejste jisti, že zpráva pro mediátora nebo rozhodce dorazila (např. Pokud jste nedostali odpověď po 1 dni), neváhejte znovu zahájit spor s Cmd/Ctrl+o. Můžete také požádat o další pomoc na fóru Haveno na adrese [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=Otevřete spor znovu portfolio.pending.openSupportTicket.headline=Otevřít úkol pro podporu -portfolio.pending.openSupportTicket.msg=Použijte tuto funkci pouze v naléhavých případech, pokud nevidíte tlačítko \"Otevřít podporu\" nebo \"Otevřít spor\".\n\nKdyž otevřete dotaz podporu, obchod bude přerušen a zpracován mediátorem nebo rozhodcem. +portfolio.pending.openSupportTicket.msg=Použijte tuto funkci pouze v naléhavých případech, \ + pokud nevidíte tlačítko \"Otevřít podporu\" nebo \"Otevřít spor\".\n\nKdyž otevřete úkol pro podporu, obchod bude přerušen \ + a zpracován mediátorem nebo rozhodcem. portfolio.pending.timeLockNotOver=Než budete moci zahájit rozhodčí spor, musíte počkat do ≈{0} ({1} dalších bloků). -portfolio.pending.error.depositTxNull=Vkladová operace je nulová. Nemůžete otevřít spor bez platné vkladové transakce. Přejděte do \"Nastavení/Informace o síti\" a proveďte resynchronizaci SPV.\n\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. -portfolio.pending.mediationResult.error.depositTxNull=Vkladová transakce je nulová. Obchod můžete přesunout do neúspěšných obchodů. -portfolio.pending.mediationResult.error.delayedPayoutTxNull=Odložená výplatní transakce je nulová. Obchod můžete přesunout do neúspěšných obchodů. -portfolio.pending.error.depositTxNotConfirmed=Vkladová transakce není potvrzena. Nemůžete zahájit rozhodčí spor s nepotvrzenou vkladovou transakcí. Počkejte prosím, až bude potvrzena, nebo přejděte do \"Nastavení/Informace o síti\" a proveďte resynchronizaci SPV.\n\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. +portfolio.pending.error.depositTxNull=Vkladová operace je nulová. Nemůžete otevřít spor v případě \ + neplatné vkladové transakce.\n\n\ + Pro další pomoc kontaktujte podporu Haveno pomocí naší místnosti na Matrixu. +portfolio.pending.mediationResult.error.depositTxNull=Vkladová transakce je nulová. Obchod můžete přesunout \ + do neúspěšných obchodů. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=Zpožděný výplatní transakce je nulová. Obchod můžete přesunout \ + do neúspěšných obchodů. +portfolio.pending.error.depositTxNotConfirmed=Vkladová transakce není potvrzena. Nemůžete zahájit rozhodčí spor s nepotvrzenou vkladovou transakcí. \ + Počkejte prosím, až bude potvrzena a dostupná.\n\n\ + Pro další pomoc kontaktujte podporu Haveno pomocí naší místnosti na Matrixu. portfolio.pending.support.headline.getHelp=Potřebujete pomoc? -portfolio.pending.support.text.getHelp=Pokud máte nějaké problémy, můžete zkusit kontaktovat obchodníka v obchodním chatu nebo požádat komunitu Haveno na adrese https://haveno.community. Pokud váš problém stále není vyřešen, můžete požádat mediátora o další pomoc. portfolio.pending.support.button.getHelp=Otevřít obchodní chat portfolio.pending.support.headline.halfPeriodOver=Zkontrolujte platbu portfolio.pending.support.headline.periodOver=Obchodní období skončilo -portfolio.pending.mediationRequested=Mediace požádána -portfolio.pending.refundRequested=Požadováno vrácení peněz +portfolio.pending.arbitrationRequested=Požádáno o arbitráž +portfolio.pending.mediationRequested=Požádáno o mediaci +portfolio.pending.refundRequested=Požádáno o vrácení peněz portfolio.pending.openSupport=Otevřít úkol pro podporu portfolio.pending.supportTicketOpened=Úkol pro podporu otevřen portfolio.pending.communicateWithArbitrator=Komunikujte prosím na obrazovce \"Podpora\" s rozhodcem. @@ -811,24 +930,84 @@ portfolio.pending.mediationResult.info.peerAccepted=Váš obchodní partner při portfolio.pending.mediationResult.button=Zobrazit navrhované řešení portfolio.pending.mediationResult.popup.headline=Výsledek mediace obchodu s ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Váš obchodní partner přijal návrh mediátora na obchod {0} -portfolio.pending.mediationResult.popup.info=Mediátor navrhl následující výplatu:\nObdržíte: {0}\nVáš obchodní partner obdrží: {1}\n\nTuto navrhovanou výplatu můžete přijmout nebo odmítnout.\n\nPřijetím podepíšete navrhovanou výplatní transakci. Pokud váš obchodní partner také přijme a podepíše, výplata bude dokončena a obchod bude uzavřen.\n\nPokud jeden nebo oba odmítnete návrh, budete muset počkat do {2} (blok {3}), abyste zahájili spor druhého kola s rozhodcem, který případ znovu prošetří a na základě svých zjištění provede výplatu.\n\nRozhodce může jako náhradu za svou práci účtovat malý poplatek (maximální poplatek: bezpečnostní záloha obchodníka). Oba obchodníci, kteří souhlasí s návrhem zprostředkovatele, jsou na dobré cestě - žádost o arbitráž je určena pro výjimečné okolnosti, například pokud je obchodník přesvědčen, že zprostředkovatel neučinil návrh na spravedlivou výplatu (nebo pokud druhý partner nereaguje).\n\nDalší podrobnosti o novém rozhodčím modelu: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Přijali jste výplatu navrženou mediátorem, ale zdá se, že váš obchodní partner ji nepřijal.\n\nPo uplynutí doby uzamčení na {0} (blok {1}) můžete zahájit spor druhého kola s rozhodcem, který případ znovu prošetří a na základě jeho zjištění provede platbu.\n\nDalší podrobnosti o rozhodčím modelu najdete na adrese: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.info=Mediátor navrhl následující výplatu:\n\ + Obdržíte: {0}\n\ + Váš obchodní partner obdrží: {1}\n\n\ + Tuto navrhovanou výplatu můžete přijmout nebo odmítnout.\n\n\ + Přijetím podepíšete navrhovanou výplatní transakci. \ + Pokud váš obchodní partner také přijme a podepíše, výplata bude dokončena a obchod bude uzavřen.\n\n\ + Pokud jeden nebo oba odmítnete návrh, budete muset počkat do {2} (blok {3}), abyste zahájili spor \ + druhého kola s rozhodcem, který případ znovu prošetří a na základě svých zjištění provede výplatu.\n\n\ + Rozhodce může jako náhradu za svou práci účtovat malý poplatek (maximální poplatek: bezpečnostní záloha obchodníka). \ + Oba obchodníci, kteří souhlasí s návrhem zprostředkovatele, jsou na dobré cestě - žádost o arbitráž je určena pro \ + výjimečné okolnosti, například pokud je obchodník přesvědčen, že zprostředkovatel neučinil návrh na spravedlivou výplatu \ + (nebo pokud druhý partner nereaguje).\n\n\ + Další podrobnosti o novém rozhodčím modelu: [HYPERLINK:https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Přijali jste výplatu navrženou mediátorem, \ + ale zdá se, že váš obchodní partner ji nepřijal.\n\n\ + Po uplynutí doby uzamčení na {0} (blok {1}) můžete zahájit spor druhého kola s rozhodcem, který případ \ + znovu prošetří a na základě jeho zjištění provede platbu.\n\n\ + Další podrobnosti o rozhodčím modelu najdete na adrese:\ + [https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] portfolio.pending.mediationResult.popup.openArbitration=Odmítnout a požádat o arbitráž portfolio.pending.mediationResult.popup.alreadyAccepted=Už jste přijali -portfolio.pending.failedTrade.taker.missingTakerFeeTx=Chybí poplatek příjemce transakce.\n\nBez tohoto tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky a nebyl zaplacen žádný obchodní poplatek. Tento obchod můžete přesunout do neúspěšných obchodů. -portfolio.pending.failedTrade.maker.missingTakerFeeTx=Chybí poplatek příjemce transakce.\n\nBez tohoto tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky. Vaše nabídka je stále k dispozici dalším obchodníkům, takže jste neztratili poplatek za vytvoření. Tento obchod můžete přesunout do neúspěšných obchodů. -portfolio.pending.failedTrade.missingDepositTx=Vkladová transakce (transakce 2-of-2 multisig) chybí.\n\nBez tohoto tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky, ale byl zaplacen váš obchodní poplatek. Zde můžete požádat o vrácení obchodního poplatku: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nKlidně můžete přesunout tento obchod do neúspěšných obchodů. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Odložená výplatní transakce chybí, ale prostředky byly uzamčeny v vkladové transakci.\n\nNezasílejte prosím fiat nebo crypto platbu prodejci XMR, protože bez odložené platby tx nelze zahájit arbitráž. Místo toho otevřete mediační úkol pomocí Cmd/Ctrl+o. Mediátor by měl navrhnout, aby oba partneři dostali zpět celou částku svých bezpečnostních vkladů (přičemž prodejce také obdrží plnou částku obchodu). Tímto způsobem nehrozí žádné bezpečnostní riziko a jsou ztraceny pouze obchodní poplatky.\n\nO vrácení ztracených obchodních poplatků můžete požádat zde: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Odložená výplatní transakce chybí, ale prostředky byly v depozitní transakci uzamčeny.\n\nPokud kupujícímu chybí také odložená výplatní transakce, bude poučen, aby platbu NEPOSLAL a místo toho otevřel mediační úkol. Měli byste také otevřít mediační úkol pomocí Cmd/Ctrl+o.\n\nPokud kupující ještě neposlal platbu, měl by zprostředkovatel navrhnout, aby oba partneři dostali zpět celou částku svých bezpečnostních vkladů (přičemž prodejce také obdrží plnou částku obchodu). Jinak by částka obchodu měla jít kupujícímu.\n\nO vrácení ztracených obchodních poplatků můžete požádat zde: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=Během provádění obchodního protokolu došlo k chybě.\n\nChyba: {0}\n\nJe možné, že tato chyba není kritická a obchod lze dokončit normálně. Pokud si nejste jisti, otevřete si mediační úkol a získejte radu od mediátorů Haveno.\n\nPokud byla chyba kritická a obchod nelze dokončit, možná jste ztratili obchodní poplatek. O vrácení ztracených obchodních poplatků požádejte zde: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=Obchodní kontrakt není stanoven.\n\nObchod nelze dokončit a možná jste ztratili poplatek za obchodování. Pokud ano, můžete požádat o vrácení ztracených obchodních poplatků zde: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.taker.missingTakerFeeTx=Chybí poplatek příjemce transakce.\n\n\ + Bez tohoto tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky a nebyl zaplacen žádný obchodní poplatek. \ + Tento obchod můžete přesunout do neúspěšných obchodů. +portfolio.pending.failedTrade.maker.missingTakerFeeTx=Chybí poplatek příjemce transakce.\n\n\ + Bez tohoto tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky. Vaše nabídka je \ + stále k dispozici dalším obchodníkům, takže jste neztratili poplatek za vytvoření. \ + Tento obchod můžete přesunout do neúspěšných obchodů. +portfolio.pending.failedTrade.missingDepositTx=Vkladová transakce (transakce 2-of-2 multisig) chybí.\n\n\ + Bez této tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky, ale byl zaplacen váš obchodní poplatek. \ + Zde můžete požádat o vrácení obchodního poplatku: \ + [HYPERLINK:https://github.com/bisq-network/support/issues]\n\n\ + Klidně můžete přesunout tento obchod do neúspěšných obchodů. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Zpožděná výplatní transakce chybí, + ale prostředky byly uzamčeny v vkladové transakci.\n\n\ + Nezasílejte prosím fiat nebo crypto platbu prodejci XMR, protože bez odložené platby tx nelze zahájit arbitráž. \ + Místo toho otevřete mediační úkol pomocí Cmd/Ctrl+o. \ + Mediátor by měl navrhnout, aby oba partneři dostali zpět celou částku svých bezpečnostních vkladů \ + (přičemž prodejce také obdrží plnou částku obchodu). \ + Tímto způsobem nehrozí žádné bezpečnostní riziko a jsou ztraceny pouze obchodní poplatky.\n\n\ + O vrácení ztracených obchodních poplatků můžete požádat zde: \ + [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Zpožděná výplatní transakce chybí, \ + ale prostředky byly v depozitní transakci uzamčeny.\n\n\ + Pokud kupujícímu chybí také odložená výplatní transakce, bude poučen, aby platbu NEPOSLAL a místo toho otevřel \ + mediační úkol. Měli byste také otevřít mediační úkol pomocí Cmd/Ctrl+o.\n\n\ + Pokud kupující ještě neposlal platbu, měl by mediátor navrhnout, aby oba partneři dostali zpět celou částku \ + svých bezpečnostních vkladů (přičemž prodejce také obdrží plnou částku obchodu). \ + Jinak by částka obchodu měla jít kupujícímu.\n\n\ + O vrácení ztracených obchodních poplatků můžete požádat zde: \ + [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.errorMsgSet=Během provádění obchodního protokolu došlo k chybě.\n\n + Chyba: {0}\n\n\ + Je možné, že tato chyba není kritická a obchod lze dokončit normálně. Pokud si nejste jisti, otevřete si mediační úkol \ + a získejte radu od mediátorů Haveno.\n\n\ + Pokud byla chyba kritická a obchod nelze dokončit, možná jste ztratili obchodní poplatek. \ + O vrácení ztracených obchodních poplatků požádejte zde: \ + [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingContract=Obchodní kontrakt není stanoven.\n\n\ +Obchod nelze dokončit a možná jste ztratili poplatek \ + za obchodování. Pokud ano, můžete požádat o vrácení ztracených obchodních poplatků zde: \ + [HYPERLINK:https://github.com/bisq-network/support/issues] portfolio.pending.failedTrade.info.popup=Obchodní protokol narazil na některé problémy.\n\n{0} -portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Obchodní protokol narazil na vážný problém.\n\n{0}\n\nChcete obchod přesunout do neúspěšných obchodů?\n\nZ obrazovky neúspěšných obchodů nemůžete otevřít mediaci nebo arbitráž, ale můžete kdykoli přesunout neúspěšný obchod zpět na obrazovku otevřených obchodů. -portfolio.pending.failedTrade.txChainValid.moveToFailed=Obchodní protokol narazil na některé problémy.\n\n{0}\n\nObchodní transakce byly zveřejněny a finanční prostředky jsou uzamčeny. Přesuňte obchod do neúspěšných obchodů, pouze pokud jste si opravdu jisti. Může to bránit možnostem řešení problému.\n\nChcete obchod přesunout do neúspěšných obchodů?\n\nZ obrazovky neúspěšných obchodů nemůžete otevřít mediaci nebo arbitráž, ale můžete kdykoli přesunout neúspěšný obchod zpět na obrazovku otevřených obchodů. +portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Obchodní protokol narazil na vážný problém.\n\n{0}\n\n\ + Chcete obchod přesunout do neúspěšných obchodů?\n\n\ +Z obrazovky neúspěšných obchodů nemůžete otevřít mediaci nebo arbitráž, ale můžete kdykoli přesunout neúspěšný obchod zpět \ + na obrazovku otevřených obchodů. +portfolio.pending.failedTrade.txChainValid.moveToFailed=Obchodní protokol narazil na některé problémy.\n\n{0}\n\n\ + Obchodní transakce byly zveřejněny a finanční prostředky jsou uzamčeny. Přesuňte obchod do neúspěšných obchodů, \ + pouze pokud jste si opravdu jisti. Může to bránit možnostem řešení problému.\n\n\ + Chcete obchod přesunout do neúspěšných obchodů?\n\n\ + Z obrazovky neúspěšných obchodů nemůžete otevřít mediaci nebo arbitráž, ale můžete kdykoli přesunout neúspěšný obchod \ + zpět na obrazovku otevřených obchodů. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Přesuňte obchod do neúspěšných obchodů portfolio.pending.failedTrade.warningIcon.tooltip=Kliknutím otevřete podrobnosti o problémech tohoto obchodu portfolio.failed.revertToPending.popup=Chcete přesunout tento obchod do otevřených obchodů? +portfolio.failed.revertToPending.failed=Selhal přesun tohoto obchodu do otevřených obchodů. portfolio.failed.revertToPending=Přesunout obchod do otevřených obchodů portfolio.closed.completed=Dokončeno @@ -836,11 +1015,18 @@ portfolio.closed.ticketClosed=Rozhodnuto portfolio.closed.mediationTicketClosed=Mediováno portfolio.closed.canceled=Zrušeno portfolio.failed.Failed=Selhalo -portfolio.failed.unfail=Před pokračováním se ujistěte, že máte zálohu vašeho datového adresáře!\nChcete tento obchod přesunout zpět do otevřených obchodů?\nJe to způsob, jak odemknout finanční prostředky uvízlé v neúspěšném obchodu. -portfolio.failed.cantUnfail=Tento obchod nelze v tuto chvíli přesunout zpět do otevřených obchodů.\nZkuste to znovu po dokončení obchodu (obchodů) {0} +portfolio.failed.unfail=Před pokračováním se ujistěte, že máte zálohu vašeho datového adresáře!\n\ + Chcete tento obchod přesunout zpět do otevřených obchodů?\n\ + Je to způsob, jak odemknout finanční prostředky uvízlé v neúspěšném obchodu. +portfolio.failed.cantUnfail=Tento obchod nelze v tuto chvíli přesunout zpět do otevřených obchodů.\n\ + Zkuste to znovu po dokončení obchodu (obchodů) {0} portfolio.failed.depositTxNull=Obchod nelze změnit zpět na otevřený obchod. Transakce s vkladem je neplatná. -portfolio.failed.delayedPayoutTxNull=Obchod nelze změnit zpět na otevřený obchod. Odložená výplatní transakce je nulová. - +portfolio.failed.delayedPayoutTxNull=Obchod nelze změnit zpět na otevřený obchod. Zpožděná výplatní transakce je nulová. +portfolio.failed.penalty.msg=Toto strhne {0}/{1} poplatek penále {2} a vrátí zbytek obchodovaných financí do jejich peněženky. Jste si jisti, že chcete odeslat?\n\n\ + Jiné info:\n\ + Transakční poplatek: {3}\n\ + Rezervní hash Tx: {4} +portfolio.failed.error.msg=Záznam obchodu neexistuje. #################################################################### # Funds @@ -854,9 +1040,12 @@ funds.tab.transactions=Transakce funds.deposit.unused=Nepoužito funds.deposit.usedInTx=Používá se v {0} transakcích +funds.deposit.baseAddress=Základní adresa +funds.deposit.offerFunding=Rezervováno pro financování nabídky ({0}) +funds.deposit.tradePayout=Rezervováno pro výplatu obchodu ({0}) funds.deposit.fundHavenoWallet=Financovat Haveno peněženku funds.deposit.noAddresses=Dosud nebyly vygenerovány žádné adresy pro vklad -funds.deposit.fundWallet=Financujte svou peněženku +funds.deposit.fundWallet=Financovat peněženku funds.deposit.withdrawFromWallet=Pošlete peníze z peněženky funds.deposit.amount=Částka v XMR (volitelná) funds.deposit.generateAddress=Vygenerujte novou adresu @@ -868,11 +1057,13 @@ funds.withdrawal.inputs=Volba vstupů funds.withdrawal.useAllInputs=Použijte všechny dostupné vstupy funds.withdrawal.useCustomInputs=Použijte vlastní vstupy funds.withdrawal.receiverAmount=Částka pro příjemce +funds.withdrawal.sendMax=Poslat max. dostupné funds.withdrawal.senderAmount=Náklad pro odesílatele funds.withdrawal.feeExcluded=Částka nezahrnuje poplatek za těžbu funds.withdrawal.feeIncluded=Částka zahrnuje poplatek za těžbu funds.withdrawal.fromLabel=Výběr z adresy funds.withdrawal.toLabel=Adresa příjemce +funds.withdrawal.maximum=MAX funds.withdrawal.memoLabel=Poznámka k výběru funds.withdrawal.memo=Volitelně vyplňte poznámku funds.withdrawal.withdrawButton=Odeslat výběr @@ -885,7 +1076,14 @@ funds.withdrawal.selectAddress=Vyberte zdrojovou adresu z tabulky funds.withdrawal.setAmount=Nastavte částku k výběru funds.withdrawal.fillDestAddress=Vyplňte svou cílovou adresu funds.withdrawal.warn.noSourceAddressSelected=Ve výše uvedené tabulce musíte vybrat zdrojovou adresu. -funds.withdrawal.warn.amountExceeds=Na vybrané adrese nemáte dostatek prostředků.\nZvažte výběr více adres ve výše uvedené tabulce nebo změňte přepínač poplatků tak, aby zahrnoval poplatek za těžbu. +funds.withdrawal.warn.amountExceeds=Na vybrané adrese nemáte dostatek prostředků.\n\ + Zvažte výběr více adres ve výše uvedené tabulce nebo změňte přepínač poplatků tak, aby zahrnoval poplatek za těžbu. +funds.withdrawal.warn.amountMissing=Zadejte částku k vybrání +funds.withdrawal.txFee=Poplatek transakce výběru (satoshi/vbyte) +funds.withdrawal.useCustomFeeValueInfo=Zadejte vlastní hodnotu poplatku transakce +funds.withdrawal.useCustomFeeValue=Použít vlastní hodnotu +funds.withdrawal.txFeeMin=Poplatek transakce musí být alespoň {0} satoshi/vbyte +funds.withdrawal.txFeeTooLarge=Vámi zadaná hodnota je nad rozumnou hodnotou (>5000 satoshi/vbyte). Poplatek transakce je obvykle v rozmezí 50-400 satoshi/vbyte. funds.reserved.noFunds=V otevřených nabídkách nejsou rezervovány žádné finanční prostředky funds.reserved.reserved=Rezervováno v místní peněžence pro nabídku s ID: {0} @@ -915,18 +1113,36 @@ funds.tx.revert=Vrátit funds.tx.txSent=Transakce byla úspěšně odeslána na novou adresu v lokální peněžence Haveno. funds.tx.direction.self=Posláno sobě funds.tx.dustAttackTx=Přijaté drobné -funds.tx.dustAttackTx.popup=Tato transakce odesílá do vaší peněženky velmi malou částku XMR a může se jednat o pokus společností provádějících analýzu blockchainu o špehování vaší peněženky.\n\nPoužijete-li tento transakční výstup ve výdajové transakci, zjistí, že jste pravděpodobně také vlastníkem jiné adresy (sloučení mincí).\n\nKvůli ochraně vašeho soukromí ignoruje peněženka Haveno takové drobné výstupy pro účely utrácení a na obrazovce zůstatku. Můžete nastavit hodnotu "drobnosti", kdy je výstup považován za drobné, v nastavení. +funds.tx.dustAttackTx.popup=Tato transakce odesílá do vaší peněženky velmi malou částku XMR a může se jednat o pokus \ + společností provádějících analýzu blockchainu o špehování vaší peněženky.\n\n\ + Použijete-li tento transakční výstup ve výdajové transakci, zjistí, že jste pravděpodobně také vlastníkem \ + jiné adresy (sloučení mincí).\n\n\ + Kvůli ochraně vašeho soukromí ignoruje peněženka Haveno takové drobné výstupy pro účely utrácení a na obrazovce zůstatku. \ + V nastavení můžete nastavit prahovou hodnotu, při které je výstup považován za drobné(dust). #################################################################### # Support #################################################################### support.tab.mediation.support=Mediace +support.tab.refund.support=Vrácení peněz support.tab.arbitration.support=Arbitráž support.tab.legacyArbitration.support=Starší arbitráž support.tab.ArbitratorsSupportTickets=Úkoly pro {0} support.filter=Hledat spory support.filter.prompt=Zadejte ID obchodu, datum, onion adresu nebo údaje o účtu +support.tab.SignedOffers=Podepsané nabídky +support.prompt.signedOffer.penalty.msg=Tím se tvůrci účtuje sankční poplatek a zbývající prostředky z obchodu se vrátí do jeho peněženky. Jste si jisti, že chcete odeslat?\n\n\ + ID nabídky: {0}\n\ + Poplatek penále tvůrce: {1}\n\ + Rezervní poplatek těžby Tx: {2}\n\ + Rezervní hash Tx: {3}\n\ + Rezervní klíčové obrázky Tx: {4}\n\ + +support.contextmenu.penalize.msg=Penalizovat {0} zveřejněním tx rezervy +support.prompt.signedOffer.error.msg=Podepsaný záznam nabídky neexistuje; kontaktujte správce. +support.info.submitTxHex=Rezervní transakce byla zveřejněna s tímto výsledkem:\n +support.result.success=Transakce hex byla úspěšně zadána. support.sigCheck.button=Ověřit podpis support.sigCheck.popup.info=Vložte souhrnnou zprávu procesu zprostředkování. S tímto nástrojem může každý uživatel zkontrolovat, zda se podpis zprostředkovatele shoduje se souhrnnou zprávou. @@ -939,6 +1155,7 @@ support.sigCheck.popup.failed=Ověření podpisu selhalo support.sigCheck.popup.invalidFormat=Zpráva nemá očekávaný formát. Zkopírujte a vložte souhrnnou zprávu ze sporu. support.reOpenByTrader.prompt=Opravdu chcete spor znovu otevřít? +support.reOpenByTrader.failed=Opětovné otevření sporu selhalo. support.reOpenButton.label=Znovu otevřít support.sendNotificationButton.label=Soukromé oznámení support.reportButton.label=Zpráva @@ -948,7 +1165,9 @@ support.sendingMessage=Odesílání zprávy... support.receiverNotOnline=Příjemce není online. Zpráva je uložena v jejich schránce. support.sendMessageError=Odeslání zprávy se nezdařilo. Chyba: {0} support.receiverNotKnown=Příjemce není znám -support.wrongVersion=Nabídka v tomto sporu byla vytvořena se starší verzí Haveno.\nTento spor nemůžete ukončit s touto verzí aplikace.\n\nPoužijte prosím starší verzi s verzí protokolu {0} +support.wrongVersion=Nabídka v tomto sporu byla vytvořena se starší verzí Haveno.\n\ +Tento spor nemůžete ukončit s touto verzí aplikace.\n\n\ +Použijte prosím starší verzi s verzí protokolu {0} support.openFile=Otevřete soubor, který chcete připojit (maximální velikost souboru: {0} kb) support.attachmentTooLarge=Celková velikost vašich příloh je {0} kb a překračuje maximální povolenou velikost zprávy {1} kB. support.maxSize=Max. povolená velikost souboru je {0} kB. @@ -963,24 +1182,71 @@ support.closeTicket=Zavřít úkol support.attachments=Přílohy: support.savedInMailbox=Zpráva uložena ve schránce příjemce support.arrived=Zpráva dorazila k příjemci +support.transient=Zpráva je na cestě k příjemci support.acknowledged=Přijetí zprávy potvrzeno příjemcem support.error=Příjemce nemohl zpracovat zprávu. Chyba: {0} +support.errorTimeout=vypršení platnosti. Zkuste zprávu odeslat znovu. support.buyerAddress=Adresa kupujícího XMR support.sellerAddress=Adresa prodejce XMR support.role=Role support.agent=Agent podpory support.state=Stav support.chat=Chat +support.requested=Požádáno support.closed=Zavřeno -support.open=Otevřené +support.open=Otevřeno +support.moreButton=VÍCE... +support.sendLogFiles=Odeslat soubory logů +support.uploadTraderChat=Nahrát obchodní chat support.process=Rozhodnout support.buyerMaker=Kupující XMR/Tvůrce support.sellerMaker=Prodejce XMR/Tvůrce support.buyerTaker=Kupující XMR/Příjemce support.sellerTaker=Prodávající XMR/Příjemce +support.sendLogs.title=Odeslat logy +support.sendLogs.backgroundInfo=Pokud se vyskytne chyba, rozhodčí a pracovníci podpory si často vyžádají kopie souborů log, aby mohli problém prozkoumat.\n\n\ + Po stisku 'Odeslat' dojde ke kompresi a odeslání logů přímo rozhodci. +support.sendLogs.step1=Vytvořit archiv Zip s log soubory +support.sendLogs.step2=Požadavek připojení k rozhodci +support.sendLogs.step3=Nahrát archivovaná data logů +support.sendLogs.send=Odeslat +support.sendLogs.cancel=Zrušit +support.sendLogs.init=Zavádění +support.sendLogs.retry=Opakování odeslání +support.sendLogs.stopped=Přenos zastaven +support.sendLogs.progress=Průběh přenosu: %.0f%% +support.sendLogs.finished=Přenos dokončen! +support.sendLogs.command=Stiskněte 'Odeslat' pro opakování, nebo 'Zastavit' pro zrušení +support.txKeyImages=Klíčové obrázky +support.txHash=Hash transakce +support.txHex=Hex transakce +support.signature=Podpis +support.maker.penalty.fee=Poplatek penále tvůrce +support.tx.miner.fee=Poplatek těžby -support.backgroundInfo=Haveno není společnost, takže spory řeší jinak.\n\nObchodníci mohou v rámci aplikace komunikovat prostřednictvím zabezpečeného chatu na obrazovce otevřených obchodů a pokusit se o řešení sporů sami. Pokud to nestačí, arbitr rozhodne o situaci a určí výplatu obchodních prostředků. -support.initialInfo=Do níže uvedeného textového pole zadejte popis problému. Přidejte co nejvíce informací k urychlení doby řešení sporu.\n\nZde je kontrolní seznam informací, které byste měli poskytnout:\n\t● Pokud kupujete XMR: Provedli jste převod Fiat nebo Cryptou? Pokud ano, klikli jste v aplikaci na tlačítko „Platba zahájena“?\n\t● Pokud jste prodejcem XMR: Obdrželi jste platbu Fiat nebo Cryptou? Pokud ano, klikli jste v aplikaci na tlačítko „Platba přijata“?\n\t● Kterou verzi Haveno používáte?\n\t● Jaký operační systém používáte?\n\t● Pokud se vyskytl problém s neúspěšnými transakcemi, zvažte přechod na nový datový adresář.\n\t Někdy dojde k poškození datového adresáře a vede to k podivným chybám.\n\t  Viz: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nSeznamte se prosím se základními pravidly procesu sporu:\n\t● Musíte odpovědět na požadavky {0} do 2 dnů.\n\t● Mediátoři reagují do 2 dnů. Rozhodci odpoví do 5 pracovních dnů.\n\t● Maximální doba sporu je 14 dní.\n\t● Musíte spolupracovat s {1} a poskytnout informace, které požaduje, aby jste vyřešili váš případ.\n\t● Při prvním spuštění aplikace jste přijali pravidla uvedena v dokumentu sporu v uživatelské smlouvě.\n\nDalší informace o procesu sporu naleznete na: {2} +support.backgroundInfo=Haveno není společnost, takže spory řeší jinak.\n\n\ +Obchodníci mohou v rámci aplikace komunikovat prostřednictvím zabezpečeného chatu na obrazovce otevřených obchodů \ + a sami se pokusit o řešení sporů. Pokud to nestačí, arbitr rozhodne o situaci \ + a určí výplatu obchodních prostředků. +support.initialInfo=Do níže uvedeného textového pole zadejte popis problému. \ + Přidejte co nejvíce informací k urychlení doby řešení sporu.\n\n\ + Zde je kontrolní seznam informací, které byste měli poskytnout:\n\ + \● Pokud kupujete XMR: Provedli jste převod Fiat nebo Cryptou? Pokud ano, klikli jste v aplikaci na tlačítko 'Platba zahájena'?\n\ + \t● Pokud jste prodejcem XMR: Obdrželi jste platbu fiat nebo kryptoměny? Pokud ano, \ +klikli jste v aplikaci \ + na tlačítko 'Platba přijata'?\n\ + \t● Kterou verzi Haveno používáte?\n\ + \t● Jaký operační systém používáte?\n\ + \t● Pokud se vyskytl problém s neúspěšnými transakcemi, zvažte přechod na nový datový adresář.\n\ + \t Někdy dojde k poškození datového adresáře a vede to k podivným chybám.\n\ + \t  Viz: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\n\ + Seznamte se prosím se základními pravidly procesu sporu:\n\ +\t● Musíte odpovědět na požadavky {0} do 2 dnů.\n\ +\t● Mediátoři reagují do 2 dnů. Rozhodci odpoví do 5 pracovních dnů.\n\ +\t● Maximální doba sporu je 14 dní.\n\ +\t● Musíte spolupracovat s {1} a poskytnout informace, které požaduje, aby jste vyřešili váš případ.\n\ +\t● Při prvním spuštění aplikace jste přijali pravidla uvedena v dokumentu sporu v uživatelské smlouvě.\n\n\ +Další informace o procesu sporu naleznete na: {2} support.systemMsg=Systémová zpráva: {0} support.youOpenedTicket=Otevřeli jste žádost o podporu.\n\n{0}\n\nVerze Haveno: {1} support.youOpenedDispute=Otevřeli jste žádost o spor.\n\n{0}\n\nVerze Haveno: {1} @@ -989,8 +1255,15 @@ support.peerOpenedTicket=Váš obchodní partner požádal o podporu kvůli tech support.peerOpenedDispute=Váš obchodní partner požádal o spor.\n\n{0}\n\nHaveno verze: {1} support.peerOpenedDisputeForMediation=Váš obchodní partner požádal o mediaci.\n\n{0}\n\nHaveno verze: {1} support.mediatorsDisputeSummary=Systémová zpráva: Shrnutí sporu mediátora:\n{0} +support.mediatorReceivedLogs=Systémová zpráva: Mediátor obdržel logy: {0} support.mediatorsAddress=Adresa uzlu mediátora: {0} -support.warning.disputesWithInvalidDonationAddress=Odložená výplatní transakce použila neplatnou adresu příjemce. Neshoduje se s žádnou z hodnot parametrů DAO pro platné dárcovské adresy.\n\nMůže to být pokus o podvod. Informujte prosím vývojáře o tomto incidentu a neuzavírejte tento případ, dokud nebude situace vyřešena!\n\nAdresa použitá ve sporu: {0}\n\nVšechny parametry pro darovací adresy DAO: {1}\n\nObchodní ID: {2} {3} +support.warning.disputesWithInvalidDonationAddress=Odložená výplatní transakce použila neplatnou adresu příjemce. \ + Neshoduje se s žádnou z hodnot parametrů DAO pro platné dárcovské adresy.\n\nMůže to být pokus o podvod. \ + Informujte prosím vývojáře o tomto incidentu a neuzavírejte tento případ, dokud nebude situace vyřešena!\n\n\ + Adresa použitá ve sporu: {0}\n\n\ + Všechny parametry pro darovací adresy DAO: {1}\n\n\ + Obchodní ID: {2}\ + {3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nStále chcete spor uzavřít? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nVýplatu nesmíte provést. support.warning.traderCloseOwnDisputeWarning=Obchodníci mohou sami zrušit úkol pro podporu pouze pokud došlo k výplatě prostředků. @@ -1004,7 +1277,7 @@ settings.tab.network=Informace o síti settings.tab.about=O Haveno setting.preferences.general=Základní nastavení -setting.preferences.explorer=Monero Explorer +setting.preferences.explorer=Průzkumník Monero setting.preferences.deviation=Max. odchylka od tržní ceny setting.preferences.avoidStandbyMode=Vyhněte se pohotovostnímu režimu setting.preferences.useSoundForNotifications=Přehrávat zvuky pro upozornění @@ -1012,12 +1285,10 @@ setting.preferences.autoConfirmXMR=Automatické potvrzení XMR setting.preferences.autoConfirmEnabled=Povoleno setting.preferences.autoConfirmRequiredConfirmations=Požadovaná potvrzení setting.preferences.autoConfirmMaxTradeSize=Max. částka obchodu (XMR) -setting.preferences.autoConfirmServiceAddresses=Monero Explorer URL (používá Tor, kromě localhost, LAN IP adres a názvů hostitele *.local) +setting.preferences.autoConfirmServiceAddresses=Adresa průzkumníka Monero (používá Tor, kromě localhost, LAN IP adres a názvů hostitele *.local) setting.preferences.deviationToLarge=Hodnoty vyšší než {0} % nejsou povoleny. setting.preferences.txFee=Poplatek za výběr transakce (satoshi/vbyte) setting.preferences.useCustomValue=Použijte vlastní hodnotu -setting.preferences.txFeeMin=Transakční poplatek musí být alespoň {0} satoshi/vbyte -setting.preferences.txFeeTooLarge=Váš vstup je nad jakoukoli rozumnou hodnotou (>5000 satoshi/vbyte). Transakční poplatek se obvykle pohybuje v rozmezí 50-400 satoshi/vbyte. setting.preferences.ignorePeers=Ignorované peer uzly [onion addresa:port] setting.preferences.ignoreDustThreshold=Min. hodnota výstupu bez drobných setting.preferences.currenciesInList=Měny v seznamu zdrojů tržních cen @@ -1037,11 +1308,12 @@ setting.preferences.sortWithNumOffers=Seřadit seznamy trhů s počtem nabídek/ setting.preferences.onlyShowPaymentMethodsFromAccount=Skrýt nepodporované způsoby platby setting.preferences.denyApiTaker=Odmítat příjemce, kteří používají API setting.preferences.notifyOnPreRelease=Získávat oznámení o beta verzích -setting.preferences.resetAllFlags=Zrušit všechny "Nezobrazovat znovu" +setting.preferences.resetAllFlags=Zrušit všechny \"Nezobrazovat znovu\" settings.preferences.languageChange=Chcete-li použít změnu jazyka na všech obrazovkách, musíte restartovat aplikaci. settings.preferences.supportLanguageWarning=V případě sporu mějte na paměti, že arbitráž je řešena v {0}. settings.preferences.editCustomExplorer.headline=Nastavení Průzkumníku -settings.preferences.editCustomExplorer.description=Ze seznamu vlevo vyberte průzkumníka definovaného systémem nebo si jej přizpůsobte podle svých vlastních preferencí. +settings.preferences.editCustomExplorer.description=Ze seznamu vlevo vyberte průzkumníka definovaného systémem a nebo \ + si jej přizpůsobte podle svých vlastních preferencí. settings.preferences.editCustomExplorer.available=Dostupní průzkumníci settings.preferences.editCustomExplorer.chosen=Nastavení zvoleného průzkumníka settings.preferences.editCustomExplorer.name=Jméno @@ -1056,14 +1328,21 @@ settings.net.moneroPeersLabel=Připojené peer uzly settings.net.connection=Připojení settings.net.connected=Připojeno settings.net.useTorForXmrJLabel=Použít Tor pro Monero síť +settings.net.useTorForXmrAfterSyncRadio=Po synchronizaci peněženky +settings.net.useTorForXmrOffRadio=Nikdy +settings.net.useTorForXmrOnRadio=Vždy settings.net.moneroNodesLabel=Monero uzly, pro připojení -settings.net.useProvidedNodesRadio=Použijte nabízené Monero Core uzly +settings.net.useProvidedNodesRadio=Použít nabízené Monero uzly settings.net.usePublicNodesRadio=Použít veřejnou síť Monero -settings.net.useCustomNodesRadio=Použijte vlastní Monero Core uzel +settings.net.useCustomNodesRadio=Použít vlastní Monero uzel settings.net.warn.usePublicNodes=Pokud používáte veřejné Monero uzly, jste vystaveni riziku spojenému s používáním nedůvěryhodných vzdálených uzlů.\n\nProsím, přečtěte si více podrobností na [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nJste si jistí, že chcete použít veřejné uzly? settings.net.warn.usePublicNodes.useProvided=Ne, použijte nabízené uzly settings.net.warn.usePublicNodes.usePublic=Ano, použít veřejnou síť -settings.net.warn.useCustomNodes.B2XWarning=Ujistěte se, že váš Monero uzel je důvěryhodný Monero Core uzel!\n\nPřipojení k uzlům, které nedodržují pravidla konsensu Monero Core, může poškodit vaši peněženku a způsobit problémy v obchodním procesu.\n\nUživatelé, kteří se připojují k uzlům, které porušují pravidla konsensu, odpovídají za případné škody, které z toho vyplývají. Jakékoli výsledné spory budou rozhodnuty ve prospěch druhého obchodníka. Uživatelům, kteří ignorují tyto varovné a ochranné mechanismy, nebude poskytována technická podpora! +settings.net.warn.useCustomNodes.B2XWarning=Ujistěte se, že váš Monero uzel je důvěryhodný Monero uzel!\n\n\ + Připojení k uzlům, které nedodržují pravidla konsensu Monero, může poškodit vaši peněženku a způsobit problémy v obchodním procesu.\n\n\ + Uživatelé, kteří se připojují k uzlům, které porušují pravidla konsensu, odpovídají za případné škody, které z toho vyplývají. \ + Jakékoli výsledné spory budou rozhodnuty ve prospěch druhého obchodníka. Uživatelům, kteří ignorují \ + tyto varovné a ochranné mechanismy, nebude poskytována technická podpora! settings.net.warn.invalidXmrConfig=Připojení k síti Monero selhalo, protože je vaše konfigurace neplatná.\n\nVaše konfigurace byla resetována, aby byly místo toho použity poskytnuté uzly Monero. Budete muset restartovat aplikaci. settings.net.localhostXmrNodeInfo=Základní informace: Haveno při spuštění hledá místní Monero uzel. Pokud je nalezen, Haveno bude komunikovat se sítí Monero výhradně skrze něj. settings.net.p2PPeersLabel=Připojené uzly @@ -1095,11 +1374,15 @@ settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Peer settings.net.inbound=příchozí settings.net.outbound=odchozí +settings.net.rescanOutputsLabel=Znovu oskenovat výstupy +settings.net.rescanOutputsButton=Znovu oskenovat výstupy peněženky +settings.net.rescanOutputsSuccess=Jste si jistí, že chcete znovu oskenovat výstupy vaší peněženky? +settings.net.rescanOutputsFailed=Nepodařilo se oskenovat výstupy peněženky.\nChyba: {0} setting.about.aboutHaveno=O projektu Haveno setting.about.about=Haveno je software s otevřeným zdrojovým kódem, který usnadňuje směnu moneroů s národními měnami (a jinými kryptoměnami) prostřednictvím decentralizované sítě typu peer-to-peer způsobem, který silně chrání soukromí uživatelů. Zjistěte více o Haveno na naší webové stránce projektu. setting.about.web=Webová stránka Haveno setting.about.code=Zdrojový kód -setting.about.agpl=AGPL Licence +setting.about.agpl=AGPL licence setting.about.support=Podpořte Haveno setting.about.def=Haveno není společnost - je to projekt otevřený komunitě. Pokud se chcete zapojit nebo podpořit Haveno, postupujte podle níže uvedených odkazů. setting.about.contribute=Přispět @@ -1123,7 +1406,7 @@ setting.about.shortcuts.close=Zavřít Haveno setting.about.shortcuts.close.value=''Ctrl + {0}'' nebo ''cmd + {0}'' nebo ''Ctrl + {1}'' nebo ''cmd + {1}'' setting.about.shortcuts.closePopup=Zavřete vyskakovací nebo dialogové okno -setting.about.shortcuts.closePopup.value=Klávesa „ESCAPE“ +setting.about.shortcuts.closePopup.value=Klávesa 'ESCAPE' setting.about.shortcuts.chatSendMsg=Odeslat obchodní soukromou zprávu setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' nebo ''alt + ENTER'' nebo ''cmd + ENTER'' @@ -1158,17 +1441,14 @@ setting.about.shortcuts.sendFilter=Nastavit filtr (privilegovaná aktivita) setting.about.shortcuts.sendPrivateNotification=Odeslat soukromé oznámení partnerovi (privilegovaná aktivita) setting.about.shortcuts.sendPrivateNotification.value=Otevřete informace o uživateli kliknutím na avatar a stiskněte: {0} -setting.info.headline=Nová funkce automatického potvrzení XMR -setting.info.msg=Při prodeji XMR za XMR můžete pomocí funkce automatického potvrzení ověřit, že do vaší peněženky bylo odesláno správné množství XMR, takže Haveno může automaticky označit obchod jako dokončený, což zrychlí obchodování pro všechny.\n\nAutomatické potvrzení zkontroluje transakci XMR alespoň na 2 uzlech průzkumníka XMR pomocí klíče soukromé transakce poskytnutého odesílatelem XMR. Ve výchozím nastavení používá Haveno uzly průzkumníka spuštěné přispěvateli Haveno, ale pro maximální soukromí a zabezpečení doporučujeme spustit vlastní uzel průzkumníka XMR.\n\nMůžete také nastavit maximální částku XMR na obchod, která se má automaticky potvrdit, a také počet požadovaných potvrzení zde v Nastavení.\n\nZobrazit další podrobnosti (včetně toho, jak nastavit vlastní uzel průzkumníka) na Haveno wiki: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### +account.tab.arbitratorRegistration=Registrace rozhodce account.tab.mediatorRegistration=Registrace mediátora account.tab.refundAgentRegistration=Registrace rozhodce pro vrácení peněz account.tab.signing=Podepisování -account.info.headline=Vítejte ve vašem účtu Haveno -account.info.msg=Zde můžete přidat obchodní účty pro národní měny & kryptoměny a vytvořit zálohu dat vaší peněženky a účtu.\n\nPři prvním spuštění Haveno byla vytvořena nová peněženka Monero.\n\nDůrazně doporučujeme zapsat si seed slova peněženek (viz záložka nahoře) a před financováním zvážit přidání hesla. Vklady a výběry moneroů jsou spravovány v sekci \ "Finance \".\n\nOchrana osobních údajů a zabezpečení: protože Haveno je decentralizovaná směnárna, všechna data jsou uložena ve vašem počítači. Neexistují žádné servery, takže nemáme přístup k vašim osobním informacím, vašim finančním prostředkům ani vaší IP adrese. Údaje, jako jsou čísla bankovních účtů, adresy cryptoů a monerou atd., jsou sdíleny pouze s obchodním partnerem za účelem uskutečnění obchodů, které zahájíte (v případě sporu uvidí Prostředník nebo Rozhodce stejná data jako váš obchodní partner). account.menu.paymentAccount=Účty v národní měně account.menu.altCoinsAccountView=Kryptoměnové účty @@ -1179,11 +1459,16 @@ account.menu.backup=Záloha account.menu.notifications=Oznámení account.menu.walletInfo.balance.headLine=Zůstatky v peněžence -account.menu.walletInfo.balance.info=Zde jsou zobrazeny celkové zůstatky v interní peněžence včetně nepotvrzených transakcí.\nInterní zůstatek XMR uvedený níže by měl odpovídat součtu hodnot 'Dostupný zůstatek' a 'Rezervováno v nabídkách' v pravém horním rohu aplikace. +account.menu.walletInfo.balance.info=Zde jsou zobrazeny celkové zůstatky v interní peněžence včetně nepotvrzených transakcí.\n\ + Interní zůstatek XMR uvedený níže by měl odpovídat součtu hodnot 'Dostupný zůstatek' a 'Rezervováno v nabídkách' v pravém horním rohu aplikace. account.menu.walletInfo.xpub.headLine=Veřejné klíče (xpub) account.menu.walletInfo.walletSelector={0} {1} peněženka account.menu.walletInfo.path.headLine=HD identifikátory klíčů -account.menu.walletInfo.path.info=Pokud importujete vaše seed slova do jiné peněženky (např. Electrum), budete muset nastavit také identifikátor klíčů (BIP32 path). Toto provádějte pouze ve výjimečných případech, např. pokud úplně ztratíte kontrolu nad Haveno peněženkou.\nMějte na paměti, že provádění transakcí pomocí jiných softwarových peněženek může snadno poškodit interní datové struktury systému Haveno, a znemožnit tak provádění obchodů.\n\nNIKDY neposílejte BSQ pomocí jiných softwarových peněženek než Haveno, protože byste tím velmi pravděpodobně vytvořili neplatnou BSQ transakci, a ztratili tak své BSQ. +account.menu.walletInfo.path.info=Pokud importujete vaše seed slova do jiné peněženky (např. Electrum), budete muset nastavit také \ + cestu. Toto provádějte pouze ve výjimečných případech, např. pokud úplně ztratíte kontrolu nad Haveno peněženkou a složkou dat.\n\ + Mějte na paměti, že provádění transakcí pomocí jiných softwarových peněženek může snadno poškodit interní datové struktury \ + systému Haveno, a znemožnit tak provádění obchodů.\n\n\ +NIKDY neposílejte BSQ pomocí jiných softwarových peněženek než Haveno, protože byste tím velmi pravděpodobně vytvořili neplatnou BSQ transakci, a ztratili tak své BSQ. account.menu.walletInfo.openDetails=Zobrazit detailní data peněženky a soukromé klíče @@ -1201,42 +1486,219 @@ account.arbitratorRegistration.registerSuccess=Úspěšně jste se zaregistroval account.arbitratorRegistration.registerFailed=Registraci se nepodařilo dokončit. {0} account.crypto.yourCryptoAccounts=Vaše kryptoměnové účty -account.crypto.popup.wallet.msg=Ujistěte se, že dodržujete požadavky na používání peněženek {0}, jak je popsáno na webové stránce {1}.\nPoužití peněženek z centralizovaných směnáren, kde (a) nevlastníte své soukromé klíče nebo (b) které nepoužívají kompatibilní software peněženky, je riskantní: může to vést ke ztrátě obchodovaných prostředků!\nMediátor nebo rozhodce není specialista {2} a v takových případech nemůže pomoci. +account.crypto.popup.wallet.msg=Ujistěte se, že dodržujete požadavky na používání peněženek {0}, jak je \ +popsáno na webové stránce {1}.\nPoužití peněženek z centralizovaných směnáren, kde (a) nevlastníte své soukromé klíče nebo \ +(b) které nepoužívají kompatibilní software peněženk, je riskantní: může to vést ke ztrátě obchodovaných prostředků!\nMediátor nebo rozhodce \ +není specialista {2} a v takových případech nemůže pomoci. account.crypto.popup.wallet.confirm=Rozumím a potvrzuji, že vím, jakou peněženku musím použít. # suppress inspection "UnusedProperty" -account.crypto.popup.upx.msg=Obchodování s UPX na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání UPX musíte použít buď oficiální peněženku GUI uPlexa nebo CLI peněženku uPlexa s povoleným příznakem store-tx-info (výchozí hodnota v nových verzích). Ujistěte se, že máte přístup ke klíči tx, který může být vyžadován v případě sporu.\nuplexa-wallet-cli (použijte příkaz get_tx_key)\nuplexa-wallet-gui (přejděte na záložku historie a pro potvrzení platby klikněte na tlačítko (P))\n\nV normálním block exploreru není přenos ověřitelný.\n\nV případě sporu musíte rozhodci poskytnout následující údaje:\n- Soukromý klíč tx\n- Hash transakce\n- Veřejnou adresa příjemce\n\nPokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke prohrání sporu. Odesílatel UPX odpovídá za zajištění ověření přenosu UPX rozhodci v případě sporu.\n\nNení požadováno žádné platební ID, pouze normální veřejná adresa.\nPokud si nejste jisti tímto procesem, vyhledejte další informace na discord kanálu uPlexa (https://discord.gg/vhdNSrV) nebo uPlexa Telegram Chatu (https://t.me/uplexaOfficial). +account.crypto.popup.upx.msg=Obchodování s UPX na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\n\ +K odeslání UPX musíte použít buď oficiální peněženku GUI uPlexa nebo CLI peněženku uPlexa \ +s povoleným příznakem store-tx-info (výchozí hodnota v nových verzích). \ +Ujistěte se, že máte přístup \ +ke klíči tx, který může být vyžadován v případě sporu.\n\ +uplexa-wallet-cli (použijte příkaz get_tx_key)\n\ +uplexa-wallet-gui (přejděte na záložku historie a pro potvrzení platby klikněte na tlačítko (P))\n\n\ +V normálním block exploreru není přenos ověřitelný.\n\n\ +V případě sporu musíte rozhodci poskytnout následující údaje:\n\ +- Soukromý klíč tx\n\ +- Hash transakce\n\ +- Veřejnou adresa příjemce\n\n\ +Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke prohrání sporu. \ +Odesílatel UPX odpovídá za zajištění ověření přenosu UPX rozhodci \ +v případě sporu.\n\n\ +Není požadováno žádné platební ID, pouze normální veřejná adresa.\n\ +Pokud si nejste jisti tímto procesem, vyhledejte další informace na discord kanálu uPlexa (https://discord.gg/vhdNSrV) \ +nebo uPlexa Telegram Chatu (https://t.me/uplexaOfficial). # suppress inspection "UnusedProperty" -account.crypto.popup.arq.msg=Obchodování ARQ na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání ARQ musíte použít buď oficiální peněženku ArQmA GUI nebo peněženku ArQmA CLI s povoleným příznakem store-tx-info (výchozí hodnota v nových verzích). Ujistěte se, že máte přístup ke klíči tx, který může být vyžadován v případě sporu.\narqma-wallet-cli (použijte příkaz get_tx_key)\narqma-wallet-gui (přejděte na kartu historie a pro potvrzení platby klikněte na tlačítko (P))\n\nV normálním blok exploreru není přenos ověřitelný.\n\nV případě sporu musíte mediátorovi nebo rozhodci poskytnout následující údaje:\n- Soukromý klíč tx\n- Hash transakce\n- Veřejnou adresu příjemce\n\nPokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke prohrání sporu. Odesílatel ARQ odpovídá za zajištění ověření převodu ARQ mediátorovi nebo rozhodci v případě sporu.\n\nNení požadováno žádné platební ID, pouze normální veřejná adresa.\nPokud si nejste jisti tímto procesem, navštivte discord kanál ArQmA (https://discord.gg/s9BQpJT) nebo fórum ArQmA (https://labs.arqma.com). +account.crypto.popup.arq.msg=Obchodování ARQ na Haveno vyžaduje, abyste pochopili \ +a splnili následující požadavky:\n\n\ +K odeslání ARQ musíte použít buď oficiální peněženku ArQmA GUI nebo peněženku ArQmA CLI \ +s povoleným příznakem store-tx-info (výchozí hodnota v nových verzích). \ +Ujistěte se, že máte přístup ke klíči tx, který může být vyžadován v případě sporu.\n\ +arqma-wallet-cli (použijte příkaz get_tx_key)\n\ +arqma-wallet-gui (přejděte na kartu historie a pro potvrzení platby klikněte na tlačítko (P))\n\n\ +V normálním prohlížeči bloků není přenos ověřitelný.\n\n\ +V případě sporu musíte mediátorovi nebo rozhodci poskytnout následující údaje:\n\ +- Soukromý klíč tx\n\ +- Hash transakce\n\ +- Veřejnou adresu příjemce\n\n\ +Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde k prohrání sporu. \ +Odesílatel ARQ odpovídá za zajištění ověření převodu ARQ mediátorovi \ +nebo rozhodci v případě sporu.\n\n\ +Není požadováno žádné platební ID, pouze normální veřejná adresa.\n\ +Pokud si nejste jisti tímto procesem, navštivte discord kanál ArQmA (https://discord.gg/s9BQpJT) \ +nebo fórum ArQmA (https://labs.arqma.com). # suppress inspection "UnusedProperty" -account.crypto.popup.xmr.msg=Obchodování s XMR na Haveno vyžaduje, abyste pochopili následující požadavek.\n\nPokud prodáváte XMR, musíte být schopni v případě sporu poskytnout mediátorovi nebo rozhodci následující informace:\n- transakční klíč (Tx klíč, Tx tajný klíč nebo Tx soukromý klíč)\n- ID transakce (Tx ID nebo Tx Hash)\n- cílová adresa (adresa příjemce)\n\nNa wiki najdete podrobnosti, kde najdete tyto informace v populárních peněženkách Monero:\n[HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\n\nNeposkytnutí požadovaných údajů o transakci bude mít za následek ztrátu sporů.\n\nVšimněte si také, že Haveno nyní nabízí automatické potvrzení transakcí XMR, aby byly obchody rychlejší, ale musíte to povolit v Nastavení.\n\nDalší informace o funkci automatického potvrzení najdete na wiki:\n[HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. +account.crypto.popup.xmr.msg=Obchodování s XMR na Haveno vyžaduje, abyste pochopili následující požadavek.\n\n\ +Pokud prodáváte XMR, musíte být schopni v případě sporu poskytnout mediátorovi nebo rozhodci následující informace:\n\ +- transakční klíč (Tx klíč, Tx tajný klíč nebo Tx soukromý klíč)\n\ +- ID transakce (Tx ID nebo Tx Hash)\n\ +- cílová adresa (adresa příjemce)\n\n\ +Na wiki najdete podrobnosti, kde najdete tyto informace v populárních peněženkách Monero:\n[HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\n\ +Neposkytnutí požadovaných údajů o transakci bude mít za následek ztrátu sporů.\n\n\ +Všimněte si také, že Haveno nyní nabízí automatické potvrzení transakcí XMR, aby byly obchody rychlejší, \ +ale musíte to povolit v Nastavení.\n\n\ +Další informace o funkci automatického potvrzení najdete na wiki: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" -account.crypto.popup.msr.msg=Obchodování MSR na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání MSR musíte použít buď oficiální peněženku Masari GUI, peněženku Masari CLI s povoleným příznakem store-tx-info (ve výchozím nastavení povoleno) nebo webovou peněženku Masari (https://wallet.getmasari.org). Ujistěte se, že máte přístup ke klíči tx, který může být vyžadován v případě sporu.\nmasari-wallet-cli (použijte příkaz get_tx_key)\nmasari-wallet-gui (přejděte na kartu historie a klikněte na tlačítko (P) pro potvrzení platby)\n\nWebová peněženka Masari (jděte do Účet -> Historie transakcí a zobrazte podrobností o odeslané transakci)\n\nOvěření lze provést v peněžence.\nmasari-wallet-cli: pomocí příkazu (check_tx_key).\nmasari-wallet-gui: na stránce Pokročilé > Dokázat/Ověřit.\nOvěření lze provést v block exploreru\nOtevřete Block explorer (https://explorer.getmasari.org), použijte vyhledávací lištu k nalezení hash transakce.\nJakmile je transakce nalezena, přejděte dolů do oblasti „Prokázat odesílání“ a podle potřeby vyplňte podrobnosti.\nV případě sporu musíte zprostředkovateli nebo rozhodci poskytnout následující údaje:\n- Soukromý klíč tx\n- Hash transakce\n- Veřejnou adresu příjemce\n\nPokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke ztrátě sporu. Odesílatel MSR odpovídá za zajištění ověření přenosu MSR mediátorovi nebo rozhodci v případě sporu.\n\nNení požadováno žádné platební ID, pouze normální veřejná adresa.\nPokud si nejste jisti tímto procesem, požádejte o pomoc oficiální Masari Discord (https://discord.gg/sMCwMqs). +account.crypto.popup.msr.msg=Obchodování MSR na Haveno vyžaduje, abyste pochopili a splnili \ +následující požadavky:\n\n\ +K odeslání MSR musíte použít buď oficiální peněženku Masari GUI, peněženku Masari CLI s povoleným příznakem \ +store-tx-info (ve výchozím nastavení povoleno) nebo webovou peněženku Masari (https://wallet.getmasari.org). Ujistěte se, že máte přístup ke klíči tx, \ +který může být vyžadován v případě sporu.\n\ +masari-wallet-cli (použijte příkaz get_tx_key)\n\ +masari-wallet-gui (přejděte na kartu historie a klikněte na tlačítko (P) pro potvrzení platby)\n\n\ +Webová peněženka Masari (jděte do Účet -> Historie transakcí a zobrazte podrobností o odeslané transakci)\n\n\ +Ověření lze provést v peněžence.\n\ +masari-wallet-cli: pomocí příkazu (check_tx_key).\n\ +masari-wallet-gui: na stránce Pokročilé > Dokázat/Ověřit.\n\ +Ověření lze provést v block exploreru\n\ +Otevřete Block explorer (https://explorer.getmasari.org), použijte vyhledávací lištu k nalezení hash transakce.\n\ +Jakmile je transakce nalezena, přejděte dolů do oblasti 'Prokázat odesílání' a podle potřeby vyplňte podrobnosti.\n\ +V případě sporu musíte zprostředkovateli nebo rozhodci poskytnout následující údaje:\n\ +- Soukromý klíč tx\n\ +- Hash transakce\n\ +- Veřejnou adresu příjemce\n\n\ +Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke ztrátě sporu. \ +Odesílatel MSR odpovídá za zajištění ověření přenosu MSR mediátorovi \ +nebo rozhodci v případě sporu.\n\n\ +Není požadováno žádné platební ID, pouze normální veřejná adresa.\n\ +Pokud si nejste jisti tímto procesem, požádejte o pomoc oficiální Masari Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" -account.crypto.popup.blur.msg=Obchodování BLUR na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání BLUR musíte použít Blur Network CLI nebo GUI peněženku.\n\nPoužíváte-li peněženku CLI, po odeslání transakce se zobrazí hash transakce (tx ID). Tyto informace si musíte uložit. Ihned po odeslání transakce musíte použít příkaz 'get_tx_key' pro načtení soukromého klíče transakce. Pokud tento krok neprovedete, pravděpodobně nebudete moci klíč získat později.\n\nPokud používáte peněženku GUI Blur Network, lze soukromý klíč transakce a ID transakce pohodlně nalézt na kartě Historie. Ihned po odeslání vyhledejte příslušnou transakci. Klikněte na "?" symbol v pravém dolním rohu pole obsahující transakci. Tyto informace si musíte uložit.\n\nV případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1.) ID transakce, 2.) soukromý klíč transakce a 3.) Adresu příjemce. Mediátor nebo rozhodce poté ověří přenos BLUR pomocí prohlížeče BLUR transakcí (https://blur.cash/#tx-viewer).\n\nNeposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese odesílatel BLUR 100% odpovědnosti za ověřování transakcí mediátorovi nebo rozhodci.\n\nPokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na Blur Network Discord (https://discord.gg/dMWaqVW). +account.crypto.popup.blur.msg=Obchodování BLUR na Haveno vyžaduje, abyste pochopili \ +a splnili následující požadavky:\n\n\ +K odeslání BLUR musíte použít Blur Network CLI nebo GUI peněženku.\n\n\ +Používáte-li peněženku CLI, po odeslání transakce se zobrazí hash transakce (tx ID). Tyto informace si musíte uložit. \ +Ihned po odeslání transakce musíte použít příkaz 'get_tx_key' pro načtení soukromého klíče transakce. \ +Pokud tento krok neprovedete, pravděpodobně nebudete moci klíč získat později.\n\n\ +Pokud používáte peněženku GUI Blur Network, lze soukromý klíč transakce a ID transakce pohodlně nalézt \ +na kartě Historie. Ihned po odeslání vyhledejte příslušnou transakci. Klikněte na "?" symbol \ +v pravém dolním rohu pole obsahující transakci. Tyto informace si musíte uložit.\n\n\ +V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1.) ID transakce, \ +2.) soukromý klíč transakce a 3.) Adresu příjemce. Mediátor nebo rozhodce poté ověří přenos BLUR \ +pomocí prohlížeče BLUR transakcí (https://blur.cash/#tx-viewer).\n\n\ +Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ +Ve všech sporných případech nese odesílatel BLUR 100% odpovědnosti za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" -account.crypto.popup.solo.msg=Obchodování Solo na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání Solo musíte použít peněženku CLI Solo Network.\n\nPoužíváte-li peněženku CLI, po odeslání přenosu se zobrazí hash transakce (tx ID). Tyto informace si musíte uložit. Ihned po odeslání převodu musíte použít příkaz 'get_tx_key' pro načtení soukromého klíče transakce. Pokud tento krok neprovedete, pravděpodobně nebudete moci klíč získat později.\n\nV případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1.) ID transakce, 2.) Soukromý klíč transakce a 3.) Adresu příjemce. Mediátor nebo rozhodce poté ověří převod Solo pomocí Solo Block Exploreru vyhledáním transakce a poté pomocí funkce „Prokažte odesílání“ (https://explorer.minesolo.com/).\n\nneposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese Solo odesílatel 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\nPokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na stránce Solo Network Discord (https://discord.minesolo.com/). +account.crypto.popup.solo.msg=Obchodování Solo na Haveno vyžaduje, abyste pochopili a splnili \ +následující požadavky:\n\n\ +K odeslání Solo musíte použít peněženku CLI Solo Network.\n\n\ +Používáte-li peněženku CLI, po odeslání přenosu se zobrazí hash transakce (tx ID). Tyto informace si musíte uložit. \ +Ihned po odeslání převodu musíte použít příkaz 'get_tx_key' pro načtení soukromého klíče transakce. \ +Pokud tento krok neprovedete, pravděpodobně nebudete moci klíč získat později.\n\n\ +V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1.) ID transakce, \ +2.) Soukromý klíč transakce a 3.) Adresu příjemce. Mediátor nebo rozhodce poté ověří převod Solo \ +pomocí Solo Block Exploreru vyhledáním transakce a poté pomocí funkce 'Prokažte odesílání' (https://explorer.minesolo.com/).\n\n\ +neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ +Ve všech sporných případech nese Solo odesílatel 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na stránce Solo Network Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" -account.crypto.popup.cash2.msg=Obchodování CASH2 na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání CASH2 musíte použít peněženku Cash2 Wallet verze 3 nebo vyšší.\n\nPo odeslání transakce se zobrazí ID transakce. Tyto informace si musíte uložit. Ihned po odeslání transakce musíte použít příkaz 'getTxKey' v simplewallet a získat tajný klíč transakce.\n\nV případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1) ID transakce, 2) Tajný klíč transakce a 3) Adresu Cash2 příjemce. Mediátor nebo rozhodce poté ověří převod CASH2 pomocí průzkumníku Cash2 Block Explorer (https://blocks.cash2.org).\n\nNeposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese odesílatel CASH2 100% odpovědnost za ověření transakcí mediátorovi nebo rozhodci.\n\nPokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na Cash2 Discord (https://discord.gg/FGfXAYN). +account.crypto.popup.cash2.msg=Obchodování CASH2 na Haveno vyžaduje, abyste pochopili \ +a splnili následující požadavky:\n\n\ +K odeslání CASH2 musíte použít peněženku Cash2 Wallet verze 3 nebo vyšší.\n\n\ +Po odeslání transakce se zobrazí ID transakce. Tyto informace si musíte uložit. \ +Ihned po odeslání transakce musíte použít příkaz 'getTxKey' v simplewallet \ +a získat tajný klíč transakce.\n\n\ +V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1) ID transakce, \ +2) Tajný klíč transakce a 3) Adresu Cash2 příjemce. Mediátor nebo rozhodce poté ověří převod CASH2 \ +pomocí průzkumníku Cash2 Block Explorer (https://blocks.cash2.org).\n\n\ +Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ +Ve všech sporných případech nese odesílatel CASH2 100% odpovědnost za ověření transakcí mediátorovi nebo rozhodci. \n\n +Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na Cash2 Discord (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" -account.crypto.popup.qwertycoin.msg=Obchodování s Qwertycoinem na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání QWC musíte použít oficiální QWC peněženku verze 5.1.3 nebo vyšší.\n\nPo odeslání transakce se zobrazí ID transakce. Tyto informace si musíte uložit. Ihned po odeslání transakce musíte použít příkaz 'get_Tx_Key' v simplewallet a získat tajný klíč transakce.\n\nV případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1) ID transakce, 2) Tajný klíč transakce a 3) Adresu QWC příjemce. Mediátor nebo rozhodce poté ověří přenos QWC pomocí Průzkumníka bloků QWC (https://explorer.qwertycoin.org).\n\nNeposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese odesílatel QWC 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\nPokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na stránce QWC Discord (https://discord.gg/rUkfnpC). +account.crypto.popup.qwertycoin.msg=Obchodování s Qwertycoinem na Haveno vyžaduje, abyste pochopili \ +a splnili následující požadavky:\n\n\ +K odeslání QWC musíte použít oficiální QWC peněženku verze 5.1.3 nebo vyšší.\n\n\ +Po odeslání transakce se zobrazí ID transakce. Tyto informace si musíte uložit. \ +Ihned po odeslání transakce musíte použít příkaz 'get_Tx_Key' v simplewallet \ +a získat tajný klíč transakce.\n\n\ +V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1) ID transakce, \ +2) Tajný klíč transakce a 3) Adresu QWC příjemce. Mediátor nebo rozhodce poté ověří přenos QWC \ +pomocí Průzkumníka bloků QWC (https://explorer.qwertycoin.org).\n\n\ +Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ +Ve všech sporných případech nese odesílatel QWC 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na stránce QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" -account.crypto.popup.drgl.msg=Obchodování Dragonglass na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nVzhledem k tomu, že Dragonglass poskytuje soukromí, není transakce na veřejném blockchainu ověřitelná. V případě potřeby můžete svou platbu prokázat pomocí vašeho soukromého klíče TXN.\nSoukromý klíč TXN je jednorázový klíč automaticky generovaný pro každou transakci, ke které lze přistupovat pouze z vaší DRGL peněženky.\nBuď pomocí GUI peněženky DRGL (uvnitř dialogu s podrobnostmi o transakci) nebo pomocí simplewallet CLI Dragonglass (pomocí příkazu "get_tx_key").\n\nVerze DRGL „Oathkeeper“ a vyšší jsou požadovány pro obě možnosti.\n\nV případě sporu musíte mediátorovi nebo rozhodci poskytnout následující údaje:\n- TXN-soukromý klíč\n- Hash transakce\n- Veřejnou adresu příjemce\n\nOvěření platby lze provést pomocí výše uvedených údajů jako vstupů na adrese (http://drgl.info/#check_txn).\n\nPokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke ztrátě sporu. Odesílatel Dragonglass odpovídá za ověření přenosu DRGL mediátorovi nebo rozhodci v případě sporu. Použití PaymentID není nutné.\n\nPokud si nejste jisti některou částí tohoto procesu, navštivte nápovědu pro Dragonglass na Discord (http://discord.drgl.info). +account.crypto.popup.drgl.msg=Obchodování Dragonglass na Haveno vyžaduje, abyste pochopili a splnili \ +následující požadavky:\n\n\ +Vzhledem k tomu, že Dragonglass poskytuje soukromí, není transakce na veřejném blockchainu ověřitelná. V případě potřeby \ +můžete svou platbu prokázat pomocí vašeho soukromého klíče TXN.\n\ +Soukromý klíč TXN je jednorázový klíč automaticky generovaný pro každou transakci, \ +ke které lze přistupovat pouze z vaší DRGL peněženky.\n\ +Buď pomocí GUI peněženky DRGL (uvnitř dialogu s podrobnostmi o transakci) nebo pomocí simplewallet CLI Dragonglass (pomocí příkazu "get_tx_key").\n\n\ +Verze DRGL 'Oathkeeper' a vyšší jsou požadovány pro obě možnosti.\n\n\ +V případě sporu musíte mediátorovi nebo rozhodci poskytnout následující údaje:\n\ +- TXN-soukromý klíč\n\ +- Hash transakce\n\ +- Veřejnou adresu příjemce\n\n +Ověření platby lze provést pomocí výše uvedených údajů jako vstupů na adrese (http://drgl.info/#check_txn).\n\n\ +Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke ztrátě sporu. \ +Odesílatel Dragonglass odpovídá za ověření přenosu DRGL mediátorovi nebo rozhodci v případě sporu. \ +Použití PaymentID není nutné.\n\n +Pokud si nejste jisti některou částí tohoto procesu, navštivte Dragonglass na Discordu (http://discord.drgl.info) pro pomoc. # suppress inspection "UnusedProperty" -account.crypto.popup.ZEC.msg=Při použití Zcash můžete použít pouze transparentní adresy (začínající t), nikoli z-adresy (soukromé), protože mediátor nebo rozhodce by nemohl ověřit transakci pomocí z-adres. +account.crypto.popup.ZEC.msg=Při použití Zcash můžete použít pouze transparentní adresy (začínající na t), nikoli \ +z-adresy (soukromé), protože mediátor nebo rozhodce by nemohl ověřit transakci pomocí z-adres. # suppress inspection "UnusedProperty" -account.crypto.popup.XZC.msg=Při použití Zcoinu můžete použít pouze transparentní (sledovatelné) adresy, nikoli nevysledovatelné adresy, protože mediátor nebo rozhodce by nemohl ověřit transakci s nevysledovatelnými adresami v blok exploreru. +account.crypto.popup.XZC.msg=Při použití Zcoinu můžete použít pouze transparentní (sledovatelné) adresy, \ +nikoli nevysledovatelné adresy, protože mediátor nebo rozhodce by nemohl ověřit transakci s nevysledovatelnými adresami v blok exploreru. # suppress inspection "UnusedProperty" -account.crypto.popup.grin.msg=GRIN vyžaduje k vytvoření transakce interaktivní proces mezi odesílatelem a příjemcem. Nezapomeňte postupovat podle pokynů z webové stránky projektu GRIN, abyste spolehlivě odeslali a přijali GRIN (příjemce musí být online nebo alespoň online v určitém časovém rozmezí).\n\nHaveno podporuje pouze formát URL peněženky Grinbox (Wallet713).\n\nOdesílatel GRIN je povinen prokázat, že GRIN úspěšně odeslal. Pokud peněženka nemůže tento důkaz poskytnout, bude potenciální spor vyřešen ve prospěch příjemce GRIN. Ujistěte se, že používáte nejnovější software Grinbox, který podporuje důkaz transakcí a že chápete proces přenosu a přijímání GRIN a také způsob, jak vytvořit důkaz.\n\nViz https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only pro více informací o nástroji Grinbox proof. +account.crypto.popup.grin.msg=GRIN vyžaduje k vytvoření transakce interaktivní proces mezi odesílatelem a příjemcem. \ + Nezapomeňte postupovat podle pokynů z webové stránky projektu GRIN, abyste spolehlivě odeslali a přijali GRIN \ + (příjemce musí být online nebo alespoň online v určitém časovém rozmezí).\n\n\ + Haveno podporuje pouze formát URL peněženky Grinbox (Wallet713).\n\n\ + Odesílatel GRIN je povinen prokázat, že GRIN úspěšně odeslal. Pokud peněženka nemůže tento důkaz poskytnout, \ + bude potenciální spor vyřešen ve prospěch příjemce GRIN. Ujistěte se, že používáte \ + nejnovější software Grinbox, který podporuje důkaz transakcí a že chápete proces přenosu \ + a přijímání GRIN a také způsob, jak vytvořit důkaz.\n\n\ + Viz https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only \ +pro více informací o nástroji Grinbox proof. # suppress inspection "UnusedProperty" -account.crypto.popup.beam.msg=BEAM vyžaduje k vytvoření transakce interaktivní proces mezi odesílatelem a příjemcem.\n\nNezapomeňte postupovat podle pokynů na webové stránce projektu BEAM, abyste spolehlivě odeslali a přijali BEAM (příjemce musí být online nebo alespoň online během určitého časového období).\n\nOdesílatel BEAM je povinen prokázat, že úspěšně odeslali BEAM. Nezapomeňte použít software peněženku, která může takový důkaz předložit. Pokud peněženka nemůže poskytnout důkaz, bude potenciální spor vyřešen ve prospěch příjemce BEAM. +account.crypto.popup.beam.msg=BEAM vyžaduje k vytvoření transakce interaktivní proces \ + mezi odesílatelem \ + a příjemcem.\n\n\ + Nezapomeňte postupovat podle pokynů na webové stránce projektu BEAM, abyste spolehlivě odeslali a přijali BEAM \ + (příjemce musí být online nebo alespoň online během určitého časového období).\n\n\ + Odesílatel BEAM je povinen prokázat, že úspěšně odeslali BEAM. \ + Nezapomeňte použít software peněženku, která může takový důkaz předložit. Pokud peněženka nemůže poskytnout důkaz, \ + bude potenciální spor vyřešen ve prospěch příjemce BEAM. # suppress inspection "UnusedProperty" -account.crypto.popup.pars.msg=Trading ParsiCoin na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání PARS musíte použít oficiální ParsiCoin peněženku verze 3.0.0 nebo vyšší.\n\nV Peněženka GUI (ParsiPay) si můžete zkontrolovat svůj Hash Transakce a Klíč Transakce v sekci Transakce.\n\nV případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit: 1) Hash Transakce, 2) Transakční Klíč a 3) Adresu PARS příjemce. Mediátor nebo rozhodce poté ověří přenos PARS pomocí Block exploreru ParsiCoin (http://explorer.parsicoin.net/#check_payment).\n\nNeposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese odesílatel ParsiCoin 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\nPokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na ParsiCoin Discord (https://discord.gg/c7qmFNh). +account.crypto.popup.pars.msg=Trading ParsiCoin na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\n\ +K odeslání PARS musíte použít oficiální ParsiCoin peněženku verze 3.0.0 nebo vyšší.\n\n\ +V Peněženka GUI (ParsiPay) si můžete zkontrolovat svůj Hash Transakce a Klíč Transakce v sekci Transakce. \ +Je zapotřebí kliknout na transakci a potom na zobrazení detailů. \n\n\ +V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit: 1) Hash Transakce, \ +2) Transakční Klíč a 3) Adresu PARS příjemce. Mediátor nebo rozhodce poté ověří přenos PARS \ +pomocí Block exploreru ParsiCoin (http://explorer.parsicoin.net/#check_payment).\n\n\ +Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese \ +odesílatel ParsiCoin 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" -account.crypto.popup.blk-burnt.msg=Chcete-li obchodovat s burnt blackcoiny, musíte znát následující:\n\nBurnt blackcoiny jsou nevyčerpatelné. Aby je bylo možné obchodovat na Haveno, musí mít výstupní skripty podobu: OP_RETURN OP_PUSHDATA, následované přidruženými datovými bajty, které po hexadecimálním zakódování tvoří adresy. Například Burnt blackcoiny s adresou 666f6f („foo“ v UTF-8) budou mít následující skript:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nPro vytvoření Burnt blackcoinů lze použít příkaz „burn“ RPC, který je k dispozici v některých peněženkách.\n\nPro možné případy použití se můžete podívat na https://ibo.laboratorium.ee.\n\nVzhledem k tomu, že Burnt blackcoiny jsou nevyčerpatelné, nelze je znovu prodat. „Prodej“ Burnt blackcoinů znamená vypalování běžných blackcoinů (s přidruženými údaji rovnými cílové adrese).\n\nV případě sporu musí prodejce BLK poskytnout hash transakce. +account.crypto.popup.blk-burnt.msg=Chcete-li obchodovat s burnt blackcoiny, musíte znát následující:\n\n\ +Burnt blackcoiny jsou nevyčerpatelné. Aby je bylo možné obchodovat na Haveno, musí mít výstupní skripty podobu: \ +OP_RETURN OP_PUSHDATA, následované přidruženými datovými bajty, které po hexadecimálním zakódování tvoří adresy. \ +Například Burnt blackcoiny s adresou 666f6f (“foo” v UTF-8) budou mít následující skript:\n\n\ +OP_RETURN OP_PUSHDATA 666f6f\n\n\ +Pro vytvoření Burnt blackcoinů lze použít příkaz ”burn” RPC, který je k dispozici v některých peněženkách.\n\n\ +Pro možné případy použití se můžete podívat na https://ibo.laboratorium.ee.\n\n\ +Vzhledem k tomu, že Burnt blackcoiny jsou nevyčerpatelné, nelze je znovu prodat. ”Prodej” \ +Burnt blackcoinů znamená vypalování běžných blackcoinů (s přidruženými údaji rovnými cílové adrese).\n\n\ +V případě sporu musí prodejce BLK poskytnout hash transakce. # suppress inspection "UnusedProperty" -account.crypto.popup.liquidbitcoin.msg=Obchodování s L-XMR na Haveno vyžaduje, abyste pochopili následující skutečnosti:\n\nKdyž přijímáte L-XMR za obchod na Haveno, nemůžete použít mobilní peněženku Green od Blockstreamu ani jinou custodial peněženku nebo peněženku na burze. L-XMR musíte přijmout pouze do peněženky Liquid Elements Core nebo do jiné L-XMR peněženky, která vám umožní získat slepý klíč pro vaši slepou adresu L-XMR.\n\nV případě, že je nutné zprostředkování, nebo pokud dojde k obchodnímu sporu, musíte zprostředkujícímu mediátorovi Haveno nebo agentovi, který vrací peníze zaslat slepý klíč pro vaši L-XMR adresu, aby mohli ověřit podrobnosti vaší důvěrné transakce na svém vlastním Elements Core full-nodu.\n\nNeposkytnutí požadovaných informací zprostředkovateli nebo agentovi pro vrácení peněz povede ke prohrání sporu. Ve všech sporných případech nese příjemce L-XMR 100% břemeno odpovědnosti za poskytnutí kryptografického důkazu zprostředkovateli nebo agentovi pro vrácení peněz.\n\nPokud těmto požadavkům nerozumíte, neobchodujte s L-XMR na Haveno. +account.crypto.popup.liquidmonero.msg=Obchodování s L-XMR na Haveno vyžaduje, abyste rozuměli následujícím skutečnostem.:\n\n\ +Při přijímání L-XMR za obchod na platformě Haveno nemůžete použít mobilní aplikaci Blockstream Green Wallet nebo \ +peněženku custodial/směnárny. L-XMR musíte přijímat pouze do peněženky Liquid Elements Core nebo do jiné \ +L-XMR peněženky, která vám umožní získat zaslepovací(blinding) klíč pro vaši zaslepenou adresu L-XMR.\n\n\ +V případě nutnosti mediace nebo v případě vzniku obchodního sporu je nutné zveřejnit zaslepovací klíč pro \ +vaši přijímající adresu L-XMR mediátorovi Haveno nebo agentovi vrácení financí, aby mohl ověřit detaily \ +vaší důvěrné transakce na vlastním Elements Core full node.\n\n\ +Neposkytnutí požadovaných informací zprostředkovateli nebo agentovi vrácení financí, bude mít za následek ztrátu sporu. \ +Ve všech případech sporu nese příjemce L-XMR 100% břemeno odpovědnosti \ +poskytnutí kryptografického důkazu mediátorovi nebo agentovi pro vrácení financí.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte s L-XMR na Havenu. account.traditional.yourTraditionalAccounts=Vaše účty v národní měně @@ -1255,16 +1717,26 @@ account.password.removePw.headline=Odstraňte ochranu peněženky pomocí hesla account.password.setPw.button=Nastavit heslo account.password.setPw.headline=Nastavte ochranu peněženky pomocí hesla account.password.info=S ochranou pomocí hesla budete muset zadat heslo při spuštění aplikace, při výběru monera z vaší peněženky a při zobrazení slov seedu peněženky. - account.seed.backup.title=Zálohujte svá klíčová slova peněženky. -account.seed.info=Prosím, zapište si jak klíčová slova peněženky, tak datum. Kdykoliv můžete obnovit svou peněženku pomocí klíčových slov a data.\n\nKlíčová slova byste měli zapsat na kus papíru. Neukládejte je na počítač.\n\nVezměte prosím na vědomí, že klíčová slova NEJSOU náhradou za zálohu.\nMusíte vytvořit zálohu celého adresáře aplikace z obrazovky "Účet/Záloha", abyste mohli obnovit stav a data aplikace. -account.seed.backup.warning=Prosím, poznamenejte si, že klíčová slova nejsou náhradou za zálohu.\nMusíte vytvořit zálohu celého adresáře aplikace z obrazovky "Účet/Záloha", abyste mohli obnovit stav a data aplikace. -account.seed.warn.noPw.msg=Nenastavili jste si heslo k peněžence, které by chránilo zobrazení seed slov.\n\nChcete zobrazit seed slova? +account.seed.info=Prosím, zapište si jak klíčová slova peněženky, tak datum. Kdykoliv můžete obnovit svou peněženku pomocí klíčových slov a data.\n\nKlíčová slova byste měli zapsat na kus papíru. Neukládejte je na počítač.\n\nVezměte prosím na vědomí, že klíčová slova NEJSOU náhradou za zálohu.\nMusíte vytvořit zálohu celého adresáře aplikace z obrazovky \"Účet/Záloha\", abyste mohli obnovit stav a data aplikace. +account.seed.backup.warning=Prosím, poznamenejte si, že klíčová slova nejsou náhradou za zálohu.\nMusíte vytvořit zálohu celého adresáře aplikace z obrazovky \"Účet/Záloha\", abyste mohli obnovit stav a data aplikace. +account.seed.warn.noPw.msg=Nenastavili jste si heslo k peněžence, které by chránilo zobrazení seed slov.\n\n\ +Chcete zobrazit seed slova? account.seed.warn.noPw.yes=Ano, a už se mě znovu nezeptat account.seed.enterPw=Chcete-li zobrazit seed slova, zadejte heslo -account.seed.restore.info=Před použitím obnovení ze seed slov si vytvořte zálohu. Uvědomte si, že obnova peněženky je pouze pro naléhavé případy a může způsobit problémy s interní databází peněženky.\nNení to způsob, jak použít zálohu! K obnovení předchozího stavu aplikace použijte zálohu z adresáře dat aplikace.\n\nPo obnovení se aplikace automaticky vypne. Po restartování aplikace se bude znovu synchronizovat se sítí Monero. To může chvíli trvat a může spotřebovat hodně CPU, zejména pokud byla peněženka starší a měla mnoho transakcí. Vyhněte se přerušování tohoto procesu, jinak budete možná muset znovu odstranit soubor řetězu SPV nebo opakovat proces obnovy. +account.seed.restore.info=Před použitím obnovení ze seed slov si vytvořte zálohu. Uvědomte si, že obnova peněženky je \ + pouze pro naléhavé případy a může způsobit problémy s interní databází peněženky.\n\ + Není to způsob, jak použít zálohu! K obnovení předchozího stavu aplikace \ + použijte zálohu z adresáře dat aplikace.\n\n\ + Po obnovení se aplikace automaticky vypne. Po restartování aplikace se bude znovu synchronizovat se sítí Monero. \ + To může chvíli trvat a může spotřebovat hodně CPU, zejména pokud byla peněženka starší a měla mnoho transakcí. \ + Vyhněte se přerušování tohoto procesu, jinak budete možná muset \ + znovu odstranit soubor řetězu SPV nebo opakovat proces obnovy. account.seed.restore.ok=Dobře, proveďte obnovu a vypněte Haveno - +account.keys.clipboard.warning=Upozorňujeme, že soukromé klíče peněženky jsou velmi citlivé finanční údaje.\n\n\ + ● Nikomu, kdo vás o ně požádá, byste neměli sdělovat své klíče, pokud si nejste naprosto jisti, že mu můžete důvěřovat při nakládání s vašimi penězi! \n\n\ + ● Data soukromých klíčů byste NEMĚLI kopírovat do schránky, pokud si nejste naprosto jisti, že používáte zabezpečené počítačové prostředí bez rizik malwaru. \n\n\ + Mnoho lidí tímto způsobem přišlo o své Monero. Pokud máte JAKÉKOLI pochybnosti, okamžitě zavřete tento dialog a vyhledejte pomoc někoho znalého. #################################################################### # Mobile notifications @@ -1292,7 +1764,8 @@ account.notifications.priceAlert.low.label=Upozorněte, pokud bude cena XMR pod account.notifications.priceAlert.setButton=Nastavit upozornění na cenu account.notifications.priceAlert.removeButton=Odstraňte upozornění na cenu account.notifications.trade.message.title=Obchodní stav se změnil -account.notifications.trade.message.msg.conf=Vkladová transakce pro obchod s ID {0} je potvrzena. Otevřete prosím svou aplikaci Haveno a začněte s platbou. +account.notifications.trade.message.msg.conf=Vkladová transakce pro obchod s ID {0} je potvrzena. \ + Otevřete prosím svou aplikaci Haveno a začněte s platbou. account.notifications.trade.message.msg.started=Kupující XMR zahájil platbu za obchod s ID {0}. account.notifications.trade.message.msg.completed=Obchod s ID {0} je dokončen. account.notifications.offer.message.title=Vaše nabídka byla přijata @@ -1306,7 +1779,10 @@ account.notifications.marketAlert.offerType.label=Typ nabídky, o kterou mám z account.notifications.marketAlert.offerType.buy=Nákupní nabídky (Chci prodat XMR) account.notifications.marketAlert.offerType.sell=Prodejní nabídky (Chci si koupit XMR) account.notifications.marketAlert.trigger=Nabídková cenová vzdálenost (%) -account.notifications.marketAlert.trigger.info=Když je nastavena cenová vzdálenost, obdržíte upozornění pouze v případě, že je zveřejněna nabídka, která splňuje (nebo překračuje) vaše požadavky. Příklad: chcete prodat XMR, ale budete prodávat pouze s 2% přirážkou k aktuální tržní ceně. Nastavení tohoto pole na 2% zajistí, že budete dostávat upozornění pouze na nabídky s cenami, které jsou o 2% (nebo více) nad aktuální tržní cenou. +account.notifications.marketAlert.trigger.info=Když je nastavena cenová vzdálenost, obdržíte upozornění pouze v případě, \ +že je zveřejněna nabídka, která splňuje (nebo překračuje) vaše požadavky. Příklad: chcete prodat XMR, \ +ale budete prodávat pouze s 2% přirážkou k aktuální tržní ceně. Nastavení tohoto pole na 2% zajistí, \ +že budete dostávat upozornění pouze na nabídky s cenami, které jsou o 2% (nebo více) nad aktuální tržní cenou. account.notifications.marketAlert.trigger.prompt=Procentní vzdálenost od tržní ceny (např. 2,50%, -0,50% atd.) account.notifications.marketAlert.addButton=Přidat upozornění na nabídku account.notifications.marketAlert.manageAlertsButton=Spravovat upozornění na nabídku @@ -1317,10 +1793,13 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Typ nabídky account.notifications.marketAlert.message.title=Upozornění na nabídku account.notifications.marketAlert.message.msg.below=pod account.notifications.marketAlert.message.msg.above=nad -account.notifications.marketAlert.message.msg=Do nabídky Haveno byla zveřejněna nová nabídka ''{0} {1}'' s cenou {2} ({3} {4} tržní cena) a způsob platby ''{5}''.\nID nabídky: {6}. +account.notifications.marketAlert.message.msg=Do Haveno byla zveřejněna nová nabídka ''{0} {1}'' s cenou {2} ({3} {4} tržní cena) a \ + způsob platby ''{5}''.\n\ + ID nabídky: {6}. account.notifications.priceAlert.message.title=Upozornění na cenu pro {0} account.notifications.priceAlert.message.msg=Vaše upozornění na cenu bylo aktivováno. Aktuální {0} cena je {1} {2} -account.notifications.noWebCamFound.warning=Nebyla nalezena žádná webkamera.\n\nPoužijte e-mailu k odeslání tokenu a šifrovacího klíče z vašeho mobilního telefonu do aplikace Haveno. +account.notifications.noWebCamFound.warning=Nebyla nalezena žádná webkamera.\n\n\ + Použijte e-mailu k odeslání tokenu a šifrovacího klíče z vašeho mobilního telefonu do aplikace Haveno. account.notifications.priceAlert.warning.highPriceTooLow=Vyšší cena musí být větší než nižší cena. account.notifications.priceAlert.warning.lowerPriceTooHigh=Nižší cena musí být nižší než vyšší cena. @@ -1352,10 +1831,14 @@ displayUpdateDownloadWindow.button.downloadLater=Stáhnout později displayUpdateDownloadWindow.button.ignoreDownload=Ignorovat tuto verzi displayUpdateDownloadWindow.headline=K dispozici je nová aktualizace Haveno! displayUpdateDownloadWindow.download.failed.headline=Stahování selhalo -displayUpdateDownloadWindow.download.failed=Stažení se nezdařilo.\nStáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] -displayUpdateDownloadWindow.installer.failed=Nelze určit správný instalační program. Stáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] -displayUpdateDownloadWindow.verify.failed=Ověření se nezdařilo.\nStáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] -displayUpdateDownloadWindow.success=Nová verze byla úspěšně stažena a podpis ověřen.\n\nOtevřete adresář ke stažení, vypněte aplikaci a nainstalujte novou verzi. +displayUpdateDownloadWindow.download.failed=Stažení se nezdařilo.\n\ + Stáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] +displayUpdateDownloadWindow.installer.failed=Nelze určit správný instalační program. Stáhněte a ručně ověřte na adrese \ + [HYPERLINK:https://haveno.exchange/downloads] +displayUpdateDownloadWindow.verify.failed=Ověření se nezdařilo.\n\ + Stáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] +displayUpdateDownloadWindow.success=Nová verze byla úspěšně stažena a podpis ověřen.\n\n\ + Otevřete adresář ke stažení, vypněte aplikaci a nainstalujte novou verzi. displayUpdateDownloadWindow.download.openDir=Otevřít adresář ke stažení disputeSummaryWindow.title=Souhrn @@ -1371,7 +1854,7 @@ disputeSummaryWindow.payoutAmount.invert=Poražený ve sporu odesílá transakci disputeSummaryWindow.reason=Důvod sporu disputeSummaryWindow.tradePeriodEnd=Konec obchodního období disputeSummaryWindow.extraInfo=Detailní informace -disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status +disputeSummaryWindow.delayedPayoutStatus=Stav zpožděné transakce # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" @@ -1405,26 +1888,47 @@ disputeSummaryWindow.close.button=Zavřít úkol # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" -disputeSummaryWindow.close.msg=Ticket uzavřen {0}\n{1} adresa uzlu: {2}\n\nSouhrn:\nObchodní ID: {3}\nMěna: {4}\nVýše obchodu: {5}\nVýplatní částka pro kupujícího XMR: {6}\nVýplatní částka pro prodejce XMR: {7}\n\nDůvod sporu: {8}\n\nSouhrnné poznámky:\n{9}\n +disputeSummaryWindow.close.msg=Úkol uzavřen {0}\n\ + {1} adresa uzlu: {2}\n\n\ + Souhrn:\n\ + Obchodní ID: {3}\n\ + Měna: {4}\n\ + Důvod sporu: {5}\n\n\ + Výše obchodu: {6}\n\ + Výplatní částka pro kupujícího XMR: {7}\n\ + Výplatní částka pro prodejce XMR: {8}\n\n\ + Souhrnné poznámky:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} -disputeSummaryWindow.close.nextStepsForMediation=\nDalší kroky:\nOtevřete obchod a přijměte nebo odmítněte návrhy od mediátora -disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nDalší kroky:\nNevyžadují se od vás žádné další kroky. Pokud rozhodce rozhodl ve váš prospěch, v sekci Prostředky/Transakce se zobrazí transakce „Vrácení peněz z rozhodčího řízení“ -disputeSummaryWindow.close.closePeer=Potřebujete také zavřít ticket obchodního partnera! +disputeSummaryWindow.close.nextStepsForMediation=\nDalší kroky:\n\ +Otevřete obchod a přijměte nebo odmítněte návrhy od mediátora +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nDalší kroky:\n\ +Nevyžadují se od vás žádné další kroky. Pokud rozhodce rozhodl ve váš prospěch, v sekci Prostředky/Transakce se zobrazí transakce 'Vrácení peněz z rozhodčího řízení' +disputeSummaryWindow.close.closePeer=Potřebujete také zavřít žádost obchodního partnera! disputeSummaryWindow.close.txDetails.headline=Zveřejněte transakci vrácení peněz # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=Kupující obdrží {0} na adresu: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Prodejce obdrží {0} na adresu: {1}\n -disputeSummaryWindow.close.txDetails=Výdaje: {0}\n{1} {2} Transakční poplatek: {3}\n\nOpravdu chcete tuto transakci zveřejnit? +disputeSummaryWindow.close.txDetails=Výdaje: {0}\n\ + {1} {2}\ + Transakční poplatek: {3}\n\n\ + Opravdu chcete tuto transakci zveřejnit? disputeSummaryWindow.close.noPayout.headline=Uzavřít bez jakékoli výplaty disputeSummaryWindow.close.noPayout.text=Chcete zavřít bez výplaty? +disputeSummaryWindow.close.alreadyPaid.headline=Výplata již proběhla +disputeSummaryWindow.close.alreadyPaid.text=Restartujte klienta pro provedení další výplaty u tohoto sporu + emptyWalletWindow.headline={0} nouzový nástroj peněženky -emptyWalletWindow.info=Použijte jej pouze v naléhavých případech, pokud nemůžete získat přístup k vašim prostředkům z uživatelského rozhraní.\n\nUpozorňujeme, že při použití tohoto nástroje budou všechny otevřené nabídky automaticky uzavřeny.\n\nPřed použitím tohoto nástroje si prosím zálohujte datový adresář. Můžete to udělat na obrazovce \"Účet/Záloha\".\n\nNahlaste nám svůj problém a nahlaste zprávu o chybě na GitHubu nebo na fóru Haveno, abychom mohli prozkoumat, co způsobilo problém. +emptyWalletWindow.info=Použijte jej pouze v naléhavých případech, pokud nemůžete získat přístup k vašim prostředkům z uživatelského rozhraní.\n\n\ +Upozorňujeme, že při použití tohoto nástroje budou všechny otevřené nabídky automaticky uzavřeny.\n\n\ +Před použitím tohoto nástroje si prosím zálohujte datový adresář. \ +Můžete to udělat na obrazovce \"Účet/Záloha\".\n\n\ +Nahlaste nám svůj problém a nahlaste nám chybu na GitHubu nebo na fóru Haveno, abychom mohli prozkoumat, co způsobilo problém. emptyWalletWindow.balance=Váš zůstatek v peněžence emptyWalletWindow.address=Vaše cílová adresa emptyWalletWindow.button=Pošlete všechny prostředky @@ -1437,12 +1941,12 @@ enterPrivKeyWindow.headline=Zadejte soukromý klíč pro registraci filterWindow.headline=Upravit seznam filtrů filterWindow.offers=Filtrované nabídky (oddělené čárkami) filterWindow.onions=Onion adresy vyloučené z obchodování (oddělené čárkami) -filterWindow.bannedFromNetwork=Onion adresy vyloučené ze síťové komunikace (oddělené čárkami) +filterWindow.bannedFromNetwork=Onion adresy blokované ze síťové komunikace (oddělené čárkami) filterWindow.accounts=Filtrovaná data obchodního účtu:\nFormát: seznam [ID platební metody | datové pole | hodnota] oddělený čárkami -filterWindow.bannedCurrencies=Filtrované kódy měn (oddělené čárkami) -filterWindow.bannedPaymentMethods=ID filtrované platební metody (oddělené čárkami) -filterWindow.bannedAccountWitnessSignerPubKeys=Filtrované veřejné klíče účtů podepisujícího svědka (hex nebo pub klíče oddělené čárkou) -filterWindow.bannedPrivilegedDevPubKeys=Filtrované privilegované klíče pub dev (hex nebo pub klíče oddělené čárkou) +filterWindow.bannedCurrencies=Blokované kódy měn (oddělené čárkami) +filterWindow.bannedPaymentMethods=ID blokované platební metody (oddělené čárkami) +filterWindow.bannedAccountWitnessSignerPubKeys=Blokované veřejné klíče účtů podepisujícího svědka (hex nebo pub klíče oddělené čárkou) +filterWindow.bannedPrivilegedDevPubKeys=Blokované privilegované klíče pub dev (hex nebo pub klíče oddělené čárkou) filterWindow.arbitrators=Filtrovaní rozhodci (onion adresy oddělené čárkami) filterWindow.mediators=Filtrovaní mediátoři (onion adresy oddělené čárkami) filterWindow.refundAgents=Filtrovaní rozhodci pro vrácení peněz (onion adresy oddělené čárkami) @@ -1463,15 +1967,15 @@ offerDetailsWindow.minXmrAmount=Min. částka XMR offerDetailsWindow.min=(min. {0}) offerDetailsWindow.distance=(vzdálenost od tržní ceny: {0}) offerDetailsWindow.myTradingAccount=Můj obchodní účet -offerDetailsWindow.offererBankId=(ID banky/BIC/SWIFT tvůrce) -offerDetailsWindow.offerersBankName=(název banky tvůrce) offerDetailsWindow.bankId=ID banky (např. BIC nebo SWIFT) offerDetailsWindow.countryBank=Země původu banky tvůrce offerDetailsWindow.commitment=Závazek offerDetailsWindow.agree=Souhlasím offerDetailsWindow.tac=Pravidla a podmínky -offerDetailsWindow.confirm.maker=Potvrďte: Umístit nabídku {0} monero -offerDetailsWindow.confirm.taker=Potvrďte: Využít nabídku {0} monero +offerDetailsWindow.confirm.maker=Potvrďte: Přidat nabídku {0} monero +offerDetailsWindow.confirm.makerCrypto=Potvrďte: Přidat nabídku do {0} {1} +offerDetailsWindow.confirm.taker=Potvrďte: Přijmout nabídku {0} monero +offerDetailsWindow.confirm.takerCrypto=Potvrďte: Přijmout nabídku {0} {1} offerDetailsWindow.creationDate=Datum vzniku offerDetailsWindow.makersOnion=Onion adresa tvůrce offerDetailsWindow.challenge=Passphrase nabídky @@ -1481,7 +1985,15 @@ qRCodeWindow.msg=Použijte tento QR kód k financování vaší peněženky Have qRCodeWindow.request=Žádost o platbu:\n{0} selectDepositTxWindow.headline=Vyberte vkladovou transakci ke sporu -selectDepositTxWindow.msg=Vkladová transakce nebyla v obchodě uložena.\nVyberte prosím jednu z existujících multisig transakcí z vaší peněženky, která byla vkladovou transakcí použitou při selhání obchodu.\n\nSprávnou transakci najdete tak, že otevřete okno s podrobnostmi o obchodu (klikněte na ID obchodu v seznamu) a sledujete výstup transakce s platebním poplatkem za obchodní transakci k následující transakci, kde uvidíte transakci s multisig vklady (adresa začíná na 3). Toto ID transakce by mělo být viditelné v seznamu zde prezentovaném. Jakmile najdete správnou transakci, vyberte ji a pokračujte.\n\nOmlouváme se za nepříjemnosti, ale tento případ chyby by se měl stát velmi zřídka a v budoucnu se pokusíme najít lepší způsoby, jak jej vyřešit. +selectDepositTxWindow.msg=Vkladová transakce nebyla v obchodě uložena.\n\ +Vyberte prosím jednu z existujících multisig transakcí z vaší peněženky, která byla \ +vkladovou transakcí použitou při selhání obchodu.\n\n\ +Správnou transakci najdete tak, že otevřete okno s podrobnostmi o obchodu (klikněte na ID obchodu v seznamu)\ + a sledujete výstup transakce s platebním poplatkem za obchodní transakci k následující transakci, \ +kde uvidíte multisig vkladovou transakci (adresa začíná na 3). Toto ID transakce by mělo být \ +viditelné v seznamu zde prezentovaném. Jakmile najdete správnou transakci, vyberte ji a pokračujte.\n\n\ +Omlouváme se za nepříjemnosti, ale tento případ chyby by se měl stát velmi zřídka \ +a v budoucnu se pokusíme najít lepší způsoby, jak jej vyřešit. selectDepositTxWindow.select=Vyberte vkladovou transakci sendAlertMessageWindow.headline=Odeslat globální oznámení @@ -1512,7 +2024,7 @@ setXMRTxKeyWindow.txKey=Transakční klíč (volitelný) tacWindow.headline=Uživatelská dohoda tacWindow.agree=Souhlasím tacWindow.disagree=Nesouhlasím a odcházím -tacWindow.arbitrationSystem=Řešení sporů +tacWindow.arbitrationSystem=Rozhodnutí sporu tradeDetailsWindow.headline=Obchod tradeDetailsWindow.disputedPayoutTxId=ID sporné platební transakce @@ -1521,6 +2033,7 @@ tradeDetailsWindow.txFee=Poplatek za těžbu tradeDetailsWindow.tradePeersOnion=Onion adresa obchodního partnera tradeDetailsWindow.tradePeersPubKeyHash=Pubkey hash obchodních partnerů tradeDetailsWindow.tradeState=Stav obchodu +tradeDetailsWindow.tradePhase=Fáze obchodu tradeDetailsWindow.agentAddresses=Rozhodce/Mediátor tradeDetailsWindow.detailData=Detailní data @@ -1530,6 +2043,7 @@ txDetailsWindow.xmr.noteReceived=Obdrželi jste XMR. txDetailsWindow.sentTo=Odesláno na txDetailsWindow.receivedWith=Přijato s txDetailsWindow.txId=TxId +txDetailsWindow.txKey=Klíč transakce closedTradesSummaryWindow.headline=Souhrn uzavřených obchodů closedTradesSummaryWindow.totalAmount.title=Celkový objem obchodů @@ -1541,6 +2055,9 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.title=Suma obchodních poplatků v closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} z celkového objemu obchodů) walletPasswordWindow.headline=Pro odemknutí zadejte heslo +connectionFallback.headline=Chyba připojení +connectionFallback.msg=Chyba při připojování k vlastním uzlům Monero.\n\nChcete vyzkoušet další nejlepší dostupný uzel Monero? + torNetworkSettingWindow.header=Nastavení sítě Tor torNetworkSettingWindow.noBridges=Nepoužívat most (bridge) torNetworkSettingWindow.providedBridges=Spojte se s poskytnutými mosty (bridges) @@ -1560,11 +2077,10 @@ torNetworkSettingWindow.deleteFiles.button=Odstranit zastaralé soubory Tor a vy torNetworkSettingWindow.deleteFiles.progress=Probíhá vypínání sítě Tor torNetworkSettingWindow.deleteFiles.success=Zastaralé soubory Tor byly úspěšně odstraněny. Prosím restartujte aplikaci. torNetworkSettingWindow.bridges.header=Je Tor blokovaný? -torNetworkSettingWindow.bridges.info=Pokud je Tor zablokován vaším internetovým poskytovatelem nebo vaší zemí, můžete zkusit použít Tor mosty (bridges).\nNavštivte webovou stránku Tor na adrese: https://bridges.torproject.org/bridges, kde se dozvíte více o mostech a připojitelných přepravách. +torNetworkSettingWindow.bridges.info=Pokud je Tor zablokován vaším internetovým poskytovatelem nebo vaší zemí, můžete zkusit použít Tor mosty (bridges).\n\ +Navštivte webovou stránku Tor na adrese: https://bridges.torproject.org/bridges, \ +kde se dozvíte více o mostech a pluggable transports. -feeOptionWindow.headline=Vyberte měnu pro platbu obchodního poplatku -feeOptionWindow.info=Můžete si vybrat, zda chcete zaplatit obchodní poplatek v BSQ nebo v XMR. Pokud zvolíte BSQ, oceníte zlevněný obchodní poplatek. -feeOptionWindow.optionsLabel=Vyberte měnu pro platbu obchodního poplatku feeOptionWindow.useXMR=Použít XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) @@ -1588,23 +2104,39 @@ popup.headline.error=Chyba popup.doNotShowAgain=Znovu nezobrazovat popup.reportError.log=Otevřít log popup.reportError.gitHub=Nahlaste problém na GitHub -popup.reportError={0}\n\nChcete-li nám pomoci vylepšit software, nahlaste tuto chybu otevřením nového problému na adrese https://github.com/bisq-network/bisq/issues.\nVýše uvedená chybová zpráva bude zkopírována do schránky po kliknutí na některé z níže uvedených tlačítek.\nUsnadníte ladění, pokud zahrnete soubor haveno.log stisknutím tlačítka „Otevřít log soubor“, uložením kopie a připojením ke zprávě o chybě. +popup.reportError={0}\n\nChcete-li nám pomoci vylepšit software, nahlaste tuto chybu otevřením nového problému na adrese https://github.com/bisq-network/bisq/issues.\n\ +Výše uvedená chybová zpráva bude zkopírována do schránky po kliknutí na některé z níže uvedených tlačítek.\n\ +Usnadníte ladění, pokud zahrnete soubor haveno.log stisknutím tlačítka 'Otevřít log soubor', uložením kopie a připojením ke hlášení chyby. popup.error.tryRestart=Zkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit. popup.error.takeOfferRequestFailed=Když se někdo pokusil využít jednu z vašich nabídek, došlo k chybě:\n{0} error.spvFileCorrupted=Při čtení řetězce SPV došlo k chybě.\nJe možné, že je poškozen řetězový soubor SPV.\n\nChybová zpráva: {0}\n\nChcete ji smazat a začít znovu synchronizovat? error.deleteAddressEntryListFailed=Soubor AddressEntryList nelze smazat.\nChyba: {0} -error.closedTradeWithUnconfirmedDepositTx=Vkladová transakce uzavřeného obchodu s obchodním ID {0} je stále nepotvrzená.\n\nProveďte prosím SPV resynchronizaci v \"Nastavení/Informace o síti\" a zkontrolujte, zda je transakce platná. -error.closedTradeWithNoDepositTx=Vkladová transakce uzavřeného obchodu s obchodním ID {0} je nulová.\n\nChcete-li vyčistit seznam uzavřených obchodů, restartujte aplikaci. +error.closedTradeWithUnconfirmedDepositTx=Vkladová transakce uzavřeného obchodu s obchodním ID {0} je stále \ + nepotvrzená.\n\n\ + Proveďte prosím SPV resynchronizaci v \"Nastavení/Informace o síti\" a zkontrolujte, zda je transakce platná. +error.closedTradeWithNoDepositTx=Vkladová transakce uzavřeného obchodu s obchodním ID {0} je nulová.\n\n\ + Chcete-li vyčistit seznam uzavřených obchodů, restartujte aplikaci. popup.warning.walletNotInitialized=Peněženka ještě není inicializována -popup.warning.osxKeyLoggerWarning=V souladu s přísnějšími bezpečnostními opatřeními v systému macOS 10.14 a novějších způsobí spuštění aplikace Java (Haveno používá Javu) upozornění na vyskakovací okno v systému MacOS („Haveno by chtěl přijímat stisknutí kláves z jakékoli aplikace“).\n\nChcete-li se tomuto problému vyhnout, otevřete své Nastavení macOS a přejděte do části "Zabezpečení a soukromí" -> "Soukromí" -> "Sledování vstupu" a ze seznamu na pravé straně odeberte „Haveno“.\n\nHaveno upgraduje na novější verzi Java, aby se tomuto problému vyhnul, jakmile budou vyřešena technická omezení (balíček Java Packager pro požadovanou verzi Java ještě není dodán). -popup.warning.wrongVersion=Pravděpodobně máte nesprávnou verzi Haveno pro tento počítač.\nArchitektura vašeho počítače je: {0}.\nBinární kód Haveno, který jste nainstalovali, je: {1}.\nVypněte prosím a znovu nainstalujte správnou verzi ({2}). -popup.warning.incompatibleDB=Zjistili jsme nekompatibilní soubory databáze!\n\nTyto databázové soubory nejsou kompatibilní s naší aktuální kódovou základnou:\n{0}\n\nVytvořili jsme zálohu poškozených souborů a aplikovali jsme výchozí hodnoty na novou verzi databáze.\n\nZáloha se nachází na adrese:\n{1}/db/backup_of_corrupted_data.\n\nZkontrolujte, zda máte nainstalovanou nejnovější verzi Haveno.\nMůžete si jej stáhnout na adrese: [HYPERLINK:https://haveno.exchange/downloads].\n\nRestartujte aplikaci. -popup.warning.startupFailed.twoInstances=Haveno již běží. Nemůžete spustit dvě instance Haveno. +popup.warning.wrongVersion=Pravděpodobně máte nesprávnou verzi Haveno pro tento počítač.\n\ +Architektura vašeho počítače je: {0}.\n\ +Binární kód Haveno, který jste nainstalovali, je: {1}.\n\ +Vypněte prosím a znovu nainstalujte správnou verzi ({2}). +popup.warning.incompatibleDB=Zjistili jsme nekompatibilní soubory databáze!\n\n\ +Tyto databázové soubory nejsou kompatibilní s naší aktuální kódovou základnou:\n{0}\n\n\ +Vytvořili jsme zálohu poškozených souborů a aplikovali jsme výchozí hodnoty na novou verzi databáze.\n\n\ +Záloha se nachází na adrese:\n\ +{1}/db/backup_of_corrupted_data.\n\n\ +Zkontrolujte, zda máte nainstalovanou nejnovější verzi Haveno.\n\ +Můžete si jej stáhnout na adrese: [HYPERLINK:https://haveno.exchange/downloads].\n\n\ +Restartujte prosím aplikaci. +popup.warning.startupFailed.twoInstances=Haveno již běží. Nemůžete mít spuštěné dvě instance Haveno. popup.warning.tradePeriod.halfReached=Váš obchod s ID {0} dosáhl poloviny max. povoleného obchodního období a stále není dokončen.\n\nObdobí obchodování končí {1}\n\nDalší informace o stavu obchodu naleznete na adrese \"Portfolio/Otevřené obchody\". -popup.warning.tradePeriod.ended=Váš obchod s ID {0} dosáhl max. povoleného obchodního období a není dokončen.\n\nObdobí obchodování skončilo {1}\n\nZkontrolujte prosím svůj obchod v sekci "Portfolio/Otevřené obchody\", abyste kontaktovali mediátora. +popup.warning.tradePeriod.ended=Váš obchod s ID {0} dosáhl max. povoleného obchodního období a není dokončen.\n\n\ + Období obchodování skončilo {1}\n\n\ + Zkontrolujte prosím svůj obchod v sekci "Portfolio/Otevřené obchody\", abyste kontaktovali mediátora. popup.warning.noTradingAccountSetup.headline=Nemáte nastaven obchodní účet popup.warning.noTradingAccountSetup.msg=Než budete moci vytvořit nabídku, musíte si nastavit národní měnu nebo kryptoměnový účet.\nChcete si založit účet? popup.warning.noArbitratorsAvailable=Nejsou k dispozici žádní rozhodci. @@ -1619,48 +2151,94 @@ popup.warning.examplePercentageValue=Zadejte procento jako číslo \"5.4\" pro 5 popup.warning.noPriceFeedAvailable=Pro tuto měnu není k dispozici žádný zdroj cen. Nelze použít procentuální cenu.\nVyberte pevnou cenu. popup.warning.sendMsgFailed=Odeslání zprávy vašemu obchodnímu partnerovi se nezdařilo.\nZkuste to prosím znovu a pokud to i nadále selže, nahlaste chybu. popup.warning.messageTooLong=Vaše zpráva překračuje max. povolená velikost. Zašlete jej prosím v několika částech nebo ji nahrajte do služby, jako je https://pastebin.com. -popup.warning.lockedUpFunds=Zamkli jste finanční prostředky z neúspěšného obchodu.\nUzamčený zůstatek: {0}\nVkladová tx adresa: {1}\nObchodní ID: {2}.\n\nOtevřete prosím úkol pro podporu výběrem obchodu na obrazovce otevřených obchodů a stisknutím \"alt + o\" nebo \"option + o\"." +popup.warning.lockedUpFunds=Zamkli jste finanční prostředky z neúspěšného obchodu.\n\ + Uzamčený zůstatek: {0}\n\ + Adresa vkladové tx: {1}\n\ + ID obchodu: {2}.\n\n\ + Otevřete prosím úkol pro podporu výběrem obchodu na obrazovce otevřených obchodů a stisknutím \"alt + o\" nebo \"option + o\"." -popup.warning.makerTxInvalid=Tato nabídka není platná. Prosím vyberte jinou nabídku.\n\n +popup.warning.makerTxInvalid=Tato nabídka není platná. Prosím vyberte jinou nabídku.\n\n takeOffer.cancelButton=Zrušit akceptaci nabídky takeOffer.warningButton=Ignorovat a přesto pokračovat # suppress inspection "UnusedProperty" -popup.warning.nodeBanned=Jeden z {0} uzlů byl zabanován. +popup.warning.nodeBanned=Jeden z {0} uzlů byl zablokován. # suppress inspection "UnusedProperty" popup.warning.priceRelay=cenové relé popup.warning.seed=seed -popup.warning.mandatoryUpdate.trading=Aktualizujte prosím na nejnovější verzi Haveno. Byla vydána povinná aktualizace, která zakazuje obchodování se starými verzemi. Další informace naleznete na fóru Haveno. -popup.warning.burnXMR=Tato transakce není možná, protože poplatky za těžbu {0} by přesáhly částku převodu {1}. Počkejte prosím, dokud nebudou poplatky za těžbu opět nízké nebo dokud nenahromadíte více XMR k převodu. +popup.warning.mandatoryUpdate.trading=Aktualizujte prosím na nejnovější verzi Haveno. \ + Byla vydána povinná aktualizace, která zakazuje obchodování se starými verzemi. \ + Další informace naleznete na fóru Haveno. +popup.warning.noFilter=Ze seed uzlů jsme neobdrželi objekt filtru. Informujte prosím správce sítě, aby objekt filtru zaregistrovali. +popup.warning.burnXMR=Tato transakce není možná, protože poplatky za těžbu {0} by přesáhly částku převodu {1}. \ + Počkejte prosím, dokud nebudou poplatky za těžbu opět nízké nebo dokud nenahromadíte více XMR k převodu. -popup.warning.openOffer.makerFeeTxRejected=Transakční poplatek tvůrce za nabídku s ID {0} byl odmítnut sítí Monero.\nID transakce = {1}.\nNabídka byla odstraněna, aby se předešlo dalším problémům.\nPřejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. +popup.warning.openOffer.makerFeeTxRejected=Transakční poplatek tvůrce za nabídku s ID {0} byl odmítnut sítí Monero.\n\ + ID transakce = {1}.\n\ + Nabídka byla odstraněna, aby se předešlo dalším problémům.\n\ + Přejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\n\ + Pro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. popup.warning.trade.txRejected.tradeFee=obchodní poplatek popup.warning.trade.txRejected.deposit=vklad -popup.warning.trade.txRejected=Síť Monero odmítla {0} transakci pro obchod s ID {1}.\nID transakce = {2}\nObchod byl přesunut do neúspěšných obchodů.\nPřejděte do části \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. +popup.warning.trade.txRejected=Síť Monero odmítla {0} transakci pro obchod s ID {1}.\n\ + ID transakce = {2}\n\ + Obchod byl přesunut do neúspěšných obchodů.\n\ + Přejděte do části \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\n\ + Pro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. -popup.warning.openOfferWithInvalidMakerFeeTx=Transakční poplatek tvůrce za nabídku s ID {0} je neplatný.\nID transakce = {1}.\nPřejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. +popup.warning.openOfferWithInvalidMakerFeeTx=Transakční poplatek tvůrce za nabídku s ID {0} je neplatný.\n\ + ID transakce = {1}.\n\ + Přejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\n\ + Pro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. -popup.info.securityDepositInfo=Aby oba obchodníci dodržovali obchodní protokol, musí oba obchodníci zaplatit kauci.\n\nTento vklad je uložen ve vaší obchodní peněžence, dokud nebude váš obchod úspěšně dokončen a poté vám bude vrácen.\n\nPoznámka: Pokud vytváříte novou nabídku, musí program Haveno běžet, aby ji převzal jiný obchodník. Chcete-li zachovat své nabídky online, udržujte Haveno spuštěný a ujistěte se, že tento počítač zůstává online (tj. Zkontrolujte, zda se nepřepne do pohotovostního režimu...pohotovostní režim monitoru je v pořádku). - -popup.info.cashDepositInfo=Ujistěte se, že ve své oblasti máte pobočku banky, abyste mohli provést hotovostní vklad.\nID banky prodávajícího (BIC/SWIFT) je: {0}. +popup.info.cashDepositInfo=Ujistěte se, že ve své oblasti máte pobočku banky, abyste mohli provést hotovostní vklad.\n\ + ID banky prodávajícího (BIC/SWIFT) je: {0}. popup.info.cashDepositInfo.confirm=Potvrzuji, že mohu provést vklad -popup.info.shutDownWithOpenOffers=Haveno se vypíná, ale existují otevřené nabídky.\n\nTyto nabídky nebudou dostupné v síti P2P, pokud bude Haveno vypnutý, ale budou znovu publikovány do sítě P2P při příštím spuštění Haveno.\n\nChcete-li zachovat své nabídky online, udržujte Haveno spuštěný a ujistěte se, že tento počítač zůstává online (tj. Ujistěte se, že nepřejde do pohotovostního režimu...pohotovostní režim monitoru není problém). -popup.info.qubesOSSetupInfo=Zdá se, že používáte Haveno na Qubes OS.\n\nUjistěte se, že je vaše Haveno qube nastaveno podle našeho průvodce nastavením na [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. +popup.info.shutDownWithOpenOffers=Haveno se vypíná, ale existují otevřené nabídky.\n\n\ + Tyto nabídky nebudou dostupné v síti P2P, pokud bude Haveno vypnutý, ale budou znovu \ + zveřejněny do sítě P2P při příštím spuštění Haveno. Chcete-li zachovat své nabídky \ + online, udržujte Haveno spuštěné a připojení k internetu (ujistěte se, že počítač \ +nepřejde do pohotovostního režimu... pohotovostní režim monitoru není problém). +popup.info.shutDownWithTradeInit={0}\n\ + Tento obchod se ještě neinicializoval; jeho ukončení by pravděpodobně vedlo k jeho poškození. Počkejte prosím minutu a zkuste to znovu. +popup.info.shutDownWithDisputeInit=Služba Haveno se vypíná, ale stále je zde čekající zpráva systému sporu.\n\ + Před vypnutím prosím počkejte minutu. +popup.info.shutDownQuery=Jste si jisti, že chcete opustit Haveno? +popup.info.qubesOSSetupInfo=Zdá se, že používáte Haveno na Qubes OS.\n\n\ + Ujistěte se, že je vaše Haveno qube nastaveno podle našeho průvodce nastavením na [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. +popup.info.p2pStatusIndicator.red={0}\n\n\ + Váš uzel není připojen k síti P2P. V tomto stavu nemůže Haveno fungovat. +popup.info.p2pStatusIndicator.yellow={0}\n\n\ + Váš uzel nemá žádná příchozí připojení Tor. Haveno bude fungovat v pořádku, ale pokud tento stav přetrvává několik hodin, může to znamenat problémy s připojením. +popup.info.p2pStatusIndicator.green={0}\n\n\ + Dobrá zpráva, stav vašeho připojení P2P vypadá zdravě! +popup.info.firewallSetupInfo=Zdá se, že tento počítač blokuje příchozí připojení Tor. \ + K tomu může dojít v prostředích virtuálních počítačů, jako je Qubes/VirtualBox/Whonix. \n\n\ + Nastavte prosím své prostředí tak, aby přijímalo příchozí připojení Tor, jinak vaše nabídky nebude moci nikdo přijmout. popup.warn.downGradePrevention=Downgrade z verze {0} na verzi {1} není podporován. Použijte prosím nejnovější verzi Haveno. +popup.warn.daoRequiresRestart=Vyskytl se problém se synchronizací stavu DAO. Pro odstranění problému je nutné restartovat aplikaci. popup.privateNotification.headline=Důležité soukromé oznámení! -popup.securityRecommendation.headline=Důležité bezpečnostní doporučení -popup.securityRecommendation.msg=Chtěli bychom vám připomenout, abyste zvážili použití ochrany heslem pro vaši peněženku, pokud jste ji již neaktivovali.\n\nDůrazně se také doporučuje zapsat seed slova peněženky. Tato seed slova jsou jako hlavní heslo pro obnovení vaší peněženky Monero.\nV sekci "Seed peněženky" naleznete další informace.\n\nDále byste měli zálohovat úplnou složku dat aplikace v sekci \"Záloha\". - -popup.xmrLocalNode.msg=Haveno zjistil, že na tomto stroji (na localhostu) běží Monero uzel.\n\nUjistěte se, prosím, že tento uzel je plně synchronizován před spuštěním Havena. - +popup.xmrLocalNode.msg=Haveno zjistil, že na tomto stroji (na localhostu) běží Monero uzel.\n\n\Prosím ujistěte se, že tento uzel je plně synchronizován před spuštěním Havena. popup.shutDownInProgress.headline=Probíhá vypínání popup.shutDownInProgress.msg=Vypnutí aplikace může trvat několik sekund.\nProsím, nepřerušujte tento proces. -popup.attention.forTradeWithId=Je třeba věnovat pozornost obchodu s ID {0} -popup.attention.reasonForPaymentRuleChange=Verze 1.5.5 přináší zásadní změnu v pravidlech obchodování ohledně \"důvodu platby\" v bankovních převodech. Prosím nechte toto pole prázdné -- ID obchodu již v poli \"důvod platby\" NEPOUŽÍVEJTE. +popup.attention.forTradeWithId=Je třeba věnovat pozornost obchodu ID {0} +popup.attention.welcome.stagenet=Vítejte v testovací instanci Haveno!\n\n\ +Tato platforma umožňuje testovat protokol Haveno. Ujistěte se, že postupujete podle pokynů [HYPERLINK:https://github.com/haveno-dex/haveno/blob/master/docs/installing.md].\n\n\ +Pokud narazíte na nějaký problém, dejte nám prosím vědět [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new].\n\n\ +Jedná se o testovací instanci. Nepoužívejte skutečné peníze! +popup.attention.welcome.mainnet=Vítejte v Haveno!\n\n\ +Tato platforma umožňuje decentralizovaně obchodovat s měnou Monero za fiat měny nebo jiné kryptoměny.\n\n\ +Začněte tím, že si vytvoříte nový platební účet a poté vytvoříte nebo přijmete nabídku.\n\n\ +Pokud narazíte na nějaký problém, dejte nám prosím vědět [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new]. +popup.attention.welcome.mainnet.test=Vítejte v Haveno!\n\n\ +Tato platforma umožňuje decentralizovaně obchodovat s měnou Monero za fiat měny nebo jiné kryptoměny.\n\n\ +Začněte tím, že si vytvoříte nový platební účet a poté vytvoříte nebo přijmete nabídku.\n\n\ +Pokud narazíte na nějaký problém, dejte nám prosím vědět [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new].\n\n\ +Systém Haveno byl nedávno uvolněn k veřejnému testování. Používejte prosím nízké částky! popup.info.multiplePaymentAccounts.headline=K dispozici jsou účty a více platebními metodami popup.info.multiplePaymentAccounts.msg=Pro tuto nabídku máte k dispozici více platebních účtů. Ujistěte se, že jste vybrali ten správný. @@ -1681,11 +2259,13 @@ popup.accountSigning.signAccounts.ECKey.error=Špatný ECKey rozhodce popup.accountSigning.success.headline=Gratulujeme popup.accountSigning.success.description=Všechny {0} platební účty byly úspěšně podepsány! -popup.accountSigning.generalInformation=Podpisový stav všech vašich účtů najdete v sekci účtu.\n\nDalší informace naleznete na adrese [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. +popup.accountSigning.generalInformation=Podpisový stav všech vašich účtů najdete v sekci účtu.\n\n + Další informace naleznete na adrese [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=Jeden z vašich platebních účtů byl ověřen a podepsán rozhodcem. Obchodování s tímto účtem po úspěšném obchodování automaticky podepíše účet vašeho obchodního partnera.\n\n{0} popup.accountSigning.signedByPeer=Jeden z vašich platebních účtů byl ověřen a podepsán obchodním partnerem. Váš počáteční obchodní limit bude zrušen a do {0} dnů budete moci podepsat další účty.\n\n{1} popup.accountSigning.peerLimitLifted=Počáteční limit pro jeden z vašich účtů byl zrušen.\n\n{0} -popup.accountSigning.peerSigner=Jeden z vašich účtů je dostatečně zralý, aby podepsal další platební účty, a počáteční limit pro jeden z vašich účtů byl zrušen.\n\n{0} +popup.accountSigning.peerSigner=Jeden z vašich účtů je dostatečně zralý, aby podepsal další platební účty, \ + a počáteční limit pro jeden z vašich účtů byl zrušen.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Importujte svědka stáří účtu k podepsání popup.accountSigning.confirmSingleAccount.headline=Potvrďte vybrané svědky o stáří účtu @@ -1701,14 +2281,21 @@ popup.accountSigning.unsignedPubKeys.result.signed=Podepsané pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Nepodařilo se podepsat popup.info.buyerAsTakerWithoutDeposit.headline=Žádný vklad není od kupujícího požadován -popup.info.buyerAsTakerWithoutDeposit=Vaše nabídka nebude vyžadovat bezpečnostní zálohu ani poplatek od kupujícího XMR.\n\nPro přijetí vaší nabídky musíte sdílet heslo se svým obchodním partnerem mimo Haveno.\n\nHeslo je automaticky vygenerováno a zobrazeno v detailech nabídky po jejím vytvoření. +popup.info.buyerAsTakerWithoutDeposit=\ + Vaše nabídka nebude vyžadovat bezpečnostní zálohu ani poplatek od kupujícího XMR.\n\n + Pro přijetí vaší nabídky musíte sdílet heslo se svým obchodním partnerem mimo Haveno.\n\n\ + Heslo je automaticky vygenerováno a zobrazeno v detailech nabídky po jejím vytvoření.\ + +popup.info.torMigration.msg=Váš uzel Haveno pravděpodobně používá zastaralou adresu Tor v2. \ + Přepněte svůj uzel Haveno na adresu Tor v3. \ + Nezapomeňte si předem zálohovat datový adresář. #################################################################### # Notifications #################################################################### notification.trade.headline=Oznámení o obchodu s ID {0} -notification.ticket.headline=Úkol na podporu pro obchod s ID {0} +notification.ticket.headline=Úkol pro podporu pro obchod s ID {0} notification.trade.completed=Obchod je nyní dokončen a můžete si vybrat své prostředky. notification.trade.accepted=Vaše nabídka byla přijata XMR {0}. notification.trade.unlocked=Váš obchod má alespoň jedno potvrzení blockchainu.\nPlatbu můžete začít hned teď. @@ -1731,7 +2318,7 @@ systemTray.show=Otevřít okno aplikace systemTray.hide=Skrýt okno aplikace systemTray.info=Informace o Haveno systemTray.exit=Odejít -systemTray.tooltip=Haveno: Decentralizovaná směnárna moneroů +systemTray.tooltip=Haveno: Decentralizovaná směnárna monero #################################################################### @@ -1749,7 +2336,12 @@ guiUtil.accountExport.exportFailed=Export do CSV selhal kvůli chybě.\nChyba = guiUtil.accountExport.selectExportPath=Vyberte složku pro export guiUtil.accountImport.imported=Obchodní účet importovaný z:\n{0}\n\nImportované účty:\n{1} guiUtil.accountImport.noAccountsFound=Nebyly nalezeny žádné exportované obchodní účty na: {0}.\nNázev souboru je {1}." -guiUtil.openWebBrowser.warning=Chystáte se otevřít webovou stránku ve webovém prohlížeči.\nChcete nyní otevřít webovou stránku?\n\nPokud nepoužíváte \"Tor Browser\" jako výchozí systémový webový prohlížeč, připojíte se k webové stránce přes nechráněné spojení.\n\nURL: \"{0}\" +guiUtil.openWebBrowser.warning=Chystáte se otevřít webovou stránku \ +ve webovém prohlížeči.\n\ +Chcete nyní otevřít webovou stránku?\n\n\ +Pokud nepoužíváte \"Tor Browser\" jako výchozí systémový webový prohlížeč, \ +připojíte se k webové stránce přes nechráněné spojení.\n\n\ +URL: \"{0}\" guiUtil.openWebBrowser.doOpen=Otevřít webovou stránku a znovu se neptat guiUtil.openWebBrowser.copyUrl=Zkopírovat URL a zrušit guiUtil.ofTradeAmount=obchodní částky @@ -1774,11 +2366,12 @@ peerInfoIcon.tooltip.trade.traded={0} onion adresa: {1}\nUž jste s tímto partn peerInfoIcon.tooltip.trade.notTraded={0} onion adresa: {1}\nDosud jste s tímto partnerem neobchodovali.\n{2} peerInfoIcon.tooltip.age=Platební účet byl vytvořen před {0}. peerInfoIcon.tooltip.unknownAge=Stáří platebního účtu není znám. +peerInfoIcon.tooltip.dispute={0}nPočet sporů: {1}.\n{2} tooltip.openPopupForDetails=Otevřít vyskakovací okno s podrobnostmi tooltip.invalidTradeState.warning=Tento obchod je v neplatném stavu. Chcete-li získat další informace, otevřete okno s podrobnostmi -tooltip.openBlockchainForAddress=Otevřít externí blockchain explorer pro adresu: {0} -tooltip.openBlockchainForTx=Otevřete externí blockchain explorer pro transakci: {0} +tooltip.openBlockchainForAddress=Otevřít externí průzkumník blockchainu pro adresu: {0} +tooltip.openBlockchainForTx=Otevřít externí průzkumník blockchainu pro transakci: {0} confidence.unknown=Neznámý stav transakce confidence.seen=Viděno {0} partnery / 0 potvrzení @@ -1798,6 +2391,10 @@ addressTextField.copyToClipboard=Zkopírujte adresu do schránky addressTextField.addressCopiedToClipboard=Adresa byla zkopírována do schránky addressTextField.openWallet.failed=Otevření výchozí peněženky Monero selhalo. Možná nemáte žádnou nainstalovanou? +explorerAddressTextField.copyToClipboard=Kopírovat adresu do schránky +explorerAddressTextField.blockExplorerIcon.tooltip=Otevřete průzkumník blockchainu s touto adresou +explorerAddressTextField.missingTx.warning.tooltip=Chybí požadovaná adresa + peerInfoIcon.tooltip={0}\nŠtítek: {1} txIdTextField.copyIcon.tooltip=Zkopírujte ID transakce do schránky @@ -1811,11 +2408,11 @@ txIdTextField.missingTx.warning.tooltip=Chybí požadovaná transakce navigation.account=\"Účet\" navigation.account.walletSeed=\"Účet/Seed peněženky\" -navigation.funds.availableForWithdrawal=\"Prostředky/Odeslat prostředky\" +navigation.funds.availableForWithdrawal=\"Prostředky/Poslat finanční prostředky\" navigation.portfolio.myOpenOffers=\"Portfolio/Moje otevřené nabídky\" navigation.portfolio.pending=\"Portfolio/Otevřené obchody\" navigation.portfolio.closedTrades=\"Portfolio/Historie\" -navigation.funds.depositFunds=\"Prostředky/Přijmout prostředky\" +navigation.funds.depositFunds=\"Prostředky/Přijmout finanční prostředky\" navigation.settings.preferences=\"Nastavení/Preference\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"Prostředky/Transakce\" @@ -1833,7 +2430,7 @@ formatter.youAreAsMaker=Jste {1} {0} (jako tvůrce) / Příjemce je {3} {2} formatter.youAreAsTaker=Jste {1} {0} (jako příjemce) / Tvůrce je {3} {2} formatter.youAre={0}te {1} ({2}te {3}) formatter.youAreCreatingAnOffer.traditional=Vytváříte nabídku: {0} {1} -formatter.youAreCreatingAnOffer.crypto=Vytváříte nabídku: {0} {1} ({2}te {3}) +formatter.youAreCreatingAnOffer.crypto=Vytváříte nabídku: {0} {1} ({2} {3}) formatter.asMaker={0} {1} jako tvůrce formatter.asTaker={0} {1} jako příjemce @@ -1853,18 +2450,20 @@ XMR_STAGENET=Monero Stagenet time.year=Rok time.month=Měsíc +time.halfYear=Půlrok +time.quarter=Čtvrtrok time.week=Týden time.day=Den time.hour=Hodina time.minute10=10 minut -time.hours=hodiny -time.days=dny +time.hours=hodin +time.days=dnů time.1hour=1 hodina time.1day=1 den time.minute=minuta time.second=sekunda -time.minutes=minuty -time.seconds=sekundy +time.minutes=minut +time.seconds=sekund password.enterPassword=Vložte heslo @@ -1874,11 +2473,12 @@ password.deriveKey=Odvozuji klíč z hesla password.walletDecrypted=Peněženka úspěšně dešifrována a ochrana heslem byla odstraněna. password.wrongPw=Zadali jste nesprávné heslo.\n\nZkuste prosím zadat heslo znovu a pečlivě zkontrolujte překlepy nebo pravopisné chyby. password.walletEncrypted=Peněženka úspěšně šifrována a ochrana heslem povolena. +password.walletEncryptionFailed=Heslo se nepodařilo nastavit password.passwordsDoNotMatch=Zadaná 2 hesla se neshodují. password.forgotPassword=Zapomněli jste heslo? -password.backupReminder=Pamatujte, že při nastavování hesla do peněženky budou odstraněny všechny automaticky vytvořené zálohy z nezašifrované peněženky.\n\nPřed nastavením hesla se důrazně doporučuje provést zálohu adresáře aplikace a zapsat si počáteční slova! -password.backupWasDone=Už jsem provedl zálohu -password.setPassword=Nastavit heslo (Už jsem provedl zálohu) +password.backupReminder=Pamatujte, že při nastavování hesla do peněženky budou smazány všechny automaticky vytvořené zálohy z nezašifrované peněženky.\n\n\ + Před nastavením hesla se důrazně doporučuje provést zálohu adresáře aplikace a zapsat si seed slova! +password.setPassword=Nastavit heslo (Už jsem provedl/a zálohu) password.makeBackup=Provést zálohu seed.seedWords=Seed slova peněženky @@ -1887,14 +2487,25 @@ seed.date=Datum peněženky seed.restore.title=Obnovit peněženky z seed slov seed.restore=Obnovit peněženky seed.creationDate=Datum vzniku -seed.warn.walletNotEmpty.msg=Vaše peněženka Monero není prázdná.\n\nTuto peněženku musíte vyprázdnit, než se pokusíte obnovit starší, protože smíchání peněženek může vést ke zneplatnění záloh.\n\nDokončete své obchody, uzavřete všechny otevřené nabídky a přejděte do sekce Prostředky, kde si můžete vybrat své moneroy.\nV případě, že nemáte přístup ke svým moneroům, můžete použít nouzový nástroj k vyprázdnění peněženky.\nNouzový nástroj otevřete stisknutím kombinace kláves \"Alt+e\" nebo \"Cmd/Ctrl+e\". +seed.warn.walletNotEmpty.msg=Vaše peněženka Monero není prázdná.\n\n\ +Tuto peněženku musíte vyprázdnit, než se pokusíte obnovit starší, protože smíchání peněženek \ +může vést ke zneplatnění záloh.\n\n\ +Dokončete své obchody, uzavřete všechny otevřené nabídky a přejděte do sekce Prostředky, kde si můžete vybrat své monero.\n\ +V případě, že nemáte přístup ke svým moneroům, můžete použít nouzový nástroj k vyprázdnění peněženky.\n\ +Nouzový nástroj otevřete stisknutím kombinace kláves \"Alt+e\" nebo \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=Chci přesto obnovit seed.warn.walletNotEmpty.emptyWallet=Nejprve vyprázdním své peněženky -seed.warn.notEncryptedAnymore=Vaše peněženky jsou šifrovány.\n\nPo obnovení již nebudou peněženky šifrovány a musíte nastavit nové heslo.\n\nChcete pokračovat? -seed.warn.walletDateEmpty=Protože jste nezadali datum peněženky, bude muset haveno skenovat blockchain od roku 2013.10.09 (datum spuštění BIP39).\n\nPeněženky BIP39 byly poprvé představeny v haveno dne 2017.06.28 (verze v0.5). Tímto datem můžete ušetřit čas.\n\nV ideálním případě byste měli určit datum, kdy byl vytvořen váš seed peněženky.\n\n\nOpravdu chcete pokračovat bez zadání data peněženky? +seed.warn.notEncryptedAnymore=Vaše peněženky jsou šifrovány.\n\n\ +Po obnovení již nebudou peněženky šifrovány a musíte nastavit nové heslo.\n\n\ +Chcete pokračovat? +seed.warn.walletDateEmpty=Protože jste nezadali datum peněženky, bude muset haveno skenovat blockchain od roku 2013.10.09 (datum spuštění BIP39).\n\n\ +Peněženky BIP39 byly poprvé představeny v Haveno dne 2017.06.28 (verze v0.5). Použitím tohoto data můžete ušetřit čas.\n\n\ +V ideálním případě byste měli určit datum, kdy byl vytvořen váš seed peněženky.\n\n\n\ +Opravdu chcete pokračovat bez zadání data peněženky? seed.restore.success=Peněženky byly úspěšně obnoveny pomocí nových seed slov.\n\nMusíte vypnout a restartovat aplikaci. seed.restore.error=Při obnově peněženek pomocí seed slov došlo k chybě. {0} -seed.restore.openOffers.warn=Máte otevřené nabídky, které budou odstraněny, pokud obnovíte ze seedu.\nJste si jisti, že chcete pokračovat? +seed.restore.openOffers.warn=Máte otevřené nabídky, které budou odstraněny, pokud obnovíte ze seedu.\n\ + Jste si jisti, že chcete pokračovat? #################################################################### @@ -1910,9 +2521,10 @@ payment.account.owner=Celé jméno vlastníka účtu payment.account.fullName=Celé jméno (křestní, střední, příjmení) payment.account.state=Stát/Provincie/Region payment.account.city=Město +payment.account.address=Adresa payment.bank.country=Země původu banky -payment.account.name.email=Celé jméno / e-mail majitele účtu -payment.account.name.emailAndHolderId=Celé jméno / e-mail / majitele účtu {0} +payment.account.name.email=Celé jméno majitele účtu / e-mail +payment.account.name.emailAndHolderId=Celé jméno majitele účtu / e-mail / {0} payment.bank.name=Jméno banky payment.select.account=Vyberte typ účtu payment.select.region=Vyberte region @@ -1924,14 +2536,58 @@ payment.email=E-mail payment.country=Země payment.extras=Zvláštní požadavky payment.email.mobile=E-mail nebo mobilní číslo -payment.crypto.address=Crypto adresa -payment.crypto.tradeInstantCheckbox=Obchodujte ihned s tímto cryptoem (do 1 hodiny) -payment.crypto.tradeInstant.popup=Pro okamžité obchodování je nutné, aby oba obchodní partneři byli online, aby mohli obchod dokončit za méně než 1 hodinu.\n\nPokud máte otevřené nabídky a nejste k dispozici, deaktivujte je na obrazovce „Portfolio“. -payment.crypto=Crypto -payment.select.crypto=Vyberte nebo vyhledejte crypto +payment.email.mobile.cashtag=Cashtag, e-mail, nebo mobilní číslo. +payment.email.mobile.username=Uživatelské jméno, e-mail, nebo mobilní číslo. +payment.crypto.address=Adresa kryptoměny +payment.crypto.tradeInstantCheckbox=Obchodujte okamžitě (do 1 hodiny) s touto kryptoměnou +payment.crypto.tradeInstant.popup=Pro okamžité obchodování je nutné, aby oba obchodní partneři byli online, aby mohli \ + obchod dokončit za méně než 1 hodinu.\n\n\ + Pokud máte otevřené nabídky a nejste k dispozici, \ + deaktivujte je na obrazovce 'Portfolio'. +payment.crypto=Kryptoměna +payment.select.crypto=Vyberte nebo vyhledejte kryptoměnu payment.secret=Tajná otázka payment.answer=Odpověď payment.wallet=ID peněženky +payment.capitual.cap=CAP kód +payment.upi.virtualPaymentAddress=Virtuální platební adresa + +# suppress inspection "UnusedProperty" +payment.swift.headline=Mezinárodní SWIFT Wire Transfer +# suppress inspection "UnusedProperty" +payment.swift.title.bank=Přijímající banka +# suppress inspection "UnusedProperty" +payment.swift.title.intermediary=Zprostředkující banka (klikněte pro rozbalení) +# suppress inspection "UnusedProperty" +payment.swift.country.bank=Země přijímající banky +# suppress inspection "UnusedProperty" +payment.swift.country.intermediary=Země zprostředkující banky +# suppress inspection "UnusedProperty" +payment.swift.swiftCode.bank=SWIFT kód přijímající banky +# suppress inspection "UnusedProperty" +payment.swift.swiftCode.intermediary=SWIFT kód zprostředkující banky +# suppress inspection "UnusedProperty" +payment.swift.name.bank=Název přijímající banky +# suppress inspection "UnusedProperty" +payment.swift.name.intermediary=Název zprostředkující banky +# suppress inspection "UnusedProperty" +payment.swift.branch.bank=Pobočka přijímající banky +# suppress inspection "UnusedProperty" +payment.swift.branch.intermediary=Pobočka zprostředkující banky +# suppress inspection "UnusedProperty" +payment.swift.address.bank=Adresa přijímající banky +# suppress inspection "UnusedProperty" +payment.swift.address.intermediary=Adresa zprostředkující banky +# suppress inspection "UnusedProperty" +payment.swift.address.beneficiary=Adresa příjemce +# suppress inspection "UnusedProperty" +payment.swift.phone.beneficiary=Telefon příjemce prostředků +payment.swift.account=Číslo účtu (nebo IBAN) +payment.swift.use.intermediary=Použít zprostředkující banku +payment.swift.showPaymentInfo=Ukázat platební informace... +payment.account.owner.address=Adresa majitele účtu +payment.transferwiseUsd.address=(musí být v US, zvažte použití adresy banky) + payment.amazon.site=Kupte Amazon eGift zde: payment.ask=Zjistěte pomocí obchodního chatu payment.uphold.accountId=Uživatelské jméno, e-mail nebo číslo telefonu @@ -1942,7 +2598,9 @@ payment.supportedCurrencies=Podporované měny payment.supportedCurrenciesForReceiver=Měny pro příjem prostředků payment.limitations=Omezení payment.salt=Salt pro ověření stáří účtu -payment.error.noHexSalt=Salt musí být ve formátu HEX.\nDoporučujeme upravit pole salt, pokud chcete salt převést ze starého účtu, aby bylo stáří vašeho účtu zachováno. Stáří účtu se ověřuje pomocí salt účtu a identifikačních údajů účtu (např. IBAN). +payment.error.noHexSalt=Salt musí být ve formátu HEX.\n\ + Doporučujeme upravit pole salt, pokud chcete salt převést ze starého účtu, aby bylo stáří vašeho účtu zachováno. \ + Stáří účtu se ověřuje pomocí salt účtu a identifikačních údajů účtu (např. IBAN). payment.accept.euro=Přijímejte obchody z těchto zemí eurozóny payment.accept.nonEuro=Přijímejte obchody z těchto zemí mimo eurozónu payment.accepted.countries=Akceptované země @@ -1972,51 +2630,421 @@ payment.bankIdOptional=ID Banky (BIC/SWIFT) (volitelné) payment.branchNr=Číslo pobočky payment.branchNrOptional=Číslo pobočky (volitelné) payment.accountNrLabel=Číslo účtu (IBAN) +payment.iban=IBAN +payment.tikkie.iban=IBAN použitý pro obchodování Haveno na Tikkie payment.accountType=Typ účtu payment.checking=Kontrola payment.savings=Úspory payment.personalId=Číslo občanského průkazu -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.zelle.info=Zelle je služba převodu peněz, která funguje nejlépe *prostřednictvím* jiné banky.\n\n1. Na této stránce zjistěte, zda (a jak) vaše banka spolupracuje se Zelle:\n[HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Zaznamenejte si zvláštní limity převodů - limity odesílání se liší podle banky a banky často určují samostatné denní, týdenní a měsíční limity.\n\n3. Pokud vaše banka s Zelle nepracuje, můžete ji stále používat prostřednictvím mobilní aplikace Zelle, ale vaše limity převodu budou mnohem nižší.\n\n4. Název uvedený na vašem účtu Haveno MUSÍ odpovídat názvu vašeho účtu Zelle/bankovního účtu.\n\nPokud nemůžete dokončit transakci Zelle, jak je uvedeno ve vaší obchodní smlouvě, můžete ztratit část (nebo vše) ze svého bezpečnostního vkladu.\n\nVzhledem k poněkud vyššímu riziku zpětného zúčtování společnosti Zelle se prodejcům doporučuje kontaktovat nepodepsané kupující prostřednictvím e-mailu nebo SMS, aby ověřili, že kupující skutečně vlastní účet Zelle uvedený v Haveno. -payment.fasterPayments.newRequirements.info=Některé banky začaly ověřovat celé jméno příjemce pro převody Faster Payments. Váš současný účet Faster Payments nepožadoval celé jméno.\n\nZvažte prosím znovu vytvoření svého Faster Payments účtu v Havenou, abyste mohli budoucím kupujícím {0} poskytnout celé jméno.\n\nPři opětovném vytvoření účtu nezapomeňte zkopírovat přesný kód řazení, číslo účtu a hodnoty soli (salt) pro ověření věku ze starého účtu do nového účtu. Tím zajistíte zachování stáří a stavu vašeho stávajícího účtu. -payment.moneyGram.info=Při používání MoneyGram musí XMR kupující zaslat autorizační číslo a fotografii potvrzení e-mailem prodejci XMR. Potvrzení musí jasně uvádět celé jméno prodejce, zemi, stát a částku. E-mail prodávajícího se kupujícímu zobrazí během procesu obchodování. -payment.westernUnion.info=Při používání služby Western Union musí kupující XMR zaslat prodejci XMR e-mailem MTCN (sledovací číslo) a fotografii potvrzení. Potvrzení musí jasně uvádět celé jméno prodejce, město, zemi a částku. E-mail prodávajícího se kupujícímu zobrazí během procesu obchodování. -payment.halCash.info=Při používání HalCash musí kupující XMR poslat prodejci XMR kód HalCash prostřednictvím textové zprávy z mobilního telefonu.\n\nUjistěte se, že nepřekračujete maximální částku, kterou vám banka umožňuje odesílat pomocí HalCash. Min. částka za výběr je 10 EUR a max. částka je 600 EUR. Pro opakované výběry je to 3000 EUR za příjemce za den a 6000 EUR za příjemce za měsíc. Zkontrolujte prosím tyto limity u své banky, abyste se ujistili, že používají stejné limity, jaké jsou zde uvedeny.\n\nČástka pro výběr musí být násobkem 10 EUR, protože z bankomatu nemůžete vybrat jiné částky. Uživatelské rozhraní na obrazovce vytvořit-nabídku and přijmout-nabídku upraví částku XMR tak, aby částka EUR byla správná. Nemůžete použít tržní cenu, protože částka v EURECH se mění s měnícími se cenami.\n\nV případě sporu musí kupující XMR poskytnout důkaz, že zaslal EURA. +payment.zelle.info=Zelle je služba převodu peněz, která funguje nejlépe *prostřednictvím* jiné banky.\n\n\ + 1. Na této stránce zjistěte, zda (a jak) vaše banka spolupracuje se Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n\ + 2. Zaznamenejte si zvláštní limity převodů - limity odesílání se liší podle banky a banky často určují samostatné denní, týdenní a měsíční limity.\n\n\ + 3. Pokud vaše banka s Zelle nepracuje, můžete ji stále používat prostřednictvím mobilní aplikace Zelle, ale vaše limity převodu budou mnohem nižší.\n\n\ + 4. Název uvedený na vašem účtu Haveno MUSÍ odpovídat názvu vašeho účtu Zelle/bankovního účtu.\n\n\ + Pokud nemůžete dokončit transakci Zelle, jak je uvedeno ve vaší obchodní smlouvě, můžete ztratit část (nebo vše) ze svého bezpečnostního vkladu.\n\n\ + Vzhledem k poněkud vyššímu riziku zpětného zúčtování společnosti Zelle se prodejcům doporučuje kontaktovat nepodepsané kupující \ + prostřednictvím e-mailu nebo SMS, aby ověřili, že kupující skutečně vlastní účet Zelle uvedený v Haveno. +payment.fasterPayments.newRequirements.info=Některé banky začaly ověřovat celé jméno příjemce pro převody \ +Faster Payments. Váš současný účet Faster Payments nepožadoval celé jméno.\n\n\ + Zvažte prosím znovu vytvoření svého Faster Payments účtu v Havenou, abyste mohli budoucím kupujícím {0} poskytnout celé jméno.\n\n\ + Při opětovném vytvoření účtu nezapomeňte zkopírovat přesný kód řazení, číslo účtu a hodnoty soli (salt) pro ověření \ + věku ze starého účtu do nového účtu. Tím zajistíte zachování stáří \ + a stavu vašeho stávajícího účtu. +payment.fasterPayments.ukSortCode="UK sort kód" +payment.moneyGram.info=Při používání MoneyGram musí XMR kupující zaslat autorizační číslo a fotografii potvrzení e-mailem prodejci XMR. \ + Potvrzení musí jasně uvádět celé jméno prodejce, zemi, stát a částku. E-mail prodávajícího se kupujícímu zobrazí během procesu obchodování. +payment.westernUnion.info=Při používání služby Western Union musí kupující XMR zaslat prodejci XMR e-mailem MTCN (sledovací číslo) a fotografii potvrzení. \ + Potvrzení musí jasně uvádět celé jméno prodejce, město, zemi a částku. E-mail prodávajícího se kupujícímu zobrazí během procesu obchodování. +payment.halCash.info=Při používání HalCash musí kupující XMR poslat prodejci XMR kód HalCash prostřednictvím textové zprávy z mobilního telefonu.\n\n\ + Ujistěte se, že nepřekračujete maximální částku, kterou vám banka umožňuje odesílat pomocí HalCash. \ + Min. částka za výběr je 10 EUR a max. částka je 600 EUR. Pro opakované výběry je to 3000 EUR za příjemce za den \ + a 6000 EUR za příjemce za měsíc. Zkontrolujte prosím tyto limity u své banky, abyste se ujistili, \ + že používají stejné limity, jaké jsou zde uvedeny.\n\n\ + Částka pro výběr musí být násobkem 10 EUR, protože z bankomatu nemůžete vybrat jiné částky. \ + Uživatelské rozhraní na obrazovce vytvořit-nabídku and přijmout-nabídku upraví částku XMR tak, aby částka EUR byla správná. \ + Nemůžete použít tržní cenu, protože částka v EURECH se mění s měnícími se cenami.\n\n\ + V případě sporu musí kupující XMR poskytnout důkaz, že zaslal EURA. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Uvědomte si, že u všech bankovních převodů existuje určité riziko zpětného zúčtování. Aby se toto riziko zmírnilo, stanoví Haveno limity pro jednotlivé obchody na základě odhadované úrovně rizika zpětného zúčtování pro použitou platební metodu.\n\nU této platební metody je váš limit pro jednotlivé obchody pro nákup a prodej {2}.\n\nToto omezení se vztahuje pouze na velikost jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\nDalší podrobnosti najdete na wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. +payment.limits.info=Uvědomte si, že u všech bankovních převodů existuje určité riziko zpětného zúčtování. Aby se toto riziko zmírnilo, \ + stanoví Haveno limity pro jednotlivé obchody na základě odhadované úrovně rizika zpětného zúčtování pro použitou platební metodu.\n\ + \n\ + U této platební metody je váš limit pro jednotlivé obchody pro nákup a prodej {2}.\n\ + \n\ + Toto omezení se vztahuje pouze na velikost jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\ + \n\ + Další podrobnosti najdete na wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=Aby se omezilo riziko zpětného zúčtování, Haveno stanoví limity pro jednotlivé obchody pro tento typ platebního účtu na základě následujících 2 faktorů:\n\n1. Obecné riziko zpětného zúčtování pro platební metodu\n2. Stav podepisování účtu\n\nTento platební účet ještě není podepsán, takže je omezen na nákup {0} za obchod. Po podpisu se limity nákupu zvýší následovně:\n\n● Před podpisem a 30 dní po podpisu bude váš limit nákupu podle obchodu {0}\n● 30 dní po podpisu bude váš limit nákupu podle obchodu {1}\n● 60 dní po podpisu bude váš limit nákupu podle obchodu {2}\n\nPodpisy účtu neovlivňují prodejní limity. Můžete okamžitě prodat {2} v jednom obchodu.\n\nTato omezení platí pouze pro objem jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\nDalší podrobnosti najdete na wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. +payment.limits.info.withSigning=Aby se omezilo riziko zpětného zúčtování, Haveno stanoví limity pro jednotlivé obchody \ + pro tento typ platebního účtu na základě následujících 2 faktorů:\n\n\ + 1. Obecné riziko zpětného zúčtování pro platební metodu\n\ + 2. Stav podepisování účtu\n\ + \n\ + Tento platební účet ještě není podepsán, takže je omezen na nákup {0} za obchod. \ + Po podpisu se limity nákupu zvýší následovně:\n\ + \n\ + ● Před podpisem a 30 dní po podpisu bude váš limit nákupu podle obchodu {0}\n\ + ● 30 dní po podpisu bude váš limit nákupu podle obchodu {1}\n\ + ● 60 dní po podpisu bude váš limit nákupu podle obchodu {2}\n\ + \n\ + Podpisy účtu neovlivňují prodejní limity. Můžete okamžitě prodat {2} v jednom obchodu.\n\ + \n\ + Tato omezení platí pouze pro objem jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\ + \n\ + Další podrobnosti najdete na wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. -payment.cashDeposit.info=Potvrďte, že vám vaše banka umožňuje odesílat hotovostní vklady na účty jiných lidí. Například Bank of America a Wells Fargo již takové vklady nepovolují. +payment.cashDeposit.info=Potvrďte, že vám vaše banka umožňuje odesílat hotovostní vklady na účty jiných lidí. \ + Například Bank of America a Wells Fargo již takové vklady nepovolují. -payment.revolut.info=Revolut vyžaduje „uživatelské jméno“ jako ID účtu, nikoli telefonní číslo nebo e-mail, jako tomu bylo v minulosti. -payment.account.revolut.addUserNameInfo={0}\nVáš stávající účet Revolut ({1}) nemá "Uživatelské jméno".\nChcete-li aktualizovat údaje o svém účtu, zadejte své "Uživatelské jméno" Revolut.\nTo neovlivní stav podepisování věku vašeho účtu. +payment.revolut.info=Revolut vyžaduje 'uživatelské jméno' jako ID účtu, nikoli telefonní číslo nebo e-mail, jako tomu bylo v minulosti. +payment.account.revolut.addUserNameInfo={0}\n\ + Váš stávající účet Revolut ({1}) nemá "Uživatelské jméno".\n\ + Chcete-li aktualizovat údaje o svém účtu, zadejte své "Uživatelské jméno" Revolut.\n\ + To neovlivní stav podepisování stáří vašeho účtu. payment.revolut.addUserNameInfo.headLine=Aktualizujte účet Revolut -payment.cashapp.info=Upozorňujeme, že Cash App má vyšší riziko zpětného strhu než většina bankovních převodů. -payment.venmo.info=Upozorňujeme, že Venmo má vyšší riziko zpětného strhu než většina bankovních převodů. -payment.paypal.info=Upozorňujeme, že PayPal má vyšší riziko zpětného strhu než většina bankovních převodů. +payment.cashapp.info=Uvědomte si, že u aplikace Cash App je riziko zpětné úhrady (chargeback) vyšší než u většiny bankovních převodů. +payment.venmo.info=Mějte na paměti, že u služby Venmo je riziko zpětné úhrady (chargeback) vyšší než u většiny bankovních převodů. +payment.paypal.info=Uvědomte si prosím, že u služby PayPal je riziko zpětné úhrady (chargeback) vyšší než u většiny bankovních převodů. payment.amazonGiftCard.upgrade=Platba kartou Amazon eGift nyní vyžaduje také nastavení země. -payment.account.amazonGiftCard.addCountryInfo={0}\nVáš stávající účet pro platbu kartou Amazon eGift ({1}) nemá nastavenou zemi.\nVyberte prosím zemi, ve které je možné vaše karty Amazon eGift uplatnit.\nTato aktualizace vašeho účtu nebude mít vliv na stáří tohoto účtu. +payment.account.amazonGiftCard.addCountryInfo={0}\n\ + Váš stávající účet pro platbu kartou Amazon eGift ({1}) nemá nastavenou zemi.\n\ + Vyberte prosím zemi, ve které je možné vaše karty Amazon eGift uplatnit.\n\ + Tato aktualizace vašeho účtu nebude mít vliv na stáří tohoto účtu. payment.amazonGiftCard.upgrade.headLine=Aktualizace účtu pro platbu kartou Amazon eGift -payment.usPostalMoneyOrder.info=Obchodování pomocí amerických poštovních poukázek (USPMO) na Haveno vyžaduje, abyste rozuměli následujícímu:\n\n- Kupující XMR musí před odesláním napsat jméno prodejce XMR do polí plátce i příjemce a pořídit fotografii USPMO a obálku s dokladem o sledování ve vysokém rozlišení.\n- Kupující XMR musí odeslat USPMO prodejci XMR s potvrzením dodávky.\n\nV případě, že je nutná mediace, nebo pokud dojde k obchodnímu sporu, budete povinni poslat fotografie mediátorovi Haveno nebo zástupci pro vrácení peněz spolu s pořadovým číslem USPMO, číslem pošty a částkou dolaru, aby mohli ověřit podrobnosti na webu US Post Office.\n\nNeposkytnutí požadovaných informací mediátorovi nebo arbitrovi bude mít za následek ztrátu případu sporu.\n\nVe všech sporných případech nese odesílatel USPMO 100% břemeno odpovědnosti za poskytnutí důkazů mediátorovi nebo arbitrovi.\n\nPokud těmto požadavkům nerozumíte, neobchodujte pomocí USPMO na Haveno. +payment.swift.info.account=Pečlivě si prostudujte základní pokyny pro používání SWIFT v Haveno:\n\ +\n\ +- vyplňte všechna pole kompletně a přesně \n\ +- kupující musí odeslat platbu v měně stanovené tvůrcem nabídky \n\ +- kupující použije u platby model sdílených poplatků (SHA) \n\ +- kupující a prodejce mohou platit poplatky, proto by se měli nejprve seznámit s poplatky své banky \n\ +\n\ +SWIFT je složitější než jiné platební metody, proto si prosím přečtěte kompletní pokyny na wiki [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. + +payment.swift.info.buyer=Pro nákup monero pomocí SWIFT, musíte:\n\ +\n\ +- odeslat platbu v měně, stanovené tvůrcem nabídky \n\ +- použít u platby model sdílených poplatků (SHA) \n\ +\n\ +Přečtěte si další pokyny na wiki, abyste se vyhnuli sankcím a zajistili hladký průběh obchodů [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. + +payment.swift.info.seller=Odesílatelé SWIFT musí při odesílání plateb používat model sdílených poplatků (SHA).\n\ +\n\ +Pokud obdržíte platbu SWIFT, která nepoužívá SHA, otevřete mediační požadavek.\n\ +\n\ +Přečtěte si další pokyny na wiki, abyste se vyhnuli sankcím a zajistili hladký průběh obchodů [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. + +payment.imps.info.account=Prosím, nezapomeňte uvést své:\n\n\ + ● Celé jméno majitele účtu\n\ + ● Číslo účtu\n\ + ● IFSC číslo\n\n\ +Tyto údaje by měly odpovídat vašemu bankovnímu účtu, který budete používat pro odesílání / přijímání plateb.\n\n\ +Upozorňujeme, že na jednu transakci lze poslat maximálně 200 000 rupií. Pokud obchodujete s částkou vyšší než tato, bude třeba provést více transakcí. Uvědomte si však, že jejich maximální limit je 1 000 000 rupií, které lze odeslat za den.\n\n\ +Některé banky mají pro své zákazníky jiné limity. +payment.imps.info.buyer=Platbu prosím zasílejte pouze na účet uvedený v systému Haveno.\n\n\ +Maximální velikost obchodu je 200 000 rupií na transakci.\n\n\ +Pokud váš obchod přesahuje 200 000 rupií, budete muset provést více převodů. Mějte však na paměti, že je stanoven maximální limit 1 000 000 rupií, který lze odeslat za den.\n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. +payment.imps.info.seller=Pokud máte v úmyslu přijmout více než 200 000 rupií za obchod, měli byste počítat s tím, že kupující bude muset provést více převodů. Mějte však na paměti, že existuje maximální limit 1 000 000 rupií, které lze odeslat za den.\n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. + +payment.neft.info.account=Prosím, nezapomeňte uvést své:\n\n\ + ● Celé jméno majitele účtu\n\ + ● Číslo účtu\n\ + ● IFSC číslo\n\n\ +Tyto údaje by měly odpovídat vašemu bankovnímu účtu, který budete používat pro odesílání/přijímání plateb.\n\n\ +Upozorňujeme, že na jednu transakci lze poslat maximálně 50 000 rupií. Pokud obchodujete s částkou vyšší než tato, bude třeba provést více transakcí.\n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. +payment.neft.info.buyer=Platbu prosím zasílejte pouze na účet uvedený v systému Haveno.\n\n\ +Maximální velikost obchodu je 50 000 rupií na transakci.\n\n\ +Pokud váš obchod přesáhne 50 000 rupií, budete muset provést více převodů.\n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty různé limity. +payment.neft.info.seller=Pokud máte v úmyslu získat více než 50 000 rupií za obchod, měli byste počítat s tím, že kupující bude muset provést více převodů.\n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. + +payment.paytm.info.account=Nezapomeňte uvést e-mail nebo telefonní číslo, které se shoduje s vaším e-mailem nebo telefonním číslem ve vašem účtu PayTM. \n\n\ +Když si uživatelé založí účet PayTM bez KYC, jsou omezeni: \n\n\ + ● Na jednu transakci lze poslat maximálně 5 000 rupií.\n\ + ● V peněžence PayTM můžete mít maximálně 10 000 rupií.\n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 5 000 za obchod, budete muset u společnosti PayTM vyplnit KYC. S KYC jsou uživatelé omezeni na:\n\n\ + ● Na jednu transakci lze poslat maximálně 100 000 rupií.\n\ + ● V peněžence PayTM můžete mít maximálně 100 000 rupií.\n\n\ +Uživatelé by také měli znát limity účtu. Obchody překračující limity účtu PayTM se pravděpodobně budou muset uskutečnit v průběhu více než jednoho dne, nebo budou zrušeny. +payment.paytm.info.buyer=Platbu zasílejte pouze na uvedenou e-mailovou adresu nebo telefonní číslo.\n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 5 000 rupií za obchod, budete muset u společnosti PayTM vyplnit KYC.\n\n\ +Bez KYC lze na jednu transakci poslat 5 000 rupií.\n\n\ +S uživateli KYC lze na jednu transakci poslat 100 000 rupií. +payment.paytm.info.seller=Pokud máte v úmyslu obchodovat s částkou vyšší než 5 000 rupií za obchod, budete muset u společnosti PayTM vyplnit KYC. S KYC jsou uživatelé omezeni na:\n\n\ + ● Na jednu transakci lze poslat maximálně 100 000 rupií.\n\ + ● V peněžence PayTM můžete mít maximálně 100 000 rupií.\n\n\ +Uživatelé by také měli znát limity účtu. Protože v peněžence PayTM můžete mít maximálně 100 000 rupií, dbejte na to, abyste své rupie pravidelně převáděli. + +payment.rtgs.info.account=RTGS je určen pro platby velkých obchodů ve výši 200 000 rupií a více.\n\n\ +Při nastavování platebního účtu RTGS nezapomeňte uvést své:\n\n\ + ● Celé jméno majitele účtu\n\ + ● Číslo účtu\n\ + ● IFSC číslo\n\n\\ +Tyto údaje by měly odpovídat vašemu bankovnímu účtu, který budete používat pro odesílání/přijímání plateb.\n\n\ +Vezměte prosím na vědomí, že minimální částka obchodu, kterou lze poslat na jednu transakci, je 200 000 rupií. Pokud obchodujete pod touto částkou, buď bude obchod zrušen, nebo se oba obchodníci budou muset dohodnout na jiném způsobu platby (např. IMPS nebo UPI). +payment.rtgs.info.buyer=Platbu prosím zasílejte pouze na účet uvedený v systému Haveno.\n\n\ +Vezměte prosím na vědomí, že minimální částka obchodu, kterou lze poslat na jednu transakci, je 200 000 rupií. Pokud obchodujete pod touto částkou, buď bude obchod zrušen, nebo se oba obchodníci budou muset dohodnout na jiném způsobu platby (např. IMPS nebo UPI). +payment.rtgs.info.seller=Vezměte prosím na vědomí, že minimální částka obchodu, kterou lze poslat na jednu transakci, je 200 000 rupií. Pokud obchodujete pod touto částkou, buď bude obchod zrušen, nebo se oba obchodníci budou muset dohodnout na jiném způsobu platby (např. IMPS nebo UPI). + +payment.upi.info.account=Nezapomeňte uvést svou virtuální platební adresu (VPA), která se také nazývá UPI ID. Formát tohoto údaje je podobný e-mailovému ID: se znakem ”@” uprostřed. Vaše UPI ID může být například ”jméno_příjemce@název_banky” nebo ”telefonní_číslo@název_banky”. \n\n\ +Pro UPI je stanoven maximální limit 100 000 rupií, které lze poslat na jednu transakci. \n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 100 000 rupií na jeden obchod, je pravděpodobné, že obchody budou muset proběhnout více převody. \n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty různé limity. +payment.upi.info.buyer=Platbu prosím zasílejte pouze na VPA / UPI ID uvedené v systému Haveno. \n\n\ +Maximální velikost obchodu je 100 000 rupií na transakci. \n\n\ +Pokud váš obchod přesahuje 100 000 rupií, budete muset provést více převodů. \n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. +payment.upi.info.seller=Pokud máte v úmyslu přijmout více než 100 000 rupií za obchod, měli byste počítat s tím, že kupující bude muset provést více převodů. \n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. + +payment.celpay.info.account=Nezapomeňte uvést e-mail, na který je váš účet Celsius registrován. \ + Tím zajistíte, že se při odesílání peněz zobrazí ze správného účtu a při jejich přijímání budou připsány na váš účet.\n\n\ +Uživatelé CelPay mohou poslat maximálně 2500 dolarů (nebo ekvivalent v jiné měně/krypto) za 24 hodin.\n\n\ +Obchody překračující limity účtu CelPay se pravděpodobně budou muset uskutečnit během více než jednoho dne, nebo budou zrušeny.\n\n\ +CelPay podporuje více stablecoinů:\n\n\ + ● Stablecoiny USD: DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ + ● Stablecoiny CAD; TrueCAD\n\ + ● Stablecoiny GBP; TrueGBP\n\ + ● Stablecoiny HKD; TrueHKD\n\ + ● Stablecoiny AUD; TrueAUD\n\n\ +Kupující XMR mohou prodávajícímu XMR poslat libovolnou odpovídající měnu stablecoin. +payment.celpay.info.buyer=Platbu prosím zasílejte pouze na e-mailovou adresu, kterou prodejce XMR uvedl zasláním platebního odkazu.\n\n\ +CelPay je omezena na odeslání 2 500 dolarů (nebo jiné měny/krypto ekvivalentu) za 24 hodin.\n\n\ +Obchody překračující limit účtu CelPay se pravděpodobně budou muset uskutečnit v průběhu více než jednoho dne, nebo budou zrušeny.\n\n\ +CelPay podporuje více stablecoinů:\n\n\ + ● USD Stablecoiny; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ + ● CAD Stablecoiny; TrueCAD\n\ + ● GBP Stablecoiny; TrueGBP\n\ + ● HKD Stablecoiny; TrueHKD\n\ + ● AUD Stablecoiny; TrueAUD\n\n\ +Kupující XMR mohou prodávajícímu XMR poslat libovolnou odpovídající měnu stablecoin. +payment.celpay.info.seller=Prodejci XMR by měli očekávat, že obdrží platbu prostřednictvím zabezpečeného platebního odkazu. \ + Ujistěte se, že odkaz na e-mailovou platbu obsahuje e-mailovou adresu zadanou kupujícím XMR.\n\n\ +Uživatelé CelPay mohou poslat maximálně 2500 dolarů (nebo ekvivalent v jiné měně/krypto) za 24 hodin.\n\n\ +Obchody překračující limity účtu CelPay se pravděpodobně budou muset uskutečnit během více než jednoho dne, nebo budou zrušeny.\n\n\ +CelPay podporuje více stablecoinů:\n\n\ + ● USD Stablecoiny; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ + ● CAD Stablecoiny; TrueCAD\n\ + ● GBP Stablecoiny; TrueGBP\n\ + ● HKD Stablecoiny; TrueHKD\n\ + ● AUD Stablecoiny; TrueAUD\n\n\ +Prodávající XMR by měli očekávat, že od kupujícího XMR obdrží odpovídající měnu stablecoin. Je možné, aby kupující XMR zaslal libovolnou odpovídající měnu stablecoin. +payment.celpay.supportedCurrenciesForReceiver=Podporované měny (Upozornění: všechny níže uvedené měny jsou v aplikaci Celcius podporovány jako stablecoiny. Obchody se týkají stablecoinů, nikoli fiat.) + +payment.nequi.info.account=Nezapomeňte uvést své telefonní číslo, které je spojeno s vaším účtem Nequi.\n\n\ +Při zakládání účtu Nequi jsou platební limity nastaveny na maximálně ~ 7 000 000 COP, které lze měsíčně odeslat.\n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 7 000 000 COP na jeden obchod, budete muset u společnosti Bancolombia provést KYC a zaplatit poplatek \ + ve výši přibližně 15 000 COP. Poté budou všechny transakce zatíženy 0,4% daní. Ujistěte se prosím, že znáte aktuální výši daní.\n\n\ +Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.nequi.info.buyer=Platbu prosím zasílejte pouze na telefonní číslo uvedené v účtu XMR prodávajícího na Havenu.\n\n\ +Když si uživatelé založí účet Nequi, jsou limity plateb nastaveny na maximálně ~ 7 000 000 COP, které lze měsíčně odeslat.\n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 7 000 000 COP na jeden obchod, budete muset u společnosti Bancolombia provést KYC a zaplatit poplatek \ + ve výši přibližně 15 000 COP. Poté budou všechny transakce zatíženy 0,4% daní. Ujistěte se prosím, že znáte aktuální výši daní.\n\n\ +Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.nequi.info.seller=Zkontrolujte, zda se přijatá platba shoduje s telefonním číslem uvedeným v účtu XMR kupujícího Haveno.\n\n\ +Když si uživatelé založí účet Nequi, jsou limity plateb nastaveny na maximálně ~ 7 000 000 COP, které lze měsíčně odeslat.\n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 7 000 000 COP na jeden obchod, budete muset u společnosti Bancolombia provést KYC a zaplatit poplatek \ + ve výši přibližně 15 000 COP. Poté budou všechny transakce zatíženy 0,4% daní. Ujistěte se prosím, že znáte aktuální výši daní.\n\n\ +Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. + +payment.bizum.info.account=K využívání služby Bizum potřebujete bankovní účet (IBAN) ve Španělsku a být zaregistrováni pro tuto službu.\n\n\ +Bizum lze použít pro obchody v rozmezí od 0,50 € do 1 000 €.\n\n\ +Maximální částka transakcí, které můžete odeslat/přijmout prostřednictvím služby Bizum, je 2 000 eur denně.\n\n\ +Uživatelé služby Bizum mohou měsíčně provést maximálně 150 operací.\n\n\ +Každá banka však může pro své klienty stanovit vlastní limity v rámci výše uvedených limitů.\n\n\ +Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.bizum.info.buyer=Platbu prosím zasílejte pouze na mobilní telefonní číslo prodávajícího XMR uvedené v systému Haveno.\n\n\ +Maximální velikost obchodu je 1 000 € na jednu platbu. Maximální objem transakcí, které můžete prostřednictvím Bizumu odeslat, je 2 000 eur za den.\n\n\ +Pokud překročíte výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.bizum.info.seller=Ujistěte se prosím, že platba byla přijata z mobilního telefonního čísla kupujícího XMR, které je uvedeno v Haveno.\n\n\ +Maximální velikost obchodu je 1 000 EUR na jednu platbu. Maximální objem transakcí, které můžete přijmout pomocí služby Bizum, je 2 000 eur za den.\n\n\ +Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. + +payment.pix.info.account=Nezapomeňte uvést vámi vybraný klíč Pix Key. Existují čtyři typy klíčů: \ + CPF (Registr fyzických osob) nebo CNPJ (Národní registr právnických osob), e-mailová adresa, telefonní číslo nebo \ + náhodný klíč vygenerovaný systémem, tzv. univerzální jedinečný identifikátor (UUID). Jiný klíč musí být použit pro \ + každý účet Pix, který máte. Jednotlivci si mohou vytvořit až pět klíčů pro každý účet, který vlastní.\n\n\ +Při obchodování v Haveno by kupující XMR měli používat své klíče Pix v popise platby, aby prodávající XMR mohli snadno identifikovat, že platba pochází od nich. +payment.pix.info.buyer=Platbu prosím zasílejte pouze na Pix Key uvedený v účtu XMR prodávajícího na Haveno.\n\n\ +Jako referenční číslo platby použijte svůj Pix Key, aby prodejce XMR mohl snadno identifikovat, že platba pochází od vás. +payment.pix.info.seller=Zkontrolujte, zda se popis přijaté platby shoduje s klíčem Pix uvedeným v účtu XMR kupujícího na Havenu. +payment.pix.key=Pix Key (CPF, CNPJ, e-mail, telefonní číslo nebo UUID) + +payment.monese.info.account=Monese je bankovní aplikace pro uživatele GBP, EUR a RON*. Monese umožňuje uživatelům posílat peníze do \ + jiných účtů Monese a to okamžitě a zdarma v jakékoli podporované měně.\n\n\ +*Chcete-li si v Monese otevřít účet v RON, musíte buď žít v Rumunsku, nebo mít rumunské občanství.\n\n\ +Při zakládání účtu Monese v Havenu nezapomeňte uvést své jméno a telefonní číslo, které se shoduje s vaším \ + Monese. Tím zajistíte, že se při odesílání peněz zobrazí ze správného účtu a při přijímání \ + budou připsány na váš účet. +payment.monese.info.buyer=Platbu prosím zasílejte pouze na telefonní číslo, které prodejce XMR uvedl ve svém účtu Haveno. Popis platby ponechte nevyplněný. +payment.monese.info.seller=Prodávající XMR by měli očekávat, že obdrží platbu z telefonního čísla / jména uvedeného v účtu XMR kupujícího Haveno. + +payment.satispay.info.account=Pro používání služby Satispay potřebujete bankovní účet (IBAN) v Itálii a být zaregistrován pro tuto službu.\n\n\ +Limity účtu Satispay se nastavují individuálně. Pokud chcete obchodovat se zvýšenými částkami, musíte se obrátit na Satispay. + podporu, aby vám zvýšila limity. Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity, \ + může být váš obchod zrušen a může být uložena pokuta. +payment.satispay.info.buyer=Platbu prosím zasílejte pouze na mobilní telefonní číslo prodávajícího XMR uvedené v Haveno.\n\n\ +Limity účtu Satispay jsou nastaveny individuálně. Pokud chcete obchodovat se zvýšenými částkami, musíte se obrátit na Satispay. + podporu, aby vám zvýšila limity. Uživatelé by si měli být vědomi také limitů na účtu. Pokud obchodujete nad výše uvedené limity \ + může být váš obchod zrušen a může být uložena pokuta. +payment.satispay.info.seller=Ujistěte se prosím, že platba byla přijata z mobilního telefonního čísla / jména kupujícího XMR, jak je uvedeno v Haveno.\n\n\ +Limity účtu Satispay jsou nastaveny individuálně. Pokud chcete obchodovat se zvýšenými částkami, budete muset kontaktovat Satispay \ + podporu, aby vám zvýšila limity. Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity \ + může být váš obchod zrušen a může být uložena pokuta. + +payment.tikkie.info.account=K používání Tikkie potřebujete bankovní účet (IBAN) v Nizozemsku a být zaregistrováni pro tuto službu.\n\n\ +Když pošlete žádost o platbu Tikkie konkrétní osobě, můžete požádat o příjem maximálně 750 EUR na Tikkie \ + žádost. Maximální částka, o kterou můžete požádat během 24 hodin, je 2 500 EUR na jeden účet Tikkie.\n\n\ +Každá banka však může v rámci těchto limitů stanovit pro své klienty vlastní limity.\n\n\ +Uživatelé by si také měli být vědomi limitů na účtech. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.tikkie.info.buyer=Vyžádejte si prosím platební odkaz od prodejce XMR v chatu obchodníků. Jakmile prodejce XMR \ + zašle platební odkaz, který odpovídá správné částce za obchod, přejděte prosím k platbě.\n\n\ +Když Prodejce XMR požádá o platbu Tikkie, může požádat o platbu maximálně 750 EUR za žádost Tikkie. Pokud \ + je obchod vyšší než tato částka, bude muset prodejce XMR odeslat více žádostí, aby dosáhl celkové částky obchodu. Maximální částka \ + o kterou můžete požádat za den, je 2 500 €.\n\n\ +Každá banka však může v rámci těchto limitů stanovit pro své klienty vlastní limity.\n\n\ +Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.tikkie.info.seller=Pošlete prosím odkaz na platbu prodejci XMR pomocí chatu obchodníků. Jakmile vám XMR \ + kupující pošle platbu, zkontrolujte prosím, zda se jeho IBAN shoduje s údaji, které má v Haveno.\n\n\ +Když Prodejce XMR požádá o platbu Tikkie, může požádat maximálně o 750 EUR na jednu žádost Tikkie. Pokud \ + je obchod vyšší než tato částka, bude muset prodejce XMR odeslat více žádostí, aby dosáhl celkové částky obchodu. Maximální částka \ + o které můžete požádat za den, je 2 500 €.\n\n\ +Každá banka však může v rámci těchto limitů stanovit pro své klienty další vlastní limity.\n\n\ +Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. + +payment.verse.info.account=Verse je způsob platby ve více měnách, který umožňuje odesílat a přijímat platby v EUR, SEK, HUF, DKK, PLN.\n\n\ +Při nastavování účtu Verse v Haveno nezapomeňte uvést uživatelské jméno, které odpovídá vašemu uživatelskému jménu ve \ + Verse. Tím zajistíte, že se při odesílání peněz zobrazí jako odeslané ze správného účtu a při přijímání \ + budou připsány na váš účet.\n\n\ +Uživatelé Verse jsou omezeni na odeslání nebo přijetí 10 000 EUR ročně (nebo ekvivalentní částky v cizí měně) pro \ + kumulované platby odeslané z jejich platebního účtu nebo přijaté na jejich platební účet. Tuto částku může Verse na požádání zvýšit. +payment.verse.info.buyer=Platbu prosím zasílejte pouze na uživatelské jméno, které prodejce XMR uvedl ve svém účtu Haveno. \ + Popis platby prosím ponechte prázdný.\n\n\ +Uživatelé Verse jsou omezeni na odeslání nebo přijetí 10 000 EUR ročně (nebo ekvivalentní částky v cizí měně) pro \ + kumulované platby odeslané z jejich platebního účtu nebo přijaté na jejich platební účet. Tuto částku může Verse na požádání zvýšit. +payment.verse.info.seller=Prodávající XMR by měli očekávat, že obdrží platbu od uživatelského jména uvedeného v účtu XMR kupujícího na Havenu.\n\n\ +Uživatelé Verse jsou omezeni na odeslání nebo přijetí 10 000 EUR ročně (nebo ekvivalentní částky v cizí měně) pro \ + kumulované platby odeslané z jejich platebního účtu nebo přijaté na jejich platební účet. Tuto částku může Verse na požádání zvýšit. + +payment.achTransfer.info.account=Při přidávání ACH jako platební metody v systému Haveno by se uživatelé měli ujistit, že vědí, \ + kolik peněz bude odeslání a přijetí ACH převodu stát. +payment.achTransfer.info.buyer=Ujistěte se, že jste si vědomi, kolik vás bude odeslání ACH převodu stát.\n\n\ + Při platbě zasílejte pouze na platební údaje uvedené v účtu prodávajícího XMR pomocí převodu ACH. +payment.achTransfer.info.seller=Ujistěte se prosím, že jste si vědomi toho, kolik vás bude přijetí ACH převodu stát peněz.\n\n\ + Při přijímání platby zkontrolujte, zda je přijata z účtu kupujícího XMR jako převod ACH. + +payment.domesticWire.info.account=Při přidávání tuzemského bankovního převodu jako platební metody v systému Haveno by se uživatelé měli ujistit, že \ + vědí, kolik je bude stát peněz odeslání a přijetí bankovního převodu. +payment.domesticWire.info.buyer=Ujistěte se, že jste si vědomi, kolik vás bude odeslání bankovního převodu stát peněz.\n\n\ + Při platbě zasílejte pouze na platební údaje uvedené v účtu prodávajícího XMR. +payment.domesticWire.info.seller=Ujistěte se prosím, že jste si vědomi toho, kolik vás bude přijetí bankovního převodu stát peněz.\n\n\ + Při přijímání platby zkontrolujte, zda byla přijata z účtu kupujícího XMR. + +payment.strike.info.account=Nezapomeňte uvést své uživatelské jméno Strike.\n\n\ +V systému Haveno se Strike používá pouze pro platby fiat na fiat.\n\n\ +Ujistěte se prosím, že znáte limity Strike:\n\n\ +Uživatelé, kteří se zaregistrovali pouze se svým e-mailem, jménem a telefonním číslem, mají následující limity:\n\n\ + ● maximálně 100 USD na vklad\n\n + ● Maximální celkový vklad 1000 USD za týden\n\n + ● maximálně 100 USD na platbu\n\n\ +Uživatelé mohou své limity zvýšit tím, že společnosti Strike poskytnou více informací. Tito uživatelé mají následující limity:\n\n\n + ● 1 000 USD maximálně na jeden vklad\n\ + ● Maximální celkový objem vkladů za týden ve výši 1 000 USD\n\ + ● 1 000 USD maximálně za platbu\n\n\ +Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.strike.info.buyer=Platbu prosím zasílejte pouze na uživatelské jméno Strike prodávajícího XMR, které je uvedené v Haveno.\n\n\ +Maximální velikost obchodu je 1 000 USD na jednu platbu.\n\n\ +Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.strike.info.seller=Ujistěte se, že platba byla přijata z uživatelského jména Strike, které patří XMR kupujícímu a které je uvedeno v Haveno.\n\n\ +Maximální velikost obchodu je 1 000 USD na jednu platbu.\n\n\ +Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. + +payment.transferwiseUsd.info.account=Vzhledem k bankovní regulaci USA má odesílání a přijímání plateb v USD více omezení \ + než u většiny ostatních měn. Z tohoto důvodu nebyl USD přidán do platební metody Haveno TransferWise.\n\n\ +Platební metoda TransferWise-USD umožňuje uživatelům Haveno obchodovat v USD.\n\n\ +Každý, kdo má účet Wise, formálně TransferWise, může přidat TransferWise-USD jako platební metodu v systému Haveno. To umožní \ + uživateli nakupovat a prodávat XMR za USD.\n\n\ +Při obchodování na Haveno by kupující XMR neměli uvádět v poznámce žádný důvod platby. Pokud je důvod platby vyžadován, \ + měli by používat pouze celé jméno majitele účtu TransferWise-USD. +payment.transferwiseUsd.info.buyer=Platbu prosím zasílejte pouze na e-mailovou adresu uvedenou v účtu Haveno TransferWise-USD prodávajícího XMR. +payment.transferwiseUsd.info.seller=Zkontrolujte, zda se přijatá platba shoduje se jménem kupujícího XMR na účtu TransferWise-USD v systému Haveno. + +payment.usPostalMoneyOrder.info=Obchodování pomocí amerických poštovních poukázek (USPMO) na Haveno vyžaduje, abyste rozuměli následujícímu:\n\ +\n\ +- Kupující XMR musí před odesláním napsat jméno prodejce XMR do polí plátce i příjemce a pořídit fotografii USPMO a obálku s dokladem o sledování ve vysokém rozlišení.\n\ +- Kupující XMR musí odeslat USPMO prodejci XMR s potvrzením dodávky.\n\ +\n\ +V případě, že je nutná mediace, nebo pokud dojde k obchodnímu sporu, budete povinni poslat fotografie mediátorovi Haveno nebo zástupci pro vrácení peněz spolu s pořadovým číslem USPMO, číslem pošty a částkou dolaru, aby mohli ověřit podrobnosti na webu US Post Office.\n\n\ +Neposkytnutí požadovaných informací mediátorovi nebo arbitrovi bude mít za následek ztrátu případu sporu.\n\n\ +Ve všech sporných případech nese odesílatel USPMO 100% břemeno odpovědnosti za poskytnutí důkazů mediátorovi nebo arbitrovi.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte pomocí USPMO na Haveno. + +payment.payByMail.info=Obchodování pomocí služby Hotovost poštou na Havenu vyžaduje, abyste rozuměli následujícím podmínkám:\n\ + \n\ + ● Kupující XMR by měl zabalit hotovost do sáčku na peníze, který je odolný proti manipulaci.\n\ + ● Kupující XMR by měl natočit nebo vyfotografovat proces balení hotovosti ve vysokém rozlišení s adresou a sledovacím číslem již připevněným na obalu.\n\ + ● Kupující XMR by měl odeslat balíček s hotovostí prodávajícímu XMR s potvrzením o doručení a příslušným pojištěním.\n\ + ● Prodávající XMR by měl natočit otevření balíku a ujistit se, že je na videu vidět sledovací číslo poskytnuté odesílatelem.\n\ + ● Tvůrce nabídky musí uvést veškeré dodatečné podmínky v poli 'Další informace' na platebním účtu.\n\ + ● Příjemce nabídky přijetím nabídky souhlasí s podmínkami tvůrce nabídky.\n\ + \n\ + Obchody Hotovost poštou kladou břemeno jednat čestně rovnoměrně na obě strany.\n\ + \n\ + ● Hotovostní poštovní obchody jsou méně ověřitelné než jiné tradiční obchody. To značně ztěžuje řešení sporů.\n\ + ● Spory se snažte řešit přímo s partnerem pomocí chatu obchodníků. To je nejslibnější cesta k vyřešení jakéhokoli sporu o Hotovost poštou.\n\ + ● Rozhodci mohou váš případ posoudit a přednést své doporučení, ale NEMOHOU vám zaručeně pomoci.\n\ + ● Rozhodci rozhodnou na základě důkazů, které jim budou poskytnuty. Proto dodržujte a dokumentujte výše uvedené postupy, abyste měli důkazy pro případ sporu.\n\ + ● Žádosti o náhradu jakýchkoli ztracených prostředků v důsledku obchodů Hotovost poštou na Haveno NEBUDOU brány v úvahu.\n\ + \n\ + Pokud těmto požadavkům nerozumíte, neobchodujte pomocí Hotovost poštou na Haveno. payment.payByMail.contact=Kontaktní informace payment.payByMail.contact.prompt=Obálka se jménem nebo pseudonymem by měla být adresována +payment.payByMail.extraInfo.prompt=Uveďte prosím ve svých nabídkách: \n\n\ +Zemi, ve které se nacházíte (např. Francie); \n\ +Země / regiony, ze kterých byste přijímali obchody (např. Francie, EU nebo jakákoli evropská země); \n\ +Jakékoli zvláštní podmínky; \n\ +Jakékoli další údaje. +payment.tradingRestrictions=Přečtěte si prosím podmínky tvůrce.\n\ + Pokud nesplňujete požadavky, nepřijímejte tento obchod. +payment.cashAtAtm.info=Hotovost u bankomatu: Výběr z bankomatu bez karty a pomocí kódu\n\n\ + Použití tohoto způsobu platby:\n\n\ + 1. Vytvořte si platební účet Hotovost u bankomatu a uveďte přijímané banky, regiony nebo jiné podmínky, které se zobrazí u nabídky.\n\n\n\ + 2. Vytvořte nebo přijměte nabídku s tímto platebním účtem.\n\n\ + 3. Po přijetí nabídky se domluvte s obchodním partnerem na čase dokončení platby a sdílejte podrobnosti o platbě.\n\n\ + Pokud se vám nepodaří dokončit transakci, jak je uvedeno v obchodní smlouvě, můžete přijít o část (nebo celou) vaší kauci. +payment.cashAtAtm.extraInfo.prompt=Uveďte prosím ve svých nabídkách: \n\n\ +Vámi přijímané banky / místa; \n\ +Jakékoli zvláštní podmínky; \n\ +Jakékoli další podrobnosti. payment.f2f.contact=Kontaktní informace payment.f2f.contact.prompt=Jak byste chtěli být kontaktováni obchodním partnerem? (e-mailová adresa, telefonní číslo, ...) -payment.f2f.city=Město pro setkání „tváří v tvář“ +payment.f2f.city=Město pro setkání 'tváří v tvář' payment.f2f.city.prompt=Město se zobrazí s nabídkou payment.shared.optionalExtra=Volitelné další informace -payment.shared.extraInfo=Dodatečné informace +payment.shared.extraInfo=Další informace payment.shared.extraInfo.prompt=Uveďte jakékoli speciální požadavky, podmínky a detaily, které chcete zobrazit u vašich nabídek s tímto platebním účtem. (Uživatelé uvidí tyto informace předtím, než akceptují vaši nabídku.) -payment.f2f.info=Obchody „tváří v tvář“ mají různá pravidla a přicházejí s jinými riziky než online transakce.\n\nHlavní rozdíly jsou:\n● Obchodní partneři si musí vyměňovat informace o místě a čase schůzky pomocí poskytnutých kontaktních údajů.\n● Obchodní partneři musí přinést své notebooky a na místě setkání potvrdit „platba odeslána“ a „platba přijata“.\n● Pokud má tvůrce speciální „podmínky“, musí uvést podmínky v textovém poli „Další informace“ na účtu.\n● Přijetím nabídky zadavatel souhlasí s uvedenými „podmínkami a podmínkami“ tvůrce.\n● V případě sporu nemůže být mediátor nebo rozhodce příliš nápomocný, protože je obvykle obtížné získat důkazy o tom, co se na schůzce stalo. V takových případech mohou být prostředky XMR uzamčeny na dobu neurčitou nebo dokud se obchodní partneři nedohodnou.\n\nAbyste si byli jisti, že plně rozumíte rozdílům v obchodech „tváří v tvář“, přečtěte si pokyny a doporučení na adrese: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] +payment.f2f.info=Obchody 'tváří v tvář' mají různá pravidla a přicházejí s jinými riziky než online transakce.\n\n\ + Hlavní rozdíly jsou:\n\ + ● Obchodní partneři si musí vyměňovat informace o místě a čase schůzky pomocí poskytnutých kontaktních údajů.\n\ + ● Obchodní partneři musí přinést své notebooky a na místě setkání potvrdit 'platba odeslána' a 'platba přijata'.\n\ + ● Pokud má tvůrce speciální 'podmínky', musí uvést podmínky v textovém poli 'Další informace' na účtu.\n\ + ● Přijetím nabídky zadavatel souhlasí s uvedenými 'podmínkami a podmínkami' tvůrce.\n\ + ● V případě sporu nemůže být mediátor nebo rozhodce příliš nápomocný, protože je obvykle obtížné získat důkazy o tom, co se na schůzce stalo. \ + V takových případech mohou být prostředky XMR uzamčeny na dobu neurčitou \ + nebo dokud se obchodní partneři nedohodnou.\n\n\ + Abyste si byli jisti, že plně rozumíte rozdílům v obchodech 'tváří v tvář', přečtěte si pokyny a doporučení \ + na adrese: [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/F2F] payment.f2f.info.openURL=Otevřít webovou stránku payment.f2f.offerbook.tooltip.countryAndCity=Země a město: {0} / {1} payment.f2f.offerbook.tooltip.extra=Další informace: {0} +payment.ifsc=IFS kód +payment.ifsc.validation=IFSC formát: XXXX0999999 payment.japan.bank=Banka payment.japan.branch=Pobočka @@ -2024,9 +3052,16 @@ payment.japan.account=Účet payment.japan.recipient=Jméno payment.australia.payid=PayID payment.payid=PayID spojené s finanční institucí. Jako e-mailová adresa nebo mobilní telefon. -payment.payid.info=PayID jako telefonní číslo, e-mailová adresa nebo australské obchodní číslo (ABN), které můžete bezpečně propojit se svou bankou, družstevní záložnou nebo účtem stavební spořitelny. Musíte mít již vytvořený PayID u své australské finanční instituce. Odesílající i přijímající finanční instituce musí podporovat PayID. Další informace najdete na [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=Chcete-li platit dárkovou kartou Amazon eGift, budete muset prodejci XMR poslat kartu Amazon eGift přes svůj účet Amazon.\n\nHaveno zobrazí e-mail nebo mobilní číslo prodejce XMR, kam bude potřeba odeslat tuto dárkovou kartu. Na kartě ve zprávě pro příjemce musí být uvedeno ID obchodu. Pro další detaily a rady viz wiki: [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card].\n\nZde jsou tři důležité poznámky:\n- Preferujte dárkové karty v hodnotě do 100 USD, protože Amazon může považovat nákupy karet s vyššími částkami jako podezřelé a zablokovat je.\n- Na kartě do zprávy pro příjemce můžete přidat i vlastní originální text (např. "Happy birthday Susan!") spolu s ID obchodu. (V takovém případě o tom informujte protistranu pomocí obchodovacího chatu, aby mohli s jistotou ověřit, že obdržená dárková karta pochází od vás.)\n- Karty Amazon eGift lze uplatnit pouze na té stránce Amazon, na které byly také koupeny (např. karta koupená na amazon.it může být uplatněna zase jen na amazon.it). - +payment.payid.info=PayID jako telefonní číslo, e-mailová adresa nebo australské obchodní číslo (ABN), které můžete bezpečně propojit se svou \ + bankou, družstevní záložnou nebo účtem stavební spořitelny. Musíte mít již vytvořený PayID u své australské finanční instituce. \ + Odesílající i přijímající finanční instituce musí podporovat PayID. Další informace najdete na [HYPERLINK:https://payid.com.au/faqs/] +payment.amazonGiftCard.info=Chcete-li platit dárkovou kartou Amazon eGift, budete muset prodejci XMR poslat kartu Amazon eGift přes svůj účet Amazon.\n\n\ + Podívejte se do wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] pro podrobnosti a rady.\n\n\ + Zde jsou tři důležité poznámky:\n\ + - Preferujte dárkové karty v hodnotě do 100 USD, protože Amazon může považovat nákupy karet s vyššími částkami jako podezřelé a zablokovat je.\n\ + - Na kartě do zprávy pro příjemce můžete přidat i vlastní originální text (např. "Happy birthday Susan!") spolu s ID obchodu (v takovém případě \ + o tom informujte protistranu pomocí obchodovacího chatu, aby mohli s jistotou ověřit, že obdržená dárková karta pochází od vás.)\n\ + - Karty Amazon eGift lze uplatnit pouze na té stránce Amazon, na které byly také koupeny (např. karta koupená na amazon.it může být uplatněna zase jen na amazon.it). # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ @@ -2036,8 +3071,9 @@ NATIONAL_BANK=Národní bankovní převod SAME_BANK=Převod ve stejné bance SPECIFIC_BANKS=Převody u konkrétních bank US_POSTAL_MONEY_ORDER=Poukázka US Postal -CASH_DEPOSIT=Vklad hotovosti na účet prodávajícího -PAY_BY_MAIL=Odeslání hotovosti poštou +CASH_DEPOSIT=Vklad hotovosti +PAY_BY_MAIL=Hotovost poštou +CASH_AT_ATM=Hotovost u bankomatu MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Tváří v tvář (osobně) @@ -2057,6 +3093,8 @@ CASH_DEPOSIT_SHORT=Vklad hotovosti # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=Hotovost poštou # suppress inspection "UnusedProperty" +CASH_AT_ATM_SHORT=Hotovost u bankomatu +# suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union @@ -2085,7 +3123,7 @@ WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" -SEPA_INSTANT=SEPA Okamžité platby +SEPA_INSTANT=SEPA okamžité platby # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" @@ -2107,9 +3145,53 @@ ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=TransferWise # suppress inspection "UnusedProperty" +TRANSFERWISE_USD=TransferWise-USD +# suppress inspection "UnusedProperty" +PAYSERA=Paysera +# suppress inspection "UnusedProperty" +PAXUM=Paxum +# suppress inspection "UnusedProperty" +NEFT=India/NEFT +# suppress inspection "UnusedProperty" +RTGS=India/RTGS +# suppress inspection "UnusedProperty" +IMPS=India/IMPS +# suppress inspection "UnusedProperty" +UPI=India/UPI +# suppress inspection "UnusedProperty" +PAYTM=India/PayTM +# suppress inspection "UnusedProperty" +NEQUI=Nequi +# suppress inspection "UnusedProperty" +BIZUM=Bizum +# suppress inspection "UnusedProperty" +PIX=Pix +# suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" -BLOCK_CHAINS_INSTANT=Instantní kryptoměny +BLOCK_CHAINS_INSTANT=Kryptoměny okamžité +# suppress inspection "UnusedProperty" +CAPITUAL=Capitual +# suppress inspection "UnusedProperty" +CELPAY=CelPay +# suppress inspection "UnusedProperty" +MONESE=Monese +# suppress inspection "UnusedProperty" +SATISPAY=Satispay +# suppress inspection "UnusedProperty" +TIKKIE=Tikkie +# suppress inspection "UnusedProperty" +VERSE=Verse +# suppress inspection "UnusedProperty" +STRIKE=Strike +# suppress inspection "UnusedProperty" +SWIFT=SWIFT mezinárodní bankovní převod +# suppress inspection "UnusedProperty" +ACH_TRANSFER=ACH Transfer +# suppress inspection "UnusedProperty" +DOMESTIC_WIRE_TRANSFER=Domestic Wire Transfer +# suppress inspection "UnusedProperty" +BSQ_SWAP=BSQ Swap # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" @@ -2118,7 +3200,7 @@ OK_PAY=OKPay CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo - +PAYPAL=PayPal # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold @@ -2159,9 +3241,53 @@ ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=TransferWise # suppress inspection "UnusedProperty" +TRANSFERWISE_USD_SHORT=TransferWise-USD +# suppress inspection "UnusedProperty" +PAYSERA_SHORT=Paysera +# suppress inspection "UnusedProperty" +PAXUM_SHORT=Paxum +# suppress inspection "UnusedProperty" +NEFT_SHORT=NEFT +# suppress inspection "UnusedProperty" +RTGS_SHORT=RTGS +# suppress inspection "UnusedProperty" +IMPS_SHORT=IMPS +# suppress inspection "UnusedProperty" +UPI_SHORT=UPI +# suppress inspection "UnusedProperty" +PAYTM_SHORT=PayTM +# suppress inspection "UnusedProperty" +NEQUI_SHORT=Nequi +# suppress inspection "UnusedProperty" +BIZUM_SHORT=Bizum +# suppress inspection "UnusedProperty" +PIX_SHORT=Pix +# suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" -BLOCK_CHAINS_INSTANT_SHORT=Instantní kryptoměny +BLOCK_CHAINS_INSTANT_SHORT=Kryptoměny okamžité +# suppress inspection "UnusedProperty" +CAPITUAL_SHORT=Capitual +# suppress inspection "UnusedProperty" +CELPAY_SHORT=CelPay +# suppress inspection "UnusedProperty" +MONESE_SHORT=Monese +# suppress inspection "UnusedProperty" +SATISPAY_SHORT=Satispay +# suppress inspection "UnusedProperty" +TIKKIE_SHORT=Tikkie +# suppress inspection "UnusedProperty" +VERSE_SHORT=Verse +# suppress inspection "UnusedProperty" +STRIKE_SHORT=Strike +# suppress inspection "UnusedProperty" +SWIFT_SHORT=SWIFT +# suppress inspection "UnusedProperty" +ACH_TRANSFER_SHORT=ACH +# suppress inspection "UnusedProperty" +DOMESTIC_WIRE_TRANSFER_SHORT=Domestic Wire +# suppress inspection "UnusedProperty" +BSQ_SWAP_SHORT=BSQ Swap # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" @@ -2170,6 +3296,7 @@ OK_PAY_SHORT=OKPay CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo +PAYPAL_SHORT=PayPal #################################################################### @@ -2224,6 +3351,7 @@ validation.iban.checkSumNotNumeric=Kontrolní součet musí být číselný validation.iban.nonNumericChars=Byl zjištěn nealfanumerický znak validation.iban.checkSumInvalid=Kontrolní součet IBAN je neplatný validation.iban.invalidLength=Číslo musí mít délku 15 až 34 znaků. +validation.iban.sepaNotSupported=SEPA není v této zemi podporována validation.interacETransfer.invalidAreaCode=Non-kanadské směrové číslo oblasti validation.interacETransfer.invalidPhone=Zadejte platné 11místné telefonní číslo (např. 1-123-456-7890) nebo e-mailovou adresu validation.interacETransfer.invalidQuestion=Musí obsahovat pouze písmena, čísla, mezery a/nebo symboly ' _ , . ? - @@ -2246,5 +3374,8 @@ validation.phone.missingCountryCode=K ověření telefonního čísla je potřeb validation.phone.invalidCharacters=Telefonní číslo {0} obsahuje neplatné znaky validation.phone.insufficientDigits=V čísle {0} není dostatek číslic, aby mohlo být platné telefonní číslo validation.phone.tooManyDigits=V čísle {0} je příliš mnoho číslic, než aby mohlo být platné telefonní číslo -validation.phone.invalidDialingCode=Telefonní předvolba země pro číslo {0} je pro zemi {1} neplatná. Správné předčíslí je {2}. +validation.phone.invalidDialingCode=Telefonní předvolba země pro číslo {0} je pro zemi {1} neplatná. \ + Správné předčíslí je {2}. validation.invalidAddressList=Seznam platných adres musí být oddělený čárkami +validation.capitual.invalidFormat=Musí jít o platný kód formátu CAP: CAP-XXXXXX (6 alfanumerických znaků). + From 3e0b694e1338bcb763838ca070097037d7fb1760 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 6 Jan 2025 09:20:07 -0500 Subject: [PATCH 071/371] update translations to register filter object --- core/src/main/resources/i18n/displayStrings_cs.properties | 2 +- core/src/main/resources/i18n/displayStrings_de.properties | 2 +- core/src/main/resources/i18n/displayStrings_es.properties | 2 +- core/src/main/resources/i18n/displayStrings_fa.properties | 1 + core/src/main/resources/i18n/displayStrings_fr.properties | 2 +- core/src/main/resources/i18n/displayStrings_it.properties | 1 + core/src/main/resources/i18n/displayStrings_ja.properties | 1 + core/src/main/resources/i18n/displayStrings_pt-br.properties | 1 + core/src/main/resources/i18n/displayStrings_pt.properties | 1 + core/src/main/resources/i18n/displayStrings_ru.properties | 1 + core/src/main/resources/i18n/displayStrings_th.properties | 1 + core/src/main/resources/i18n/displayStrings_tr.properties | 2 +- core/src/main/resources/i18n/displayStrings_vi.properties | 1 + core/src/main/resources/i18n/displayStrings_zh-hans.properties | 1 + core/src/main/resources/i18n/displayStrings_zh-hant.properties | 1 + 15 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 7c313343ba..4f2b4d3a87 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -2169,7 +2169,7 @@ popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Aktualizujte prosím na nejnovější verzi Haveno. \ Byla vydána povinná aktualizace, která zakazuje obchodování se starými verzemi. \ Další informace naleznete na fóru Haveno. -popup.warning.noFilter=Ze seed uzlů jsme neobdrželi objekt filtru. Informujte prosím správce sítě, aby objekt filtru zaregistrovali. +popup.warning.noFilter=Nepřijali jsme objekt filtru od seedových uzlů. Prosím informujte správce sítě, aby zaregistrovali objekt filtru. popup.warning.burnXMR=Tato transakce není možná, protože poplatky za těžbu {0} by přesáhly částku převodu {1}. \ Počkejte prosím, dokud nebudou poplatky za těžbu opět nízké nebo dokud nenahromadíte více XMR k převodu. diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 2c9683be7d..c6002f049a 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1631,7 +1631,7 @@ popup.warning.nodeBanned=Einer der {0} Nodes wurde gebannt. popup.warning.priceRelay=Preisrelais popup.warning.seed=Seed popup.warning.mandatoryUpdate.trading=Bitte aktualisieren Sie auf die neueste Haveno-Version. Es wurde ein obligatorisches Update veröffentlicht, das den Handel mit alten Versionen deaktiviert. Bitte besuchen Sie das Haveno-Forum für weitere Informationen. -popup.warning.noFilter=Wir haben kein Filterobjekt von den Seed Nodes erhalten. Diese Situation ist unerwartet. Bitte informieren Sie die Haveno Entwickler. +popup.warning.noFilter=Wir haben kein Filterobjekt von den Seed-Knoten erhalten. Bitte informieren Sie die Netzwerkadministratoren, ein Filterobjekt zu registrieren. popup.warning.burnXMR=Die Transaktion ist nicht möglich, da die Mininggebühren von {0} den übertragenen Betrag von {1} überschreiten würden. Bitte warten Sie, bis die Gebühren wieder niedrig sind, oder Sie mehr XMR zum übertragen angesammelt haben. popup.warning.openOffer.makerFeeTxRejected=Die Verkäufergebühren-Transaktion für das Angebot mit der ID {0} wurde vom Monero-Netzwerk abgelehnt.\nTransaktions-ID={1}.\nDas Angebot wurde entfernt, um weitere Probleme zu vermeiden.\nBitte gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie eine SPV-Resynchronisierung durch.\nFür weitere Hilfe wenden Sie sich bitte an den Haveno-Support-Kanal des Haveno Keybase Teams. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 45b3cfeb84..303330e489 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -1632,7 +1632,7 @@ popup.warning.nodeBanned=Uno de los nodos {0} ha sido baneado. popup.warning.priceRelay=retransmisión de precio popup.warning.seed=semilla popup.warning.mandatoryUpdate.trading=Por favor, actualice a la última versión de Haveno. Se lanzó una actualización obligatoria que inhabilita intercambios con versiones anteriores. Por favor, lea el Foro de Haveno para más información\n -popup.warning.noFilter=No hemos recibido un objeto de filtro desde los nodos semilla. Esta situación no se esperaba. Por favor, informe a los desarrolladores Haveno. +popup.warning.noFilter=No recibimos un objeto de filtro de los nodos semilla. Por favor, informe a los administradores de la red que registren un objeto de filtro. popup.warning.burnXMR=Esta transacción no es posible, ya que las comisiones de minado de {0} excederían la cantidad a transferir de {1}. Por favor, espere a que las comisiones de minado bajen o hasta que haya acumulado más XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=La tasa de transacción para la oferta con ID {0} se rechazó por la red Monero.\nID de transacción={1}\nLa oferta se ha eliminado para evitar futuros problemas.\nPor favor vaya a \"Configuración/Información de red\" y haga una resincronización SPV.\nPara más ayuda por favor contacte con el equipo de soporte de Haveno en el canal de Haveno en Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index ed81083c42..647b9dc735 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1627,6 +1627,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=رله قیمت popup.warning.seed=دانه popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. +popup.warning.noFilter=ما شیء فیلتر را از گره‌های اولیه دریافت نکردیم. لطفاً به مدیران شبکه اطلاع دهید که یک شیء فیلتر ثبت کنند. popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index b7a76ab299..7ee0f6a420 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -1633,7 +1633,7 @@ popup.warning.nodeBanned=Un des noeuds {0} a été banni. popup.warning.priceRelay=Relais de prix popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Veuillez faire une mise à jour vers la dernière version de Haveno. Une mise à jour obligatoire a été publiée, laquelle désactive le trading sur les anciennes versions. Veuillez consulter le Forum Haveno pour obtenir plus d'informations. -popup.warning.noFilter=Nous n'avons pas reçu d'object de filtre de la part des noeuds source. Ceci n'est pas une situation attendue. Veuillez informer les développeurs de Haveno +popup.warning.noFilter=Nous n'avons pas reçu d'objet de filtre des nœuds de seed. Veuillez informer les administrateurs du réseau d'enregistrer un objet de filtre. popup.warning.burnXMR=Cette transaction n''est pas possible, car les frais de minage de {0} dépasseraient le montant à transférer de {1}. Veuillez patienter jusqu''à ce que les frais de minage soient de nouveau bas ou jusqu''à ce que vous ayez accumulé plus de XMR à transférer. popup.warning.openOffer.makerFeeTxRejected=La transaction de frais de maker pour l''offre avec ID {0} a été rejetée par le réseau Monero.\nID de transaction={1}.\nL''offre a été retirée pour éviter d''autres problèmes.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l''aide, le canal support de l''équipe Haveno disposible sur Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 367dd03974..495c2380f8 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1630,6 +1630,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=ripetitore di prezzo popup.warning.seed=seme popup.warning.mandatoryUpdate.trading=Si prega di aggiornare Haveno all'ultima versione. È stato rilasciato un aggiornamento obbligatorio che disabilita il trading per le vecchie versioni. Per ulteriori informazioni, consultare il forum Haveno. +popup.warning.noFilter=Non abbiamo ricevuto un oggetto filtro dai nodi seme. Si prega di informare gli amministratori di rete di registrare un oggetto filtro. popup.warning.burnXMR=Questa transazione non è possibile, poiché le commissioni di mining di {0} supererebbero l'importo da trasferire di {1}. Attendi fino a quando le commissioni di mining non saranno nuovamente basse o fino a quando non avrai accumulato più XMR da trasferire. popup.warning.openOffer.makerFeeTxRejected=La commissione della transazione del creatore dell'offerta con ID {0} è stata rifiutata dalla rete Monero.\nTransazione ID={1}.\nL'offerta è stata rimossa per evitare ulteriori problemi.\nVai su \"Impostazioni/Informazioni di rete\" ed esegui una risincronizzazione SPV.\nPer ulteriore assistenza, contattare il canale di supporto Haveno nel team di Haveno Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index d2a32da7f5..b31773063e 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -1631,6 +1631,7 @@ popup.warning.nodeBanned={0}ノードの1つが禁止されました。 popup.warning.priceRelay=価格中継 popup.warning.seed=シード popup.warning.mandatoryUpdate.trading=最新のHavenoバージョンに更新してください。古いバージョンのトレードを無効にする必須の更新プログラムがリリースされました。詳細については、Havenoフォーラムをご覧ください。 +popup.warning.noFilter=シードノードからフィルターオブジェクトを受け取っていません。ネットワーク管理者にフィルターオブジェクトを登録するように通知してください。 popup.warning.burnXMR={0}のマイニング手数料が{1}の送金額を超えるため、このトランザクションは利用不可です。マイニング手数料が再び低くなるか、送金するXMRがさらに蓄積されるまでお待ちください。 popup.warning.openOffer.makerFeeTxRejected=ID{0}で識別されるオファーのためのメイカー手数料トランザクションがビットコインネットワークに拒否されました。\nトランザクションID= {1} 。\n更なる問題を避けるため、そのオファーは削除されました。\n\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\nさらにサポートを受けるため、Haveno Keybaseチームのサポートチャンネルに連絡して下さい。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index d6c907975b..6ac9da556c 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -1637,6 +1637,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=transmissão de preço popup.warning.seed=semente popup.warning.mandatoryUpdate.trading=Faça o update para a última versão do Haveno. Um update obrigatório foi lançado e desabilita negociações em versões antigas. Por favor, veja o Fórum do Haveno para mais informações. +popup.warning.noFilter=Não recebemos um objeto de filtro dos nós seed. Por favor, informe aos administradores da rede para registrar um objeto de filtro. popup.warning.burnXMR=Esta transação não é possível, pois as taxas de mineração de {0} excederiam o montante a transferir de {1}. Aguarde até que as taxas de mineração estejam novamente baixas ou até você ter acumulado mais XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=A transação de taxa de ofertante para a oferta com ID {0} foi rejeitada pela rede Monero.\nID da transação: {1}.\nA oferta foi removida para evitar problemas adicionais.\nPor favor, vá até "Configurações/Informações da rede" e ressincronize o arquivo SPV.\nPara mais informações, por favor acesse o canal #support do time da Haveno na Keybase. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index f97d482965..45cd1170dc 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1627,6 +1627,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=transmissão de preço popup.warning.seed=semente popup.warning.mandatoryUpdate.trading=Por favor, atualize para a versão mais recente do Haveno. Uma atualização obrigatória que desativa negociação para versões antigas foi lançada. Por favor, confira o Fórum Haveno para mais informações. +popup.warning.noFilter=Não recebemos um objeto de filtro dos nós sementes. Por favor, informe os administradores da rede para registrar um objeto de filtro. popup.warning.burnXMR=Esta transação não é possível, pois as taxas de mineração de {0} excederia o montante a transferir de {1}. Aguarde até que as taxas de mineração estejam novamente baixas ou até você ter acumulado mais XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=A transação da taxa de ofertante para a oferta com o ID {0} foi rejeitada pela rede do Monero.\nID da transação={1}.\nA oferta foi removida para evitar futuros problemas.\nPor favor vá à \"Definições/Informação da Rede\" e re-sincronize o ficheiro SPV.\nPara mais ajuda por favor contacte o canal de apoio do Haveno na equipa Keybase do Haveno. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 2562c75e5f..ac05e10b83 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1628,6 +1628,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=ретранслятор курса popup.warning.seed=мнемоническая фраза popup.warning.mandatoryUpdate.trading=Обновите Haveno до последней версии. Вышло обязательное обновление, которое делает невозможной торговлю в старых версиях приложения. Посетите форум Haveno, чтобы узнать подробности. +popup.warning.noFilter=Мы не получили объект фильтра от узлов-источников. Пожалуйста, сообщите администраторам сети, чтобы они зарегистрировали объект фильтра. popup.warning.burnXMR=Данную транзакцию невозможно завершить, так как плата за нее ({0}) превышает сумму перевода ({1}). Подождите, пока плата за транзакцию не снизится или пока у вас не появится больше XMR для завершения перевода. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 6d739c8e05..5e25ee8149 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1628,6 +1628,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=ราคาผลัดเปลี่ยน popup.warning.seed=รหัสลับเพื่อกู้ข้อมูล popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. +popup.warning.noFilter=เราไม่ได้รับวัตถุกรองจากโหนดต้นทาง กรุณาแจ้งผู้ดูแลระบบเครือข่ายให้ลงทะเบียนวัตถุกรอง popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 1f2d2c536b..cdba715870 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -2160,7 +2160,7 @@ popup.warning.seed=anahtar kelime popup.warning.mandatoryUpdate.trading=Lütfen en son Haveno sürümüne güncelleyin. \ Eski sürümler için ticareti devre dışı bırakan zorunlu bir güncelleme yayınlandı. \ Daha fazla bilgi için lütfen Haveno Forumunu kontrol edin. -popup.warning.noFilter=Tohum düğümlerinden bir filtre nesnesi almadık. Lütfen Haveno ağ yöneticilerini bir filtre nesnesi kaydetmek için ctrl + f ile bilgilendirin. +popup.warning.noFilter=Tohum düğümlerinden bir filtre nesnesi almadık. Lütfen ağ yöneticilerine bir filtre nesnesi kaydetmeleri için bilgi verin. popup.warning.burnXMR=Bu işlem mümkün değil, çünkü {0} tutarındaki madencilik ücretleri, transfer edilecek {1} tutarını aşacaktır. \ Lütfen madencilik ücretleri tekrar düşük olana kadar bekleyin veya transfer etmek için daha fazla XMR biriktirin. diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 78db6a6a8c..7ea2eb9d27 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1630,6 +1630,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=rơle giá popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. +popup.warning.noFilter=Chúng tôi không nhận được đối tượng bộ lọc từ các nút hạt giống. Vui lòng thông báo cho quản trị viên mạng để đăng ký một đối tượng bộ lọc. popup.warning.burnXMR=Không thể thực hiện giao dịch, vì phí đào {0} vượt quá số lượng {1} cần chuyển. Vui lòng chờ tới khi phí đào thấp xuống hoặc khi bạn tích lũy đủ XMR để chuyển. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 809d61f235..9f3e495ba7 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -1635,6 +1635,7 @@ popup.warning.nodeBanned=其中一个 {0} 节点已被禁用 popup.warning.priceRelay=价格传递 popup.warning.seed=种子 popup.warning.mandatoryUpdate.trading=请更新到最新的 Haveno 版本。强制更新禁止了旧版本进行交易。更多信息请访问 Haveno 论坛。 +popup.warning.noFilter=我们没有从种子节点接收到过滤器对象。请通知网络管理员注册一个过滤器对象。 popup.warning.burnXMR=这笔交易是无法实现,因为 {0} 的挖矿手续费用会超过 {1} 的转账金额。请等到挖矿手续费再次降低或您积累了更多的 XMR 来转账。 popup.warning.openOffer.makerFeeTxRejected=交易 ID 为 {0} 的挂单费交易被比特币网络拒绝。\n交易 ID = {1}\n交易已被移至失败交易。\n请到“设置/网络信息”进行 SPV 重新同步。\n如需更多帮助,请联系 Haveno Keybase 团队的 Support 频道 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index c6239bee95..c802eba7b8 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -1631,6 +1631,7 @@ popup.warning.nodeBanned=其中一個 {0} 節點已被禁用 popup.warning.priceRelay=價格傳遞 popup.warning.seed=種子 popup.warning.mandatoryUpdate.trading=請更新到最新的 Haveno 版本。強制更新禁止了舊版本進行交易。更多信息請訪問 Haveno 論壇。 +popup.warning.noFilter=我們未從種子節點收到過濾器物件。請通知網路管理員註冊過濾器物件。 popup.warning.burnXMR=這筆交易是無法實現,因為 {0} 的挖礦手續費用會超過 {1} 的轉賬金額。請等到挖礦手續費再次降低或您積累了更多的 XMR 來轉賬。 popup.warning.openOffer.makerFeeTxRejected=交易 ID 為 {0} 的掛單費交易被比特幣網絡拒絕。\n交易 ID = {1}\n交易已被移至失敗交易。\n請到“設置/網絡信息”進行 SPV 重新同步。\n如需更多幫助,請聯繫 Haveno Keybase 團隊的 Support 頻道 From e8d53669413e2c5ffc7ecf6bf379b860f717dbf7 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 5 Jan 2025 18:23:21 -0500 Subject: [PATCH 072/371] load offer book views off main thread #1518 --- .../main/offer/offerbook/OfferBookView.java | 398 +++++++++--------- 1 file changed, 208 insertions(+), 190 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index 3bcb3cb03f..80b3752722 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -788,11 +788,13 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { - super.updateItem(item, empty); - if (item != null && !empty) - setGraphic(new ColoredDecimalPlacesWithZerosText(model.getAmount(item), GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); - else - setGraphic(null); + UserThread.execute(() -> { + super.updateItem(item, empty); + if (item != null && !empty) + setGraphic(new ColoredDecimalPlacesWithZerosText(model.getAmount(item), GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); + else + setGraphic(null); + }); } }; } @@ -817,12 +819,13 @@ abstract public class OfferBookView { + super.updateItem(item, empty); + if (item != null && !empty) + setText(CurrencyUtil.getCurrencyPair(item.getOffer().getCurrencyCode())); + else + setText(""); + }); } }; } @@ -852,13 +855,15 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { - super.updateItem(item, empty); + UserThread.execute(() -> { + super.updateItem(item, empty); - if (item != null && !empty) { - setGraphic(getPriceAndPercentage(item)); - } else { - setGraphic(null); - } + if (item != null && !empty) { + setGraphic(getPriceAndPercentage(item)); + } else { + setGraphic(null); + } + }); } private HBox getPriceAndPercentage(OfferBookListItem item) { @@ -934,21 +939,23 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { - super.updateItem(item, empty); + UserThread.execute(() -> { + super.updateItem(item, empty); - if (item != null && !empty) { - if (item.getOffer().getPrice() == null) { - setText(Res.get("shared.na")); - setGraphic(null); + if (item != null && !empty) { + if (item.getOffer().getPrice() == null) { + setText(Res.get("shared.na")); + setGraphic(null); + } else { + setText(""); + setGraphic(new ColoredDecimalPlacesWithZerosText(model.getVolume(item), + model.getNumberOfDecimalsForVolume(item))); + } } else { setText(""); - setGraphic(new ColoredDecimalPlacesWithZerosText(model.getVolume(item), - model.getNumberOfDecimalsForVolume(item))); + setGraphic(null); } - } else { - setText(""); - setGraphic(null); - } + }); } }; } @@ -974,30 +981,32 @@ abstract public class OfferBookView { + super.updateItem(item, empty); - if (item != null && !empty) { + if (item != null && !empty) { - Offer offer = item.getOffer(); - if (model.isOfferBanned(offer)) { - setGraphic(new AutoTooltipLabel(model.getPaymentMethod(item))); - } else { - if (offer.isXmrAutoConf()) { - field = new HyperlinkWithIcon(model.getPaymentMethod(item), AwesomeIcon.ROCKET); + Offer offer = item.getOffer(); + if (model.isOfferBanned(offer)) { + setGraphic(new AutoTooltipLabel(model.getPaymentMethod(item))); } else { - field = new HyperlinkWithIcon(model.getPaymentMethod(item)); + if (offer.isXmrAutoConf()) { + field = new HyperlinkWithIcon(model.getPaymentMethod(item), AwesomeIcon.ROCKET); + } else { + field = new HyperlinkWithIcon(model.getPaymentMethod(item)); + } + field.setOnAction(event -> { + offerDetailsWindow.show(offer); + }); + field.setTooltip(new Tooltip(model.getPaymentMethodToolTip(item))); + setGraphic(field); } - field.setOnAction(event -> { - offerDetailsWindow.show(offer); - }); - field.setTooltip(new Tooltip(model.getPaymentMethodToolTip(item))); - setGraphic(field); + } else { + setGraphic(null); + if (field != null) + field.setOnAction(null); } - } else { - setGraphic(null); - if (field != null) - field.setOnAction(null); - } + }); } }; } @@ -1026,25 +1035,28 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { - super.updateItem(item, empty); - if (item != null && !empty) { - var isSellOffer = item.getOffer().getDirection() == OfferDirection.SELL; - var deposit = isSellOffer ? item.getOffer().getMaxBuyerSecurityDeposit() : - item.getOffer().getMaxSellerSecurityDeposit(); - if (deposit == null) { - setText(Res.get("shared.na")); - setGraphic(null); + UserThread.execute(() -> { + super.updateItem(item, empty); + + if (item != null && !empty) { + var isSellOffer = item.getOffer().getDirection() == OfferDirection.SELL; + var deposit = isSellOffer ? item.getOffer().getMaxBuyerSecurityDeposit() : + item.getOffer().getMaxSellerSecurityDeposit(); + if (deposit == null) { + setText(Res.get("shared.na")); + setGraphic(null); + } else { + setText(""); + String rangePrefix = item.getOffer().isRange() ? "<= " : ""; + setGraphic(new ColoredDecimalPlacesWithZerosText(rangePrefix + model.formatDepositString( + deposit, item.getOffer().getAmount().longValueExact()), + GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); + } } else { setText(""); - String rangePrefix = item.getOffer().isRange() ? "<= " : ""; - setGraphic(new ColoredDecimalPlacesWithZerosText(rangePrefix + model.formatDepositString( - deposit, item.getOffer().getAmount().longValueExact()), - GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); + setGraphic(null); } - } else { - setText(""); - setGraphic(null); - } + }); } }; } @@ -1071,112 +1083,114 @@ abstract public class OfferBookView { + super.updateItem(item, empty); - final ImageView iconView = new ImageView(); - final AutoTooltipButton button = new AutoTooltipButton(); - - { - button.setGraphic(iconView); - button.setGraphicTextGap(10); - button.setPrefWidth(10000); - } - - final ImageView iconView2 = new ImageView(); - final AutoTooltipButton button2 = new AutoTooltipButton(); - - { - button2.setGraphic(iconView2); - button2.setGraphicTextGap(10); - button2.setPrefWidth(10000); - } - - final HBox hbox = new HBox(); - - { - hbox.setSpacing(8); - hbox.setAlignment(Pos.CENTER); - hbox.getChildren().add(button); - hbox.getChildren().add(button2); - HBox.setHgrow(button, Priority.ALWAYS); - HBox.setHgrow(button2, Priority.ALWAYS); - } - - TableRow tableRow = getTableRow(); - if (item != null && !empty) { - Offer offer = item.getOffer(); - boolean myOffer = model.isMyOffer(offer); - - // https://github.com/bisq-network/bisq/issues/4986 - if (tableRow != null) { - canTakeOfferResult = model.offerFilterService.canTakeOffer(offer, false); - tableRow.setOpacity(canTakeOfferResult.isValid() || myOffer ? 1 : 0.4); - - if (myOffer) { - button.setDefaultButton(false); - tableRow.setOnMousePressed(null); - } else if (canTakeOfferResult.isValid()) { - // set first row button as default - button.setDefaultButton(getIndex() == 0); - tableRow.setOnMousePressed(null); - } else { - button.setDefaultButton(false); - tableRow.setOnMousePressed(e -> { - // ugly hack to get the icon clickable when deactivated - if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas)) - onShowInfo(offer, canTakeOfferResult); - }); - } + final ImageView iconView = new ImageView(); + final AutoTooltipButton button = new AutoTooltipButton(); + + { + button.setGraphic(iconView); + button.setGraphicTextGap(10); + button.setPrefWidth(10000); } - - String title; - if (myOffer) { - iconView.setId("image-remove"); - title = Res.get("shared.remove"); - button.setOnAction(e -> onRemoveOpenOffer(offer)); - - iconView2.setId("image-edit"); - button2.updateText(Res.get("shared.edit")); - button2.setOnAction(e -> onEditOpenOffer(offer)); - button2.setManaged(true); - button2.setVisible(true); - } else { - boolean isSellOffer = OfferViewUtil.isShownAsSellOffer(offer); - boolean isPrivateOffer = offer.isPrivateOffer(); - iconView.setId(isPrivateOffer ? "image-lock2x" : isSellOffer ? "image-buy-white" : "image-sell-white"); - iconView.setFitHeight(16); - iconView.setFitWidth(16); - button.setId(isSellOffer ? "buy-button" : "sell-button"); - button.setStyle("-fx-text-fill: white"); - title = Res.get("offerbook.takeOffer"); - button.setTooltip(new Tooltip(Res.get("offerbook.takeOfferButton.tooltip", model.getDirectionLabelTooltip(offer)))); - button.setOnAction(e -> onTakeOffer(offer)); - button2.setManaged(false); - button2.setVisible(false); + + final ImageView iconView2 = new ImageView(); + final AutoTooltipButton button2 = new AutoTooltipButton(); + + { + button2.setGraphic(iconView2); + button2.setGraphicTextGap(10); + button2.setPrefWidth(10000); } - - if (!myOffer) { - if (canTakeOfferResult == null) { + + final HBox hbox = new HBox(); + + { + hbox.setSpacing(8); + hbox.setAlignment(Pos.CENTER); + hbox.getChildren().add(button); + hbox.getChildren().add(button2); + HBox.setHgrow(button, Priority.ALWAYS); + HBox.setHgrow(button2, Priority.ALWAYS); + } + + TableRow tableRow = getTableRow(); + if (item != null && !empty) { + Offer offer = item.getOffer(); + boolean myOffer = model.isMyOffer(offer); + + // https://github.com/bisq-network/bisq/issues/4986 + if (tableRow != null) { canTakeOfferResult = model.offerFilterService.canTakeOffer(offer, false); + tableRow.setOpacity(canTakeOfferResult.isValid() || myOffer ? 1 : 0.4); + + if (myOffer) { + button.setDefaultButton(false); + tableRow.setOnMousePressed(null); + } else if (canTakeOfferResult.isValid()) { + // set first row button as default + button.setDefaultButton(getIndex() == 0); + tableRow.setOnMousePressed(null); + } else { + button.setDefaultButton(false); + tableRow.setOnMousePressed(e -> { + // ugly hack to get the icon clickable when deactivated + if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas)) + onShowInfo(offer, canTakeOfferResult); + }); + } } - - if (!canTakeOfferResult.isValid()) { - button.setOnAction(e -> onShowInfo(offer, canTakeOfferResult)); + + String title; + if (myOffer) { + iconView.setId("image-remove"); + title = Res.get("shared.remove"); + button.setOnAction(e -> onRemoveOpenOffer(offer)); + + iconView2.setId("image-edit"); + button2.updateText(Res.get("shared.edit")); + button2.setOnAction(e -> onEditOpenOffer(offer)); + button2.setManaged(true); + button2.setVisible(true); + } else { + boolean isSellOffer = OfferViewUtil.isShownAsSellOffer(offer); + boolean isPrivateOffer = offer.isPrivateOffer(); + iconView.setId(isPrivateOffer ? "image-lock2x" : isSellOffer ? "image-buy-white" : "image-sell-white"); + iconView.setFitHeight(16); + iconView.setFitWidth(16); + button.setId(isSellOffer ? "buy-button" : "sell-button"); + button.setStyle("-fx-text-fill: white"); + title = Res.get("offerbook.takeOffer"); + button.setTooltip(new Tooltip(Res.get("offerbook.takeOfferButton.tooltip", model.getDirectionLabelTooltip(offer)))); + button.setOnAction(e -> onTakeOffer(offer)); + button2.setManaged(false); + button2.setVisible(false); + } + + if (!myOffer) { + if (canTakeOfferResult == null) { + canTakeOfferResult = model.offerFilterService.canTakeOffer(offer, false); + } + + if (!canTakeOfferResult.isValid()) { + button.setOnAction(e -> onShowInfo(offer, canTakeOfferResult)); + } + } + + button.updateText(title); + setPadding(new Insets(0, 15, 0, 0)); + setGraphic(hbox); + } else { + setGraphic(null); + button.setOnAction(null); + button2.setOnAction(null); + if (tableRow != null) { + tableRow.setOpacity(1); + tableRow.setOnMousePressed(null); } } - - button.updateText(title); - setPadding(new Insets(0, 15, 0, 0)); - setGraphic(hbox); - } else { - setGraphic(null); - button.setOnAction(null); - button2.setOnAction(null); - if (tableRow != null) { - tableRow.setOpacity(1); - tableRow.setOnMousePressed(null); - } - } + }); } }; } @@ -1204,17 +1218,19 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { - super.updateItem(item, empty); + UserThread.execute(() -> { + super.updateItem(item, empty); - if (item != null && !empty) { - var witnessAgeData = item.getWitnessAgeData(accountAgeWitnessService, signedWitnessService); - var label = witnessAgeData.isSigningRequired() - ? new AccountStatusTooltipLabel(witnessAgeData) - : new InfoAutoTooltipLabel(witnessAgeData.getDisplayString(), witnessAgeData.getIcon(), ContentDisplay.RIGHT, witnessAgeData.getInfo()); - setGraphic(label); - } else { - setGraphic(null); - } + if (item != null && !empty) { + var witnessAgeData = item.getWitnessAgeData(accountAgeWitnessService, signedWitnessService); + var label = witnessAgeData.isSigningRequired() + ? new AccountStatusTooltipLabel(witnessAgeData) + : new InfoAutoTooltipLabel(witnessAgeData.getDisplayString(), witnessAgeData.getIcon(), ContentDisplay.RIGHT, witnessAgeData.getInfo()); + setGraphic(label); + } else { + setGraphic(null); + } + }); } }; } @@ -1240,24 +1256,26 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem newItem, boolean empty) { - super.updateItem(newItem, empty); - if (newItem != null && !empty) { - final Offer offer = newItem.getOffer(); - final NodeAddress makersNodeAddress = offer.getOwnerNodeAddress(); - String role = Res.get("peerInfoIcon.tooltip.maker"); - int numTrades = model.getNumTrades(offer); - PeerInfoIconTrading peerInfoIcon = new PeerInfoIconTrading(makersNodeAddress, - role, - numTrades, - privateNotificationManager, - offer, - model.preferences, - model.accountAgeWitnessService, - useDevPrivilegeKeys); - setGraphic(peerInfoIcon); - } else { - setGraphic(null); - } + UserThread.execute(() -> { + super.updateItem(newItem, empty); + if (newItem != null && !empty) { + final Offer offer = newItem.getOffer(); + final NodeAddress makersNodeAddress = offer.getOwnerNodeAddress(); + String role = Res.get("peerInfoIcon.tooltip.maker"); + int numTrades = model.getNumTrades(offer); + PeerInfoIconTrading peerInfoIcon = new PeerInfoIconTrading(makersNodeAddress, + role, + numTrades, + privateNotificationManager, + offer, + model.preferences, + model.accountAgeWitnessService, + useDevPrivilegeKeys); + setGraphic(peerInfoIcon); + } else { + setGraphic(null); + } + }); } }; } From 944c189166dcf3f5cb03f14dba3d1a2a04050740 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 7 Jan 2025 09:34:19 -0500 Subject: [PATCH 073/371] show CashApp payment method field in account form --- .../haveno/desktop/components/paymentmethods/CashAppForm.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java index 60fe712709..c0bb16f5e0 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java @@ -115,6 +115,8 @@ public class CashAppForm extends PaymentMethodForm { public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(cashAppAccount.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"), cashAppAccount.getEmailOrMobileNrOrCashtag()).second; From e426f4d8f12e1efd28638f4bb76968f1fbd2c78a Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 8 Jan 2025 15:27:54 -0500 Subject: [PATCH 074/371] update to monero-java v0.8.34 --- build.gradle | 2 +- gradle/verification-metadata.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 67a6a60fab..f4751dbdbd 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ configure(subprojects) { gsonVersion = '2.8.5' guavaVersion = '32.1.1-jre' guiceVersion = '7.0.0' - moneroJavaVersion = '0.8.33' + moneroJavaVersion = '0.8.34' httpclient5Version = '5.0' hamcrestVersion = '2.2' httpclientVersion = '4.5.12' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 5bc11c0134..de5d0b4403 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -878,9 +878,9 @@ - - - + + + From 1ac4c45f6d3e4e1d9977a232c27334b13847a701 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 8 Jan 2025 21:14:40 -0500 Subject: [PATCH 075/371] ignore task cancelled error in broadcast handler after shut down --- .../java/haveno/network/p2p/peers/BroadcastHandler.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java index fca906ff1c..9320f978cd 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java @@ -277,9 +277,6 @@ public class BroadcastHandler implements PeerManager.Listener { @Override public void onFailure(@NotNull Throwable throwable) { - log.warn("Broadcast to " + connection.getPeersNodeAddressOptional() + " failed. ", throwable); - numOfFailedBroadcasts.incrementAndGet(); - if (stopped.get()) { return; } @@ -356,7 +353,8 @@ public class BroadcastHandler implements PeerManager.Listener { try { future.cancel(true); } catch (Exception e) { - if (!networkNode.isShutDownStarted()) throw e; + if (networkNode.isShutDownStarted()) return; // ignore if shut down + throw e; } }); sendMessageFutures.clear(); From d9f9c1e7368101af1b982e6c7794643b552b5f57 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 8 Jan 2025 21:24:31 -0500 Subject: [PATCH 076/371] do not restore backup wallet cache if shutting down --- .../haveno/core/xmr/wallet/XmrWalletService.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 3bf8cfdeb1..4c45907720 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1343,6 +1343,7 @@ public class XmrWalletService extends XmrWalletBase { try { doMaybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); } catch (Exception e) { + if (isShutDownStarted) return; log.warn("Error initializing main wallet: {}\n", e.getMessage(), e); HavenoUtils.setTopError(e.getMessage()); throw e; @@ -1510,10 +1511,11 @@ public class XmrWalletService extends XmrWalletBase { // try opening wallet config.setNetworkType(getMoneroNetworkType()); config.setServer(connection); - log.info("Opening full wallet " + config.getPath() + " with monerod=" + connection.getUri() + ", proxyUri=" + connection.getProxyUri()); + log.info("Opening full wallet '{}' with monerod={}, proxyUri={}", config.getPath(), connection.getUri(), connection.getProxyUri()); try { walletFull = MoneroWalletFull.openWallet(config); } catch (Exception e) { + if (isShutDownStarted) throw e; log.warn("Failed to open full wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage()); boolean retrySuccessful = false; try { @@ -1551,7 +1553,7 @@ public class XmrWalletService extends XmrWalletBase { // retry opening wallet after cache deleted try { - log.warn("Failed to open full wallet using backup cache files, retrying with cache deleted"); + log.warn("Failed to open full wallet '{}' using backup cache files, retrying with cache deleted", config.getPath()); walletFull = MoneroWalletFull.openWallet(config); log.warn("Successfully opened full wallet after cache deleted"); retrySuccessful = true; @@ -1565,7 +1567,7 @@ public class XmrWalletService extends XmrWalletBase { } else { // restore original wallet cache - log.warn("Failed to open full wallet after deleting cache, restoring original cache"); + log.warn("Failed to open full wallet '{}' after deleting cache, restoring original cache", config.getPath()); File cacheFile = new File(cachePath); if (cacheFile.exists()) cacheFile.delete(); if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath)); @@ -1637,11 +1639,12 @@ public class XmrWalletService extends XmrWalletBase { if (!applyProxyUri) connection.setProxyUri(null); // try opening wallet - log.info("Opening RPC wallet " + config.getPath() + " with monerod=" + connection.getUri() + ", proxyUri=" + connection.getProxyUri()); + log.info("Opening RPC wallet '{}' with monerod={}, proxyUri={}", config.getPath(), connection.getUri(), connection.getProxyUri()); config.setServer(connection); try { walletRpc.openWallet(config); } catch (Exception e) { + if (isShutDownStarted) throw e; log.warn("Failed to open RPC wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage()); boolean retrySuccessful = false; try { @@ -1679,7 +1682,7 @@ public class XmrWalletService extends XmrWalletBase { // retry opening wallet after cache deleted try { - log.warn("Failed to open RPC wallet using backup cache files, retrying with cache deleted"); + log.warn("Failed to open RPC wallet '{}' using backup cache files, retrying with cache deleted", config.getPath()); walletRpc.openWallet(config); log.warn("Successfully opened RPC wallet after cache deleted"); retrySuccessful = true; @@ -1693,7 +1696,7 @@ public class XmrWalletService extends XmrWalletBase { } else { // restore original wallet cache - log.warn("Failed to open RPC wallet after deleting cache, restoring original cache"); + log.warn("Failed to open RPC wallet '{}' after deleting cache, restoring original cache", config.getPath()); File cacheFile = new File(cachePath); if (cacheFile.exists()) cacheFile.delete(); if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath)); From b0c1dceb56e73d4fcbee96ca69da480908c9b78c Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 9 Jan 2025 11:01:25 -0500 Subject: [PATCH 077/371] render offer view after main thread loaded --- .../main/offer/offerbook/OfferBookView.java | 396 +++++++++--------- 1 file changed, 189 insertions(+), 207 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index 80b3752722..b15f43db8a 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -424,7 +424,7 @@ abstract public class OfferBookView { }); - tableView.setItems(model.getOfferList()); + UserThread.execute(() -> tableView.setItems(model.getOfferList())); model.getOfferList().addListener(offerListListener); nrOfOffersLabel.setText(Res.get("offerbook.nrOffers", model.getOfferList().size())); @@ -788,13 +788,11 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { - UserThread.execute(() -> { - super.updateItem(item, empty); - if (item != null && !empty) - setGraphic(new ColoredDecimalPlacesWithZerosText(model.getAmount(item), GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); - else - setGraphic(null); - }); + super.updateItem(item, empty); + if (item != null && !empty) + setGraphic(new ColoredDecimalPlacesWithZerosText(model.getAmount(item), GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); + else + setGraphic(null); } }; } @@ -819,13 +817,11 @@ abstract public class OfferBookView { - super.updateItem(item, empty); - if (item != null && !empty) - setText(CurrencyUtil.getCurrencyPair(item.getOffer().getCurrencyCode())); - else - setText(""); - }); + super.updateItem(item, empty); + if (item != null && !empty) + setText(CurrencyUtil.getCurrencyPair(item.getOffer().getCurrencyCode())); + else + setText(""); } }; } @@ -855,15 +851,13 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { - UserThread.execute(() -> { - super.updateItem(item, empty); + super.updateItem(item, empty); - if (item != null && !empty) { - setGraphic(getPriceAndPercentage(item)); - } else { - setGraphic(null); - } - }); + if (item != null && !empty) { + setGraphic(getPriceAndPercentage(item)); + } else { + setGraphic(null); + } } private HBox getPriceAndPercentage(OfferBookListItem item) { @@ -939,23 +933,21 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { - UserThread.execute(() -> { - super.updateItem(item, empty); + super.updateItem(item, empty); - if (item != null && !empty) { - if (item.getOffer().getPrice() == null) { - setText(Res.get("shared.na")); - setGraphic(null); - } else { - setText(""); - setGraphic(new ColoredDecimalPlacesWithZerosText(model.getVolume(item), - model.getNumberOfDecimalsForVolume(item))); - } + if (item != null && !empty) { + if (item.getOffer().getPrice() == null) { + setText(Res.get("shared.na")); + setGraphic(null); } else { setText(""); - setGraphic(null); + setGraphic(new ColoredDecimalPlacesWithZerosText(model.getVolume(item), + model.getNumberOfDecimalsForVolume(item))); } - }); + } else { + setText(""); + setGraphic(null); + } } }; } @@ -981,32 +973,30 @@ abstract public class OfferBookView { - super.updateItem(item, empty); + super.updateItem(item, empty); - if (item != null && !empty) { + if (item != null && !empty) { - Offer offer = item.getOffer(); - if (model.isOfferBanned(offer)) { - setGraphic(new AutoTooltipLabel(model.getPaymentMethod(item))); - } else { - if (offer.isXmrAutoConf()) { - field = new HyperlinkWithIcon(model.getPaymentMethod(item), AwesomeIcon.ROCKET); - } else { - field = new HyperlinkWithIcon(model.getPaymentMethod(item)); - } - field.setOnAction(event -> { - offerDetailsWindow.show(offer); - }); - field.setTooltip(new Tooltip(model.getPaymentMethodToolTip(item))); - setGraphic(field); - } + Offer offer = item.getOffer(); + if (model.isOfferBanned(offer)) { + setGraphic(new AutoTooltipLabel(model.getPaymentMethod(item))); } else { - setGraphic(null); - if (field != null) - field.setOnAction(null); + if (offer.isXmrAutoConf()) { + field = new HyperlinkWithIcon(model.getPaymentMethod(item), AwesomeIcon.ROCKET); + } else { + field = new HyperlinkWithIcon(model.getPaymentMethod(item)); + } + field.setOnAction(event -> { + offerDetailsWindow.show(offer); + }); + field.setTooltip(new Tooltip(model.getPaymentMethodToolTip(item))); + setGraphic(field); } - }); + } else { + setGraphic(null); + if (field != null) + field.setOnAction(null); + } } }; } @@ -1035,28 +1025,26 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { - UserThread.execute(() -> { - super.updateItem(item, empty); + super.updateItem(item, empty); - if (item != null && !empty) { - var isSellOffer = item.getOffer().getDirection() == OfferDirection.SELL; - var deposit = isSellOffer ? item.getOffer().getMaxBuyerSecurityDeposit() : - item.getOffer().getMaxSellerSecurityDeposit(); - if (deposit == null) { - setText(Res.get("shared.na")); - setGraphic(null); - } else { - setText(""); - String rangePrefix = item.getOffer().isRange() ? "<= " : ""; - setGraphic(new ColoredDecimalPlacesWithZerosText(rangePrefix + model.formatDepositString( - deposit, item.getOffer().getAmount().longValueExact()), - GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); - } + if (item != null && !empty) { + var isSellOffer = item.getOffer().getDirection() == OfferDirection.SELL; + var deposit = isSellOffer ? item.getOffer().getMaxBuyerSecurityDeposit() : + item.getOffer().getMaxSellerSecurityDeposit(); + if (deposit == null) { + setText(Res.get("shared.na")); + setGraphic(null); } else { setText(""); - setGraphic(null); + String rangePrefix = item.getOffer().isRange() ? "<= " : ""; + setGraphic(new ColoredDecimalPlacesWithZerosText(rangePrefix + model.formatDepositString( + deposit, item.getOffer().getAmount().longValueExact()), + GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); } - }); + } else { + setText(""); + setGraphic(null); + } } }; } @@ -1083,114 +1071,112 @@ abstract public class OfferBookView { - super.updateItem(item, empty); + super.updateItem(item, empty); - final ImageView iconView = new ImageView(); - final AutoTooltipButton button = new AutoTooltipButton(); - - { - button.setGraphic(iconView); - button.setGraphicTextGap(10); - button.setPrefWidth(10000); - } - - final ImageView iconView2 = new ImageView(); - final AutoTooltipButton button2 = new AutoTooltipButton(); - - { - button2.setGraphic(iconView2); - button2.setGraphicTextGap(10); - button2.setPrefWidth(10000); - } - - final HBox hbox = new HBox(); - - { - hbox.setSpacing(8); - hbox.setAlignment(Pos.CENTER); - hbox.getChildren().add(button); - hbox.getChildren().add(button2); - HBox.setHgrow(button, Priority.ALWAYS); - HBox.setHgrow(button2, Priority.ALWAYS); - } + final ImageView iconView = new ImageView(); + final AutoTooltipButton button = new AutoTooltipButton(); - TableRow tableRow = getTableRow(); - if (item != null && !empty) { - Offer offer = item.getOffer(); - boolean myOffer = model.isMyOffer(offer); + { + button.setGraphic(iconView); + button.setGraphicTextGap(10); + button.setPrefWidth(10000); + } - // https://github.com/bisq-network/bisq/issues/4986 - if (tableRow != null) { - canTakeOfferResult = model.offerFilterService.canTakeOffer(offer, false); - tableRow.setOpacity(canTakeOfferResult.isValid() || myOffer ? 1 : 0.4); + final ImageView iconView2 = new ImageView(); + final AutoTooltipButton button2 = new AutoTooltipButton(); - if (myOffer) { - button.setDefaultButton(false); - tableRow.setOnMousePressed(null); - } else if (canTakeOfferResult.isValid()) { - // set first row button as default - button.setDefaultButton(getIndex() == 0); - tableRow.setOnMousePressed(null); - } else { - button.setDefaultButton(false); - tableRow.setOnMousePressed(e -> { - // ugly hack to get the icon clickable when deactivated - if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas)) - onShowInfo(offer, canTakeOfferResult); - }); - } - } + { + button2.setGraphic(iconView2); + button2.setGraphicTextGap(10); + button2.setPrefWidth(10000); + } - String title; + final HBox hbox = new HBox(); + + { + hbox.setSpacing(8); + hbox.setAlignment(Pos.CENTER); + hbox.getChildren().add(button); + hbox.getChildren().add(button2); + HBox.setHgrow(button, Priority.ALWAYS); + HBox.setHgrow(button2, Priority.ALWAYS); + } + + TableRow tableRow = getTableRow(); + if (item != null && !empty) { + Offer offer = item.getOffer(); + boolean myOffer = model.isMyOffer(offer); + + // https://github.com/bisq-network/bisq/issues/4986 + if (tableRow != null) { + canTakeOfferResult = model.offerFilterService.canTakeOffer(offer, false); + tableRow.setOpacity(canTakeOfferResult.isValid() || myOffer ? 1 : 0.4); + if (myOffer) { - iconView.setId("image-remove"); - title = Res.get("shared.remove"); - button.setOnAction(e -> onRemoveOpenOffer(offer)); - - iconView2.setId("image-edit"); - button2.updateText(Res.get("shared.edit")); - button2.setOnAction(e -> onEditOpenOffer(offer)); - button2.setManaged(true); - button2.setVisible(true); - } else { - boolean isSellOffer = OfferViewUtil.isShownAsSellOffer(offer); - boolean isPrivateOffer = offer.isPrivateOffer(); - iconView.setId(isPrivateOffer ? "image-lock2x" : isSellOffer ? "image-buy-white" : "image-sell-white"); - iconView.setFitHeight(16); - iconView.setFitWidth(16); - button.setId(isSellOffer ? "buy-button" : "sell-button"); - button.setStyle("-fx-text-fill: white"); - title = Res.get("offerbook.takeOffer"); - button.setTooltip(new Tooltip(Res.get("offerbook.takeOfferButton.tooltip", model.getDirectionLabelTooltip(offer)))); - button.setOnAction(e -> onTakeOffer(offer)); - button2.setManaged(false); - button2.setVisible(false); - } - - if (!myOffer) { - if (canTakeOfferResult == null) { - canTakeOfferResult = model.offerFilterService.canTakeOffer(offer, false); - } - - if (!canTakeOfferResult.isValid()) { - button.setOnAction(e -> onShowInfo(offer, canTakeOfferResult)); - } - } - - button.updateText(title); - setPadding(new Insets(0, 15, 0, 0)); - setGraphic(hbox); - } else { - setGraphic(null); - button.setOnAction(null); - button2.setOnAction(null); - if (tableRow != null) { - tableRow.setOpacity(1); + button.setDefaultButton(false); tableRow.setOnMousePressed(null); + } else if (canTakeOfferResult.isValid()) { + // set first row button as default + button.setDefaultButton(getIndex() == 0); + tableRow.setOnMousePressed(null); + } else { + button.setDefaultButton(false); + tableRow.setOnMousePressed(e -> { + // ugly hack to get the icon clickable when deactivated + if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas)) + onShowInfo(offer, canTakeOfferResult); + }); } } - }); + + String title; + if (myOffer) { + iconView.setId("image-remove"); + title = Res.get("shared.remove"); + button.setOnAction(e -> onRemoveOpenOffer(offer)); + + iconView2.setId("image-edit"); + button2.updateText(Res.get("shared.edit")); + button2.setOnAction(e -> onEditOpenOffer(offer)); + button2.setManaged(true); + button2.setVisible(true); + } else { + boolean isSellOffer = OfferViewUtil.isShownAsSellOffer(offer); + boolean isPrivateOffer = offer.isPrivateOffer(); + iconView.setId(isPrivateOffer ? "image-lock2x" : isSellOffer ? "image-buy-white" : "image-sell-white"); + iconView.setFitHeight(16); + iconView.setFitWidth(16); + button.setId(isSellOffer ? "buy-button" : "sell-button"); + button.setStyle("-fx-text-fill: white"); + title = Res.get("offerbook.takeOffer"); + button.setTooltip(new Tooltip(Res.get("offerbook.takeOfferButton.tooltip", model.getDirectionLabelTooltip(offer)))); + button.setOnAction(e -> onTakeOffer(offer)); + button2.setManaged(false); + button2.setVisible(false); + } + + if (!myOffer) { + if (canTakeOfferResult == null) { + canTakeOfferResult = model.offerFilterService.canTakeOffer(offer, false); + } + + if (!canTakeOfferResult.isValid()) { + button.setOnAction(e -> onShowInfo(offer, canTakeOfferResult)); + } + } + + button.updateText(title); + setPadding(new Insets(0, 15, 0, 0)); + setGraphic(hbox); + } else { + setGraphic(null); + button.setOnAction(null); + button2.setOnAction(null); + if (tableRow != null) { + tableRow.setOpacity(1); + tableRow.setOnMousePressed(null); + } + } } }; } @@ -1218,19 +1204,17 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { - UserThread.execute(() -> { - super.updateItem(item, empty); + super.updateItem(item, empty); - if (item != null && !empty) { - var witnessAgeData = item.getWitnessAgeData(accountAgeWitnessService, signedWitnessService); - var label = witnessAgeData.isSigningRequired() - ? new AccountStatusTooltipLabel(witnessAgeData) - : new InfoAutoTooltipLabel(witnessAgeData.getDisplayString(), witnessAgeData.getIcon(), ContentDisplay.RIGHT, witnessAgeData.getInfo()); - setGraphic(label); - } else { - setGraphic(null); - } - }); + if (item != null && !empty) { + var witnessAgeData = item.getWitnessAgeData(accountAgeWitnessService, signedWitnessService); + var label = witnessAgeData.isSigningRequired() + ? new AccountStatusTooltipLabel(witnessAgeData) + : new InfoAutoTooltipLabel(witnessAgeData.getDisplayString(), witnessAgeData.getIcon(), ContentDisplay.RIGHT, witnessAgeData.getInfo()); + setGraphic(label); + } else { + setGraphic(null); + } } }; } @@ -1256,26 +1240,24 @@ abstract public class OfferBookView() { @Override public void updateItem(final OfferBookListItem newItem, boolean empty) { - UserThread.execute(() -> { - super.updateItem(newItem, empty); - if (newItem != null && !empty) { - final Offer offer = newItem.getOffer(); - final NodeAddress makersNodeAddress = offer.getOwnerNodeAddress(); - String role = Res.get("peerInfoIcon.tooltip.maker"); - int numTrades = model.getNumTrades(offer); - PeerInfoIconTrading peerInfoIcon = new PeerInfoIconTrading(makersNodeAddress, - role, - numTrades, - privateNotificationManager, - offer, - model.preferences, - model.accountAgeWitnessService, - useDevPrivilegeKeys); - setGraphic(peerInfoIcon); - } else { - setGraphic(null); - } - }); + super.updateItem(newItem, empty); + if (newItem != null && !empty) { + final Offer offer = newItem.getOffer(); + final NodeAddress makersNodeAddress = offer.getOwnerNodeAddress(); + String role = Res.get("peerInfoIcon.tooltip.maker"); + int numTrades = model.getNumTrades(offer); + PeerInfoIconTrading peerInfoIcon = new PeerInfoIconTrading(makersNodeAddress, + role, + numTrades, + privateNotificationManager, + offer, + model.preferences, + model.accountAgeWitnessService, + useDevPrivilegeKeys); + setGraphic(peerInfoIcon); + } else { + setGraphic(null); + } } }; } From 533527e362ebaa88bb4f873ec58009688360e580 Mon Sep 17 00:00:00 2001 From: boldsuck Date: Sat, 11 Jan 2025 19:27:53 +0100 Subject: [PATCH 078/371] Update Tor browser version 14.0.3 and tor binary version 0.4.8.13 (#1534) --- build.gradle | 2 +- gradle/verification-metadata.xml | 111 +++++++------------------------ 2 files changed, 25 insertions(+), 88 deletions(-) diff --git a/build.gradle b/build.gradle index f4751dbdbd..9a74612ea6 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ configure(subprojects) { loggingVersion = '1.2' lombokVersion = '1.18.30' mockitoVersion = '5.10.0' - netlayerVersion = 'e2ce2a142c' // Tor browser version 13.0.15 and tor binary version: 0.4.8.11 + netlayerVersion = '700ec94f0f' // Tor browser version 14.0.3 and tor binary version: 0.4.8.13 protobufVersion = '3.19.1' protocVersion = protobufVersion pushyVersion = '0.13.2' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index de5d0b4403..810a3799b8 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -205,107 +205,44 @@ - - - + + + - - - + + + - - - + + + - - - - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + From 2f322674f8e878a3f2cd89fb411de012d73cdb68 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 13 Jan 2025 09:51:31 -0500 Subject: [PATCH 079/371] fix showing extra info in offer details --- core/src/main/java/haveno/core/offer/Offer.java | 2 ++ core/src/main/java/haveno/core/offer/OfferPayload.java | 1 + core/src/main/java/haveno/core/offer/OfferUtil.java | 6 ++++++ .../desktop/main/overlays/windows/OfferDetailsWindow.java | 5 +++-- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index 3dc566b308..e84e4fa329 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -432,6 +432,8 @@ public class Offer implements NetworkPayload, PersistablePayload { return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.CASHAPP_EXTRA_INFO); + else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASH_AT_ATM_EXTRA_INFO)) + return getExtraDataMap().get(OfferPayload.CASH_AT_ATM_EXTRA_INFO); else return ""; } diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index 0dadf882ba..bd10719ec3 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -102,6 +102,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo"; public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo"; public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo"; + public static final String CASH_AT_ATM_EXTRA_INFO = "cashAtAtmExtraInfo"; // Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of // Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java index 3d4c1c376e..2e2644630a 100644 --- a/core/src/main/java/haveno/core/offer/OfferUtil.java +++ b/core/src/main/java/haveno/core/offer/OfferUtil.java @@ -37,6 +37,7 @@ import haveno.core.monetary.Volume; import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH; import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO; import static haveno.core.offer.OfferPayload.CAPABILITIES; +import static haveno.core.offer.OfferPayload.CASH_AT_ATM_EXTRA_INFO; import static haveno.core.offer.OfferPayload.CASHAPP_EXTRA_INFO; import static haveno.core.offer.OfferPayload.F2F_CITY; import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO; @@ -48,6 +49,7 @@ import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE; import haveno.core.payment.AustraliaPayidAccount; import haveno.core.payment.CashAppAccount; +import haveno.core.payment.CashAtAtmAccount; import haveno.core.payment.F2FAccount; import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayPalAccount; @@ -217,6 +219,10 @@ public class OfferUtil { extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo()); } + if (paymentAccount instanceof CashAtAtmAccount) { + extraDataMap.put(CASH_AT_ATM_EXTRA_INFO, ((CashAtAtmAccount) paymentAccount).getExtraInfo()); + } + extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList()); if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) { diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index adf841a147..7732b7b70d 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -180,8 +180,9 @@ public class OfferDetailsWindow extends Overlay { boolean showExtraInfo = offer.getPaymentMethod().equals(PaymentMethod.F2F) || offer.getPaymentMethod().equals(PaymentMethod.PAY_BY_MAIL) || offer.getPaymentMethod().equals(PaymentMethod.AUSTRALIA_PAYID)|| - offer.getPaymentMethod().equals(PaymentMethod.PAYPAL_ID)|| - offer.getPaymentMethod().equals(PaymentMethod.CASH_APP_ID); + offer.getPaymentMethod().equals(PaymentMethod.PAYPAL)|| + offer.getPaymentMethod().equals(PaymentMethod.CASH_APP) || + offer.getPaymentMethod().equals(PaymentMethod.CASH_AT_ATM); if (!takeOfferHandlerOptional.isPresent()) rows++; if (showAcceptedBanks) From 6301bde10eaee0f1920637ff7d1f6e8c200d9e30 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 14 Jan 2025 07:33:48 -0500 Subject: [PATCH 080/371] replace Thread.dumpStack() to write stack traces to log files --- core/src/main/java/haveno/core/api/XmrConnectionService.java | 3 +-- .../protocol/tasks/ArbitratorProcessDepositRequest.java | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 664daaca8a..2ffcc95d65 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -654,8 +654,7 @@ public final class XmrConnectionService { private void onConnectionChanged(MoneroRpcConnection currentConnection) { if (isShutDownStarted || !accountService.isAccountOpen()) return; if (currentConnection == null) { - log.warn("Setting daemon connection to null"); - Thread.dumpStack(); + log.warn("Setting daemon connection to null", new Throwable("Stack trace")); } synchronized (lock) { if (currentConnection == null) { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java index 5bd7ab9d80..be9d528f35 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java @@ -211,10 +211,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask { // log error if (errorMessage != null) { - log.warn("Sending deposit responses with error={}", errorMessage); - Thread.dumpStack(); + log.warn("Sending deposit responses with error={}", errorMessage, new Throwable("Stack trace")); } - + // create deposit response DepositResponse response = new DepositResponse( trade.getOffer().getId(), From 0f5f7ae46e96ca6778960148aa76670163cc02a1 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 12 Jan 2025 11:58:09 -0500 Subject: [PATCH 081/371] add startup flag 'updateXmrBinaries=true|false' --- common/src/main/java/haveno/common/config/Config.java | 10 ++++++++++ core/src/main/java/haveno/core/app/HavenoSetup.java | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/haveno/common/config/Config.java b/common/src/main/java/haveno/common/config/Config.java index 92359c76f9..897a795a2b 100644 --- a/common/src/main/java/haveno/common/config/Config.java +++ b/common/src/main/java/haveno/common/config/Config.java @@ -117,6 +117,7 @@ public class Config { public static final String BTC_FEE_INFO = "bitcoinFeeInfo"; public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation"; public static final String PASSWORD_REQUIRED = "passwordRequired"; + public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries"; // Default values for certain options public static final int UNSPECIFIED_PORT = -1; @@ -204,6 +205,7 @@ public class Config { public final boolean republishMailboxEntries; public final boolean bypassMempoolValidation; public final boolean passwordRequired; + public final boolean updateXmrBinaries; // Properties derived from options but not exposed as options themselves public final File torDir; @@ -621,6 +623,13 @@ public class Config { .ofType(boolean.class) .defaultsTo(false); + ArgumentAcceptingOptionSpec updateXmrBinariesOpt = + parser.accepts(UPDATE_XMR_BINARIES, + "Update Monero binaries if applicable") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(true); + try { CompositeOptionSet options = new CompositeOptionSet(); @@ -733,6 +742,7 @@ public class Config { this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt); this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt); this.passwordRequired = options.valueOf(passwordRequiredOpt); + this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt); } catch (OptionException ex) { throw new ConfigException("problem parsing option '%s': %s", ex.options().get(0), diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index a291da5001..eee13f3eec 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -369,7 +369,7 @@ public class HavenoSetup { // install monerod File monerodFile = new File(XmrLocalNode.MONEROD_PATH); String monerodResourcePath = "bin/" + XmrLocalNode.MONEROD_NAME; - if (!monerodFile.exists() || !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile)) { + if (!monerodFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile))) { log.info("Installing monerod"); monerodFile.getParentFile().mkdirs(); FileUtil.resourceToFile("bin/" + XmrLocalNode.MONEROD_NAME, monerodFile); @@ -379,7 +379,7 @@ public class HavenoSetup { // install monero-wallet-rpc File moneroWalletRpcFile = new File(XmrWalletService.MONERO_WALLET_RPC_PATH); String moneroWalletRpcResourcePath = "bin/" + XmrWalletService.MONERO_WALLET_RPC_NAME; - if (!moneroWalletRpcFile.exists() || !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile)) { + if (!moneroWalletRpcFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile))) { log.info("Installing monero-wallet-rpc"); moneroWalletRpcFile.getParentFile().mkdirs(); FileUtil.resourceToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile); From 5e6bf9e22b6bb21fef17da48c32483af8ed1129e Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 14 Jan 2025 09:10:12 -0500 Subject: [PATCH 082/371] fix fallback prompt with null daemon connection --- core/src/main/java/haveno/core/api/XmrConnectionService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 2ffcc95d65..dfac9bc85a 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -725,8 +725,8 @@ public final class XmrConnectionService { // poll daemon if (daemon == null) switchToBestConnection(); - if (daemon == null) throw new RuntimeException("No connection to Monero daemon"); try { + if (daemon == null) throw new RuntimeException("No connection to Monero daemon"); lastInfo = daemon.getInfo(); } catch (Exception e) { @@ -753,6 +753,7 @@ public final class XmrConnectionService { // switch to best connection switchToBestConnection(); + if (daemon == null) throw new RuntimeException("No connection to Monero daemon after error handling"); lastInfo = daemon.getInfo(); // caught internally if still fails } From 7fba0faac1fd4341e487e4c47077bf97f367bbd5 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 14 Jan 2025 11:02:03 -0500 Subject: [PATCH 083/371] best connection defaults to singular instance --- core/src/main/java/haveno/core/api/CoreApi.java | 4 ++-- .../haveno/core/api/XmrConnectionService.java | 17 ++++++++--------- .../daemon/grpc/GrpcXmrConnectionService.java | 16 ++++++++-------- proto/src/main/proto/grpc.proto | 6 +++--- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index c91d4a405b..5b0c9e247a 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -239,8 +239,8 @@ public class CoreApi { xmrConnectionService.stopCheckingConnection(); } - public MoneroRpcConnection getBestAvailableXmrConnection() { - return xmrConnectionService.getBestAvailableConnection(); + public MoneroRpcConnection getBestXmrConnection() { + return xmrConnectionService.getBestConnection(); } public void setXmrConnectionAutoSwitch(boolean autoSwitch) { diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index dfac9bc85a..640aa2b404 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -255,17 +255,16 @@ public final class XmrConnectionService { updatePolling(); } - public MoneroRpcConnection getBestAvailableConnection() { + public MoneroRpcConnection getBestConnection() { accountService.checkAccountOpen(); - List ignoredConnections = new ArrayList(); - addLocalNodeIfIgnored(ignoredConnections); - return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0])); + return getBestConnection(new ArrayList()); } - private MoneroRpcConnection getBestAvailableConnection(Collection ignoredConnections) { + private MoneroRpcConnection getBestConnection(Collection ignoredConnections) { accountService.checkAccountOpen(); Set ignoredConnectionsSet = new HashSet<>(ignoredConnections); addLocalNodeIfIgnored(ignoredConnectionsSet); + if (connectionManager.getConnections().size() == 1 && !ignoredConnectionsSet.contains(connectionManager.getConnections().get(0))) return connectionManager.getConnections().get(0); return connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); } @@ -278,7 +277,7 @@ public final class XmrConnectionService { log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled"); return; } - MoneroRpcConnection bestConnection = getBestAvailableConnection(); + MoneroRpcConnection bestConnection = getBestConnection(); if (bestConnection != null) setConnection(bestConnection); } @@ -329,7 +328,7 @@ public final class XmrConnectionService { if (currentConnection != null) excludedConnections.add(currentConnection); // get connection to switch to - MoneroRpcConnection bestConnection = getBestAvailableConnection(excludedConnections); + MoneroRpcConnection bestConnection = getBestConnection(excludedConnections); // remove from excluded connections after period UserThread.runAfter(() -> { @@ -545,7 +544,7 @@ public final class XmrConnectionService { if (isConnected) { setConnection(connection.getUri()); } else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) { - MoneroRpcConnection bestConnection = getBestAvailableConnection(); + MoneroRpcConnection bestConnection = getBestConnection(); if (bestConnection != null) setConnection(bestConnection); // switch to best connection } } @@ -610,7 +609,7 @@ public final class XmrConnectionService { // update connection if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) { - MoneroRpcConnection bestConnection = getBestAvailableConnection(); + MoneroRpcConnection bestConnection = getBestConnection(); if (bestConnection != null) setConnection(bestConnection); } } else if (!isInitialized) { diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java index a03dc5a73e..f0fcdcc984 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java @@ -47,8 +47,8 @@ import haveno.proto.grpc.CheckConnectionsReply; import haveno.proto.grpc.CheckConnectionsRequest; import haveno.proto.grpc.GetAutoSwitchReply; import haveno.proto.grpc.GetAutoSwitchRequest; -import haveno.proto.grpc.GetBestAvailableConnectionReply; -import haveno.proto.grpc.GetBestAvailableConnectionRequest; +import haveno.proto.grpc.GetBestConnectionReply; +import haveno.proto.grpc.GetBestConnectionRequest; import haveno.proto.grpc.GetConnectionReply; import haveno.proto.grpc.GetConnectionRequest; import haveno.proto.grpc.GetConnectionsReply; @@ -68,7 +68,7 @@ import static haveno.proto.grpc.XmrConnectionsGrpc.XmrConnectionsImplBase; import static haveno.proto.grpc.XmrConnectionsGrpc.getAddConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getCheckConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getCheckConnectionsMethod; -import static haveno.proto.grpc.XmrConnectionsGrpc.getGetBestAvailableConnectionMethod; +import static haveno.proto.grpc.XmrConnectionsGrpc.getGetBestConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getGetConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getGetConnectionsMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getRemoveConnectionMethod; @@ -201,12 +201,12 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { } @Override - public void getBestAvailableConnection(GetBestAvailableConnectionRequest request, - StreamObserver responseObserver) { + public void getBestConnection(GetBestConnectionRequest request, + StreamObserver responseObserver) { handleRequest(responseObserver, () -> { - MoneroRpcConnection connection = coreApi.getBestAvailableXmrConnection(); + MoneroRpcConnection connection = coreApi.getBestXmrConnection(); UrlConnection replyConnection = toUrlConnection(connection); - GetBestAvailableConnectionReply.Builder builder = GetBestAvailableConnectionReply.newBuilder(); + GetBestConnectionReply.Builder builder = GetBestConnectionReply.newBuilder(); if (replyConnection != null) { builder.setConnection(replyConnection); } @@ -314,7 +314,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { put(getCheckConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getStartCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getStopCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); - put(getGetBestAvailableConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); + put(getGetBestConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getSetAutoSwitchMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); }} ))); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index c9b8a75445..f99a0feae5 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -319,7 +319,7 @@ service XmrConnections { } rpc StopCheckingConnection(StopCheckingConnectionRequest) returns (StopCheckingConnectionReply) { } - rpc GetBestAvailableConnection(GetBestAvailableConnectionRequest) returns (GetBestAvailableConnectionReply) { + rpc GetBestConnection(GetBestConnectionRequest) returns (GetBestConnectionReply) { } rpc SetAutoSwitch(SetAutoSwitchRequest) returns (SetAutoSwitchReply) { } @@ -400,9 +400,9 @@ message StopCheckingConnectionRequest {} message StopCheckingConnectionReply {} -message GetBestAvailableConnectionRequest {} +message GetBestConnectionRequest {} -message GetBestAvailableConnectionReply { +message GetBestConnectionReply { UrlConnection connection = 1; } From e1b3cdce2851a3c6547bbbd1f1e139ec5bc04ed9 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 14 Jan 2025 11:50:50 -0500 Subject: [PATCH 084/371] move version to last on password and startup screen --- desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java | 2 +- desktop/src/main/java/haveno/desktop/main/MainView.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java index 99cd7a17d6..ed7e956eba 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java @@ -216,7 +216,7 @@ public class HavenoAppMain extends HavenoExecutable { // Set the dialog content VBox vbox = new VBox(10); - vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), versionField, passwordField, errorMessageField); + vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), passwordField, errorMessageField, versionField); vbox.setAlignment(Pos.TOP_CENTER); getDialogPane().setContent(vbox); diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index 7d170c873b..f294eea7bf 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -511,8 +511,6 @@ public class MainView extends InitializableView { ImageView logo = new ImageView(); logo.setId(Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_MAINNET ? "image-splash-logo" : "image-splash-testnet-logo"); - Label versionLabel = new Label("v" + Version.VERSION); - // createBitcoinInfoBox xmrSplashInfo = new AutoTooltipLabel(); xmrSplashInfo.textProperty().bind(model.getXmrInfo()); @@ -624,7 +622,9 @@ public class MainView extends InitializableView { splashP2PNetworkBox.setPrefHeight(40); splashP2PNetworkBox.getChildren().addAll(splashP2PNetworkLabel, splashP2PNetworkBusyAnimation, splashP2PNetworkIcon, showTorNetworkSettingsButton); - vBox.getChildren().addAll(logo, versionLabel, blockchainSyncBox, xmrSyncIndicator, splashP2PNetworkBox); + Label versionLabel = new Label("v" + Version.VERSION); + + vBox.getChildren().addAll(logo, blockchainSyncBox, xmrSyncIndicator, splashP2PNetworkBox, versionLabel); return vBox; } From 69da8583652c4eee454d077ee228a66559898176 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 14 Jan 2025 14:24:17 -0500 Subject: [PATCH 085/371] check for best connection before returning singular connection --- .../main/java/haveno/core/api/XmrConnectionService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 640aa2b404..88d0117f8e 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -256,7 +256,6 @@ public final class XmrConnectionService { } public MoneroRpcConnection getBestConnection() { - accountService.checkAccountOpen(); return getBestConnection(new ArrayList()); } @@ -264,8 +263,9 @@ public final class XmrConnectionService { accountService.checkAccountOpen(); Set ignoredConnectionsSet = new HashSet<>(ignoredConnections); addLocalNodeIfIgnored(ignoredConnectionsSet); - if (connectionManager.getConnections().size() == 1 && !ignoredConnectionsSet.contains(connectionManager.getConnections().get(0))) return connectionManager.getConnections().get(0); - return connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); + MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections + if (bestConnection == null && connectionManager.getConnections().size() == 1 && !ignoredConnectionsSet.contains(connectionManager.getConnections().get(0))) bestConnection = connectionManager.getConnections().get(0); + return bestConnection; } private void addLocalNodeIfIgnored(Collection ignoredConnections) { @@ -336,7 +336,7 @@ public final class XmrConnectionService { }, EXCLUDE_CONNECTION_SECONDS); // return if no connection to switch to - if (bestConnection == null) { + if (bestConnection == null || !Boolean.TRUE.equals(bestConnection.isConnected())) { log.warn("No connection to switch to"); return false; } From 97475d84e9cb15f6810da41a143471845ad298c1 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 15 Jan 2025 09:20:52 -0500 Subject: [PATCH 086/371] use ubuntu 22.04 for all github actions --- .github/workflows/codacy-code-reporter.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/label.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codacy-code-reporter.yml b/.github/workflows/codacy-code-reporter.yml index 1bf5b3cec5..be76ef35ef 100644 --- a/.github/workflows/codacy-code-reporter.yml +++ b/.github/workflows/codacy-code-reporter.yml @@ -9,7 +9,7 @@ jobs: build: if: github.repository == 'haveno-dex/haveno' name: Publish coverage - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8b8699ad69..e6498b3e16 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,7 +18,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 permissions: actions: read contents: read diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index d29b0e28eb..50ece9050c 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -7,7 +7,7 @@ on: jobs: issueLabeled: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Bounty explanation uses: peter-evans/create-or-update-comment@v3 From 88b6bed93e16aed7663ddab960178f6045e4a8fa Mon Sep 17 00:00:00 2001 From: boldsuck Date: Wed, 15 Jan 2025 21:28:59 +0100 Subject: [PATCH 087/371] Upgrade GH workflows to remove deprecation notices (#1545) --- .github/workflows/build.yml | 21 +++++++++++---------- .github/workflows/codeql-analysis.yml | 4 ++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d66321d71..8fc6b6a481 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,22 +26,23 @@ jobs: cache: gradle - name: Build with Gradle run: ./gradlew build --stacktrace --scan - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: error-reports-${{ matrix.os }} path: ${{ github.workspace }}/desktop/build/reports - name: cache nodes dependencies - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: include-hidden-files: true name: cached-localnet path: .localnet + overwrite: true - name: Install dependencies if: ${{ matrix.os == 'ubuntu-22.04' }} run: | - sudo apt update - sudo apt install -y rpm libfuse2 flatpak flatpak-builder appstream + sudo apt-get update + sudo apt-get install -y rpm libfuse2 flatpak flatpak-builder appstream flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo - name: Install WiX Toolset if: ${{ matrix.os == 'windows-latest' }} @@ -99,41 +100,41 @@ jobs: shell: powershell # win - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: "Windows artifacts" if: ${{ matrix.os == 'windows-latest'}} with: name: haveno-windows path: ${{ github.workspace }}/release-windows # macos - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: "macOS artifacts" if: ${{ matrix.os == 'macos-13' }} with: name: haveno-macos path: ${{ github.workspace }}/release-macos # linux - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: "Linux - deb artifact" if: ${{ matrix.os == 'ubuntu-22.04' }} with: name: haveno-linux-deb path: ${{ github.workspace }}/release-linux-deb - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: "Linux - rpm artifact" if: ${{ matrix.os == 'ubuntu-22.04' }} with: name: haveno-linux-rpm path: ${{ github.workspace }}/release-linux-rpm - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: "Linux - AppImage artifact" if: ${{ matrix.os == 'ubuntu-22.04' }} with: name: haveno-linux-appimage path: ${{ github.workspace }}/release-linux-appimage - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: "Linux - flatpak artifact" if: ${{ matrix.os == 'ubuntu-22.04' }} with: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e6498b3e16..7e0fefe9e7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,7 +44,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -68,4 +68,4 @@ jobs: run: ./gradlew build --stacktrace -x test -x checkstyleMain -x checkstyleTest - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From b571b39790a28d246b476b8d82678b3f3912e99b Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 15 Jan 2025 09:44:24 -0500 Subject: [PATCH 088/371] support --xmrBlockchainPath startup flag for local Monero node --- .../java/haveno/common/config/Config.java | 20 ++++++++++++++----- .../java/haveno/core/api/XmrLocalNode.java | 15 ++++++++++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/common/src/main/java/haveno/common/config/Config.java b/common/src/main/java/haveno/common/config/Config.java index 897a795a2b..b162e211b4 100644 --- a/common/src/main/java/haveno/common/config/Config.java +++ b/common/src/main/java/haveno/common/config/Config.java @@ -118,6 +118,7 @@ public class Config { public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation"; public static final String PASSWORD_REQUIRED = "passwordRequired"; public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries"; + public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath"; // Default values for certain options public static final int UNSPECIFIED_PORT = -1; @@ -206,6 +207,7 @@ public class Config { public final boolean bypassMempoolValidation; public final boolean passwordRequired; public final boolean updateXmrBinaries; + public final String xmrBlockchainPath; // Properties derived from options but not exposed as options themselves public final File torDir; @@ -630,6 +632,13 @@ public class Config { .ofType(boolean.class) .defaultsTo(true); + ArgumentAcceptingOptionSpec xmrBlockchainPathOpt = + parser.accepts(XMR_BLOCKCHAIN_PATH, + "Path to Monero blockchain when using local Monero node") + .withRequiredArg() + .ofType(String.class) + .defaultsTo(""); + try { CompositeOptionSet options = new CompositeOptionSet(); @@ -743,6 +752,7 @@ public class Config { this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt); this.passwordRequired = options.valueOf(passwordRequiredOpt); this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt); + this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt); } catch (OptionException ex) { throw new ConfigException("problem parsing option '%s': %s", ex.options().get(0), @@ -752,11 +762,11 @@ public class Config { } // Create all appDataDir subdirectories and assign to their respective properties - File btcNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase()); - this.keyStorageDir = mkdir(btcNetworkDir, "keys"); - this.storageDir = mkdir(btcNetworkDir, "db"); - this.torDir = mkdir(btcNetworkDir, "tor"); - this.walletDir = mkdir(btcNetworkDir, "wallet"); + File xmrNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase()); + this.keyStorageDir = mkdir(xmrNetworkDir, "keys"); + this.storageDir = mkdir(xmrNetworkDir, "db"); + this.torDir = mkdir(xmrNetworkDir, "tor"); + this.walletDir = mkdir(xmrNetworkDir, "wallet"); // Assign values to special-case static fields APP_DATA_DIR_VALUE = appDataDir; diff --git a/core/src/main/java/haveno/core/api/XmrLocalNode.java b/core/src/main/java/haveno/core/api/XmrLocalNode.java index cd5ed266f1..5a424dad38 100644 --- a/core/src/main/java/haveno/core/api/XmrLocalNode.java +++ b/core/src/main/java/haveno/core/api/XmrLocalNode.java @@ -166,11 +166,18 @@ public class XmrLocalNode { var args = new ArrayList<>(MONEROD_ARGS); - var dataDir = settings.getBlockchainPath(); - if (dataDir == null || dataDir.isEmpty()) { - dataDir = MONEROD_DATADIR; + var dataDir = ""; + if (config.xmrBlockchainPath == null || config.xmrBlockchainPath.isEmpty()) { + dataDir = settings.getBlockchainPath(); + if (dataDir == null || dataDir.isEmpty()) { + dataDir = MONEROD_DATADIR; + } + } else { + dataDir = config.xmrBlockchainPath; // startup config overrides settings + } + if (dataDir != null && !dataDir.isEmpty()) { + args.add("--data-dir=" + dataDir); } - if (dataDir != null) args.add("--data-dir=" + dataDir); var bootstrapUrl = settings.getBootstrapUrl(); if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) { From 130a45c99a1ae812d3c6d6d8603005a79d2cfc19 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 17 Jan 2025 07:28:27 -0500 Subject: [PATCH 089/371] serialize payment account form lists to comma delimited string --- .../src/main/java/haveno/core/payment/PaymentAccount.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/payment/PaymentAccount.java b/core/src/main/java/haveno/core/payment/PaymentAccount.java index a9e03d8016..8dda8fdedd 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccount.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccount.java @@ -378,6 +378,7 @@ public abstract class PaymentAccount implements PersistablePayload { @NonNull public abstract List getInputFieldIds(); + @SuppressWarnings("unchecked") public PaymentAccountForm toForm() { // convert to json map @@ -387,7 +388,12 @@ public abstract class PaymentAccount implements PersistablePayload { PaymentAccountForm form = new PaymentAccountForm(PaymentAccountForm.FormId.valueOf(paymentMethod.getId())); for (PaymentAccountFormField.FieldId fieldId : getInputFieldIds()) { PaymentAccountFormField field = getEmptyFormField(fieldId); - field.setValue((String) jsonMap.get(HavenoUtils.toCamelCase(field.getId().toString()))); + Object value = jsonMap.get(HavenoUtils.toCamelCase(field.getId().toString())); + if (value instanceof List) { // TODO: list should already be serialized to comma delimited string in PaymentAccount.toJson() (PaymentAccountTypeAdapter?) + field.setValue(String.join(",", (List) value)); + } else { + field.setValue((String) value); + } form.getFields().add(field); } return form; From fac901331faa8ee6acb5525d02af78de5534445f Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 17 Jan 2025 10:24:47 -0500 Subject: [PATCH 090/371] always round offer amounts to 4 decimal places --- .../haveno/core/offer/CreateOfferService.java | 8 +++----- .../java/haveno/core/util/coin/CoinUtil.java | 16 ++++++++-------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index e135bfa123..35ece3b10e 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -151,11 +151,9 @@ public class CreateOfferService { throw new IllegalArgumentException("Must provide fixed price"); } - // adjust amount and min amount for fixed-price offer - if (fixedPrice != null) { - amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); - minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); - } + // adjust amount and min amount + amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); + minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); // generate one-time challenge for private offer String challenge = null; diff --git a/core/src/main/java/haveno/core/util/coin/CoinUtil.java b/core/src/main/java/haveno/core/util/coin/CoinUtil.java index bf194b3ea0..4085a63b6a 100644 --- a/core/src/main/java/haveno/core/util/coin/CoinUtil.java +++ b/core/src/main/java/haveno/core/util/coin/CoinUtil.java @@ -76,14 +76,14 @@ public class CoinUtil { } public static BigInteger getRoundedAmount(BigInteger amount, Price price, Long maxTradeLimit, String currencyCode, String paymentMethodId) { - if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) { - return getRoundedAtmCashAmount(amount, price, maxTradeLimit); - } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) { - return getRoundedAmountUnit(amount, price, maxTradeLimit); - } else if (CurrencyUtil.isFiatCurrency(currencyCode)) { - return getRoundedAmount4Decimals(amount, price, maxTradeLimit); + if (price != null) { + if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) { + return getRoundedAtmCashAmount(amount, price, maxTradeLimit); + } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) { + return getRoundedAmountUnit(amount, price, maxTradeLimit); + } } - return amount; + return getRoundedAmount4Decimals(amount, maxTradeLimit); } public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, Long maxTradeLimit) { @@ -103,7 +103,7 @@ public class CoinUtil { return getAdjustedAmount(amount, price, maxTradeLimit, 1); } - public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Price price, Long maxTradeLimit) { + public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Long maxTradeLimit) { DecimalFormat decimalFormat = new DecimalFormat("#.####"); double roundedXmrAmount = Double.parseDouble(decimalFormat.format(HavenoUtils.atomicUnitsToXmr(amount))); return HavenoUtils.xmrToAtomicUnits(roundedXmrAmount); From bf8f4cea7301fffb4c0df6d6ea9effbf1c8f57ad Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 17 Jan 2025 16:07:41 -0500 Subject: [PATCH 091/371] update to monero-java v0.8.35 --- build.gradle | 2 +- gradle/verification-metadata.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 9a74612ea6..72f98398b1 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ configure(subprojects) { gsonVersion = '2.8.5' guavaVersion = '32.1.1-jre' guiceVersion = '7.0.0' - moneroJavaVersion = '0.8.34' + moneroJavaVersion = '0.8.35' httpclient5Version = '5.0' hamcrestVersion = '2.2' httpclientVersion = '4.5.12' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 810a3799b8..97ff8bba43 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -815,9 +815,9 @@ - - - + + + From 2d4455b1a29ee02229f054973b4449a97377ed58 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 17 Jan 2025 16:09:12 -0500 Subject: [PATCH 092/371] update atomic unit conversion utils to use monero-java --- .../java/haveno/core/trade/HavenoUtils.java | 9 ++++---- .../core/xmr/wallet/XmrWalletService.java | 21 +++++++++++++++++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index fcd5c556e1..d238d78843 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -71,6 +71,7 @@ import javax.sound.sampled.SourceDataLine; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; +import monero.common.MoneroUtils; import monero.daemon.model.MoneroOutput; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroTxWallet; @@ -204,11 +205,11 @@ public class HavenoUtils { } public static double atomicUnitsToXmr(BigInteger atomicUnits) { - return new BigDecimal(atomicUnits).divide(new BigDecimal(XMR_AU_MULTIPLIER)).doubleValue(); + return MoneroUtils.atomicUnitsToXmr(atomicUnits); } public static BigInteger xmrToAtomicUnits(double xmr) { - return new BigDecimal(xmr).multiply(new BigDecimal(XMR_AU_MULTIPLIER)).toBigInteger(); + return MoneroUtils.xmrToAtomicUnits(xmr); } public static long xmrToCentineros(double xmr) { @@ -220,11 +221,11 @@ public class HavenoUtils { } public static double divide(BigInteger auDividend, BigInteger auDivisor) { - return atomicUnitsToXmr(auDividend) / atomicUnitsToXmr(auDivisor); + return MoneroUtils.divide(auDividend, auDivisor); } public static BigInteger multiply(BigInteger amount1, double amount2) { - return amount1 == null ? null : new BigDecimal(amount1).multiply(BigDecimal.valueOf(amount2)).toBigInteger(); + return MoneroUtils.multiply(amount1, amount2); } // ------------------------- FORMAT UTILS --------------------------------- diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 4c45907720..8432da2aed 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -781,11 +781,23 @@ public class XmrWalletService extends XmrWalletBase { BigInteger actualSendAmount = transferCheck.getReceivedAmount(); // verify trade fee amount - if (!actualTradeFee.equals(tradeFeeAmount)) throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee); + if (!actualTradeFee.equals(tradeFeeAmount)) { + if (equalsWithinFractionError(actualTradeFee, tradeFeeAmount)) { + log.warn("Trade tx fee amount is within fraction error, expected " + tradeFeeAmount + " but was " + actualTradeFee); + } else { + throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee); + } + } // verify send amount BigInteger expectedSendAmount = sendAmount.subtract(tx.getFee()); - if (!actualSendAmount.equals(expectedSendAmount)) throw new RuntimeException("Invalid send amount, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee()); + if (!actualSendAmount.equals(expectedSendAmount)) { + if (equalsWithinFractionError(actualSendAmount, expectedSendAmount)) { + log.warn("Trade tx send amount is within fraction error, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee()); + } else { + throw new RuntimeException("Invalid send amount, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee()); + } + } return tx; } catch (Exception e) { log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=\n" + tx) + ": " + e.getMessage()); @@ -801,6 +813,11 @@ public class XmrWalletService extends XmrWalletBase { } } + // TODO: old bug in atomic unit conversion could cause fractional difference error, remove this in future release, maybe re-sign all offers then + private static boolean equalsWithinFractionError(BigInteger a, BigInteger b) { + return a.subtract(b).abs().compareTo(new BigInteger("1")) <= 0; + } + /** * Get the tx fee estimate based on its weight. * From a8fb63859411b1eb0f409a91cc0f57ec76701079 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 18 Jan 2025 07:46:00 -0500 Subject: [PATCH 093/371] align extra info textarea in column for cash at atm buyer step --- .../haveno/desktop/components/paymentmethods/CashAtAtmForm.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java index 339dce099e..728f860104 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java @@ -43,7 +43,7 @@ public class CashAtAtmForm extends PaymentMethodForm { PaymentAccountPayload paymentAccountPayload) { CashAtAtmAccountPayload cbm = (CashAtAtmAccountPayload) paymentAccountPayload; - TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; + TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, 0, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); textExtraInfo.setText(cbm.getExtraInfo()); From 9a74856fa2f743b2f3d39db4d63f65119b255779 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 18 Jan 2025 07:48:51 -0500 Subject: [PATCH 094/371] increase trade limit of no deposit offers to 1.5 xmr --- core/src/main/java/haveno/core/payment/TradeLimits.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/payment/TradeLimits.java b/core/src/main/java/haveno/core/payment/TradeLimits.java index 92ac29b0a8..a33ce57ca4 100644 --- a/core/src/main/java/haveno/core/payment/TradeLimits.java +++ b/core/src/main/java/haveno/core/payment/TradeLimits.java @@ -31,7 +31,7 @@ import lombok.extern.slf4j.Slf4j; @Singleton public class TradeLimits { private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(528); // max trade limit for lowest risk payment method. Others will get derived from that. - private static final BigInteger MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT = HavenoUtils.xmrToAtomicUnits(1); // max trade limit without deposit from buyer + private static final BigInteger MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT = HavenoUtils.xmrToAtomicUnits(1.5); // max trade limit without deposit from buyer @Nullable @Getter From 7bc341d69ff21d9dab8570f91780c135ffca4b67 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 18 Jan 2025 09:58:22 -0500 Subject: [PATCH 095/371] rename 'Cash at ATM' to 'Cardless Cash' --- core/src/main/resources/i18n/displayStrings.properties | 8 ++++---- core/src/main/resources/i18n/displayStrings_cs.properties | 4 ++-- core/src/main/resources/i18n/displayStrings_tr.properties | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index ecb66ca760..26023eff08 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -3012,9 +3012,9 @@ Any special terms/conditions; \n\ Any other details. payment.tradingRestrictions=Please review the maker's terms and conditions.\n\ If you do not meet the requirements do not take this trade. -payment.cashAtAtm.info=Cash at ATM: Cardless withdraw at ATM using code\n\n\ +payment.cashAtAtm.info=Cardless Cash: Cardless withdraw at ATM using code\n\n\ To use this payment method:\n\n\ - 1. Create a Cash at ATM payment account, lising your accepted banks, regions, or other terms to be shown with the offer.\n\n\ + 1. Create a Cardless Cash payment account, lising your accepted banks, regions, or other terms to be shown with the offer.\n\n\ 2. Create or take an offer with the payment account.\n\n\ 3. When the offer is taken, chat with your peer to coordinate a time to complete the payment and share the payment details.\n\n\ If you cannot complete the transaction as specified in your trade contract, you may lose some (or all) of your security deposit. @@ -3073,7 +3073,7 @@ SPECIFIC_BANKS=Transfers with specific banks US_POSTAL_MONEY_ORDER=US Postal Money Order CASH_DEPOSIT=Cash Deposit PAY_BY_MAIL=Pay By Mail -CASH_AT_ATM=Cash at ATM +CASH_AT_ATM=Cardless Cash MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Face to face (in person) @@ -3093,7 +3093,7 @@ CASH_DEPOSIT_SHORT=Cash Deposit # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=Pay By Mail # suppress inspection "UnusedProperty" -CASH_AT_ATM_SHORT=Cash At ATM +CASH_AT_ATM_SHORT=Cardless Cash # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 4f2b4d3a87..1769788e97 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -3012,9 +3012,9 @@ Jakékoli zvláštní podmínky; \n\ Jakékoli další údaje. payment.tradingRestrictions=Přečtěte si prosím podmínky tvůrce.\n\ Pokud nesplňujete požadavky, nepřijímejte tento obchod. -payment.cashAtAtm.info=Hotovost u bankomatu: Výběr z bankomatu bez karty a pomocí kódu\n\n\ +payment.cashAtAtm.info=Výběr bez karty: Výběr z bankomatu bez karty a pomocí kódu\n\n\ Použití tohoto způsobu platby:\n\n\ - 1. Vytvořte si platební účet Hotovost u bankomatu a uveďte přijímané banky, regiony nebo jiné podmínky, které se zobrazí u nabídky.\n\n\n\ + 1. Vytvořte si platební účet Výběr bez karty a uveďte přijímané banky, regiony nebo jiné podmínky, které se zobrazí u nabídky.\n\n\n\ 2. Vytvořte nebo přijměte nabídku s tímto platebním účtem.\n\n\ 3. Po přijetí nabídky se domluvte s obchodním partnerem na čase dokončení platby a sdílejte podrobnosti o platbě.\n\n\ Pokud se vám nepodaří dokončit transakci, jak je uvedeno v obchodní smlouvě, můžete přijít o část (nebo celou) vaší kauci. diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index cdba715870..4ff0b4f610 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -2999,9 +2999,9 @@ payment.payByMail.extraInfo.prompt=Tekliflerinize lütfen şunları belirtin: \n Diğer detaylar. payment.tradingRestrictions=Lütfen yapıcı tarafın şartlarını ve koşullarını gözden geçirin.\n\ Gereksinimleri karşılamıyorsanız bu ticareti yapmayın. -payment.cashAtAtm.info=ATM'de Nakit: Kod kullanarak ATM'den kartsız para çekme\n\n\ +payment.cashAtAtm.info=Kartsız Nakit: Kod kullanarak ATM'den kartsız para çekme\n\n\ Bu ödeme yöntemini kullanmak için:\n\n\ - 1. Kabul ettiğiniz bankaları, bölgeleri veya teklifle birlikte gösterilecek diğer şartları listeleyerek bir ATM'de Nakit ödeme hesabı oluşturun.\n\n\ + 1. Kabul ettiğiniz bankaları, bölgeleri veya teklifle birlikte gösterilecek diğer şartları listeleyerek bir Kartsız Nakit ödeme hesabı oluşturun.\n\n\ 2. Ödeme hesabı ile bir teklif oluşturun veya bir teklifi kabul edin.\n\n\ 3. Teklif kabul edildiğinde, ödeme detaylarını paylaşmak ve ödemeyi tamamlamak için eşinizle sohbet edin.\n\n\ Ticaret sözleşmenizde belirtilen şekilde işlemi tamamlayamazsanız, güvenlik teminatınızın bir kısmını (veya tamamını) kaybedebilirsiniz. From 39bc54df73b39202c1105d61819faffbe4334b47 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 19 Jan 2025 08:55:17 -0500 Subject: [PATCH 096/371] bump version to 1.0.18 --- build.gradle | 2 +- common/src/main/java/haveno/common/app/Version.java | 2 +- desktop/package/linux/exchange.haveno.Haveno.metainfo.xml | 2 +- desktop/package/macosx/Info.plist | 4 ++-- docs/deployment-guide.md | 2 +- seednode/src/main/java/haveno/seednode/SeedNodeMain.java | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 72f98398b1..c8a80a9075 100644 --- a/build.gradle +++ b/build.gradle @@ -610,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.17-SNAPSHOT' + version = '1.0.18-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 8f2c2e135e..d39016dc31 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.0.17"; + public static final String VERSION = "1.0.18"; /** * Holds a list of the tagged resource files for optimizing the getData requests. diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index 6e4605f4e5..a298688669 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -60,6 +60,6 @@ - + diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index 88fa15378e..7693e124b7 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.17 + 1.0.18 CFBundleShortVersionString - 1.0.17 + 1.0.18 CFBundleExecutable Haveno diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index e4496fcc37..42f89b115b 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -270,7 +270,7 @@ Then follow these instructions: https://github.com/haveno-dex/haveno/blob/master Set the mandatory minimum version for trading (optional) -If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.17) in the field labeled "Min. version required for trading". +If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.18) in the field labeled "Min. version required for trading". Send update alert diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index 7482cdbc7a..281e138c0b 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.0.17"; + private static final String VERSION = "1.0.18"; private SeedNode seedNode; private Timer checkConnectionLossTime; From 66770cc98fc0a777feb791ce472e572627552ecc Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 19 Jan 2025 14:27:49 -0500 Subject: [PATCH 097/371] use fixed localization for parsing offer amounts --- core/src/main/java/haveno/core/util/coin/CoinUtil.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/util/coin/CoinUtil.java b/core/src/main/java/haveno/core/util/coin/CoinUtil.java index 4085a63b6a..f0711a635f 100644 --- a/core/src/main/java/haveno/core/util/coin/CoinUtil.java +++ b/core/src/main/java/haveno/core/util/coin/CoinUtil.java @@ -31,6 +31,8 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; import static com.google.common.base.Preconditions.checkArgument; import static haveno.core.util.VolumeUtil.getAdjustedVolumeUnit; @@ -104,7 +106,7 @@ public class CoinUtil { } public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Long maxTradeLimit) { - DecimalFormat decimalFormat = new DecimalFormat("#.####"); + DecimalFormat decimalFormat = new DecimalFormat("#.####", HavenoUtils.DECIMAL_FORMAT_SYMBOLS); double roundedXmrAmount = Double.parseDouble(decimalFormat.format(HavenoUtils.atomicUnitsToXmr(amount))); return HavenoUtils.xmrToAtomicUnits(roundedXmrAmount); } From 535b71adc56917495b15510b247c7675de29c079 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 19 Jan 2025 14:37:48 -0500 Subject: [PATCH 098/371] remove unused imports --- core/src/main/java/haveno/core/util/coin/CoinUtil.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/java/haveno/core/util/coin/CoinUtil.java b/core/src/main/java/haveno/core/util/coin/CoinUtil.java index f0711a635f..6c163ede6a 100644 --- a/core/src/main/java/haveno/core/util/coin/CoinUtil.java +++ b/core/src/main/java/haveno/core/util/coin/CoinUtil.java @@ -31,8 +31,6 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; -import java.util.Locale; import static com.google.common.base.Preconditions.checkArgument; import static haveno.core.util.VolumeUtil.getAdjustedVolumeUnit; From 3847d1bd3a590eaf847e86afd2efcb0d4eb44cfe Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 21 Jan 2025 09:49:42 -0500 Subject: [PATCH 099/371] WeChat Pay supports CNY, USD, EUR, and GBP --- .../main/java/haveno/core/payment/WeChatPayAccount.java | 8 ++++++-- .../desktop/components/paymentmethods/WeChatPayForm.java | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/payment/WeChatPayAccount.java b/core/src/main/java/haveno/core/payment/WeChatPayAccount.java index e7099879ea..297968ef0c 100644 --- a/core/src/main/java/haveno/core/payment/WeChatPayAccount.java +++ b/core/src/main/java/haveno/core/payment/WeChatPayAccount.java @@ -31,11 +31,15 @@ import java.util.List; @EqualsAndHashCode(callSuper = true) public final class WeChatPayAccount extends PaymentAccount { - public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY")); + public static final List SUPPORTED_CURRENCIES = List.of( + new TraditionalCurrency("CNY"), + new TraditionalCurrency("USD"), + new TraditionalCurrency("EUR"), + new TraditionalCurrency("GBP") + ); public WeChatPayAccount() { super(PaymentMethod.WECHAT_PAY); - setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/WeChatPayForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/WeChatPayForm.java index eb384687f2..cb1b40f6b7 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/WeChatPayForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/WeChatPayForm.java @@ -26,6 +26,7 @@ import haveno.core.payment.payload.WeChatPayAccountPayload; import haveno.core.payment.validation.WeChatPayValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; +import javafx.collections.FXCollections; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; @@ -44,6 +45,12 @@ public class WeChatPayForm extends GeneralAccountNumberForm { this.weChatPayAccount = (WeChatPayAccount) paymentAccount; } + @Override + public void addTradeCurrency() { + addTradeCurrencyComboBox(); + currencyComboBox.setItems(FXCollections.observableArrayList(weChatPayAccount.getSupportedCurrencies())); + } + @Override void setAccountNumber(String newValue) { weChatPayAccount.setAccountNr(newValue); From c3f7f194b0a031d00036827ecd0c99ff41afbaee Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 21 Jan 2025 10:10:57 -0500 Subject: [PATCH 100/371] AliPay supports all supported currencies --- .../haveno/core/payment/AliPayAccount.java | 29 ++++++++++++++++++- .../components/paymentmethods/AliPayForm.java | 7 +++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/payment/AliPayAccount.java b/core/src/main/java/haveno/core/payment/AliPayAccount.java index b6f93b364d..1bff92b5cd 100644 --- a/core/src/main/java/haveno/core/payment/AliPayAccount.java +++ b/core/src/main/java/haveno/core/payment/AliPayAccount.java @@ -31,7 +31,34 @@ import java.util.List; @EqualsAndHashCode(callSuper = true) public final class AliPayAccount extends PaymentAccount { - public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY")); + public static final List SUPPORTED_CURRENCIES = List.of( + new TraditionalCurrency("AED"), + new TraditionalCurrency("AUD"), + new TraditionalCurrency("CAD"), + new TraditionalCurrency("CHF"), + new TraditionalCurrency("CNY"), + new TraditionalCurrency("CZK"), + new TraditionalCurrency("DKK"), + new TraditionalCurrency("EUR"), + new TraditionalCurrency("GBP"), + new TraditionalCurrency("HKD"), + new TraditionalCurrency("IDR"), + new TraditionalCurrency("ILS"), + new TraditionalCurrency("JPY"), + new TraditionalCurrency("KRW"), + new TraditionalCurrency("LKR"), + new TraditionalCurrency("MUR"), + new TraditionalCurrency("MYR"), + new TraditionalCurrency("NOK"), + new TraditionalCurrency("NZD"), + new TraditionalCurrency("PHP"), + new TraditionalCurrency("RUB"), + new TraditionalCurrency("SEK"), + new TraditionalCurrency("SGD"), + new TraditionalCurrency("THB"), + new TraditionalCurrency("USD"), + new TraditionalCurrency("ZAR") + ); public AliPayAccount() { super(PaymentMethod.ALI_PAY); diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AliPayForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AliPayForm.java index f4d7cce695..6688bb591b 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AliPayForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AliPayForm.java @@ -26,6 +26,7 @@ import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.AliPayValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; +import javafx.collections.FXCollections; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; @@ -44,6 +45,12 @@ public class AliPayForm extends GeneralAccountNumberForm { this.aliPayAccount = (AliPayAccount) paymentAccount; } + @Override + public void addTradeCurrency() { + addTradeCurrencyComboBox(); + currencyComboBox.setItems(FXCollections.observableArrayList(aliPayAccount.getSupportedCurrencies())); + } + @Override void setAccountNumber(String newValue) { aliPayAccount.setAccountNr(newValue); From dc7a8e420112428829af755f5dad056d8143f12f Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 23 Jan 2025 10:12:42 -0500 Subject: [PATCH 101/371] do not override trade currency on country-based form deserialization --- .../haveno/core/payment/PaymentAccountTypeAdapter.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java b/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java index 0160a1a736..0226fe4530 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java @@ -24,7 +24,6 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; -import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; @@ -42,7 +41,6 @@ import java.util.Map; import java.util.Optional; import java.util.function.Predicate; -import static com.google.common.base.Preconditions.checkNotNull; import static haveno.common.util.ReflectionUtils.getSetterMethodForFieldInClassHierarchy; import static haveno.common.util.ReflectionUtils.getVisibilityModifierAsString; import static haveno.common.util.ReflectionUtils.handleSetFieldValueError; @@ -50,7 +48,6 @@ import static haveno.common.util.ReflectionUtils.isSetterOnClass; import static haveno.common.util.ReflectionUtils.loadFieldListForClassHierarchy; import static haveno.common.util.Utilities.decodeFromHex; import static haveno.core.locale.CountryUtil.findCountryByCode; -import static haveno.core.locale.CurrencyUtil.getCurrencyByCountryCode; import static haveno.core.locale.CurrencyUtil.getTradeCurrenciesInList; import static haveno.core.locale.CurrencyUtil.getTradeCurrency; import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID; @@ -435,8 +432,10 @@ class PaymentAccountTypeAdapter extends TypeAdapter { if (account.isCountryBasedPaymentAccount()) { ((CountryBasedPaymentAccount) account).setCountry(country.get()); - TraditionalCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode)); - account.setSingleTradeCurrency(fiatCurrency); + + // TODO: applying single trade currency default can overwrite provided currencies, apply elsewhere? + // TraditionalCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode)); + // account.setSingleTradeCurrency(fiatCurrency); } else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) { ((MoneyGramAccount) account).setCountry(country.get()); } else { From 0d2c1fe8fd183acb54ad13a6a0010204455c6491 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 23 Jan 2025 10:27:22 -0500 Subject: [PATCH 102/371] show extra info popup on take f2f offer --- .../main/offer/takeoffer/TakeOfferView.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index 1d662aebc7..e0385bb5b3 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -162,7 +162,7 @@ public class TakeOfferView extends ActivatableViewAndModel paymentAccountWarningDisplayed = new HashMap<>(); private boolean offerDetailsWindowDisplayed, zelleWarningDisplayed, fasterPaymentsWarningDisplayed, takeOfferFromUnsignedAccountWarningDisplayed, payByMailWarningDisplayed, cashAtAtmWarningDisplayed, - australiaPayidWarningDisplayed, paypalWarningDisplayed, cashAppWarningDisplayed; + australiaPayidWarningDisplayed, paypalWarningDisplayed, cashAppWarningDisplayed, F2FWarningDisplayed; private SimpleBooleanProperty errorPopupDisplayed; private ChangeListener amountFocusedListener, getShowWalletFundedNotificationListener; @@ -276,6 +276,7 @@ public class TakeOfferView extends ActivatableViewAndModel { + new GenericMessageWindow() + .preamble(Res.get("payment.tradingRestrictions")) + .instruction(offer.getExtraInfo()) + .actionButtonText(Res.get("shared.iConfirm")) + .closeButtonText(Res.get("shared.close")) + .width(Layout.INITIAL_WINDOW_WIDTH) + .onClose(() -> close(false)) + .show(); + }, 500, TimeUnit.MILLISECONDS); + } + } + private Tuple2 getTradeInputBox(HBox amountValueBox, String promptText) { Label descriptionLabel = new AutoTooltipLabel(promptText); descriptionLabel.setId("input-description-label"); From e4714aab89130b0f66228e6b7ae80a6da9f0382b Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 24 Jan 2025 09:04:48 -0500 Subject: [PATCH 103/371] display N/A for buyer contact in seller step 2 form --- .../haveno/desktop/components/paymentmethods/F2FForm.java | 6 +++--- .../portfolio/pendingtrades/steps/buyer/BuyerStep2View.java | 2 +- .../pendingtrades/steps/seller/SellerStep2View.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java index ada24a71d4..aebd470d31 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java @@ -52,15 +52,15 @@ public class F2FForm extends PaymentMethodForm { private final F2FValidator f2fValidator; private Country selectedCountry; - public static int addFormForBuyer(GridPane gridPane, int gridRow, - PaymentAccountPayload paymentAccountPayload, Offer offer, double top) { + public static int addStep2Form(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload, Offer offer, double top, boolean isBuyer) { F2FAccountPayload f2fAccountPayload = (F2FAccountPayload) paymentAccountPayload; addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, Res.get("shared.country"), CountryUtil.getNameAndCode(f2fAccountPayload.getCountryCode()), top); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.f2f.city"), offer.getF2FCity(), top); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.f2f.contact"), - f2fAccountPayload.getContact()); + isBuyer ? f2fAccountPayload.getContact() : Res.get("shared.na")); TextArea textArea = addTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; textArea.setMinHeight(70); textArea.setEditable(false); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java index e36cba49d4..3872ef85ab 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java @@ -326,7 +326,7 @@ public class BuyerStep2View extends TradeStepView { case PaymentMethod.F2F_ID: checkNotNull(model.dataModel.getTrade(), "model.dataModel.getTrade() must not be null"); checkNotNull(model.dataModel.getTrade().getOffer(), "model.dataModel.getTrade().getOffer() must not be null"); - gridRow = F2FForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload, model.dataModel.getTrade().getOffer(), 0); + gridRow = F2FForm.addStep2Form(gridPane, gridRow, paymentAccountPayload, model.dataModel.getTrade().getOffer(), 0, true); break; case PaymentMethod.BLOCK_CHAINS_ID: case PaymentMethod.BLOCK_CHAINS_INSTANT_ID: diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java index 06ceba45a2..e9905c3f82 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java @@ -46,8 +46,8 @@ public class SellerStep2View extends TradeStepView { if (model.dataModel.getSellersPaymentAccountPayload() instanceof F2FAccountPayload) { addTitledGroupBg(gridPane, ++gridRow, 4, Res.get("portfolio.pending.step2_seller.f2fInfo.headline"), Layout.COMPACT_GROUP_DISTANCE); - gridRow = F2FForm.addFormForBuyer(gridPane, --gridRow, model.dataModel.getSellersPaymentAccountPayload(), - model.dataModel.getTrade().getOffer(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + gridRow = F2FForm.addStep2Form(gridPane, --gridRow, model.dataModel.getSellersPaymentAccountPayload(), + model.dataModel.getTrade().getOffer(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, false); } } From a6af1550a4874bb779da08318177231fc39518b9 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 24 Jan 2025 09:31:19 -0500 Subject: [PATCH 104/371] set arbitrator payment account payloads on dispute opened --- .../haveno/core/support/dispute/DisputeManager.java | 10 ++++++++-- .../haveno/core/support/dispute/DisputeValidation.java | 7 +++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index d6b2469744..64de6b3f2d 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -537,15 +537,21 @@ public abstract class DisputeManager> extends Sup throw e; } - // try to validate payment account + // try to validate payment accounts try { - DisputeValidation.validatePaymentAccountPayload(dispute); // TODO: add field to dispute details: valid, invalid, missing + DisputeValidation.validatePaymentAccountPayloads(dispute); // TODO: add field to dispute details: valid, invalid, missing } catch (Exception e) { log.error(ExceptionUtils.getStackTrace(e)); trade.prependErrorMessage(e.getMessage()); throw e; } + // set arbitrator's payment account payloads + if (trade.isArbitrator()) { + if (trade.getBuyer().getPaymentAccountPayload() == null) trade.getBuyer().setPaymentAccountPayload(dispute.getBuyerPaymentAccountPayload()); + if (trade.getSeller().getPaymentAccountPayload() == null) trade.getSeller().setPaymentAccountPayload(dispute.getSellerPaymentAccountPayload()); + } + // get sender TradePeer sender; if (reOpen) { // re-open can come from either peer diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java b/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java index 4591a6fbc2..0905af4a1d 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java @@ -41,9 +41,12 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class DisputeValidation { - public static void validatePaymentAccountPayload(Dispute dispute) throws ValidationException { + public static void validatePaymentAccountPayloads(Dispute dispute) throws ValidationException { if (dispute.getSellerPaymentAccountPayload() == null) throw new ValidationException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId()); - if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of maker's payment account payload does not match contract"); + if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of seller's payment account payload does not match contract"); + if (dispute.getBuyerPaymentAccountPayload() != null) { + if (!Arrays.equals(dispute.getBuyerPaymentAccountPayload().getHash(), dispute.getContract().getBuyerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of buyer's payment account payload does not match contract"); + } } public static void validateDisputeData(Dispute dispute) throws ValidationException { From 6c6c6e2dd5fdab6d2caa3e9887b472e287fc25ec Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 25 Jan 2025 12:41:20 -0500 Subject: [PATCH 105/371] support additional info on all offers --- .../main/java/haveno/core/api/CoreApi.java | 8 +- .../haveno/core/api/CoreOffersService.java | 10 +- .../java/haveno/core/api/model/OfferInfo.java | 7 +- .../api/model/builder/OfferInfoBuilder.java | 6 + .../haveno/core/offer/CreateOfferService.java | 12 +- .../main/java/haveno/core/offer/Offer.java | 18 ++- .../java/haveno/core/offer/OfferPayload.java | 23 ++- .../haveno/core/offer/OpenOfferManager.java | 3 +- .../java/haveno/core/payment/F2FAccount.java | 2 +- .../resources/i18n/displayStrings.properties | 7 +- .../i18n/displayStrings_cs.properties | 7 +- .../i18n/displayStrings_de.properties | 7 +- .../i18n/displayStrings_es.properties | 7 +- .../i18n/displayStrings_fa.properties | 7 +- .../i18n/displayStrings_fr.properties | 7 +- .../i18n/displayStrings_it.properties | 7 +- .../i18n/displayStrings_ja.properties | 7 +- .../i18n/displayStrings_pt-br.properties | 7 +- .../i18n/displayStrings_pt.properties | 7 +- .../i18n/displayStrings_ru.properties | 7 +- .../i18n/displayStrings_th.properties | 7 +- .../i18n/displayStrings_tr.properties | 7 +- .../i18n/displayStrings_vi.properties | 7 +- .../i18n/displayStrings_zh-hans.properties | 7 +- .../i18n/displayStrings_zh-hant.properties | 7 +- .../java/haveno/core/offer/OfferMaker.java | 3 +- .../haveno/daemon/grpc/GrpcOffersService.java | 1 + .../paymentmethods/AustraliaPayidForm.java | 2 +- .../paymentmethods/CashAppForm.java | 2 +- .../components/paymentmethods/F2FForm.java | 4 +- .../components/paymentmethods/PayPalForm.java | 2 +- .../main/offer/MutableOfferDataModel.java | 40 +++-- .../desktop/main/offer/MutableOfferView.java | 76 ++++++++- .../main/offer/MutableOfferViewModel.java | 28 ++++ .../offer/offerbook/OfferBookViewModel.java | 4 +- .../main/offer/takeoffer/TakeOfferView.java | 151 +++++------------- .../overlays/windows/OfferDetailsWindow.java | 13 +- .../overlays/windows/TradeDetailsWindow.java | 31 +++- .../editoffer/EditOfferDataModel.java | 4 +- .../portfolio/editoffer/EditOfferView.java | 3 +- .../trades/TradesChartsViewModelTest.java | 1 + .../offerbook/OfferBookViewModelTest.java | 1 + .../java/haveno/desktop/maker/OfferMaker.java | 1 + proto/src/main/proto/grpc.proto | 2 + proto/src/main/proto/pb.proto | 1 + 45 files changed, 367 insertions(+), 204 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index 5b0c9e247a..99fb3bc74f 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -425,6 +425,7 @@ public class CoreApi { String paymentAccountId, boolean isPrivateOffer, boolean buyerAsTakerWithoutDeposit, + String extraInfo, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { coreOffersService.postOffer(currencyCode, @@ -440,6 +441,7 @@ public class CoreApi { paymentAccountId, isPrivateOffer, buyerAsTakerWithoutDeposit, + extraInfo, resultHandler, errorMessageHandler); } @@ -455,7 +457,8 @@ public class CoreApi { double securityDepositPct, PaymentAccount paymentAccount, boolean isPrivateOffer, - boolean buyerAsTakerWithoutDeposit) { + boolean buyerAsTakerWithoutDeposit, + String extraInfo) { return coreOffersService.editOffer(offerId, currencyCode, direction, @@ -467,7 +470,8 @@ public class CoreApi { securityDepositPct, paymentAccount, isPrivateOffer, - buyerAsTakerWithoutDeposit); + buyerAsTakerWithoutDeposit, + extraInfo); } public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index 8232827887..a66388c040 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -178,6 +178,7 @@ public class CoreOffersService { String paymentAccountId, boolean isPrivateOffer, boolean buyerAsTakerWithoutDeposit, + String extraInfo, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { coreWalletsService.verifyWalletsAreAvailable(); @@ -204,7 +205,8 @@ public class CoreOffersService { securityDepositPct, paymentAccount, isPrivateOffer, - buyerAsTakerWithoutDeposit); + buyerAsTakerWithoutDeposit, + extraInfo); verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); @@ -230,7 +232,8 @@ public class CoreOffersService { double securityDepositPct, PaymentAccount paymentAccount, boolean isPrivateOffer, - boolean buyerAsTakerWithoutDeposit) { + boolean buyerAsTakerWithoutDeposit, + String extraInfo) { return createOfferService.createAndGetOffer(offerId, direction, currencyCode.toUpperCase(), @@ -242,7 +245,8 @@ public class CoreOffersService { securityDepositPct, paymentAccount, isPrivateOffer, - buyerAsTakerWithoutDeposit); + buyerAsTakerWithoutDeposit, + extraInfo); } void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { diff --git a/core/src/main/java/haveno/core/api/model/OfferInfo.java b/core/src/main/java/haveno/core/api/model/OfferInfo.java index 537cc9ab26..76de24401a 100644 --- a/core/src/main/java/haveno/core/api/model/OfferInfo.java +++ b/core/src/main/java/haveno/core/api/model/OfferInfo.java @@ -80,6 +80,7 @@ public class OfferInfo implements Payload { private final long splitOutputTxFee; private final boolean isPrivateOffer; private final String challenge; + private final String extraInfo; public OfferInfo(OfferInfoBuilder builder) { this.id = builder.getId(); @@ -115,6 +116,7 @@ public class OfferInfo implements Payload { this.splitOutputTxFee = builder.getSplitOutputTxFee(); this.isPrivateOffer = builder.isPrivateOffer(); this.challenge = builder.getChallenge(); + this.extraInfo = builder.getExtraInfo(); } public static OfferInfo toOfferInfo(Offer offer) { @@ -184,7 +186,8 @@ public class OfferInfo implements Payload { .withProtocolVersion(offer.getOfferPayload().getProtocolVersion()) .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress()) .withIsPrivateOffer(offer.isPrivateOffer()) - .withChallenge(offer.getChallenge()); + .withChallenge(offer.getChallenge()) + .withExtraInfo(offer.getCombinedExtraInfo()); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -227,6 +230,7 @@ public class OfferInfo implements Payload { Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner); Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash); Optional.ofNullable(challenge).ifPresent(builder::setChallenge); + Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo); return builder.build(); } @@ -266,6 +270,7 @@ public class OfferInfo implements Payload { .withSplitOutputTxFee(proto.getSplitOutputTxFee()) .withIsPrivateOffer(proto.getIsPrivateOffer()) .withChallenge(proto.getChallenge()) + .withExtraInfo(proto.getExtraInfo()) .build(); } } diff --git a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java index 36801cdbb6..23e403fcd2 100644 --- a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java +++ b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java @@ -65,6 +65,7 @@ public final class OfferInfoBuilder { private long splitOutputTxFee; private boolean isPrivateOffer; private String challenge; + private String extraInfo; public OfferInfoBuilder withId(String id) { this.id = id; @@ -246,6 +247,11 @@ public final class OfferInfoBuilder { return this; } + public OfferInfoBuilder withExtraInfo(String extraInfo) { + this.extraInfo = extraInfo; + return this; + } + public OfferInfo build() { return new OfferInfo(this); } diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index 35ece3b10e..508a3f44e3 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -103,7 +103,8 @@ public class CreateOfferService { double securityDepositPct, PaymentAccount paymentAccount, boolean isPrivateOffer, - boolean buyerAsTakerWithoutDeposit) { + boolean buyerAsTakerWithoutDeposit, + String extraInfo) { log.info("create and get offer with offerId={}, " + "currencyCode={}, " + "direction={}, " + @@ -114,7 +115,8 @@ public class CreateOfferService { "minAmount={}, " + "securityDepositPct={}, " + "isPrivateOffer={}, " + - "buyerAsTakerWithoutDeposit={}", + "buyerAsTakerWithoutDeposit={}, " + + "extraInfo={}", offerId, currencyCode, direction, @@ -125,7 +127,8 @@ public class CreateOfferService { minAmount, securityDepositPct, isPrivateOffer, - buyerAsTakerWithoutDeposit); + buyerAsTakerWithoutDeposit, + extraInfo); // verify buyer as taker security deposit boolean isBuyerMaker = offerUtil.isBuyOffer(direction); @@ -225,7 +228,8 @@ public class CreateOfferService { Version.TRADE_PROTOCOL_VERSION, null, null, - null); + null, + extraInfo); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); offer.setChallenge(challenge); diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index e84e4fa329..fac72f827d 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -421,7 +421,23 @@ public class Offer implements NetworkPayload, PersistablePayload { return ""; } - public String getExtraInfo() { + public String getCombinedExtraInfo() { + StringBuilder sb = new StringBuilder(); + if (getOfferExtraInfo() != null && !getOfferExtraInfo().isEmpty()) { + sb.append(getOfferExtraInfo()); + } + if (getPaymentAccountExtraInfo() != null && !getPaymentAccountExtraInfo().isEmpty()) { + if (sb.length() > 0) sb.append("\n\n"); + sb.append(getPaymentAccountExtraInfo()); + } + return sb.toString(); + } + + public String getOfferExtraInfo() { + return offerPayload.getExtraInfo(); + } + + public String getPaymentAccountExtraInfo() { if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAY_BY_MAIL_EXTRA_INFO)) diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index bd10719ec3..8da91b4b15 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -158,6 +158,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay private final boolean isPrivateOffer; @Nullable private final String challengeHash; + @Nullable + private final String extraInfo; /////////////////////////////////////////////////////////////////////////////////////////// @@ -201,7 +203,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay int protocolVersion, @Nullable NodeAddress arbitratorSigner, @Nullable byte[] arbitratorSignature, - @Nullable List reserveTxKeyImages) { + @Nullable List reserveTxKeyImages, + @Nullable String extraInfo) { this.id = id; this.date = date; this.ownerNodeAddress = ownerNodeAddress; @@ -240,6 +243,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay this.upperClosePrice = upperClosePrice; this.isPrivateOffer = isPrivateOffer; this.challengeHash = challengeHash; + this.extraInfo = extraInfo; } public byte[] getHash() { @@ -290,7 +294,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay protocolVersion, arbitratorSigner, null, - reserveTxKeyImages + reserveTxKeyImages, + null ); return signee.getHash(); @@ -387,6 +392,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage())); Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e))); Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages); + Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo); return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build(); } @@ -398,7 +404,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay null : new ArrayList<>(proto.getAcceptedCountryCodesList()); List reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ? null : new ArrayList<>(proto.getReserveTxKeyImagesList()); - String challengeHash = ProtoUtil.stringOrNullFromProto(proto.getChallengeHash()); Map extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(); @@ -434,12 +439,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay proto.getLowerClosePrice(), proto.getUpperClosePrice(), proto.getIsPrivateOffer(), - challengeHash, + ProtoUtil.stringOrNullFromProto(proto.getChallengeHash()), extraDataMapMap, proto.getProtocolVersion(), proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null, ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()), - reserveTxKeyImages); + reserveTxKeyImages, + ProtoUtil.stringOrNullFromProto(proto.getExtraInfo())); } @Override @@ -481,14 +487,15 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay ",\r\n lowerClosePrice=" + lowerClosePrice + ",\r\n upperClosePrice=" + upperClosePrice + ",\r\n isPrivateOffer=" + isPrivateOffer + - ",\r\n challengeHash='" + challengeHash + '\'' + + ",\r\n challengeHash='" + challengeHash + ",\r\n arbitratorSigner=" + arbitratorSigner + ",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + + ",\r\n extraInfo='" + extraInfo + "\r\n} "; } // For backward compatibility we need to ensure same order for json fields as with 1.7.5. and earlier versions. - // The json is used for the hash in the contract and change of oder would cause a different hash and + // The json is used for the hash in the contract and change of order would cause a different hash and // therefore a failure during trade. public static class JsonSerializer implements com.google.gson.JsonSerializer { @Override @@ -525,6 +532,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion())); object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner())); object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature())); + object.add("extraInfo", context.serialize(offerPayload.getExtraInfo())); + // reserveTxKeyImages and challengeHash are purposely excluded because they are not relevant to existing trades and would break existing contracts return object; } } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index b7cb7255f8..ab52d00b38 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1788,7 +1788,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe protocolVersion, originalOfferPayload.getArbitratorSigner(), originalOfferPayload.getArbitratorSignature(), - originalOfferPayload.getReserveTxKeyImages()); + originalOfferPayload.getReserveTxKeyImages(), + originalOfferPayload.getExtraInfo()); // Save states from original data to use for the updated Offer.State originalOfferState = originalOffer.getState(); diff --git a/core/src/main/java/haveno/core/payment/F2FAccount.java b/core/src/main/java/haveno/core/payment/F2FAccount.java index b75718ec68..a16b4daf6c 100644 --- a/core/src/main/java/haveno/core/payment/F2FAccount.java +++ b/core/src/main/java/haveno/core/payment/F2FAccount.java @@ -93,7 +93,7 @@ public final class F2FAccount extends CountryBasedPaymentAccount { if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); if (field.getId() == PaymentAccountFormField.FieldId.CITY) field.setLabel(Res.get("payment.f2f.city")); if (field.getId() == PaymentAccountFormField.FieldId.CONTACT) field.setLabel(Res.get("payment.f2f.contact")); - if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.extraInfo.prompt")); + if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.extraInfo.prompt.paymentAccount")); return field; } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 26023eff08..c759cd61da 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -3028,7 +3028,10 @@ payment.f2f.city=City for 'Face to face' meeting payment.f2f.city.prompt=The city will be displayed with the offer payment.shared.optionalExtra=Optional additional information payment.shared.extraInfo=Additional information -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=Additional offer information +payment.shared.extraInfo.prompt.paymentAccount=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.prompt.offer=Define any special terms, conditions, or details you would like to be displayed with your offer. +payment.shared.extraInfo.noDeposit=Contact details and offer terms payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\n\ The main differences are:\n\ ● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n\ @@ -3042,7 +3045,7 @@ payment.f2f.info='Face to Face' trades have different rules and come with differ recommendations at: [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/F2F] payment.f2f.info.openURL=Open web page payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Additional information: {0} +payment.shared.extraInfo.tooltip=Additional information: {0} payment.ifsc=IFS Code payment.ifsc.validation=IFSC format: XXXX0999999 diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 1769788e97..32f326ca91 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -3028,7 +3028,10 @@ payment.f2f.city=Město pro setkání 'tváří v tvář' payment.f2f.city.prompt=Město se zobrazí s nabídkou payment.shared.optionalExtra=Volitelné další informace payment.shared.extraInfo=Další informace -payment.shared.extraInfo.prompt=Uveďte jakékoli speciální požadavky, podmínky a detaily, které chcete zobrazit u vašich nabídek s tímto platebním účtem. (Uživatelé uvidí tyto informace předtím, než akceptují vaši nabídku.) +payment.shared.extraInfo.offer=Další informace o nabídce +payment.shared.extraInfo.prompt.paymentAccount=Uveďte jakékoli speciální požadavky, podmínky a detaily, které chcete zobrazit u vašich nabídek s tímto platebním účtem. (Uživatelé uvidí tyto informace předtím, než akceptují vaši nabídku.) +payment.shared.extraInfo.prompt.offer=Definujte jakékoli speciální podmínky, podmínky nebo detaily, které chcete zobrazit u své nabídky. +payment.shared.extraInfo.noDeposit=Kontaktní údaje a podmínky nabídky payment.f2f.info=Obchody 'tváří v tvář' mají různá pravidla a přicházejí s jinými riziky než online transakce.\n\n\ Hlavní rozdíly jsou:\n\ ● Obchodní partneři si musí vyměňovat informace o místě a čase schůzky pomocí poskytnutých kontaktních údajů.\n\ @@ -3042,7 +3045,7 @@ payment.f2f.info=Obchody 'tváří v tvář' mají různá pravidla a přicháze na adrese: [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/F2F] payment.f2f.info.openURL=Otevřít webovou stránku payment.f2f.offerbook.tooltip.countryAndCity=Země a město: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Další informace: {0} +payment.shared.extraInfo.tooltip=Další informace: {0} payment.ifsc=IFS kód payment.ifsc.validation=IFSC formát: XXXX0999999 diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index c6002f049a..ef8fb9afe9 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -2032,11 +2032,14 @@ payment.f2f.city=Stadt für ein "Angesicht zu Angesicht" Treffen payment.f2f.city.prompt=Die Stadt wird mit dem Angebot angezeigt payment.shared.optionalExtra=Freiwillige zusätzliche Informationen payment.shared.extraInfo=Zusätzliche Informationen -payment.shared.extraInfo.prompt=Gib spezielle Bedingungen, Abmachungen oder Details die bei ihren Angeboten unter diesem Zahlungskonto angezeigt werden sollen an. Nutzer werden diese Informationen vor der Annahme des Angebots sehen. +payment.shared.extraInfo.offer=Zusätzliche Angebotsinformationen +payment.shared.extraInfo.prompt.paymentAccount=Gib spezielle Bedingungen, Abmachungen oder Details die bei ihren Angeboten unter diesem Zahlungskonto angezeigt werden sollen an. Nutzer werden diese Informationen vor der Annahme des Angebots sehen. +payment.shared.extraInfo.prompt.offer=Definieren Sie alle speziellen Begriffe, Bedingungen oder Details, die Sie mit Ihrem Angebot anzeigen möchten. +payment.shared.extraInfo.noDeposit=Kontaktdaten und Angebotsbedingungen payment.f2f.info=Persönliche 'Face to Face' Trades haben unterschiedliche Regeln und sind mit anderen Risiken verbunden als gewöhnliche Online-Trades.\n\nDie Hauptunterschiede sind:\n● Die Trading Partner müssen die Kontaktdaten und Informationen über den Ort und die Uhrzeit des Treffens austauschen.\n● Die Trading Partner müssen ihre Laptops mitbringen und die Bestätigung der "gesendeten Zahlung" und der "erhaltenen Zahlung" am Treffpunkt vornehmen.\n● Wenn ein Ersteller eines Angebots spezielle "Allgemeine Geschäftsbedingungen" hat, muss er diese im Textfeld "Zusatzinformationen" des Kontos angeben.\n● Mit der Annahme eines Angebots erklärt sich der Käufer mit den vom Anbieter angegebenen "Allgemeinen Geschäftsbedingungen" einverstanden.\n● Im Konfliktfall kann der Mediator oder Arbitrator nicht viel tun, da es in der Regel schwierig ist zu bestimmen, was beim Treffen passiert ist. In solchen Fällen können die Monero auf unbestimmte Zeit oder bis zu einer Einigung der Trading Peers gesperrt werden.\n\nUm sicherzustellen, dass Sie die Besonderheiten der persönlichen 'Face to Face' Trades vollständig verstehen, lesen Sie bitte die Anweisungen und Empfehlungen unter: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Webseite öffnen payment.f2f.offerbook.tooltip.countryAndCity=Land und Stadt: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Zusätzliche Informationen: {0} +payment.shared.extraInfo.tooltip=Zusätzliche Informationen: {0} payment.japan.bank=Bank payment.japan.branch=Filiale diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 303330e489..e5ab6b4537 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -2033,11 +2033,14 @@ payment.f2f.city=Ciudad para la reunión 'cara a cara' payment.f2f.city.prompt=La ciudad se mostrará con la oferta payment.shared.optionalExtra=Información adicional opcional payment.shared.extraInfo=Información adicional -payment.shared.extraInfo.prompt=Defina cualquier término especial, condiciones o detalles que quiera mostrar junto a sus ofertas para esta cuenta de pago (otros usuarios podrán ver esta información antes de aceptar las ofertas). +payment.shared.extraInfo.offer=Información adicional de la oferta +payment.shared.extraInfo.prompt.paymentAccount=Defina cualquier término especial, condiciones o detalles que quiera mostrar junto a sus ofertas para esta cuenta de pago (otros usuarios podrán ver esta información antes de aceptar las ofertas). +payment.shared.extraInfo.prompt.offer=Defina cualquier término, condición o detalle especial que le gustaría mostrar con su oferta. +payment.shared.extraInfo.noDeposit=Detalles de contacto y términos de la oferta payment.f2f.info=Los intercambios 'Cara a Cara' tienen diferentes reglas y riesgos que las transacciones en línea.\n\nLas principales diferencias son:\n● Los pares de intercambio necesitan intercambiar información acerca del punto de reunión y la hora usando los detalles de contacto proporcionados.\n● Los pares de intercambio tienen que traer sus portátiles y hacer la confirmación de 'pago enviado' y 'pago recibido' en el lugar de reunión.\n● Si un creador tiene 'términos y condiciones' especiales necesita declararlos en el campo de texto 'información adicional' en la cuenta.\n● Tomando una oferta el tomador está de acuerdo con los 'términos y condiciones' declarados por el creador.\n● En caso de disputa el árbitro no puede ayudar mucho ya que normalmente es complicado obtener evidencias no manipulables de lo que ha pasado en una reunión. En estos casos los fondos XMR pueden bloquearse indefinidamente o hasta que los pares lleguen a un acuerdo.\n\nPara asegurarse de que comprende las diferencias con los intercambios 'Cara a Cara' por favor lea las instrucciones y recomendaciones en: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Abrir paǵina web payment.f2f.offerbook.tooltip.countryAndCity=País y ciudad: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Información adicional: {0} +payment.shared.extraInfo.tooltip=Información adicional: {0} payment.japan.bank=Banco payment.japan.branch=Branch diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 647b9dc735..cd739ed362 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -2007,11 +2007,14 @@ payment.f2f.city=شهر جهت ملاقات 'رو در رو' payment.f2f.city.prompt=نام شهر به همراه پیشنهاد نمایش داده خواهد شد payment.shared.optionalExtra=اطلاعات اضافی اختیاری payment.shared.extraInfo=اطلاعات اضافی -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=اطلاعات اضافی پیشنهاد +payment.shared.extraInfo.prompt.paymentAccount=هرگونه اصطلاحات، شرایط یا جزئیات خاصی که می‌خواهید همراه با پیشنهادات شما برای این حساب پرداخت نمایش داده شود را تعریف کنید (کاربران قبل از پذیرش پیشنهادات این اطلاعات را مشاهده خواهند کرد). +payment.shared.extraInfo.prompt.offer=هر اصطلاح، شرایط یا جزئیات خاصی که مایلید همراه با پیشنهاد خود نمایش داده شود را تعریف کنید. +payment.shared.extraInfo.noDeposit=جزئیات تماس و شرایط پیشنهاد payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=باز کردن صفحه وب payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} -payment.f2f.offerbook.tooltip.extra=اطلاعات اضافی: {0} +payment.shared.extraInfo.tooltip=اطلاعات اضافی: {0} payment.japan.bank=بانک payment.japan.branch=Branch diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 7ee0f6a420..858cbee06c 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -2034,11 +2034,14 @@ payment.f2f.city=Ville pour la rencontre en face à face payment.f2f.city.prompt=La ville sera affichée en même temps que l'ordre payment.shared.optionalExtra=Informations complémentaires facultatives payment.shared.extraInfo=Informations complémentaires -payment.shared.extraInfo.prompt=Définissez n'importe quels termes spécifiques, conditons ou détails que vous souhaiteriez voir affichés avec vos offres pour ce compte de paiement (les utilisateurs verront ces informations avant d'accepter les offres). +payment.shared.extraInfo.offer=Informations supplémentaires sur l'offre +payment.shared.extraInfo.prompt.paymentAccount=Définissez n'importe quels termes spécifiques, conditons ou détails que vous souhaiteriez voir affichés avec vos offres pour ce compte de paiement (les utilisateurs verront ces informations avant d'accepter les offres). +payment.shared.extraInfo.prompt.offer=Définissez tous les termes, conditions ou détails spéciaux que vous souhaitez afficher avec votre offre. +payment.shared.extraInfo.noDeposit=Coordonnées et conditions de l'offre payment.f2f.info=Les transactions en 'face à face' ont des règles différentes et comportent des risques différents de ceux des transactions en ligne.\n\nLes principales différences sont les suivantes:\n● Les pairs de trading doivent échanger des informations sur le lieu et l'heure de la réunion en utilisant les coordonnées de contanct qu'ils ont fournies.\n● Les pairs de trading doivent apporter leur ordinateur portable et faire la confirmation du 'paiement envoyé' et du 'paiement reçu' sur le lieu de la réunion.\n● Si un maker a des 'termes et conditions' spéciaux, il doit les indiquer dans le champ 'Informations supplémentaires' dans le compte.\n● En acceptant une offre, le taker accepte les 'termes et conditions' du maker.\n● En cas de litige, le médiateur ou l'arbitre ne peut pas beaucoup aider car il est généralement difficile d'obtenir des preuves irréfutables de ce qui s'est passé lors de la réunion. Dans ce cas, les fonds en XMR peuvent être bloqué s indéfiniment tant que les pairs ne parviennent pas à un accord.\n\nPour vous assurer de bien comprendre les spécificités des transactions 'face à face', veuillez lire les instructions et les recommandations à [LIEN:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Ouvrir la page web payment.f2f.offerbook.tooltip.countryAndCity=Pays et ville: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Informations complémentaires: {0} +payment.shared.extraInfo.tooltip=Informations complémentaires: {0} payment.japan.bank=Banque payment.japan.branch=Filiale diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 495c2380f8..f4dff1e8f5 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -2010,11 +2010,14 @@ payment.f2f.city=Città per l'incontro 'Faccia a faccia' payment.f2f.city.prompt=La città verrà visualizzata con l'offerta payment.shared.optionalExtra=Ulteriori informazioni opzionali payment.shared.extraInfo=Informazioni aggiuntive -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=Informazioni aggiuntive sull'offerta +payment.shared.extraInfo.prompt.paymentAccount=Definisci eventuali termini, condizioni o dettagli speciali che desideri vengano visualizzati con le tue offerte per questo account di pagamento (gli utenti vedranno queste informazioni prima di accettare le offerte). +payment.shared.extraInfo.prompt.offer=Definisci eventuali termini, condizioni o dettagli speciali che desideri mostrare con la tua offerta. +payment.shared.extraInfo.noDeposit=Dettagli di contatto e termini dell'offerta payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Apri sito web payment.f2f.offerbook.tooltip.countryAndCity=Paese e città: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Ulteriori informazioni: {0} +payment.shared.extraInfo.tooltip=Ulteriori informazioni: {0} payment.japan.bank=Banca payment.japan.branch=Filiale diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index b31773063e..f4348c9e68 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -2032,11 +2032,14 @@ payment.f2f.city=「対面」で会うための市区町村 payment.f2f.city.prompt=オファーとともに市区町村が表示されます payment.shared.optionalExtra=オプションの追加情報 payment.shared.extraInfo=追加情報 -payment.shared.extraInfo.prompt=この支払いアカウントのオファーと一緒に表示したい特別な契約条件または詳細を定義して下さい(オファーを受ける前に、ユーザはこの情報を見れます)。 +payment.shared.extraInfo.offer=追加のオファー情報 +payment.shared.extraInfo.prompt.paymentAccount=この支払いアカウントのオファーと一緒に表示したい特別な契約条件または詳細を定義して下さい(オファーを受ける前に、ユーザはこの情報を見れます)。 +payment.shared.extraInfo.prompt.offer=提供内容と共に表示したい特別な用語、条件、または詳細を定義してください。 +payment.shared.extraInfo.noDeposit=連絡先詳細およびオファー条件 payment.f2f.info=「対面」トレードには違うルールがあり、オンライントレードとは異なるリスクを伴います。\n\n主な違いは以下の通りです。\n●取引者は、提供される連絡先の詳細を使用して、出会う場所と時間に関する情報を交換する必要があります。\n●取引者は自分のノートパソコンを持ってきて、集合場所で「送金」と「入金」の確認をする必要があります。\n●メイカーに特別な「取引条件」がある場合は、アカウントの「追加情報」テキストフィールドにその旨を記載する必要があります。\n●オファーを受けると、テイカーはメイカーの「トレード条件」に同意したものとします。\n●係争が発生した場合、集合場所で何が起きたのかについての改ざん防止証明を入手することは通常困難であるため、調停者や調停人はあまりサポートをできません。このような場合、XMRの資金は無期限に、または取引者が合意に達するまでロックされる可能性があります。\n\n「対面」トレードでの違いを完全に理解しているか確認するためには、次のURLにある手順と推奨事項をお読みください:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Webページを開く payment.f2f.offerbook.tooltip.countryAndCity=国と都市: {0} / {1} -payment.f2f.offerbook.tooltip.extra=追加情報: {0} +payment.shared.extraInfo.tooltip=追加情報: {0} payment.japan.bank=銀行 payment.japan.branch=支店 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 6ac9da556c..9a1f111b62 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -2017,11 +2017,14 @@ payment.f2f.city=Cidade para se encontrar 'Cara-a-cara' payment.f2f.city.prompt=A cidade será exibida na oferta payment.shared.optionalExtra=Informações adicionais opcionais payment.shared.extraInfo=Informações adicionais -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=Informações adicionais sobre a oferta +payment.shared.extraInfo.prompt.paymentAccount=Defina quaisquer termos, condições ou detalhes especiais que você gostaria que fossem exibidos com suas ofertas para esta conta de pagamento (os usuários verão estas informações antes de aceitar as ofertas). +payment.shared.extraInfo.prompt.offer=Defina quaisquer termos, condições ou detalhes especiais que você gostaria de exibir com sua oferta. +payment.shared.extraInfo.noDeposit=Detalhes de contato e termos da oferta payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Abrir site payment.f2f.offerbook.tooltip.countryAndCity=País e cidade: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Informações adicionais: {0} +payment.shared.extraInfo.tooltip=Informações adicionais: {0} payment.japan.bank=Banco payment.japan.branch=Ramo diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 45cd1170dc..3165205bd5 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -2007,11 +2007,14 @@ payment.f2f.city=Cidade para o encontro 'Face à face' payment.f2f.city.prompt=A cidade será exibida com a oferta payment.shared.optionalExtra=Informação adicional opcional payment.shared.extraInfo=Informação adicional -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=Informações adicionais sobre a oferta +payment.shared.extraInfo.prompt.paymentAccount=Defina quaisquer termos especiais, condições ou detalhes que você gostaria de exibir com suas ofertas para esta conta de pagamento (os usuários verão essas informações antes de aceitar as ofertas). +payment.shared.extraInfo.prompt.offer=Defina quaisquer termos, condições ou detalhes especiais que você gostaria de exibir com sua oferta. +payment.shared.extraInfo.noDeposit=Detalhes de contato e termos da oferta payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Abrir página web payment.f2f.offerbook.tooltip.countryAndCity=País e cidade: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Informação adicional: {0} +payment.shared.extraInfo.tooltip=Informação adicional: {0} payment.japan.bank=Banco payment.japan.branch=Agência diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index ac05e10b83..0e693049c7 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -2008,11 +2008,14 @@ payment.f2f.city=Город для личной встречи payment.f2f.city.prompt=Город будет указан в предложении payment.shared.optionalExtra=Дополнительная необязательная информация payment.shared.extraInfo=Дополнительная информация -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=Дополнительная информация о предложении +payment.shared.extraInfo.prompt.paymentAccount=Определите любые специальные термины, условия или детали, которые вы хотите, чтобы отображались с вашими предложениями для этого платежного аккаунта (пользователи увидят эту информацию перед принятием предложений). +payment.shared.extraInfo.prompt.offer=Определите любые специальные условия, требования или детали, которые вы хотели бы указать в своем предложении. +payment.shared.extraInfo.noDeposit=Контактные данные и условия предложения payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Открыть веб-страницу payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Дополнительная информация: {0} +payment.shared.extraInfo.tooltip=Дополнительная информация: {0} payment.japan.bank=Банк payment.japan.branch=Branch diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 5e25ee8149..5863219f9c 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -2008,11 +2008,14 @@ payment.f2f.city=เมืองสำหรับการประชุมแ payment.f2f.city.prompt=ชื่อเมืองจะแสดงพร้อมกับข้อเสนอ payment.shared.optionalExtra=ข้อมูลตัวเลือกเพิ่มเติม payment.shared.extraInfo=ข้อมูลเพิ่มเติม -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=ข้อมูลเพิ่มเติมเกี่ยวกับข้อเสนอ +payment.shared.extraInfo.prompt.paymentAccount=กำหนดคำศัพท์ เงื่อนไข หรือรายละเอียดพิเศษใดๆ ที่คุณต้องการให้แสดงพร้อมข้อเสนอของคุณสำหรับบัญชีการชำระเงินนี้ (ผู้ใช้จะเห็นข้อมูลนี้ก่อนที่จะยอมรับข้อเสนอ) +payment.shared.extraInfo.prompt.offer=กำหนดเงื่อนไขพิเศษ ข้อกำหนด หรือรายละเอียดใด ๆ ที่คุณต้องการแสดงพร้อมกับข้อเสนอของคุณ +payment.shared.extraInfo.noDeposit=รายละเอียดการติดต่อและเงื่อนไขข้อเสนอ payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=เปิดหน้าเว็บ payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} -payment.f2f.offerbook.tooltip.extra=ข้อมูลเพิ่มเติม: {0} +payment.shared.extraInfo.tooltip=ข้อมูลเพิ่มเติม: {0} payment.japan.bank=ธนาคาร payment.japan.branch=Branch diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 4ff0b4f610..d506f428a8 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -3015,7 +3015,10 @@ payment.f2f.city='Yüz yüze' buluşma için şehir payment.f2f.city.prompt=Şehir teklifle birlikte gösterilecektir payment.shared.optionalExtra=İsteğe bağlı ek bilgi payment.shared.extraInfo=Ek bilgi -payment.shared.extraInfo.prompt=Bu ödeme hesabınız için tekliflerinize eklemek istediğiniz özel şart, koşul veya detayları tanımlayın (kullanıcılar bu bilgileri teklifleri kabul etmeden önce görecektir). +payment.shared.extraInfo.offer=Ek teklif bilgileri +payment.shared.extraInfo.prompt.paymentAccount=Bu ödeme hesabınız için tekliflerinize eklemek istediğiniz özel şart, koşul veya detayları tanımlayın (kullanıcılar bu bilgileri teklifleri kabul etmeden önce görecektir). +payment.shared.extraInfo.prompt.offer=Teklifinizle birlikte göstermek istediğiniz özel terimleri, koşulları veya detayları tanımlayın. +payment.shared.extraInfo.noDeposit=İletişim detayları ve teklif şartları payment.f2f.info='Yüz Yüze' ticaretler farklı kurallara sahiptir ve çevrimiçi işlemlerden farklı riskler içerir.\n\n\ Başlıca farklar şunlardır:\n\ ● Ticaret eşleri, sağlanan iletişim bilgilerini kullanarak buluşma yeri ve zamanını paylaşmalıdır.\n\ @@ -3029,7 +3032,7 @@ payment.f2f.info='Yüz Yüze' ticaretler farklı kurallara sahiptir ve çevrimi ve tavsiyeleri okuyun: [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/F2F] payment.f2f.info.openURL=Web sayfasını aç payment.f2f.offerbook.tooltip.countryAndCity=Ülke ve şehir: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Ek bilgi: {0} +payment.shared.extraInfo.tooltip=Ek bilgi: {0} payment.ifsc=IFS Kodu payment.ifsc.validation=IFSC formatı: XXXX0999999 diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 7ea2eb9d27..5476c67d17 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -2010,11 +2010,14 @@ payment.f2f.city=Thành phố để gặp mặt trực tiếp payment.f2f.city.prompt=Thành phố sẽ được hiển thị cùng báo giá payment.shared.optionalExtra=Thông tin thêm tuỳ chọn. payment.shared.extraInfo=thông tin thêm -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=Thông tin bổ sung về ưu đãi +payment.shared.extraInfo.prompt.paymentAccount=Xác định bất kỳ điều khoản, điều kiện hoặc chi tiết đặc biệt nào bạn muốn hiển thị cùng với các ưu đãi của mình cho tài khoản thanh toán này (người dùng sẽ thấy thông tin này trước khi chấp nhận các ưu đãi). +payment.shared.extraInfo.prompt.offer=Xác định bất kỳ thuật ngữ, điều kiện hoặc chi tiết đặc biệt nào bạn muốn hiển thị cùng với đề nghị của mình. +payment.shared.extraInfo.noDeposit=Chi tiết liên hệ và điều khoản ưu đãi payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Mở trang web payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Thông tin thêm: {0} +payment.shared.extraInfo.tooltip=Thông tin thêm: {0} payment.japan.bank=Ngân hàng payment.japan.branch=Branch diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 9f3e495ba7..2e5a0c9945 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -2017,11 +2017,14 @@ payment.f2f.city=“面对面”会议的城市 payment.f2f.city.prompt=城市将与报价一同显示 payment.shared.optionalExtra=可选的附加信息 payment.shared.extraInfo=附加信息 -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=附加报价信息 +payment.shared.extraInfo.prompt.paymentAccount=定义您希望在此支付账户的报价中显示的任何特殊术语、条件或细节(用户在接受报价之前将看到这些信息)。 +payment.shared.extraInfo.prompt.offer=定义您希望随您的报价一起显示的任何特殊条款、条件或详细信息。 +payment.shared.extraInfo.noDeposit=联系方式和优惠条款 payment.f2f.info=与网上交易相比,“面对面”交易有不同的规则,也有不同的风险。\n\n主要区别是:\n●交易伙伴需要使用他们提供的联系方式交换关于会面地点和时间的信息。\n●交易双方需要携带笔记本电脑,在会面地点确认“已发送付款”和“已收到付款”。\n●如果交易方有特殊的“条款和条件”,他们必须在账户的“附加信息”文本框中声明这些条款和条件。\n●在发生争议时,调解员或仲裁员不能提供太多帮助,因为通常很难获得有关会面上所发生情况的篡改证据。在这种情况下,XMR 资金可能会被无限期锁定,或者直到交易双方达成协议。\n\n为确保您完全理解“面对面”交易的不同之处,请阅读以下说明和建议:“https://docs.haveno.exchange/trading-rules.html#f2f-trading” payment.f2f.info.openURL=打开网页 payment.f2f.offerbook.tooltip.countryAndCity=国家或地区及城市:{0} / {1} -payment.f2f.offerbook.tooltip.extra=附加信息:{0} +payment.shared.extraInfo.tooltip=附加信息:{0} payment.japan.bank=银行 payment.japan.branch=分行 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index c802eba7b8..5bca299cfc 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -2011,11 +2011,14 @@ payment.f2f.city=“面對面”會議的城市 payment.f2f.city.prompt=城市將與報價一同顯示 payment.shared.optionalExtra=可選的附加信息 payment.shared.extraInfo=附加信息 -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=額外的優惠資訊 +payment.shared.extraInfo.prompt.paymentAccount=定義您希望在此付款帳戶的報價中顯示的任何特殊術語、條件或細節(用戶在接受報價之前將看到這些資訊)。 +payment.shared.extraInfo.prompt.offer=定義您希望在您的報價中顯示的任何特殊條款、條件或詳細資訊。 +payment.shared.extraInfo.noDeposit=聯絡詳情及優惠條款 payment.f2f.info=與網上交易相比,“面對面”交易有不同的規則,也有不同的風險。\n\n主要區別是:\n●交易夥伴需要使用他們提供的聯繫方式交換關於會面地點和時間的信息。\n●交易雙方需要攜帶筆記本電腦,在會面地點確認“已發送付款”和“已收到付款”。\n●如果交易方有特殊的“條款和條件”,他們必須在賬户的“附加信息”文本框中聲明這些條款和條件。\n●在發生爭議時,調解員或仲裁員不能提供太多幫助,因為通常很難獲得有關會面上所發生情況的篡改證據。在這種情況下,XMR 資金可能會被無限期鎖定,或者直到交易雙方達成協議。\n\n為確保您完全理解“面對面”交易的不同之處,請閲讀以下説明和建議:“https://docs.haveno.exchange/trading-rules.html#f2f-trading” payment.f2f.info.openURL=打開網頁 payment.f2f.offerbook.tooltip.countryAndCity=國家或地區及城市:{0} / {1} -payment.f2f.offerbook.tooltip.extra=附加信息:{0} +payment.shared.extraInfo.tooltip=附加信息:{0} payment.japan.bank=銀行 payment.japan.branch=分行 diff --git a/core/src/test/java/haveno/core/offer/OfferMaker.java b/core/src/test/java/haveno/core/offer/OfferMaker.java index 52084f209b..2f839e1a38 100644 --- a/core/src/test/java/haveno/core/offer/OfferMaker.java +++ b/core/src/test/java/haveno/core/offer/OfferMaker.java @@ -73,7 +73,8 @@ public class OfferMaker { 0, null, null, - null)); + null, + "My extra info")); public static final Maker btcUsdOffer = a(Offer); } diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index 7768522b23..a3ceba75db 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -156,6 +156,7 @@ class GrpcOffersService extends OffersImplBase { req.getPaymentAccountId(), req.getIsPrivateOffer(), req.getBuyerAsTakerWithoutDeposit(), + req.getExtraInfo(), offer -> { // This result handling consumer's accept operation will return // the new offer to the gRPC client after async placement is done. diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java index 234b5e88b0..615663bbbf 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java @@ -91,7 +91,7 @@ public class AustraliaPayidForm extends PaymentMethodForm { }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, - Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java index c0bb16f5e0..e1376c2643 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java @@ -79,7 +79,7 @@ public class CashAppForm extends PaymentMethodForm { }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, - Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java index aebd470d31..2a7e7f4f2a 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java @@ -65,7 +65,7 @@ public class F2FForm extends PaymentMethodForm { textArea.setMinHeight(70); textArea.setEditable(false); textArea.setId("text-area-disabled"); - textArea.setText(offer.getExtraInfo()); + textArea.setText(offer.getPaymentAccountExtraInfo()); return gridRow; } @@ -106,7 +106,7 @@ public class F2FForm extends PaymentMethodForm { }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, - Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); //extraTextArea.setValidator(f2fValidator); diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java index 8e0d48ee6e..83e4614a97 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java @@ -79,7 +79,7 @@ public class PayPalForm extends PaymentMethodForm { }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, - Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index bad2ee75f8..68c5edc733 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -105,6 +105,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { protected final ObjectProperty price = new SimpleObjectProperty<>(); protected final ObjectProperty volume = new SimpleObjectProperty<>(); protected final ObjectProperty minVolume = new SimpleObjectProperty<>(); + protected final ObjectProperty extraInfo = new SimpleObjectProperty<>(); // Percentage value of buyer security deposit. E.g. 0.01 means 1% of trade amount protected final DoubleProperty securityDepositPct = new SimpleDoubleProperty(); @@ -305,7 +306,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel { securityDepositPct.get(), paymentAccount, buyerAsTakerWithoutDeposit.get(), // private offer if buyer as taker without deposit - buyerAsTakerWithoutDeposit.get()); + buyerAsTakerWithoutDeposit.get(), + extraInfo.get()); } void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -583,6 +585,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { this.amount.set(amount); } + protected void setMinAmount(BigInteger minAmount) { + this.minAmount.set(minAmount); + } + protected void setPrice(Price price) { this.price.set(price); } @@ -595,6 +601,22 @@ public abstract class MutableOfferDataModel extends OfferDataModel { this.securityDepositPct.set(value); } + public void setMarketPriceAvailable(boolean marketPriceAvailable) { + this.marketPriceAvailable = marketPriceAvailable; + } + + public void setTriggerPrice(long triggerPrice) { + this.triggerPrice = triggerPrice; + } + + public void setReserveExactAmount(boolean reserveExactAmount) { + this.reserveExactAmount = reserveExactAmount; + } + + protected void setExtraInfo(String extraInfo) { + this.extraInfo.set(extraInfo); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @@ -627,10 +649,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return buyerAsTakerWithoutDeposit; } - protected void setMinAmount(BigInteger minAmount) { - this.minAmount.set(minAmount); - } - public ReadOnlyStringProperty getTradeCurrencyCode() { return tradeCurrencyCode; } @@ -670,10 +688,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return totalToPay; } - public void setMarketPriceAvailable(boolean marketPriceAvailable) { - this.marketPriceAvailable = marketPriceAvailable; - } - public BigInteger getMaxMakerFee() { return HavenoUtils.multiply(amount.get(), buyerAsTakerWithoutDeposit.get() ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT); } @@ -687,11 +701,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return getSecurityDeposit().compareTo(Restrictions.getMinSecurityDeposit()) <= 0; } - public void setTriggerPrice(long triggerPrice) { - this.triggerPrice = triggerPrice; - } - - public void setReserveExactAmount(boolean reserveExactAmount) { - this.reserveExactAmount = reserveExactAmount; + public ReadOnlyObjectProperty getExtraInfo() { + return extraInfo; } } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 0bcfa79050..35a2da8b60 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -44,6 +44,7 @@ import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.BalanceTextField; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.FundsTextField; +import haveno.desktop.components.HavenoTextArea; import haveno.desktop.components.InfoInputTextField; import haveno.desktop.components.InputTextField; import haveno.desktop.components.TitledGroupBg; @@ -75,6 +76,7 @@ import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; +import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; @@ -126,7 +128,7 @@ public abstract class MutableOfferView> exten private ScrollPane scrollPane; protected GridPane gridPane; - private TitledGroupBg payFundsTitledGroupBg, setDepositTitledGroupBg, paymentTitledGroupBg; + private TitledGroupBg payFundsTitledGroupBg, setDepositTitledGroupBg, extraInfoTitledGroupBg, paymentTitledGroupBg; protected TitledGroupBg amountTitledGroupBg; private BusyAnimation waitingForFundsSpinner; private AutoTooltipButton nextButton, cancelButton1, cancelButton2, placeOfferButton, fundFromSavingsWalletButton; @@ -138,6 +140,7 @@ public abstract class MutableOfferView> exten private BalanceTextField balanceTextField; private ToggleButton reserveExactAmountSlider; private ToggleButton buyerAsTakerWithoutDepositSlider; + protected TextArea extraInfoTextArea; private FundsTextField totalToPayTextField; private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel, waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescriptionLabel, tradeFeeDescriptionLabel, @@ -156,10 +159,10 @@ public abstract class MutableOfferView> exten private ChangeListener amountFocusedListener, minAmountFocusedListener, volumeFocusedListener, securityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener, priceAsPercentageFocusedListener, getShowWalletFundedNotificationListener, - isMinSecurityDepositListener, buyerAsTakerWithoutDepositListener, triggerPriceFocusedListener; + isMinSecurityDepositListener, buyerAsTakerWithoutDepositListener, triggerPriceFocusedListener, extraInfoFocusedListener; private ChangeListener missingCoinListener; private ChangeListener tradeCurrencyCodeListener, errorMessageListener, - marketPriceMarginListener, volumeListener, securityDepositInXMRListener; + marketPriceMarginListener, volumeListener, securityDepositInXMRListener, extraInfoListener; private ChangeListener marketPriceAvailableListener; private EventHandler currencyComboBoxSelectionHandler, paymentAccountsComboBoxSelectionHandler; private OfferView.CloseHandler closeHandler; @@ -202,6 +205,8 @@ public abstract class MutableOfferView> exten addPaymentGroup(); addAmountPriceGroup(); addOptionsGroup(); + addExtraInfoGroup(); + addNextButtons(); addFundingGroup(); createListeners(); @@ -257,6 +262,8 @@ public abstract class MutableOfferView> exten triggerPriceInfoInputTextField.setContentForPopOver(popOverLabel, AwesomeIcon.SHIELD); buyerAsTakerWithoutDepositSlider.setSelected(model.dataModel.getBuyerAsTakerWithoutDeposit().get()); + + extraInfoTextArea.setText(model.dataModel.extraInfo.get()); } } @@ -389,6 +396,11 @@ public abstract class MutableOfferView> exten buyerAsTakerWithoutDepositSlider.setVisible(false); buyerAsTakerWithoutDepositSlider.setManaged(false); + extraInfoTitledGroupBg.setVisible(false); + extraInfoTitledGroupBg.setManaged(false); + extraInfoTextArea.setVisible(false); + extraInfoTextArea.setManaged(false); + updateQrCode(); model.onShowPayFundsScreen(() -> { @@ -571,6 +583,7 @@ public abstract class MutableOfferView> exten securityDepositLabel.textProperty().bind(model.securityDepositLabel); tradeFeeInXmrLabel.textProperty().bind(model.tradeFeeInXmrWithFiat); tradeFeeDescriptionLabel.textProperty().bind(model.tradeFeeDescription); + extraInfoTextArea.textProperty().bindBidirectional(model.extraInfo); // Validation amountTextField.validationResultProperty().bind(model.amountValidationResult); @@ -621,6 +634,7 @@ public abstract class MutableOfferView> exten tradeFeeDescriptionLabel.textProperty().unbind(); tradeFeeInXmrLabel.visibleProperty().unbind(); tradeFeeDescriptionLabel.visibleProperty().unbind(); + extraInfoTextArea.textProperty().unbindBidirectional(model.extraInfo); // Validation amountTextField.validationResultProperty().unbind(); @@ -694,11 +708,14 @@ public abstract class MutableOfferView> exten model.onFocusOutSecurityDepositTextField(oldValue, newValue); securityDepositInputTextField.setText(model.securityDeposit.get()); }; - triggerPriceFocusedListener = (o, oldValue, newValue) -> { model.onFocusOutTriggerPriceTextField(oldValue, newValue); triggerPriceInputTextField.setText(model.triggerPrice.get()); }; + extraInfoFocusedListener = (observable, oldValue, newValue) -> { + model.onFocusOutExtraInfoTextField(oldValue, newValue); + extraInfoTextArea.setText(model.extraInfo.get()); + }; errorMessageListener = (o, oldValue, newValue) -> { if (model.createOfferCanceled) return; @@ -822,6 +839,12 @@ public abstract class MutableOfferView> exten buyerAsTakerWithoutDepositListener = ((observable, oldValue, newValue) -> { updateSecurityDepositLabels(); }); + + extraInfoListener = (observable, oldValue, newValue) -> { + if (newValue != null && !newValue.equals("")) { + // no action + } + }; } private void updateSecurityDepositLabels() { @@ -881,6 +904,7 @@ public abstract class MutableOfferView> exten model.securityDepositInXMR.addListener(securityDepositInXMRListener); model.isMinSecurityDeposit.addListener(isMinSecurityDepositListener); model.getDataModel().buyerAsTakerWithoutDeposit.addListener(buyerAsTakerWithoutDepositListener); + model.getDataModel().extraInfo.addListener(extraInfoListener); // focus out amountTextField.focusedProperty().addListener(amountFocusedListener); @@ -890,6 +914,7 @@ public abstract class MutableOfferView> exten marketBasedPriceTextField.focusedProperty().addListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().addListener(volumeFocusedListener); securityDepositInputTextField.focusedProperty().addListener(securityDepositFocusedListener); + extraInfoTextArea.focusedProperty().addListener(extraInfoFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().addListener(getShowWalletFundedNotificationListener); @@ -914,6 +939,7 @@ public abstract class MutableOfferView> exten model.securityDepositInXMR.removeListener(securityDepositInXMRListener); model.isMinSecurityDeposit.removeListener(isMinSecurityDepositListener); model.getDataModel().buyerAsTakerWithoutDeposit.removeListener(buyerAsTakerWithoutDepositListener); + model.getDataModel().extraInfo.removeListener(extraInfoListener); // focus out amountTextField.focusedProperty().removeListener(amountFocusedListener); @@ -923,6 +949,7 @@ public abstract class MutableOfferView> exten marketBasedPriceTextField.focusedProperty().removeListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().removeListener(volumeFocusedListener); securityDepositInputTextField.focusedProperty().removeListener(securityDepositFocusedListener); + extraInfoTextArea.focusedProperty().removeListener(extraInfoFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().removeListener(getShowWalletFundedNotificationListener); @@ -1062,7 +1089,30 @@ public abstract class MutableOfferView> exten }); GridPane.setHalignment(buyerAsTakerWithoutDepositSlider, HPos.LEFT); GridPane.setMargin(buyerAsTakerWithoutDepositSlider, new Insets(0, 0, 0, 0)); + } + private void addExtraInfoGroup() { + + extraInfoTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, + Res.get("payment.shared.optionalExtra"), 25 + heightAdjustment); + GridPane.setColumnSpan(extraInfoTitledGroupBg, 3); + + extraInfoTextArea = new HavenoTextArea(); + extraInfoTextArea.setPromptText(Res.get("payment.shared.extraInfo.prompt.offer")); + extraInfoTextArea.getStyleClass().add("text-area"); + extraInfoTextArea.setWrapText(true); + extraInfoTextArea.setPrefHeight(75); + extraInfoTextArea.setMinHeight(75); + extraInfoTextArea.setMaxHeight(75); + GridPane.setRowIndex(extraInfoTextArea, gridRow); + GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING); + GridPane.setColumnIndex(extraInfoTextArea, 0); + GridPane.setHalignment(extraInfoTextArea, HPos.LEFT); + GridPane.setMargin(extraInfoTextArea, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); + gridPane.getChildren().add(extraInfoTextArea); + } + + private void addNextButtons() { Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++gridRow, Res.get("shared.nextStep"), Res.get("shared.cancel")); nextButton = (AutoTooltipButton) tuple.first; @@ -1105,16 +1155,26 @@ public abstract class MutableOfferView> exten protected void hideOptionsGroup() { setDepositTitledGroupBg.setVisible(false); setDepositTitledGroupBg.setManaged(false); - nextButton.setVisible(false); - nextButton.setManaged(false); - cancelButton1.setVisible(false); - cancelButton1.setManaged(false); securityDepositAndFeeBox.setVisible(false); securityDepositAndFeeBox.setManaged(false); buyerAsTakerWithoutDepositSlider.setVisible(false); buyerAsTakerWithoutDepositSlider.setManaged(false); } + protected void hideExtraInfoGroup() { + extraInfoTitledGroupBg.setVisible(false); + extraInfoTitledGroupBg.setManaged(false); + extraInfoTextArea.setVisible(false); + extraInfoTextArea.setManaged(false); + } + + protected void hideNextButtons() { + nextButton.setVisible(false); + nextButton.setManaged(false); + cancelButton1.setVisible(false); + cancelButton1.setManaged(false); + } + private VBox getSecurityDepositBox() { Tuple3 tuple = getEditableValueBoxWithInfo( Res.get("createOffer.securityDeposit.prompt")); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index d92a0322d2..2907fe8979 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -144,6 +144,7 @@ public abstract class MutableOfferViewModel ext final StringProperty waitingForFundsText = new SimpleStringProperty(""); final StringProperty triggerPriceDescription = new SimpleStringProperty(""); final StringProperty percentagePriceDescription = new SimpleStringProperty(""); + final StringProperty extraInfo = new SimpleStringProperty(""); final BooleanProperty isPlaceOfferButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty cancelButtonDisabled = new SimpleBooleanProperty(); @@ -166,6 +167,7 @@ public abstract class MutableOfferViewModel ext private ChangeListener priceStringListener, marketPriceMarginStringListener; private ChangeListener volumeStringListener; private ChangeListener securityDepositStringListener; + private ChangeListener extraInfoStringListener; private ChangeListener amountListener; private ChangeListener minAmountListener; @@ -238,6 +240,7 @@ public abstract class MutableOfferViewModel ext dataModel.calculateTotalToPay(); updateButtonDisableState(); updateSpinnerInfo(); + setExtraInfoToModel(); }, 100, TimeUnit.MILLISECONDS); } @@ -498,6 +501,14 @@ public abstract class MutableOfferViewModel ext updateButtonDisableState(); }; + extraInfoStringListener = (ov, oldValue, newValue) -> { + if (newValue != null) { + extraInfo.set(newValue); + } else { + extraInfo.set(""); + } + }; + isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); /* feeFromFundingTxListener = (ov, oldValue, newValue) -> { updateButtonDisableState(); @@ -542,6 +553,7 @@ public abstract class MutableOfferViewModel ext dataModel.getUseMarketBasedPrice().addListener(useMarketBasedPriceListener); volume.addListener(volumeStringListener); securityDeposit.addListener(securityDepositStringListener); + extraInfo.addListener(extraInfoStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().addListener(amountListener); @@ -550,6 +562,7 @@ public abstract class MutableOfferViewModel ext dataModel.getVolume().addListener(volumeListener); dataModel.getSecurityDepositPct().addListener(securityDepositAsDoubleListener); dataModel.getBuyerAsTakerWithoutDeposit().addListener(buyerAsTakerWithoutDepositListener); + dataModel.getExtraInfo().addListener(extraInfoStringListener); // dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().addListener(isWalletFundedListener); @@ -565,6 +578,7 @@ public abstract class MutableOfferViewModel ext dataModel.getUseMarketBasedPrice().removeListener(useMarketBasedPriceListener); volume.removeListener(volumeStringListener); securityDeposit.removeListener(securityDepositStringListener); + extraInfo.removeListener(extraInfoStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().removeListener(amountListener); @@ -827,6 +841,12 @@ public abstract class MutableOfferViewModel ext } } + public void onFocusOutExtraInfoTextField(boolean oldValue, boolean newValue) { + if (oldValue && !newValue) { + dataModel.setExtraInfo(extraInfo.get()); + } + } + void onFocusOutTriggerPriceTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { onTriggerPriceTextFieldChanged(); @@ -1233,6 +1253,14 @@ public abstract class MutableOfferViewModel ext } } + private void setExtraInfoToModel() { + if (extraInfo.get() != null && !extraInfo.get().isEmpty()) { + dataModel.setExtraInfo(extraInfo.get()); + } else { + dataModel.setExtraInfo(null); + } + } + private void validateAndSetSecurityDepositToModel() { // If the security deposit in the model is not valid percent String value = FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get()); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index 967e29b6e3..d82b854d2f 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -483,8 +483,6 @@ abstract class OfferBookViewModel extends ActivatableViewModel { if (countryCode != null) { result += "\n" + Res.get("payment.f2f.offerbook.tooltip.countryAndCity", CountryUtil.getNameByCode(countryCode), offer.getF2FCity()); - - result += "\n" + Res.get("payment.f2f.offerbook.tooltip.extra", offer.getExtraInfo()); } } else { if (countryCode != null) { @@ -514,6 +512,8 @@ abstract class OfferBookViewModel extends ActivatableViewModel { result += "\n" + Res.getWithCol("shared.acceptedBanks") + " " + Joiner.on(", ").join(acceptedBanks); } } + if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) + result += "\n" + Res.get("payment.shared.extraInfo.tooltip", offer.getCombinedExtraInfo()); } return result; } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index e0385bb5b3..dc11216f53 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -69,6 +69,7 @@ import static haveno.desktop.util.FormBuilder.add2ButtonsWithBox; import static haveno.desktop.util.FormBuilder.addAddressTextField; import static haveno.desktop.util.FormBuilder.addBalanceTextField; import static haveno.desktop.util.FormBuilder.addComboBoxTopLabelTextField; +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addFundsTextfield; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.getEditableValueBox; @@ -98,6 +99,7 @@ import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; +import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; @@ -125,6 +127,7 @@ public class TakeOfferView extends ActivatableViewAndModel paymentAccountsComboBox; + private TextArea extraInfoTextArea; private Label amountDescriptionLabel, paymentMethodLabel, priceCurrencyLabel, priceAsPercentageLabel, @@ -160,7 +164,7 @@ public class TakeOfferView extends ActivatableViewAndModel paymentAccountWarningDisplayed = new HashMap<>(); - private boolean offerDetailsWindowDisplayed, zelleWarningDisplayed, fasterPaymentsWarningDisplayed, + private boolean offerDetailsWindowDisplayed, extraInfoPopupDisplayed, zelleWarningDisplayed, fasterPaymentsWarningDisplayed, takeOfferFromUnsignedAccountWarningDisplayed, payByMailWarningDisplayed, cashAtAtmWarningDisplayed, australiaPayidWarningDisplayed, paypalWarningDisplayed, cashAppWarningDisplayed, F2FWarningDisplayed; private SimpleBooleanProperty errorPopupDisplayed; @@ -267,16 +271,11 @@ public class TakeOfferView extends ActivatableViewAndModel close(false)) .show(); + + if (offer.hasBuyerAsTakerWithoutDeposit() && offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) { + + // attach extra info text area + extraInfoTextArea = addCompactTopLabelTextArea(gridPane, ++lastGridRowNoFundingRequired, Res.get("payment.shared.extraInfo.noDeposit"), "").second; + extraInfoTextArea.setText(offer.getCombinedExtraInfo()); + extraInfoTextArea.getStyleClass().add("text-area"); + extraInfoTextArea.setWrapText(true); + extraInfoTextArea.setPrefHeight(75); + extraInfoTextArea.setMinHeight(75); + extraInfoTextArea.setMaxHeight(150); + extraInfoTextArea.setEditable(false); + GridPane.setRowIndex(extraInfoTextArea, lastGridRowNoFundingRequired); + GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING); + GridPane.setColumnIndex(extraInfoTextArea, 0); + + // move up take offer buttons + GridPane.setRowIndex(takeOfferBox, lastGridRowNoFundingRequired + 1); + GridPane.setMargin(takeOfferBox, new Insets(15, 0, 0, 0)); + } } @Override @@ -871,6 +890,7 @@ public class TakeOfferView extends ActivatableViewAndModel { + new GenericMessageWindow() + .preamble(Res.get("payment.tradingRestrictions")) + .instruction(offer.getCombinedExtraInfo()) + .actionButtonText(Res.get("shared.iConfirm")) + .closeButtonText(Res.get("shared.close")) + .width(Layout.INITIAL_WINDOW_WIDTH) + .onClose(() -> close(false)) + .show(); + }, 500, TimeUnit.MILLISECONDS); + } + } private void maybeShowTakeOfferFromUnsignedAccountWarning(Offer offer) { // warn if you are selling BTC to unsigned account (#5343) @@ -1151,108 +1186,6 @@ public class TakeOfferView extends ActivatableViewAndModel { - new GenericMessageWindow() - .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getExtraInfo()) - .actionButtonText(Res.get("shared.iConfirm")) - .closeButtonText(Res.get("shared.close")) - .width(Layout.INITIAL_WINDOW_WIDTH) - .onClose(() -> close(false)) - .show(); - }, 500, TimeUnit.MILLISECONDS); - } - } - - private void maybeShowCashAtAtmWarning(PaymentAccount paymentAccount, Offer offer) { - if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.CASH_AT_ATM_ID) && - !cashAtAtmWarningDisplayed && !offer.getExtraInfo().isEmpty()) { - cashAtAtmWarningDisplayed = true; - UserThread.runAfter(() -> { - new GenericMessageWindow() - .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getExtraInfo()) - .actionButtonText(Res.get("shared.iConfirm")) - .closeButtonText(Res.get("shared.close")) - .width(Layout.INITIAL_WINDOW_WIDTH) - .onClose(() -> close(false)) - .show(); - }, 500, TimeUnit.MILLISECONDS); - } - } - - private void maybeShowAustraliaPayidWarning(PaymentAccount paymentAccount, Offer offer) { - if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.AUSTRALIA_PAYID_ID) && - !australiaPayidWarningDisplayed && !offer.getExtraInfo().isEmpty()) { - australiaPayidWarningDisplayed = true; - UserThread.runAfter(() -> { - new GenericMessageWindow() - .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getExtraInfo()) - .actionButtonText(Res.get("shared.iConfirm")) - .closeButtonText(Res.get("shared.close")) - .width(Layout.INITIAL_WINDOW_WIDTH) - .onClose(() -> close(false)) - .show(); - }, 500, TimeUnit.MILLISECONDS); - } - } - - private void maybeShowPayPalWarning(PaymentAccount paymentAccount, Offer offer) { - if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.PAYPAL_ID) && - !paypalWarningDisplayed && !offer.getExtraInfo().isEmpty()) { - paypalWarningDisplayed = true; - UserThread.runAfter(() -> { - new GenericMessageWindow() - .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getExtraInfo()) - .actionButtonText(Res.get("shared.iConfirm")) - .closeButtonText(Res.get("shared.close")) - .width(Layout.INITIAL_WINDOW_WIDTH) - .onClose(() -> close(false)) - .show(); - }, 500, TimeUnit.MILLISECONDS); - } - } - - private void maybeShowCashAppWarning(PaymentAccount paymentAccount, Offer offer) { - if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.CASH_APP_ID) && - !cashAppWarningDisplayed && !offer.getExtraInfo().isEmpty()) { - cashAppWarningDisplayed = true; - UserThread.runAfter(() -> { - new GenericMessageWindow() - .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getExtraInfo()) - .actionButtonText(Res.get("shared.iConfirm")) - .closeButtonText(Res.get("shared.close")) - .width(Layout.INITIAL_WINDOW_WIDTH) - .onClose(() -> close(false)) - .show(); - }, 500, TimeUnit.MILLISECONDS); - } - } - - private void maybeShowF2FWarning(PaymentAccount paymentAccount, Offer offer) { - if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.F2F_ID) && - !F2FWarningDisplayed && !offer.getExtraInfo().isEmpty()) { - F2FWarningDisplayed = true; - UserThread.runAfter(() -> { - new GenericMessageWindow() - .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getExtraInfo()) - .actionButtonText(Res.get("shared.iConfirm")) - .closeButtonText(Res.get("shared.close")) - .width(Layout.INITIAL_WINDOW_WIDTH) - .onClose(() -> close(false)) - .show(); - }, 500, TimeUnit.MILLISECONDS); - } - } - private Tuple2 getTradeInputBox(HBox amountValueBox, String promptText) { Label descriptionLabel = new AutoTooltipLabel(promptText); descriptionLabel.setId("input-description-label"); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index 7732b7b70d..3ef8ca521b 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -177,19 +177,14 @@ public class OfferDetailsWindow extends Overlay { List acceptedCountryCodes = offer.getAcceptedCountryCodes(); boolean showAcceptedCountryCodes = acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty(); boolean isF2F = offer.getPaymentMethod().equals(PaymentMethod.F2F); - boolean showExtraInfo = offer.getPaymentMethod().equals(PaymentMethod.F2F) || - offer.getPaymentMethod().equals(PaymentMethod.PAY_BY_MAIL) || - offer.getPaymentMethod().equals(PaymentMethod.AUSTRALIA_PAYID)|| - offer.getPaymentMethod().equals(PaymentMethod.PAYPAL)|| - offer.getPaymentMethod().equals(PaymentMethod.CASH_APP) || - offer.getPaymentMethod().equals(PaymentMethod.CASH_AT_ATM); + boolean showOfferExtraInfo = offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty(); if (!takeOfferHandlerOptional.isPresent()) rows++; if (showAcceptedBanks) rows++; if (showAcceptedCountryCodes) rows++; - if (showExtraInfo) + if (showOfferExtraInfo) rows++; if (isF2F) rows++; @@ -320,9 +315,9 @@ public class OfferDetailsWindow extends Overlay { if (isF2F) { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("payment.f2f.city"), offer.getF2FCity()); } - if (showExtraInfo) { + if (showOfferExtraInfo) { TextArea textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("payment.shared.extraInfo"), "", 0).second; - textArea.setText(offer.getExtraInfo()); + textArea.setText(offer.getCombinedExtraInfo()); textArea.setMaxHeight(200); textArea.sceneProperty().addListener((o, oldScene, newScene) -> { if (newScene != null) { diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java index 0e3397ca54..004cc0a3f5 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java @@ -38,6 +38,7 @@ import haveno.core.xmr.wallet.BtcWalletService; import haveno.desktop.components.HavenoTextArea; import haveno.desktop.main.MainView; import haveno.desktop.main.overlays.Overlay; +import haveno.desktop.util.CssTheme; import haveno.desktop.util.DisplayUtils; import static haveno.desktop.util.DisplayUtils.getAccountWitnessDescription; import static haveno.desktop.util.FormBuilder.add2ButtonsWithBox; @@ -47,6 +48,8 @@ import static haveno.desktop.util.FormBuilder.addLabelTxIdTextField; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.Layout; import haveno.network.p2p.NodeAddress; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; @@ -165,9 +168,12 @@ public class TradeDetailsWindow extends Overlay { // second group rows = 7; + + if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) + rows++; + PaymentAccountPayload buyerPaymentAccountPayload = null; PaymentAccountPayload sellerPaymentAccountPayload = null; - if (contract != null) { rows++; @@ -219,6 +225,29 @@ public class TradeDetailsWindow extends Overlay { addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradePeersOnion"), trade.getTradePeerNodeAddress().getFullAddress()); + if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) { + TextArea textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("payment.shared.extraInfo.offer"), "", 0).second; + textArea.setText(offer.getCombinedExtraInfo()); + textArea.setMaxHeight(200); + textArea.sceneProperty().addListener((o, oldScene, newScene) -> { + if (newScene != null) { + // avoid javafx css warning + CssTheme.loadSceneStyles(newScene, CssTheme.CSS_THEME_LIGHT, false); + textArea.applyCss(); + var text = textArea.lookup(".text"); + + textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() -> { + return textArea.getFont().getSize() + text.getBoundsInLocal().getHeight(); + }, text.boundsInLocalProperty())); + + text.boundsInLocalProperty().addListener((observableBoundsAfter, boundsBefore, boundsAfter) -> { + Platform.runLater(() -> textArea.requestLayout()); + }); + } + }); + textArea.setEditable(false); + } + if (contract != null) { buyersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), buyerPaymentAccountPayload, contract.getBuyerPubKeyRing()); sellersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), sellerPaymentAccountPayload, contract.getSellerPubKeyRing()); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java index d7ab366b75..b9cd38efb5 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -166,6 +166,7 @@ class EditOfferDataModel extends MutableOfferDataModel { if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } + setExtraInfo(offer.getOfferExtraInfo()); } public void onStartEditOffer(ErrorMessageHandler errorMessageHandler) { @@ -216,7 +217,8 @@ class EditOfferDataModel extends MutableOfferDataModel { offerPayload.getProtocolVersion(), offerPayload.getArbitratorSigner(), offerPayload.getArbitratorSignature(), - offerPayload.getReserveTxKeyImages()); + offerPayload.getReserveTxKeyImages(), + newOfferPayload.getExtraInfo()); final Offer editedOffer = new Offer(editedPayload); editedOffer.setPriceFeedService(priceFeedService); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java index 60aa3c4dec..3752ab9dcb 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java @@ -90,6 +90,7 @@ public class EditOfferView extends MutableOfferView { addBindings(); hideOptionsGroup(); + hideNextButtons(); // Lock amount field as it would require bigger changes to support increased amount values. amountTextField.setDisable(true); @@ -178,7 +179,7 @@ public class EditOfferView extends MutableOfferView { private void addConfirmEditGroup() { - int tmpGridRow = 4; + int tmpGridRow = 6; final Tuple4 editOfferTuple = addButtonBusyAnimationLabelAfterGroup(gridPane, tmpGridRow++, Res.get("editOffer.confirmEdit")); final HBox editOfferConfirmationBox = editOfferTuple.fourth; diff --git a/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java index 19c2359245..971c15bff9 100644 --- a/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java @@ -94,6 +94,7 @@ public class TradesChartsViewModelTest { 0, null, null, + null, null); @BeforeEach diff --git a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java index 3b648533c0..b7cc852ee1 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java @@ -632,6 +632,7 @@ public class OfferBookViewModelTest { 0, null, null, + null, null)); } } diff --git a/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java b/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java index ae6f73ac52..2496dbdbbd 100644 --- a/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java +++ b/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java @@ -111,6 +111,7 @@ public class OfferMaker { lookup.valueOf(protocolVersion, 0), getLocalHostNodeWithPort(99999), null, + null, null)); public static final Maker xmrUsdOffer = a(Offer); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index f99a0feae5..75ad4a0658 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -527,6 +527,7 @@ message PostOfferRequest { string payment_account_id = 11; bool is_private_offer = 12; bool buyer_as_taker_without_deposit = 13; + string extra_info = 14; } message PostOfferReply { @@ -574,6 +575,7 @@ message OfferInfo { uint64 split_output_tx_fee = 31 [jstype = JS_STRING]; bool is_private_offer = 32; string challenge = 33; + string extra_info = 34; } message AvailabilityResultWithDescription { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index b52274ae57..18962d0c56 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -657,6 +657,7 @@ message OfferPayload { NodeAddress arbitrator_signer = 36; bytes arbitrator_signature = 37; repeated string reserve_tx_key_images = 38; + string extra_info = 39; } enum OfferDirection { From 9b50cd7ba71f2bd6a7b4a1e6019e1ae77a176efc Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 26 Jan 2025 21:04:05 -0500 Subject: [PATCH 106/371] fix contract mismatch by nullifying extra info empty string --- .../java/haveno/core/offer/CreateOfferService.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index 508a3f44e3..bca446827c 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -130,6 +130,9 @@ public class CreateOfferService { buyerAsTakerWithoutDeposit, extraInfo); + // must nullify empty string so contracts match + if ("".equals(extraInfo)) extraInfo = null; + // verify buyer as taker security deposit boolean isBuyerMaker = offerUtil.isBuyOffer(direction); if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) { @@ -142,14 +145,11 @@ public class CreateOfferService { if (marketPriceMargin != 0) throw new IllegalArgumentException("Cannot set market price margin with fixed price"); } - long creationTime = new Date().getTime(); - NodeAddress makerAddress = p2PService.getAddress(); + // verify price boolean useMarketBasedPriceValue = fixedPrice == null && useMarketBasedPrice && isMarketPriceAvailable(currencyCode) && !PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId()); - - // verify price if (fixedPrice == null && !useMarketBasedPriceValue) { throw new IllegalArgumentException("Must provide fixed price"); } @@ -166,6 +166,8 @@ public class CreateOfferService { challengeHash = HavenoUtils.getChallengeHash(challenge); } + long creationTime = new Date().getTime(); + NodeAddress makerAddress = p2PService.getAddress(); long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L; double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0; long amountAsLong = amount != null ? amount.longValueExact() : 0L; From a01474001448911640f2ad0ebdf45add51f27ac0 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 26 Jan 2025 09:39:17 -0500 Subject: [PATCH 107/371] rename TransferWise to Wise --- .../resources/i18n/displayStrings.properties | 20 +++++++++---------- .../i18n/displayStrings_cs.properties | 20 +++++++++---------- .../i18n/displayStrings_de.properties | 4 ++-- .../i18n/displayStrings_es.properties | 4 ++-- .../i18n/displayStrings_fa.properties | 4 ++-- .../i18n/displayStrings_fr.properties | 4 ++-- .../i18n/displayStrings_it.properties | 4 ++-- .../i18n/displayStrings_ja.properties | 4 ++-- .../i18n/displayStrings_pt-br.properties | 4 ++-- .../i18n/displayStrings_pt.properties | 4 ++-- .../i18n/displayStrings_ru.properties | 4 ++-- .../i18n/displayStrings_th.properties | 4 ++-- .../i18n/displayStrings_tr.properties | 20 +++++++++---------- .../i18n/displayStrings_vi.properties | 4 ++-- .../i18n/displayStrings_zh-hans.properties | 4 ++-- .../i18n/displayStrings_zh-hant.properties | 4 ++-- .../offer/offerbook/OfferBookViewModel.java | 2 +- 17 files changed, 57 insertions(+), 57 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index c759cd61da..05ecbdebfd 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2965,14 +2965,14 @@ The maximum trade size is $1,000 per payment.\n\n\ If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.transferwiseUsd.info.account=Due to US banking regulation, sending and receiving USD payments has more restrictions \ - than most other currencies. For this reason USD was not added to Haveno TransferWise payment method.\n\n\ -The TransferWise-USD payment method allows Haveno users to trade in USD.\n\n\ -Anyone with a Wise, formally TransferWise account, can add TransferWise-USD as a payment method in Haveno. This will \ + than most other currencies. For this reason USD was not added to Haveno Wise payment method.\n\n\ +The Wise-USD payment method allows Haveno users to trade in USD.\n\n\ +Anyone with a Wise, formally Wise account, can add Wise-USD as a payment method in Haveno. This will \ allow them to buy and sell XMR with USD.\n\n\ When trading on Haveno XMR Buyers should not use any reference for reason for payment. If reason for payment is required \ - they should only use the full name of the TransferWise-USD account owner. -payment.transferwiseUsd.info.buyer=Please send payment only to the email address in the XMR Seller's Haveno TransferWise-USD account. -payment.transferwiseUsd.info.seller=Please check that the payment received matches the XMR Buyer's name of the TransferWise-USD account in Haveno. + they should only use the full name of the Wise-USD account owner. +payment.transferwiseUsd.info.buyer=Please send payment only to the email address in the XMR Seller's Haveno Wise-USD account. +payment.transferwiseUsd.info.seller=Please check that the payment received matches the XMR Buyer's name of the Wise-USD account in Haveno. payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\ \n\ @@ -3146,9 +3146,9 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" -TRANSFERWISE_USD=TransferWise-USD +TRANSFERWISE_USD=Wise-USD # suppress inspection "UnusedProperty" PAYSERA=Paysera # suppress inspection "UnusedProperty" @@ -3242,9 +3242,9 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" -TRANSFERWISE_USD_SHORT=TransferWise-USD +TRANSFERWISE_USD_SHORT=Wise-USD # suppress inspection "UnusedProperty" PAYSERA_SHORT=Paysera # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 32f326ca91..233a9b2a39 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -2965,14 +2965,14 @@ Maximální velikost obchodu je 1 000 USD na jednu platbu.\n\n\ Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.transferwiseUsd.info.account=Vzhledem k bankovní regulaci USA má odesílání a přijímání plateb v USD více omezení \ - než u většiny ostatních měn. Z tohoto důvodu nebyl USD přidán do platební metody Haveno TransferWise.\n\n\ -Platební metoda TransferWise-USD umožňuje uživatelům Haveno obchodovat v USD.\n\n\ -Každý, kdo má účet Wise, formálně TransferWise, může přidat TransferWise-USD jako platební metodu v systému Haveno. To umožní \ + než u většiny ostatních měn. Z tohoto důvodu nebyl USD přidán do platební metody Haveno Wise.\n\n\ +Platební metoda Wise-USD umožňuje uživatelům Haveno obchodovat v USD.\n\n\ +Každý, kdo má účet Wise, formálně Wise, může přidat Wise-USD jako platební metodu v systému Haveno. To umožní \ uživateli nakupovat a prodávat XMR za USD.\n\n\ Při obchodování na Haveno by kupující XMR neměli uvádět v poznámce žádný důvod platby. Pokud je důvod platby vyžadován, \ - měli by používat pouze celé jméno majitele účtu TransferWise-USD. -payment.transferwiseUsd.info.buyer=Platbu prosím zasílejte pouze na e-mailovou adresu uvedenou v účtu Haveno TransferWise-USD prodávajícího XMR. -payment.transferwiseUsd.info.seller=Zkontrolujte, zda se přijatá platba shoduje se jménem kupujícího XMR na účtu TransferWise-USD v systému Haveno. + měli by používat pouze celé jméno majitele účtu Wise-USD. +payment.transferwiseUsd.info.buyer=Platbu prosím zasílejte pouze na e-mailovou adresu uvedenou v účtu Haveno Wise-USD prodávajícího XMR. +payment.transferwiseUsd.info.seller=Zkontrolujte, zda se přijatá platba shoduje se jménem kupujícího XMR na účtu Wise-USD v systému Haveno. payment.usPostalMoneyOrder.info=Obchodování pomocí amerických poštovních poukázek (USPMO) na Haveno vyžaduje, abyste rozuměli následujícímu:\n\ \n\ @@ -3146,9 +3146,9 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" -TRANSFERWISE_USD=TransferWise-USD +TRANSFERWISE_USD=Wise-USD # suppress inspection "UnusedProperty" PAYSERA=Paysera # suppress inspection "UnusedProperty" @@ -3242,9 +3242,9 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" -TRANSFERWISE_USD_SHORT=TransferWise-USD +TRANSFERWISE_USD_SHORT=Wise-USD # suppress inspection "UnusedProperty" PAYSERA_SHORT=Paysera # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index ef8fb9afe9..cf72fdc883 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -2128,7 +2128,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon Gift-Karte # suppress inspection "UnusedProperty" @@ -2180,7 +2180,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon Gift-Karte # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index e5ab6b4537..1cfdcf0306 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -2129,7 +2129,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Tarjeta Amazon eGift # suppress inspection "UnusedProperty" @@ -2181,7 +2181,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Tarjeta Amazon eGift # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index cd739ed362..82ab9895e8 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -2103,7 +2103,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2155,7 +2155,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 858cbee06c..17398abddf 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -2130,7 +2130,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=eCarte cadeau Amazon # suppress inspection "UnusedProperty" @@ -2182,7 +2182,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=eCarte cadeau Amazon # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index f4dff1e8f5..62cbaa9d3a 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -2106,7 +2106,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2158,7 +2158,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index f4348c9e68..226ccb1a3e 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -2128,7 +2128,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=アマゾンeGiftカード # suppress inspection "UnusedProperty" @@ -2180,7 +2180,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=アマゾンeGiftカード # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 9a1f111b62..a6a6014c13 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -2113,7 +2113,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2165,7 +2165,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 3165205bd5..01b7b47357 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -2103,7 +2103,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2155,7 +2155,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 0e693049c7..40fb02023f 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -2104,7 +2104,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2156,7 +2156,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 5863219f9c..ae6775875a 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -2104,7 +2104,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2156,7 +2156,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index d506f428a8..c24769b955 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -2952,14 +2952,14 @@ payment.strike.info.seller=Ödemenin XMR Alıcısının Haveno'da sağlanan Stri Bu limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.transferwiseUsd.info.account=ABD bankacılık düzenlemeleri nedeniyle, USD ödemeleri göndermek ve almak, çoğu diğer \ -para biriminden daha fazla sınırlamaya tabidir. Bu nedenle USD, Haveno TransferWise ödeme yöntemine eklenmemiştir.\n\n\ - TransferWise-USD ödeme yöntemi, Haveno kullanıcılarının USD cinsinden ticaret yapmasına olanak tanır.\n\n\ -Wise, eski adıyla TransferWise hesabı olan herkes, Haveno'da TransferWise-USD'yi bir ödeme yöntemi olarak ekleyebilir. \ +para biriminden daha fazla sınırlamaya tabidir. Bu nedenle USD, Haveno Wise ödeme yöntemine eklenmemiştir.\n\n\ + Wise-USD ödeme yöntemi, Haveno kullanıcılarının USD cinsinden ticaret yapmasına olanak tanır.\n\n\ +Wise, eski adıyla Wise hesabı olan herkes, Haveno'da Wise-USD'yi bir ödeme yöntemi olarak ekleyebilir. \ Bu, USD ile XMR alıp satmalarını sağlar.\n\n\ Haveno'da ticaret yaparken XMR Alıcıları, ödeme nedeni için herhangi bir referans kullanmamalıdır. Ödeme nedeni \ - gerekliyse, yalnızca TransferWise-USD hesap sahibinin tam adını kullanmalıdırlar. -payment.transferwiseUsd.info.buyer=Ödemeyi yalnızca XMR Satıcısının Haveno TransferWise-USD hesabındaki e-posta adresine gönderin. -payment.transferwiseUsd.info.seller=Alınan ödemenin, Haveno'daki TransferWise-USD hesabındaki XMR Alıcısının adıyla eşleştiğini kontrol edin. + gerekliyse, yalnızca Wise-USD hesap sahibinin tam adını kullanmalıdırlar. +payment.transferwiseUsd.info.buyer=Ödemeyi yalnızca XMR Satıcısının Haveno Wise-USD hesabındaki e-posta adresine gönderin. +payment.transferwiseUsd.info.seller=Alınan ödemenin, Haveno'daki Wise-USD hesabındaki XMR Alıcısının adıyla eşleştiğini kontrol edin. payment.usPostalMoneyOrder.info=Haveno'da ABD Posta Havale Emirleri (USPMO) kullanarak ticaret yapmak için aşağıdakileri anladığınızdan emin olmanız gerekmektedir:\n\ \n\ @@ -3133,9 +3133,9 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" -TRANSFERWISE_USD=TransferWise-USD +TRANSFERWISE_USD=Wise-USD # suppress inspection "UnusedProperty" PAYSERA=Paysera # suppress inspection "UnusedProperty" @@ -3229,9 +3229,9 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" -TRANSFERWISE_USD_SHORT=TransferWise-USD +TRANSFERWISE_USD_SHORT=Wise-USD # suppress inspection "UnusedProperty" PAYSERA_SHORT=Paysera # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 5476c67d17..45e4a0426f 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -2106,7 +2106,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2158,7 +2158,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 2e5a0c9945..2a6fac3999 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -2113,7 +2113,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=亚马逊电子礼品卡 # suppress inspection "UnusedProperty" @@ -2165,7 +2165,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=亚马逊电子礼品卡 # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 5bca299cfc..3c00232559 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -2107,7 +2107,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=亞馬遜電子禮品卡 # suppress inspection "UnusedProperty" @@ -2159,7 +2159,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=亞馬遜電子禮品卡 # suppress inspection "UnusedProperty" diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index d82b854d2f..9990573155 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -290,7 +290,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { if (!showAllPaymentMethods) { this.selectedPaymentMethod = paymentMethod; - // If we select TransferWise we switch to show all currencies as TransferWise supports + // If we select Wise we switch to show all currencies as Wise supports // sending to most currencies. if (paymentMethod.getId().equals(PaymentMethod.TRANSFERWISE_ID)) { onSetTradeCurrency(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); From 1d4dbe7ce0ddd4cef4c4fb3331464b85241cb7e7 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 27 Jan 2025 09:31:51 -0500 Subject: [PATCH 108/371] increase rate limit to get offers to 3 per second --- .../src/main/java/haveno/daemon/grpc/GrpcOffersService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index a3ceba75db..84443da4d5 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -203,9 +203,9 @@ class GrpcOffersService extends OffersImplBase { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ - put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, SECONDS)); - put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, SECONDS)); - put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 1, SECONDS)); + put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, SECONDS)); + put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, SECONDS)); + put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, SECONDS)); put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); From 97569bad37825c04bbb9b4bd46dcb23f1b81ef80 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 28 Jan 2025 10:47:23 -0500 Subject: [PATCH 109/371] do not require extra info in cardless cash form --- .../components/paymentmethods/CashAtAtmForm.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java index 728f860104..b6447a868d 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java @@ -41,12 +41,12 @@ public class CashAtAtmForm extends PaymentMethodForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { - CashAtAtmAccountPayload cbm = (CashAtAtmAccountPayload) paymentAccountPayload; + CashAtAtmAccountPayload cashAtAtmPayload = (CashAtAtmAccountPayload) paymentAccountPayload; TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, 0, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); - textExtraInfo.setText(cbm.getExtraInfo()); + textExtraInfo.setText(cashAtAtmPayload.getExtraInfo()); return gridRow; } @@ -79,7 +79,11 @@ public class CashAtAtmForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(cashAtAtmAccount.getExtraInfo().substring(0, Math.min(50, cashAtAtmAccount.getExtraInfo().length()))); + if (cashAtAtmAccount.getExtraInfo() != null && !cashAtAtmAccount.getExtraInfo().isEmpty()) { + setAccountNameWithString(cashAtAtmAccount.getExtraInfo().substring(0, Math.min(50, cashAtAtmAccount.getExtraInfo().length()))); + } else { + setAccountNameWithString(cashAtAtmAccount.getSelectedTradeCurrency().getCode()); + } } @Override @@ -104,7 +108,6 @@ public class CashAtAtmForm extends PaymentMethodForm { @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() - && !cashAtAtmAccount.getExtraInfo().isEmpty() && paymentAccount.getSingleTradeCurrency() != null); } } From 88c3f04be09d5647bda250de349077d37d7151d6 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 29 Jan 2025 09:24:20 -0500 Subject: [PATCH 110/371] enable floating price offers for cardless cash --- .../core/payment/payload/PaymentMethod.java | 3 +- .../resources/i18n/displayStrings.properties | 4 ++- .../offer/takeoffer/TakeOfferDataModel.java | 17 +++++---- .../offer/takeoffer/TakeOfferViewModel.java | 36 +++++-------------- 4 files changed, 24 insertions(+), 36 deletions(-) diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index 49302f610a..e8f27c9d7f 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -597,7 +597,6 @@ public final class PaymentMethod implements PersistablePayload, Comparable= 0 && amount.compareTo(BigInteger.valueOf(getMaxTradeLimit())) <= 0) { + this.amount.set(amount); + } calculateTotalToPay(); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java index c8f21ff0aa..c73ce988c6 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java @@ -208,7 +208,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im errorMessage.set(offer.getErrorMessage()); xmrValidator.setMaxValue(offer.getAmount()); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit()).min(offer.getAmount())); + xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); xmrValidator.setMinValue(offer.getMinAmount()); } @@ -237,7 +237,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im public void onPaymentAccountSelected(PaymentAccount paymentAccount) { dataModel.onPaymentAccountSelected(paymentAccount); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit()).min(offer.getAmount())); + xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); updateButtonDisableState(); } @@ -299,20 +299,13 @@ class TakeOfferViewModel extends ActivatableWithDataModel im Price tradePrice = dataModel.tradePrice; long maxTradeLimit = dataModel.getMaxTradeLimit(); if (PaymentMethod.isRoundedForAtmCash(dataModel.getPaymentMethod().getId())) { - BigInteger adjustedAmountForAtm = CoinUtil.getRoundedAtmCashAmount(dataModel.getAmount().get(), - tradePrice, - maxTradeLimit); - dataModel.applyAmount(adjustedAmountForAtm); - amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); + BigInteger adjustedAmountForAtm = CoinUtil.getRoundedAtmCashAmount(dataModel.getAmount().get(), tradePrice, maxTradeLimit); + dataModel.maybeApplyAmount(adjustedAmountForAtm); } else if (dataModel.getOffer().isTraditionalOffer()) { - if (!isAmountEqualMinAmount(dataModel.getAmount().get()) && (!isAmountEqualMaxAmount(dataModel.getAmount().get()))) { - // We only apply the rounding if the amount is variable (minAmount is lower as amount). - // Otherwise we could get an amount lower then the minAmount set by rounding - BigInteger roundedAmount = CoinUtil.getRoundedAmount(dataModel.getAmount().get(), tradePrice, maxTradeLimit, dataModel.getOffer().getCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); - dataModel.applyAmount(roundedAmount); - } - amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); + BigInteger roundedAmount = CoinUtil.getRoundedAmount(dataModel.getAmount().get(), tradePrice, maxTradeLimit, dataModel.getOffer().getCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); + dataModel.maybeApplyAmount(roundedAmount); } + amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); if (!dataModel.isMinAmountLessOrEqualAmount()) amountValidationResult.set(new InputValidator.ValidationResult(false, @@ -580,25 +573,14 @@ class TakeOfferViewModel extends ActivatableWithDataModel im if (price != null) { if (dataModel.isRoundedForAtmCash()) { amount = CoinUtil.getRoundedAtmCashAmount(amount, price, maxTradeLimit); - } else if (dataModel.getOffer().isTraditionalOffer() - && !isAmountEqualMinAmount(amount) && !isAmountEqualMaxAmount(amount)) { - // We only apply the rounding if the amount is variable (minAmount is lower as amount). - // Otherwise we could get an amount lower then the minAmount set by rounding + } else if (dataModel.getOffer().isTraditionalOffer()) { amount = CoinUtil.getRoundedAmount(amount, price, maxTradeLimit, dataModel.getOffer().getCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); } } - dataModel.applyAmount(amount); + dataModel.maybeApplyAmount(amount); } } - private boolean isAmountEqualMinAmount(BigInteger amount) { - return offer.getMinAmount().equals(amount); - } - - private boolean isAmountEqualMaxAmount(BigInteger amount) { - return offer.getAmount().equals(amount); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// From 12def5f1b584e6c304180337eea77266f5c4eed9 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 29 Jan 2025 09:41:32 -0500 Subject: [PATCH 111/371] remove extra input from tab navigation in create offer view --- .../main/java/haveno/desktop/main/offer/MutableOfferView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 35a2da8b60..e3f9132a84 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -1104,6 +1104,7 @@ public abstract class MutableOfferView> exten extraInfoTextArea.setPrefHeight(75); extraInfoTextArea.setMinHeight(75); extraInfoTextArea.setMaxHeight(75); + extraInfoTextArea.setFocusTraversable(false); GridPane.setRowIndex(extraInfoTextArea, gridRow); GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING); GridPane.setColumnIndex(extraInfoTextArea, 0); From 352384b41edf6ce4e5ac7860b8bbbab5e51c22e7 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 30 Jan 2025 08:14:04 -0500 Subject: [PATCH 112/371] log expected vs actual maker fee on error --- core/src/main/java/haveno/core/offer/OpenOfferManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index ab52d00b38..0920c036d7 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1346,7 +1346,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // verify maker's trade fee if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT) { - errorMessage = "Wrong maker fee for offer " + request.offerId; + errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT + " but got " + offer.getMakerFeePct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; @@ -1379,7 +1379,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // verify maker's trade fee if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) { - errorMessage = "Wrong maker fee for offer " + request.offerId; + errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_PCT + " but got " + offer.getMakerFeePct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; From e6b29b88f5020c3f895dcf2c936a6b646c5b9dd8 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 31 Jan 2025 07:55:42 -0500 Subject: [PATCH 113/371] replace issues link from bisq to haveno --- core/src/main/resources/i18n/displayStrings_cs.properties | 2 +- core/src/main/resources/i18n/displayStrings_de.properties | 2 +- core/src/main/resources/i18n/displayStrings_es.properties | 2 +- core/src/main/resources/i18n/displayStrings_fa.properties | 2 +- core/src/main/resources/i18n/displayStrings_fr.properties | 2 +- core/src/main/resources/i18n/displayStrings_it.properties | 2 +- core/src/main/resources/i18n/displayStrings_ja.properties | 2 +- core/src/main/resources/i18n/displayStrings_pt-br.properties | 2 +- core/src/main/resources/i18n/displayStrings_pt.properties | 2 +- core/src/main/resources/i18n/displayStrings_ru.properties | 2 +- core/src/main/resources/i18n/displayStrings_th.properties | 2 +- core/src/main/resources/i18n/displayStrings_vi.properties | 2 +- core/src/main/resources/i18n/displayStrings_zh-hans.properties | 2 +- core/src/main/resources/i18n/displayStrings_zh-hant.properties | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 233a9b2a39..9fe2018c32 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -2104,7 +2104,7 @@ popup.headline.error=Chyba popup.doNotShowAgain=Znovu nezobrazovat popup.reportError.log=Otevřít log popup.reportError.gitHub=Nahlaste problém na GitHub -popup.reportError={0}\n\nChcete-li nám pomoci vylepšit software, nahlaste tuto chybu otevřením nového problému na adrese https://github.com/bisq-network/bisq/issues.\n\ +popup.reportError={0}\n\nChcete-li nám pomoci vylepšit software, nahlaste tuto chybu otevřením nového problému na adrese https://github.com/haveno-dex/haveno/issues.\n\ Výše uvedená chybová zpráva bude zkopírována do schránky po kliknutí na některé z níže uvedených tlačítek.\n\ Usnadníte ladění, pokud zahrnete soubor haveno.log stisknutím tlačítka 'Otevřít log soubor', uložením kopie a připojením ke hlášení chyby. diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index cf72fdc883..bca39463b7 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1588,7 +1588,7 @@ popup.headline.error=Fehler popup.doNotShowAgain=Nicht erneut anzeigen popup.reportError.log=Protokolldatei öffnen popup.reportError.gitHub=Auf GitHub-Issue-Tracker melden -popup.reportError={0}\n\nUm uns bei der Verbesserung der Software zu helfen, erstellen Sie bitte einen Fehler-Bericht auf https://github.com/bisq-network/bisq/issues.\nDie Fehlermeldung wird in die Zwischenablage kopiert, wenn Sie auf einen der Knöpfe unten klicken.\nEs wird das Debuggen einfacher machen, wenn Sie die haveno.log Datei anfügen indem Sie "Logdatei öffnen" klicken, eine Kopie speichern und diese dem Fehler-Bericht anfügen. +popup.reportError={0}\n\nUm uns bei der Verbesserung der Software zu helfen, erstellen Sie bitte einen Fehler-Bericht auf https://github.com/haveno-dex/haveno/issues.\nDie Fehlermeldung wird in die Zwischenablage kopiert, wenn Sie auf einen der Knöpfe unten klicken.\nEs wird das Debuggen einfacher machen, wenn Sie die haveno.log Datei anfügen indem Sie "Logdatei öffnen" klicken, eine Kopie speichern und diese dem Fehler-Bericht anfügen. popup.error.tryRestart=Versuchen Sie bitte Ihre Anwendung neu zu starten und überprüfen Sie Ihre Netzwerkverbindung um zu sehen, ob Sie das Problem beheben können. popup.error.takeOfferRequestFailed=Es ist ein Fehler aufgetreten, als jemand versuchte eins Ihrer Angebote anzunehmen:\n{0} diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 1cfdcf0306..ed6f83a79d 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -1589,7 +1589,7 @@ popup.headline.error=Error popup.doNotShowAgain=No mostrar de nuevo popup.reportError.log=Abrir archivo de registro popup.reportError.gitHub=Reportar al rastreador de problemas de Github -popup.reportError={0}\n\nPara ayudarnos a mejorar el software por favor reporte el fallo en nuestro rastreador de fallos en https://github.com/bisq-network/bisq/issues.\nEl mensaje de error será copiado al portapapeles cuando haga clic en cualquiera de los botones inferiores.\nHará el depurado de fallos más fácil si puede adjuntar el archivo haveno.log presionando "Abrir archivo de log", guardando una copia y adjuntándola en su informe de errores. +popup.reportError={0}\n\nPara ayudarnos a mejorar el software por favor reporte el fallo en nuestro rastreador de fallos en https://github.com/haveno-dex/haveno/issues.\nEl mensaje de error será copiado al portapapeles cuando haga clic en cualquiera de los botones inferiores.\nHará el depurado de fallos más fácil si puede adjuntar el archivo haveno.log presionando "Abrir archivo de log", guardando una copia y adjuntándola en su informe de errores. popup.error.tryRestart=Por favor pruebe reiniciar la aplicación y comprobar su conexión a la red para ver si puede resolver el problema. popup.error.takeOfferRequestFailed=Un error ocurrió cuando alguien intentó tomar una de sus ofertas:\n{0} diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 82ab9895e8..1b64e45e5b 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1584,7 +1584,7 @@ popup.headline.error=خطا popup.doNotShowAgain=دوباره نشان نده popup.reportError.log=باز کردن فایل گزارش popup.reportError.gitHub=گزارش به پیگیر مسائل GitHub  -popup.reportError={0}\n\nTo help us to improve the software please report this bug by opening a new issue at https://github.com/bisq-network/bisq/issues.\nThe above error message will be copied to the clipboard when you click either of the buttons below.\nIt will make debugging easier if you include the haveno.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. +popup.reportError={0}\n\nTo help us to improve the software please report this bug by opening a new issue at https://github.com/haveno-dex/haveno/issues.\nThe above error message will be copied to the clipboard when you click either of the buttons below.\nIt will make debugging easier if you include the haveno.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. popup.error.tryRestart=لطفاً سعی کنید برنامه را مجدداً راه اندازی کنید و اتصال شبکه خود را بررسی کنید تا ببینید آیا می توانید مشکل را حل کنید یا خیر. popup.error.takeOfferRequestFailed=وقتی کسی تلاش کرد تا یکی از پیشنهادات شما را بپذیرد خطایی رخ داد:\n{0} diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 17398abddf..6bfffcfca4 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -1590,7 +1590,7 @@ popup.headline.error=Erreur popup.doNotShowAgain=Ne plus montrer popup.reportError.log=Ouvrir le dossier de log popup.reportError.gitHub=Signaler au Tracker de problème GitHub -popup.reportError={0}\n\nAfin de nous aider à améliorer le logiciel, veuillez signaler ce bug en ouvrant un nouveau ticket de support sur https://github.com/bisq-network/bisq/issues.\nLe message d''erreur ci-dessus sera copié dans le presse-papier lorsque vous cliquerez sur l''un des boutons ci-dessous.\nCela facilitera le dépannage si vous incluez le fichier haveno.log en appuyant sur "ouvrir le fichier de log", en sauvegardant une copie, et en l''attachant à votre rapport de bug. +popup.reportError={0}\n\nAfin de nous aider à améliorer le logiciel, veuillez signaler ce bug en ouvrant un nouveau ticket de support sur https://github.com/haveno-dex/haveno/issues.\nLe message d''erreur ci-dessus sera copié dans le presse-papier lorsque vous cliquerez sur l''un des boutons ci-dessous.\nCela facilitera le dépannage si vous incluez le fichier haveno.log en appuyant sur "ouvrir le fichier de log", en sauvegardant une copie, et en l''attachant à votre rapport de bug. popup.error.tryRestart=Veuillez essayer de redémarrer votre application et vérifier votre connexion réseau pour voir si vous pouvez résoudre ce problème. popup.error.takeOfferRequestFailed=Une erreur est survenue pendant que quelqu''un essayait d''accepter l''un de vos ordres:\n{0} diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 62cbaa9d3a..13b0ee355a 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1587,7 +1587,7 @@ popup.headline.error=Errore popup.doNotShowAgain=Non mostrare di nuovo popup.reportError.log=Apri file di registro popup.reportError.gitHub=Segnala sugli errori di GitHub -popup.reportError={0}\n\nPer aiutarci a migliorare il software, segnala questo errore aprendo un nuova segnalazione su https://github.com/bisq-network/bisq/issues.\nIl messaggio di errore sopra verrà copiato negli appunti quando si fa clic su uno dei pulsanti di seguito.\nFaciliterà il debug se includi il file haveno.log premendo "Apri file di registro", salvando una copia e allegandolo alla tua segnalazione di bug.\n  +popup.reportError={0}\n\nPer aiutarci a migliorare il software, segnala questo errore aprendo un nuova segnalazione su https://github.com/haveno-dex/haveno/issues.\nIl messaggio di errore sopra verrà copiato negli appunti quando si fa clic su uno dei pulsanti di seguito.\nFaciliterà il debug se includi il file haveno.log premendo "Apri file di registro", salvando una copia e allegandolo alla tua segnalazione di bug.\n  popup.error.tryRestart=Prova a riavviare l'applicazione e controlla la connessione di rete per vedere se riesci a risolvere il problema. popup.error.takeOfferRequestFailed=Si è verificato un errore quando qualcuno ha tentato di accettare una delle tue offerte:\n{0} diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 226ccb1a3e..c4f9f502ee 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -1588,7 +1588,7 @@ popup.headline.error=エラー popup.doNotShowAgain=次回から表示しない popup.reportError.log=ログファイルを開く popup.reportError.gitHub=GitHub issue trackerに報告 -popup.reportError={0}\n\nソフトウェアの改善に役立てるため、https://github.com/bisq-network/bisq/issues で新しい issue を開いてこのバグを報告してください。\n下のボタンのいずれかをクリックすると、上記のエラーメッセージがクリップボードにコピーされます。\n「ログファイルを開く」を押して、コピーを保存し、バグレポートに添付されるhaveno.logファイル含めると、デバッグが容易になります。 +popup.reportError={0}\n\nソフトウェアの改善に役立てるため、https://github.com/haveno-dex/haveno/issues で新しい issue を開いてこのバグを報告してください。\n下のボタンのいずれかをクリックすると、上記のエラーメッセージがクリップボードにコピーされます。\n「ログファイルを開く」を押して、コピーを保存し、バグレポートに添付されるhaveno.logファイル含めると、デバッグが容易になります。 popup.error.tryRestart=アプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。 popup.error.takeOfferRequestFailed=誰かがあなたのいずれかのオファーを受けようと時にエラーが発生しました:\n{0} diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index a6a6014c13..756c939e80 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -1592,7 +1592,7 @@ popup.headline.error=Erro popup.doNotShowAgain=Não mostrar novamente popup.reportError.log=Abrir arquivo de log popup.reportError.gitHub=Reportar à lista de problemas no GitHub -popup.reportError={0}\n\nPara nos ajudar a melhorar o aplicativo, reporte o bug criando um relatório (Issue) em nossa página do GitHub em https://github.com/bisq-network/bisq/issues.\n\nA mensagem de erro exibida acima será copiada para a área de transferência quando você clicar qualquer um dos botões abaixo.\nA solução de problemas será mais fácil se você anexar o arquivo haveno.log ao clicar em "Abrir arquivo de log", salvando uma cópia e incluindo-a em seu relatório do problema (Issue) no GitHub. +popup.reportError={0}\n\nPara nos ajudar a melhorar o aplicativo, reporte o bug criando um relatório (Issue) em nossa página do GitHub em https://github.com/haveno-dex/haveno/issues.\n\nA mensagem de erro exibida acima será copiada para a área de transferência quando você clicar qualquer um dos botões abaixo.\nA solução de problemas será mais fácil se você anexar o arquivo haveno.log ao clicar em "Abrir arquivo de log", salvando uma cópia e incluindo-a em seu relatório do problema (Issue) no GitHub. popup.error.tryRestart=Por favor, reinicie o aplicativo e verifique sua conexão de Internet para ver se o problema foi resolvido. popup.error.takeOfferRequestFailed=Houve um quando alguém tentou aceitar uma de suas ofertas:\n{0} diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 01b7b47357..7b483b2dac 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1584,7 +1584,7 @@ popup.headline.error=Erro popup.doNotShowAgain=Não mostrar novamente popup.reportError.log=Abrir ficheiro de log popup.reportError.gitHub=Relatar ao GitHub issue tracker -popup.reportError={0}\n\nPara nos ajudar a melhorar o software, por favor reporte este erro abrindo um novo issue em https://github.com/bisq-network/bisq/issues.\nA mensagem de erro acima será copiada para a área de transferência quando você clicar num dos botões abaixo.\nSerá mais fácil fazer a depuração se você incluir o ficheiro haveno.log clicando "Abrir arquivo de log", salvando uma cópia e anexando-a ao seu relatório de erros. +popup.reportError={0}\n\nPara nos ajudar a melhorar o software, por favor reporte este erro abrindo um novo issue em https://github.com/haveno-dex/haveno/issues.\nA mensagem de erro acima será copiada para a área de transferência quando você clicar num dos botões abaixo.\nSerá mais fácil fazer a depuração se você incluir o ficheiro haveno.log clicando "Abrir arquivo de log", salvando uma cópia e anexando-a ao seu relatório de erros. popup.error.tryRestart=Por favor tente reiniciar o programa e verifique a sua conexão de Internet para ver se pode resolver o problema. popup.error.takeOfferRequestFailed=Ocorreu um erro quando alguém tentou aceitar uma das suas ofertas:\n{0} diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 40fb02023f..90718c597c 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1585,7 +1585,7 @@ popup.headline.error=Ошибка popup.doNotShowAgain=Не показывать снова popup.reportError.log=Открыть файл журнала popup.reportError.gitHub=Сообщить о проблеме в Github -popup.reportError={0}\n\nЧтобы помочь нам улучшить приложение, просьба сообщить об ошибке, открыв новую тему на https://github.com/bisq-network/bisq/issues. \nСообщение об ошибке будет скопировано в буфер обмена при нажатии любой из кнопок ниже.\nЕсли вы прикрепите к отчету о неисправности файл журнала haveno.log, нажав «Открыть файл журнала» и сохранив его копию, это поможет нам разобраться с проблемой быстрее. +popup.reportError={0}\n\nЧтобы помочь нам улучшить приложение, просьба сообщить об ошибке, открыв новую тему на https://github.com/haveno-dex/haveno/issues. \nСообщение об ошибке будет скопировано в буфер обмена при нажатии любой из кнопок ниже.\nЕсли вы прикрепите к отчету о неисправности файл журнала haveno.log, нажав «Открыть файл журнала» и сохранив его копию, это поможет нам разобраться с проблемой быстрее. popup.error.tryRestart=Попробуйте перезагрузить приложение и проверьте подключение к сети, чтобы попробовать решить проблему. popup.error.takeOfferRequestFailed=Произошла ошибка, когда контрагент попытался принять одно из ваших предложений:\n{0} diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index ae6775875a..3b9b8f18df 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1585,7 +1585,7 @@ popup.headline.error=ผิดพลาด popup.doNotShowAgain=ไม่ต้องแสดงอีกครั้ง popup.reportError.log=เปิดไฟล์ที่บันทึก popup.reportError.gitHub=รายงานไปที่ตัวติดตามปัญหา GitHub -popup.reportError={0}\n\nTo help us to improve the software please report this bug by opening a new issue at https://github.com/bisq-network/bisq/issues.\nThe above error message will be copied to the clipboard when you click either of the buttons below.\nIt will make debugging easier if you include the haveno.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. +popup.reportError={0}\n\nTo help us to improve the software please report this bug by opening a new issue at https://github.com/haveno-dex/haveno/issues.\nThe above error message will be copied to the clipboard when you click either of the buttons below.\nIt will make debugging easier if you include the haveno.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. popup.error.tryRestart=โปรดลองเริ่มแอปพลิเคชั่นของคุณใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณเพื่อดูว่าคุณสามารถแก้ไขปัญหาได้หรือไม่ popup.error.takeOfferRequestFailed=เกิดข้อผิดพลาดขึ้นเมื่อมีคนพยายามรับข้อเสนอของคุณ: \n{0} diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 45e4a0426f..3d9f859acc 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1587,7 +1587,7 @@ popup.headline.error=Lỗi popup.doNotShowAgain=Không hiển thị lại popup.reportError.log=Mở log file popup.reportError.gitHub=Báo cáo cho người theo dõi vấn đề GitHub -popup.reportError={0}\n\nĐể giúp chúng tôi cải tiến phần mềm, vui lòng báo cáo lỗi này bằng cách mở một thông báo vấn đề mới tại https://github.com/bisq-network/bisq/issues.\nTin nhắn lỗi phía trên sẽ được sao chép tới clipboard khi bạn ấn vào một nút bên dưới.\nSự cố sẽ được xử lý dễ dàng hơn nếu bạn đính kèm haveno.log file bằng cách nhấn "Mở log file", lưu bản sao, và đính kèm vào báo cáo lỗi. +popup.reportError={0}\n\nĐể giúp chúng tôi cải tiến phần mềm, vui lòng báo cáo lỗi này bằng cách mở một thông báo vấn đề mới tại https://github.com/haveno-dex/haveno/issues.\nTin nhắn lỗi phía trên sẽ được sao chép tới clipboard khi bạn ấn vào một nút bên dưới.\nSự cố sẽ được xử lý dễ dàng hơn nếu bạn đính kèm haveno.log file bằng cách nhấn "Mở log file", lưu bản sao, và đính kèm vào báo cáo lỗi. popup.error.tryRestart=Hãy khởi động lại ứng dụng và kiểm tra kết nối mạng để xem bạn có thể xử lý vấn đề này hay không. popup.error.takeOfferRequestFailed=Có lỗi xảy ra khi ai đó cố gắng để nhận một trong các chào giá của bạn:\n{0} diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 2a6fac3999..7f08cd5716 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -1590,7 +1590,7 @@ popup.headline.error=错误 popup.doNotShowAgain=不要再显示 popup.reportError.log=打开日志文件 popup.reportError.gitHub=报告至 Github issue tracker -popup.reportError={0}\n\n为了帮助我们改进软件,请在 https://github.com/bisq-network/bisq/issues 上打开一个新问题来报告这个 bug 。\n\n当您单击下面任意一个按钮时,上面的错误消息将被复制到剪贴板。\n\n如果您通过按下“打开日志文件”,保存一份副本,并将其附加到 bug 报告中,如果包含 haveno.log 文件,那么调试就会变得更容易。 +popup.reportError={0}\n\n为了帮助我们改进软件,请在 https://github.com/haveno-dex/haveno/issues 上打开一个新问题来报告这个 bug 。\n\n当您单击下面任意一个按钮时,上面的错误消息将被复制到剪贴板。\n\n如果您通过按下“打开日志文件”,保存一份副本,并将其附加到 bug 报告中,如果包含 haveno.log 文件,那么调试就会变得更容易。 popup.error.tryRestart=请尝试重启您的应用程序或者检查您的网络连接。 popup.error.takeOfferRequestFailed=当有人试图接受你的报价时发生了一个错误:\n{0} diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 3c00232559..bc2cc931dc 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -1588,7 +1588,7 @@ popup.headline.error=錯誤 popup.doNotShowAgain=不要再顯示 popup.reportError.log=打開日誌文件 popup.reportError.gitHub=報吿至 Github issue tracker -popup.reportError={0}\n\n為了幫助我們改進軟件,請在 https://github.com/bisq-network/bisq/issues 上打開一個新問題來報吿這個 bug 。\n\n當您單擊下面任意一個按鈕時,上面的錯誤消息將被複制到剪貼板。\n\n如果您通過按下“打開日誌文件”,保存一份副本,並將其附加到 bug 報吿中,如果包含 haveno.log 文件,那麼調試就會變得更容易。 +popup.reportError={0}\n\n為了幫助我們改進軟件,請在 https://github.com/haveno-dex/haveno/issues 上打開一個新問題來報吿這個 bug 。\n\n當您單擊下面任意一個按鈕時,上面的錯誤消息將被複制到剪貼板。\n\n如果您通過按下“打開日誌文件”,保存一份副本,並將其附加到 bug 報吿中,如果包含 haveno.log 文件,那麼調試就會變得更容易。 popup.error.tryRestart=請嘗試重啟您的應用程序或者檢查您的網絡連接。 popup.error.takeOfferRequestFailed=當有人試圖接受你的報價時發生了一個錯誤:\n{0} From c3338039174ec8cdc85460371b71105eb2700949 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 31 Jan 2025 08:53:57 -0500 Subject: [PATCH 114/371] fix npe duplicating offer with deleted payment account --- .../portfolio/duplicateoffer/DuplicateOfferDataModel.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java index e3fd44c0ba..21ab8a7788 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java @@ -79,7 +79,11 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { public void populateData(Offer offer) { if (offer == null) return; - paymentAccount = user.getPaymentAccount(offer.getMakerPaymentAccountId()); + + PaymentAccount account = user.getPaymentAccount(offer.getMakerPaymentAccountId()); + if (account != null) { + this.paymentAccount = account; + } setMinAmount(offer.getMinAmount()); setAmount(offer.getAmount()); setPrice(offer.getPrice()); From 71fab722eef74ad337e045888d2c676590ba02bd Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 31 Jan 2025 08:56:23 -0500 Subject: [PATCH 115/371] remove make offer to unsigned account warning --- .../resources/i18n/displayStrings_de.properties | 2 -- .../resources/i18n/displayStrings_es.properties | 2 -- .../resources/i18n/displayStrings_fa.properties | 2 -- .../resources/i18n/displayStrings_it.properties | 2 -- .../resources/i18n/displayStrings_ja.properties | 2 -- .../i18n/displayStrings_pt-br.properties | 2 -- .../resources/i18n/displayStrings_pt.properties | 2 -- .../resources/i18n/displayStrings_ru.properties | 2 -- .../resources/i18n/displayStrings_th.properties | 2 -- .../resources/i18n/displayStrings_vi.properties | 2 -- .../i18n/displayStrings_zh-hans.properties | 2 -- .../i18n/displayStrings_zh-hant.properties | 2 -- .../main/offer/MutableOfferViewModel.java | 16 ---------------- 13 files changed, 40 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index bca39463b7..d4821f7821 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1977,8 +1977,6 @@ payment.accountType=Kontotyp payment.checking=Überprüfe payment.savings=Ersparnisse payment.personalId=Personalausweis -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle ist ein Geldtransferdienst, der am besten *durch* eine andere Bank funktioniert.\n\n1. Sehen Sie auf dieser Seite nach, ob (und wie) Ihre Bank mit Zelle zusammenarbeitet:\nhttps://www.zellepay.com/get-started\n\n2. Achten Sie besonders auf Ihre Überweisungslimits - die Sendelimits variieren je nach Bank, und die Banken geben oft separate Tages-, Wochen- und Monatslimits an.\n\n3. Wenn Ihre Bank nicht mit Zelle zusammenarbeitet, können Sie die Zahlungsmethode trotzdem über die Zelle Mobile App benutzen, aber Ihre Überweisungslimits werden viel niedriger sein.\n\n4. Der auf Ihrem Haveno-Konto angegebene Name MUSS mit dem Namen auf Ihrem Zelle/Bankkonto übereinstimmen. \n\nWenn Sie eine Zelle Transaktion nicht wie in Ihrem Handelsvertrag angegeben durchführen können, verlieren Sie möglicherweise einen Teil (oder die gesamte) Sicherheitskaution.\n\nWegen des etwas höheren Chargeback-Risikos von Zelle wird Verkäufern empfohlen, nicht unterzeichnete Käufer per E-Mail oder SMS zu kontaktieren, um zu überprüfen, ob der Käufer wirklich das in Haveno angegebene Zelle-Konto besitzt. payment.fasterPayments.newRequirements.info=Einige Banken haben damit begonnen, den vollständigen Namen des Empfängers für Faster Payments Überweisungen zu überprüfen. Ihr aktuelles Faster Payments-Konto gibt keinen vollständigen Namen an.\n\nBitte erwägen Sie, Ihr Faster Payments-Konto in Haveno neu einzurichten, um zukünftigen {0} Käufern einen vollständigen Namen zu geben.\n\nWenn Sie das Konto neu erstellen, stellen Sie sicher, dass Sie die genaue Bankleitzahl, Kontonummer und die "Salt"-Werte für die Altersverifikation von Ihrem alten Konto auf Ihr neues Konto kopieren. Dadurch wird sichergestellt, dass das Alter und der Unterschriftsstatus Ihres bestehenden Kontos erhalten bleiben. payment.moneyGram.info=Bei der Nutzung von MoneyGram, muss der XMR Käufer die MoneyGram Zulassungsnummer und ein Foto der Quittung per E-Mail an den XMR-Verkäufer senden. Die Quittung muss den vollständigen Namen, das Land, das Bundesland des Verkäufers und den Betrag deutlich zeigen. Der Käufer bekommt die E-Mail-Adresse des Verkäufers im Handelsprozess angezeigt. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index ed6f83a79d..f6e0947499 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -1978,8 +1978,6 @@ payment.accountType=Tipo de cuenta payment.checking=Comprobando payment.savings=Ahorros payment.personalId=ID personal: -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle es un servicio de transmisión de dinero que funciona mejor *a través* de otro banco..\n\n1. Compruebe esta página para ver si (y cómo) trabaja su banco con Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Preste atención a los límites de transferencia -límites de envío- que varían entre bancos, y que los bancos especifican a menudo diferentes límites diarios, semanales y mensuales..\n\n3. Si su banco no trabaja con Zelle, aún puede usarlo a través de la app móvil de Zelle, pero sus límites de transferencia serán mucho menores.\n\n4. El nombre especificado en su cuenta Haveno DEBE ser igual que el nombre en su cuenta de Zelle/bancaria. \n\nSi no puede completar una transacción Zelle tal como se especifica en el contrato, puede perder algo (o todo) el depósito de seguridad!\n\nDebido a que Zelle tiene cierto riesgo de reversión de pago, se aconseja que los vendedores contacten con los compradores no firmados a través de email o SMS para verificar que el comprador realmente tiene la cuenta de Zelle especificada en Haveno. payment.fasterPayments.newRequirements.info=Algunos bancos han comenzado a verificar el nombre completo del receptor para las transferencias Faster Payments. Su cuenta actual Faster Payments no especifica un nombre completo.\n\nConsidere recrear su cuenta Faster Payments en Haveno para proporcionarle a los futuros compradores {0} un nombre completo.\n\nCuando vuelva a crear la cuenta, asegúrese de copiar el UK Short Code de forma precisa , el número de cuenta y los valores salt de la cuenta anterior a su cuenta nueva para la verificación de edad. Esto asegurará que la edad de su cuenta existente y el estado de la firma se conserven. payment.moneyGram.info=Al utilizar MoneyGram, el comprador de XMR tiene que enviar el número de autorización y una foto del recibo al vendedor de XMR por correo electrónico. El recibo debe mostrar claramente el nobre completo del vendedor, país, estado y cantidad. El email del vendedor se mostrará al comprador durante el proceso de intercambio. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 1b64e45e5b..91bf61259c 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1971,8 +1971,6 @@ payment.accountType=نوع حساب payment.checking=بررسی payment.savings=اندوخته ها payment.personalId=شناسه شخصی -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 13b0ee355a..1a9dc9ea21 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1974,8 +1974,6 @@ payment.accountType=Tipologia conto payment.checking=Verifica payment.savings=Risparmi payment.personalId=ID personale -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Alcune banche hanno iniziato a verificare il nome completo del destinatario per i trasferimenti di Faster Payments (UK). Il tuo attuale account Faster Payments non specifica un nome completo.\n\nTi consigliamo di ricreare il tuo account Faster Payments in Haveno per fornire ai futuri acquirenti {0} un nome completo.\n\nQuando si ricrea l'account, assicurarsi di copiare il codice di ordinamento preciso, il numero di account e i valori salt della verifica dell'età dal vecchio account al nuovo account. Ciò garantirà il mantenimento dell'età del tuo account esistente e lo stato della firma.\n  payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index c4f9f502ee..78ade2a04f 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -1977,8 +1977,6 @@ payment.accountType=口座種別 payment.checking=当座口座 payment.savings=普通口座 payment.personalId=個人ID -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelleは他の銀行を介して利用するとよりうまくいく送金サービスです。\n\n1. あなたの銀行がZelleと協力するか(そして利用の方法)をここから確認して下さい: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. 送金制限に注意して下さい。制限は銀行によって異なり、1日、1週、1月当たりの制限に分けられていることが多い。\n\n3. 銀行がZelleと協力しない場合でも、Zelleのモバイルアプリ版を使えますが、送金制限ははるかに低くなります。\n\n4. Havenoアカウントで特定される名前は必ずZelleアカウントと銀行口座に特定される名前と合う必要があります。\n\nトレード契約書とおりにZelleトランザクションを完了できなければ、一部(あるいは全て)のセキュリティデポジットを失う可能性はあります。\n\nZelleにおいてやや高い支払取り消しリスクがあるので、売り手はメールやSMSで無署名買い手に連絡して、Havenoに特定されるZelleアカウントの所有者かどうかを確かめるようにおすすめします。 payment.fasterPayments.newRequirements.info=「Faster Payments」で送金する場合、銀行が受信者の姓名を確認するケースが最近多くなりました。現在の「Faster Payments」アカウントは姓名を特定しません。\n\nこれからの{0}買い手に姓名を提供するため、Haveno内に新しい「Faster Payments」アカウントを作成するのを検討して下さい。\n\n新しいアカウントを作成すると、完全に同じ分類コード、アカウントの口座番号、そしてアカウント年齢検証ソルト値を古いアカウントから新しいアカウントにコピーして下さい。こうやって現在のアカウントの年齢そして署名状況は維持されます。 payment.moneyGram.info=MoneyGramを使用する場合、XMRの買い手は認証番号と領収書の写真をEメールでXMRの売り手に送信する必要があります。領収書には、売り手の氏名、市区町村、国、金額を明確に記載する必要があります。トレードプロセスにて、売り手のEメールは買い手に表示されます。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 756c939e80..c9622afc15 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -1981,8 +1981,6 @@ payment.accountType=Tipo de conta payment.checking=Conta Corrente payment.savings=Poupança payment.personalId=Identificação pessoal -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 7b483b2dac..3cfb931833 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1971,8 +1971,6 @@ payment.accountType=Tipo de conta payment.checking=Conta Corrente payment.savings=Poupança payment.personalId=ID pessoal -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 90718c597c..84d3778f81 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1972,8 +1972,6 @@ payment.accountType=Тип счёта payment.checking=Текущий payment.savings=Сберегательный payment.personalId=Личный идентификатор -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 3b9b8f18df..de5a2456db 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1972,8 +1972,6 @@ payment.accountType=ประเภทบัญชี payment.checking=การตรวจสอบ payment.savings=ออมทรัพย์ payment.personalId=รหัส ID ประจำตัวบุคคล -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 3d9f859acc..8d8ec4e5be 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1974,8 +1974,6 @@ payment.accountType=Loại tài khoản payment.checking=Đang kiểm tra payment.savings=Tiết kiệm payment.personalId=ID cá nhân -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 7f08cd5716..fc59e3c6c1 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -1981,8 +1981,6 @@ payment.accountType=账户类型 payment.checking=检查 payment.savings=保存 payment.personalId=个人 ID -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle是一项转账服务,转账到其他银行做的很好。\n\n1.检查此页面以查看您的银行是否(以及如何)与 Zelle 合作:\nhttps://www.zellepay.com/get-started\n\n2.特别注意您的转账限额-汇款限额因银行而异,银行通常分别指定每日,每周和每月的限额。\n\n3.如果您的银行不能使用 Zelle,您仍然可以通过 Zelle 移动应用程序使用它,但是您的转账限额会低得多。\n\n4.您的 Haveno 帐户上指定的名称必须与 Zelle/银行帐户上的名称匹配。 \n\n如果您无法按照贸易合同中的规定完成 Zelle 交易,则可能会损失部分(或全部)保证金。\n\n由于 Zelle 的拒付风险较高,因此建议卖家通过电子邮件或 SMS 与未签名的买家联系,以确认买家确实拥有 Haveno 中指定的 Zelle 帐户。 payment.fasterPayments.newRequirements.info=有些银行已经开始核实快捷支付收款人的全名。您当前的快捷支付帐户没有填写全名。\n\n请考虑在 Haveno 中重新创建您的快捷支付帐户,为将来的 {0} 买家提供一个完整的姓名。\n\n重新创建帐户时,请确保将银行区号、帐户编号和帐龄验证盐值从旧帐户复制到新帐户。这将确保您现有的帐龄和签名状态得到保留。 payment.moneyGram.info=使用 MoneyGram 时,XMR 买方必须将授权号码和收据的照片通过电子邮件发送给 XMR 卖方。收据必须清楚地显示卖方的全名、国家或地区、州和金额。买方将在交易过程中显示卖方的电子邮件。 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index bc2cc931dc..ef8e2ca964 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -1975,8 +1975,6 @@ payment.accountType=賬户類型 payment.checking=檢查 payment.savings=保存 payment.personalId=個人 ID -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle是一項轉賬服務,轉賬到其他銀行做的很好。\n\n1.檢查此頁面以查看您的銀行是否(以及如何)與 Zelle 合作:\nhttps://www.zellepay.com/get-started\n\n2.特別注意您的轉賬限額-匯款限額因銀行而異,銀行通常分別指定每日,每週和每月的限額。\n\n3.如果您的銀行不能使用 Zelle,您仍然可以通過 Zelle 移動應用程序使用它,但是您的轉賬限額會低得多。\n\n4.您的 Haveno 帳户上指定的名稱必須與 Zelle/銀行帳户上的名稱匹配。 \n\n如果您無法按照貿易合同中的規定完成 Zelle 交易,則可能會損失部分(或全部)保證金。\n\n由於 Zelle 的拒付風險較高,因此建議賣家通過電子郵件或 SMS 與未簽名的買家聯繫,以確認買家確實擁有 Haveno 中指定的 Zelle 帳户。 payment.fasterPayments.newRequirements.info=有些銀行已經開始核實快捷支付收款人的全名。您當前的快捷支付帳户沒有填寫全名。\n\n請考慮在 Haveno 中重新創建您的快捷支付帳户,為將來的 {0} 買家提供一個完整的姓名。\n\n重新創建帳户時,請確保將銀行區號、帳户編號和帳齡驗證鹽值從舊帳户複製到新帳户。這將確保您現有的帳齡和簽名狀態得到保留。 payment.moneyGram.info=使用 MoneyGram 時,XMR 買方必須將授權號碼和收據的照片通過電子郵件發送給 XMR 賣方。收據必須清楚地顯示賣方的全名、國家或地區、州和金額。買方將在交易過程中顯示賣方的電子郵件。 diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index 2907fe8979..5588bb53c0 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -39,7 +39,6 @@ import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; -import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.FiatVolumeValidator; import haveno.core.payment.validation.SecurityDepositValidator; import haveno.core.payment.validation.XmrValidator; @@ -186,7 +185,6 @@ public abstract class MutableOfferViewModel ext final IntegerProperty marketPriceAvailableProperty = new SimpleIntegerProperty(-1); private ChangeListener currenciesUpdateListener; protected boolean syncMinAmountWithAmount = true; - private boolean makeOfferFromUnsignedAccountWarningDisplayed; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -695,7 +693,6 @@ public abstract class MutableOfferViewModel ext xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); - maybeShowMakeOfferToUnsignedAccountWarning(); securityDepositValidator.setPaymentAccount(paymentAccount); } @@ -832,8 +829,6 @@ public abstract class MutableOfferViewModel ext syncMinAmountWithAmount = true; } - maybeShowMakeOfferToUnsignedAccountWarning(); - // trigger recalculation of the security deposit UserThread.execute(() -> { onFocusOutSecurityDepositTextField(true, false); @@ -1269,17 +1264,6 @@ public abstract class MutableOfferViewModel ext } } - private void maybeShowMakeOfferToUnsignedAccountWarning() { - if (!makeOfferFromUnsignedAccountWarningDisplayed && - dataModel.getDirection() == OfferDirection.SELL && - PaymentMethod.hasChargebackRisk(dataModel.getPaymentAccount().getPaymentMethod(), dataModel.getTradeCurrency().getCode())) { - BigInteger checkAmount = dataModel.getMinAmount().get() == null ? dataModel.getAmount().get() : dataModel.getMinAmount().get(); - if (checkAmount != null && checkAmount.compareTo(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT) <= 0) { - makeOfferFromUnsignedAccountWarningDisplayed = true; - } - } - } - private InputValidator.ValidationResult isXmrInputValid(String input) { return xmrValidator.validate("" + HavenoUtils.atomicUnitsToXmr(HavenoUtils.parseXmr(input))); } From ae8760d72c5d5871ff9273f23f220151f141e703 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 2 Feb 2025 07:48:20 -0500 Subject: [PATCH 116/371] add paysafe payment method --- .../core/api/model/PaymentAccountForm.java | 3 +- .../core/payment/PaymentAccountFactory.java | 2 + .../haveno/core/payment/PaysafeAccount.java | 113 ++++++++++++++++++ .../core/payment/payload/PaymentMethod.java | 10 +- .../payload/PaysafeAccountPayload.java | 96 +++++++++++++++ .../haveno/core/proto/CoreProtoResolver.java | 3 + .../resources/i18n/displayStrings.properties | 5 + .../i18n/displayStrings_cs.properties | 4 + .../i18n/displayStrings_de.properties | 3 + .../i18n/displayStrings_es.properties | 3 + .../i18n/displayStrings_fa.properties | 3 + .../i18n/displayStrings_fr.properties | 3 + .../i18n/displayStrings_it.properties | 3 + .../i18n/displayStrings_ja.properties | 3 + .../i18n/displayStrings_pt-br.properties | 3 + .../i18n/displayStrings_pt.properties | 3 + .../i18n/displayStrings_ru.properties | 3 + .../i18n/displayStrings_th.properties | 3 + .../i18n/displayStrings_tr.properties | 3 + .../i18n/displayStrings_vi.properties | 3 + .../i18n/displayStrings_zh-hans.properties | 3 + .../i18n/displayStrings_zh-hant.properties | 3 + .../paymentmethods/PaysafeForm.java | 111 +++++++++++++++++ .../TraditionalAccountsView.java | 11 ++ .../steps/buyer/BuyerStep2View.java | 4 + proto/src/main/proto/pb.proto | 9 +- 26 files changed, 409 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/haveno/core/payment/PaysafeAccount.java create mode 100644 core/src/main/java/haveno/core/payment/payload/PaysafeAccountPayload.java create mode 100644 desktop/src/main/java/haveno/desktop/components/paymentmethods/PaysafeForm.java diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java index 6650265fbb..6b1b494047 100644 --- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java @@ -77,7 +77,8 @@ public final class PaymentAccountForm implements PersistablePayload { AUSTRALIA_PAYID, CASH_APP, PAYPAL, - VENMO; + VENMO, + PAYSAFE; public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); diff --git a/core/src/main/java/haveno/core/payment/PaymentAccountFactory.java b/core/src/main/java/haveno/core/payment/PaymentAccountFactory.java index 64c8bdbc52..d2f6ab2bbc 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccountFactory.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccountFactory.java @@ -136,6 +136,8 @@ public class PaymentAccountFactory { return new CashAppAccount(); case PaymentMethod.VENMO_ID: return new VenmoAccount(); + case PaymentMethod.PAYSAFE_ID: + return new PaysafeAccount(); // Cannot be deleted as it would break old trade history entries case PaymentMethod.OK_PAY_ID: diff --git a/core/src/main/java/haveno/core/payment/PaysafeAccount.java b/core/src/main/java/haveno/core/payment/PaysafeAccount.java new file mode 100644 index 0000000000..7d756ca362 --- /dev/null +++ b/core/src/main/java/haveno/core/payment/PaysafeAccount.java @@ -0,0 +1,113 @@ +/* + * This file is part of Haveno. + * + * Haveno 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. + * + * Haveno 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 Haveno. If not, see . + */ + +package haveno.core.payment; + +import haveno.core.api.model.PaymentAccountFormField; +import haveno.core.locale.TraditionalCurrency; +import haveno.core.locale.TradeCurrency; +import haveno.core.payment.payload.PaymentAccountPayload; +import haveno.core.payment.payload.PaymentMethod; +import haveno.core.payment.payload.PaysafeAccountPayload; +import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +public final class PaysafeAccount extends PaymentAccount { + + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.EMAIL, + PaymentAccountFormField.FieldId.TRADE_CURRENCIES, + PaymentAccountFormField.FieldId.SALT + ); + + // https://developer.paysafe.com/en/support/reference-information/codes/ + public static final List SUPPORTED_CURRENCIES = List.of( + new TraditionalCurrency("AED"), + new TraditionalCurrency("ARS"), + new TraditionalCurrency("AUD"), + new TraditionalCurrency("BGN"), + new TraditionalCurrency("BRL"), + new TraditionalCurrency("CAD"), + new TraditionalCurrency("CHF"), + new TraditionalCurrency("CZK"), + new TraditionalCurrency("DKK"), + new TraditionalCurrency("EGP"), + new TraditionalCurrency("EUR"), + new TraditionalCurrency("GBP"), + new TraditionalCurrency("GEL"), + new TraditionalCurrency("HRK"), + new TraditionalCurrency("HUF"), + new TraditionalCurrency("ILS"), + new TraditionalCurrency("INR"), + new TraditionalCurrency("JPY"), + new TraditionalCurrency("ISK"), + new TraditionalCurrency("KWD"), + new TraditionalCurrency("KRW"), + new TraditionalCurrency("MXN"), + new TraditionalCurrency("NOK"), + new TraditionalCurrency("NZD"), + new TraditionalCurrency("PEN"), + new TraditionalCurrency("PHP"), + new TraditionalCurrency("PLN"), + new TraditionalCurrency("RON"), + new TraditionalCurrency("RSD"), + new TraditionalCurrency("RUB"), + new TraditionalCurrency("SAR"), + new TraditionalCurrency("SEK"), + new TraditionalCurrency("TRY"), + new TraditionalCurrency("USD"), + new TraditionalCurrency("UYU") + ); + + public PaysafeAccount() { + super(PaymentMethod.PAYSAFE); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new PaysafeAccountPayload(paymentMethod.getId(), id); + } + + @Override + public @NotNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + + @Override + public @NotNull List getInputFieldIds() { + return INPUT_FIELD_IDS; + } + + public void setEmail(String accountId) { + ((PaysafeAccountPayload) paymentAccountPayload).setEmail(accountId); + } + + public String getEmail() { + return ((PaysafeAccountPayload) paymentAccountPayload).getEmail(); + } + + @Override + protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { + var field = super.getEmptyFormField(fieldId); + if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setValue(""); + return field; + } +} diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index e8f27c9d7f..4a8246fe78 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -51,6 +51,7 @@ import haveno.core.payment.CashAppAccount; import haveno.core.payment.CashAtAtmAccount; import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayPalAccount; +import haveno.core.payment.PaysafeAccount; import haveno.core.payment.CashDepositAccount; import haveno.core.payment.CelPayAccount; import haveno.core.payment.ZelleAccount; @@ -193,6 +194,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } @@ -588,7 +593,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable. + */ + +package haveno.core.payment.payload; + +import com.google.protobuf.Message; +import haveno.core.locale.Res; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class PaysafeAccountPayload extends PaymentAccountPayload { + private String email = ""; + + public PaysafeAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PaysafeAccountPayload(String paymentMethod, + String id, + String email, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.email = email; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setPaysafeAccountPayload(protobuf.PaysafeAccountPayload.newBuilder().setEmail(email)) + .build(); + } + + public static PaysafeAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new PaysafeAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getPaysafeAccountPayload().getEmail(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/src/main/java/haveno/core/proto/CoreProtoResolver.java b/core/src/main/java/haveno/core/proto/CoreProtoResolver.java index 504d59dc58..f7f8298308 100644 --- a/core/src/main/java/haveno/core/proto/CoreProtoResolver.java +++ b/core/src/main/java/haveno/core/proto/CoreProtoResolver.java @@ -54,6 +54,7 @@ import haveno.core.payment.payload.NequiAccountPayload; import haveno.core.payment.payload.OKPayAccountPayload; import haveno.core.payment.payload.PaxumAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; +import haveno.core.payment.payload.PaysafeAccountPayload; import haveno.core.payment.payload.PayPalAccountPayload; import haveno.core.payment.payload.PayseraAccountPayload; import haveno.core.payment.payload.PaytmAccountPayload; @@ -239,6 +240,8 @@ public class CoreProtoResolver implements ProtoResolver { return VenmoAccountPayload.fromProto(proto); case PAYPAL_ACCOUNT_PAYLOAD: return PayPalAccountPayload.fromProto(proto); + case PAYSAFE_ACCOUNT_PAYLOAD: + return PaysafeAccountPayload.fromProto(proto); default: throw new ProtobufferRuntimeException("Unknown proto message case(PB.PaymentAccountPayload). messageCase=" + messageCase); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 5cc6a41b62..b6c5671ae2 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -3067,6 +3067,9 @@ payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send - try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat \ to tell your trading peer the reference text you picked so they can verify your payment)\n\ - Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.paysafe.info=For your protection, we strongly discourage using Paysafecard PINs for payment.\n\n\ + Transactions made via PINs cannot be independently verified for dispute resolution. If an issue arises, recovering funds may not be possible.\n\n\ + To ensure transaction security with dispute resolution, always use payment methods that provide verifiable records. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ @@ -3302,6 +3305,8 @@ CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo PAYPAL_SHORT=PayPal +# suppress inspection "UnusedProperty" +PAYSAFE=Paysafe #################################################################### diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 9fe2018c32..5b64d1434a 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -3065,6 +3065,10 @@ payment.amazonGiftCard.info=Chcete-li platit dárkovou kartou Amazon eGift, bude - Na kartě do zprávy pro příjemce můžete přidat i vlastní originální text (např. "Happy birthday Susan!") spolu s ID obchodu (v takovém případě \ o tom informujte protistranu pomocí obchodovacího chatu, aby mohli s jistotou ověřit, že obdržená dárková karta pochází od vás.)\n\ - Karty Amazon eGift lze uplatnit pouze na té stránce Amazon, na které byly také koupeny (např. karta koupená na amazon.it může být uplatněna zase jen na amazon.it). +payment.paysafe.info=Pro vaši ochranu důrazně nedoporučujeme používat Paysafecard PINy pro platby.\n\n\ + Transakce provedené pomocí PINů nelze nezávisle ověřit pro řešení sporů. Pokud nastane problém, obnova prostředků nemusí být možná.\n\n\ + Pro zajištění bezpečnosti transakcí a podpory řešení sporů vždy používejte platební metody, které poskytují ověřitelné záznamy. + # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index d4821f7821..a4ff4da840 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -2047,6 +2047,9 @@ payment.australia.payid=PayID payment.payid=PayIDs wie E-Mail Adressen oder Telefonnummern die mit Finanzinstitutionen verbunden sind. payment.payid.info=Eine PayID wie eine Telefonnummer, E-Mail Adresse oder Australische Business Number (ABN) mit der Sie sicher Ihre Bank, Kreditgenossenschaft oder Bausparkassenkonto verlinken können. Sie müssen bereits eine PayID mit Ihrer Australischen Finanzinstitution erstellt haben. Beide Institutionen, die die sendet und die die empfängt, müssen PayID unterstützen. Weitere informationen finden Sie unter [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=Um mit einer Amazon eGift Geschenkkarte zu bezahlen, müssen Sie eine Amazon eGift Geschenkkarte über Ihr Amazon-Konto an den XMR-Verkäufer senden. \n\nHaveno zeigt die E-Mail-Adresse oder Telefonnummer des XMR-Verkäufers an, an die die Geschenkkarte gesendet werden soll, und Sie müssen die Handels-ID in das Nachrichtenfeld der Geschenkkarte eintragen. Bitte lesen Sie das Wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] für weitere Details und empfohlene Vorgehensweisen. \n\nDrei wichtige Hinweise:\n- Versuchen Sie Geschenkkarten mit Beträgen von 100 USD oder weniger zu versenden, weil Amazon größere Geschenkkarten gerne als betrügerisch kennzeichnet\n- Versuchen Sie einen kreativen, glaubwürdigen Text für die Nachricht der Geschenkkarten zu verwenden (z.B. "Alles Gute zum Geburtstag Susi!"), zusammen mit der Handels-ID (und verwenden Sie den Handels-Chat, um Ihrem Handelspartner den von Ihnen gewählten Referenztext mitzuteilen, damit er Ihre Zahlung überprüfen kann)\n- Amazon Geschenkkarten können nur auf der Amazon-Website eingelöst werden, auf der sie gekauft wurden (z. B. kann eine auf amazon.it gekaufte Geschenkkarte nur auf amazon.it eingelöst werden) +payment.paysafe.info=Zum Schutz Ihrer Sicherheit raten wir dringend davon ab, Paysafecard-PINs für Zahlungen zu verwenden.\n\n\ + Transaktionen, die über PINs durchgeführt werden, können nicht unabhängig zur Streitbeilegung überprüft werden. Wenn ein Problem auftritt, kann die Rückerstattung von Geldern möglicherweise nicht möglich sein.\n\n\ + Um die Transaktionssicherheit mit Streitbeilegung zu gewährleisten, verwenden Sie immer Zahlungsmethoden, die überprüfbare Aufzeichnungen bieten. # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index f6e0947499..d0248368a2 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -2048,6 +2048,9 @@ payment.australia.payid=PayID payment.payid=PayID conectado a una institución financiera. Como la dirección email o el número de móvil. payment.payid.info=Un PayID como un número de teléfono, dirección email o Australian Business Number (ABN), que puede conectar con seguridad a su banco, unión de crédito o cuenta de construcción de sociedad. Necesita haber creado una PayID con su institución financiera australiana. Tanto para enviar y recibir las instituciones financieras deben soportar PayID. Para más información por favor compruebe [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=Para pagar con Tarjeta eGift Amazon. necesitará enviar una Tarjeta eGift Amazon al vendedor XMR a través de su cuenta Amazon.\n\nHaveno mostrará la dirección e-mail del vendedor de XMR o el número de teléfono donde la tarjeta de regalo deberá enviarse. Por favor vea la wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] para más detalles y mejores prácticas.\n\nNotas importantes:\n- Pruebe a enviar las tarjetas regalo en cantidades de 100USD o menores, ya que Amazon está señalando tarjetas regalo mayores como fraudulentas.\n- Intente usar textos para el mensaje de la tarjeta regalo creíbles y creativos ("Feliz cumpleaños!").\n- Las tarjetas Amazon eGift pueden ser redimidas únicamente en la web de Amazon en la que se compraron (por ejemplo, una tarjeta comprada en amazon.it solo puede ser redimida en amazon.it) +payment.paysafe.info=Por su protección, desaconsejamos encarecidamente el uso de PINs de Paysafecard para pagos.\n\n\ + Las transacciones realizadas mediante PINs no pueden ser verificadas de forma independiente para la resolución de disputas. Si surge un problema, recuperar los fondos puede no ser posible.\n\n\ + Para garantizar la seguridad de las transacciones con resolución de disputas, utilice siempre métodos de pago que proporcionen registros verificables. # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 91bf61259c..af5737106d 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -2022,6 +2022,9 @@ payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.paysafe.info=برای حفاظت از شما، به شدت از استفاده از پین‌های Paysafecard برای پرداخت جلوگیری می‌کنیم.\n\n\ + تراکنش‌های انجام شده از طریق پین‌ها نمی‌توانند به طور مستقل برای حل اختلاف تأیید شوند. اگر مشکلی پیش آید، بازیابی وجوه ممکن است غیرممکن باشد.\n\n\ + برای اطمینان از امنیت تراکنش و حل اختلاف، همیشه از روش‌های پرداختی استفاده کنید که سوابق قابل تاییدی ارائه می‌دهند. # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 6bfffcfca4..0c1abbd22d 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -2051,6 +2051,9 @@ payment.australia.payid=ID de paiement payment.payid=ID de paiement lié à une institution financière. Comme l'addresse email ou le téléphone portable. payment.payid.info=Un PayID, tel qu'un numéro de téléphone, une adresse électronique ou un numéro d'entreprise australien (ABN), que vous pouvez lier en toute sécurité à votre compte bancaire, votre crédit mutuel ou votre société de crédit immobilier. Vous devez avoir déjà créé un PayID auprès de votre institution financière australienne. Les institutions financières émettrices et réceptrices doivent toutes deux prendre en charge PayID. Pour plus d'informations, veuillez consulter [LIEN:https://payid.com.au/faqs/]. payment.amazonGiftCard.info=Pour payer avec une carte cadeau Amazon eGift Card, vous devrez envoyer une carte cadeau Amazon eGift Card au vendeur de XMR via votre compte Amazon. \n\nHaveno indiquera l'adresse e-mail ou le numéro de téléphone du vendeur XMR où la carte cadeau doit être envoyée, et vous devrez inclure l'ID du trade dans le champ de messagerie de la carte cadeau. Veuillez consulter le wiki [LIEN:https://haveno.exchange/wiki/Amazon_eGift_card] pour plus de détails et pour les meilleures pratiques à adopter. \n\nTrois remarques importantes :\n- essayez d'envoyer des cartes-cadeaux d'un montant inférieur ou égal à 100 USD, car Amazon est connu pour signaler les cartes-cadeaux plus importantes comme frauduleuses\n- essayez d'utiliser un texte créatif et crédible pour le message de la carte cadeau (par exemple, "Joyeux anniversaire Susan !") ainsi que l'ID du trade (et utilisez le chat du trader pour indiquer à votre pair de trading le texte de référence que vous avez choisi afin qu'il puisse vérifier votre paiement).\n- Les cartes cadeaux électroniques Amazon ne peuvent être échangées que sur le site Amazon où elles ont été achetées (par exemple, une carte cadeau achetée sur amazon.it ne peut être échangée que sur amazon.it). +payment.paysafe.info=Pour votre protection, nous déconseillons fortement d'utiliser les PINs Paysafecard pour les paiements.\n\n\ + Les transactions effectuées via des PINs ne peuvent pas être vérifiées de manière indépendante pour la résolution des litiges. En cas de problème, la récupération des fonds peut ne pas être possible.\n\n\ + Pour garantir la sécurité des transactions et la résolution des litiges, utilisez toujours des méthodes de paiement qui fournissent des preuves vérifiables. # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 1a9dc9ea21..093b8ca26b 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -2025,6 +2025,9 @@ payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.paysafe.info=Per la tua protezione, sconsigliamo vivamente di utilizzare i PIN di Paysafecard per i pagamenti.\n\n\ + Le transazioni effettuate tramite PIN non possono essere verificate in modo indipendente per la risoluzione delle controversie. Se si verifica un problema, il recupero dei fondi potrebbe non essere possibile.\n\n\ + Per garantire la sicurezza delle transazioni con risoluzione delle controversie, utilizza sempre metodi di pagamento che forniscono registrazioni verificabili. # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 78ade2a04f..10090e7ca5 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -2047,6 +2047,9 @@ payment.australia.payid=PayID payment.payid=金融機関と繋がっているPayID。例えばEメールアドレスそれとも携帯電話番号。 payment.payid.info=銀行、信用金庫、あるいは住宅金融組合アカウントと安全に繋がれるPayIDとして使われる電話番号、Eメールアドレス、それともオーストラリア企業番号(ABN)。すでにオーストラリアの金融機関とPayIDを作った必要があります。送金と受取の金融機関は両方PayIDをサポートする必要があります。詳しくは以下を訪れて下さい [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=アマゾンeGiftカードで支払うには、アマゾンアカウントを使ってeGiftカードをXMR売り手に送る必要があります。\n\nHavenoはeGiftカードの送り先になるXMR売り手のメールアドレスそれとも電話番号を表示します。そしてeGiftカードのメッセージフィールドに、必ずトレードIDを入力して下さい。最良の慣行について詳しくはWikiを参照して下さい:[HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card]\n\n3つの注意点:\n- 可能であれば、100米ドル価格以下のeGiftカードを送って下さい。それ以上の価格はアマゾンに不正な取引というフラグが立てられることがあります。\n- eGiftカードのメッセージフィールドに、トレードIDと一緒に信ぴょう性のあるメッセージを入力して下さい。(例えば隆さん、「お誕生日おめでとう!」)。(そして確認のため、取引者チャットでトレードピアにメッセージの内容を伝えて下さい)。\n- アマゾンeGiftカードは買われたサイトのみに交換できます(例えば、amazon.jpから買われたカードはamazon.jpのみに交換できます)。 +payment.paysafe.info=あなたの保護のため、支払いにPaysafecard PINの使用は強くお勧めしません。\n\n\ + PINを使用した取引は、紛争解決のために独立して確認することができません。問題が発生した場合、資金の回収が不可能になることがあります。\n\n\ + 取引の安全性と紛争解決を確保するため、常に確認可能な記録を提供する支払い方法を使用してください。 # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index c9622afc15..35fdafd993 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -2032,6 +2032,9 @@ payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.paysafe.info=Para sua proteção, desaconselhamos fortemente o uso de PINs do Paysafecard para pagamento.\n\n\ + Transações feitas por PINs não podem ser verificadas de forma independente para resolução de disputas. Se ocorrer um problema, a recuperação de fundos pode não ser possível.\n\n\ + Para garantir a segurança das transações com resolução de disputas, sempre utilize métodos de pagamento que forneçam registros verificáveis. # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 3cfb931833..1878af937e 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -2022,6 +2022,9 @@ payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.paysafe.info=Para sua proteção, desaconselhamos fortemente o uso de PINs do Paysafecard para pagamento.\n\n\ + Transações feitas por PINs não podem ser verificadas de forma independente para resolução de disputas. Se ocorrer um problema, a recuperação dos fundos pode não ser possível.\n\n\ + Para garantir a segurança das transações com resolução de disputas, sempre use métodos de pagamento que forneçam registros verificáveis. # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 84d3778f81..f94de3c4da 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -2023,6 +2023,9 @@ payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.paysafe.info=Для вашей защиты мы настоятельно не рекомендуем использовать PIN-коды Paysafecard для платежей.\n\n\ + Транзакции, выполненные с помощью PIN-кодов, не могут быть независимо подтверждены для разрешения споров. В случае возникновения проблемы возврат средств может быть невозможен.\n\n\ + Чтобы обеспечить безопасность транзакций с возможностью разрешения споров, всегда используйте методы оплаты, предоставляющие проверяемые записи. # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index de5a2456db..f77cea2f45 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -2023,6 +2023,9 @@ payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.paysafe.info=เพื่อความปลอดภัยของคุณ เราขอแนะนำอย่างยิ่งให้หลีกเลี่ยงการใช้ Paysafecard PINs ในการชำระเงิน\n\n\ + ธุรกรรมที่ดำเนินการผ่าน PIN ไม่สามารถตรวจสอบได้อย่างอิสระสำหรับการระงับข้อพิพาท หากเกิดปัญหา อาจไม่สามารถกู้คืนเงินได้\n\n\ + เพื่อความปลอดภัยของธุรกรรมและรองรับการระงับข้อพิพาท โปรดใช้วิธีการชำระเงินที่มีบันทึกการทำธุรกรรมที่ตรวจสอบได้ # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index c24769b955..e46f5a3e10 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -3052,6 +3052,9 @@ payment.amazonGiftCard.info=Amazon eGift Kart ile ödeme yapmak için, Amazon he - hediye kartının mesajı için yaratıcı, inanılabilir bir metin kullanmaya çalışın (örneğin, "Doğum günün kutlu olsun Metin Torun!") ve ticaret \ kimliğini ekleyin (ve ticaret sohbetinde ticaret eşinize seçtiğiniz referans metnini söyleyin, böylece ödemenizi doğrulayabilirler)\n\ - Amazon eGift Kartları, yalnızca satın alındıkları Amazon web sitesinde kullanılabilir (örneğin, amazon.it üzerinden satın alınan bir hediye kartı yalnızca amazon.it üzerinde kullanılabilir) +payment.paysafe.info=Sizin korumanız için, Paysafecard PIN'lerini ödeme için kullanmanızı kesinlikle önermiyoruz.\n\n\ + PIN'ler ile yapılan işlemler, ihtilaf çözümü için bağımsız olarak doğrulanamaz. Bir sorun oluşursa, fonların geri alınması mümkün olmayabilir.\n\n\ + İhtilaf çözümü ile işlem güvenliğini sağlamak için, her zaman doğrulanabilir kayıtlar sağlayan ödeme yöntemlerini kullanın. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 8d8ec4e5be..7c6605d7e2 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -2025,6 +2025,9 @@ payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.paysafe.info=Vì sự bảo vệ của bạn, chúng tôi khuyến cáo không nên sử dụng mã PIN Paysafecard để thanh toán.\n\n\ + Các giao dịch được thực hiện bằng mã PIN không thể được xác minh độc lập để giải quyết tranh chấp. Nếu có vấn đề xảy ra, có thể không thể khôi phục số tiền đã mất.\n\n\ + Để đảm bảo an toàn giao dịch và có thể giải quyết tranh chấp, hãy luôn sử dụng các phương thức thanh toán có hồ sơ xác minh được. # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index fc59e3c6c1..3376029d07 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -2032,6 +2032,9 @@ payment.australia.payid=PayID payment.payid=PayID 需链接至金融机构。例如电子邮件地址或手机。 payment.payid.info=PayID,如电话号码、电子邮件地址或澳大利亚商业号码(ABN),您可以安全地连接到您的银行、信用合作社或建立社会帐户。你需要在你的澳大利亚金融机构创建一个 PayID。发送和接收金融机构都必须支持 PayID。更多信息请查看[HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.paysafe.info=为了保障您的安全,我们强烈不建议使用 Paysafecard PIN 进行支付。\n\n\ + 通过 PIN 进行的交易无法被独立验证以解决争议。如果出现问题,资金可能无法追回。\n\n\ + 为确保交易安全并支持争议解决,请始终使用提供可验证记录的支付方式。 # We use constants from the code so we do not use our normal naming convention diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index ef8e2ca964..2bba3084ef 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -2026,6 +2026,9 @@ payment.australia.payid=PayID payment.payid=PayID 需鏈接至金融機構。例如電子郵件地址或手機。 payment.payid.info=PayID,如電話號碼、電子郵件地址或澳大利亞商業號碼(ABN),您可以安全地連接到您的銀行、信用合作社或建立社會帳户。你需要在你的澳大利亞金融機構創建一個 PayID。發送和接收金融機構都必須支持 PayID。更多信息請查看[HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.paysafe.info=為了保護您的安全,我們強烈不建議使用 Paysafecard PIN 進行付款。\n\n\ + 透過 PIN 進行的交易無法獨立驗證以進行爭議解決。如果發生問題,可能無法追回資金。\n\n\ + 為確保交易安全並支持爭議解決,請始終使用可驗證記錄的付款方式。 # We use constants from the code so we do not use our normal naming convention diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaysafeForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaysafeForm.java new file mode 100644 index 0000000000..3eba4c78fa --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaysafeForm.java @@ -0,0 +1,111 @@ +/* + * This file is part of Haveno. + * + * Haveno 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. + * + * Haveno 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 Haveno. If not, see . + */ + +package haveno.desktop.components.paymentmethods; + +import haveno.core.account.witness.AccountAgeWitnessService; +import haveno.core.locale.Res; +import haveno.core.payment.PaymentAccount; +import haveno.core.payment.PaysafeAccount; +import haveno.core.payment.payload.PaysafeAccountPayload; +import haveno.core.payment.payload.PaymentAccountPayload; +import haveno.core.payment.validation.EmailValidator; +import haveno.core.util.coin.CoinFormatter; +import haveno.core.util.validation.InputValidator; +import haveno.desktop.components.InputTextField; +import haveno.desktop.util.FormBuilder; +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; + +public class PaysafeForm extends PaymentMethodForm { + private final PaysafeAccount account; + private final EmailValidator validator = new EmailValidator(); + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"), + ((PaysafeAccountPayload) paymentAccountPayload).getEmail()); + return gridRow; + } + + public PaysafeForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (PaysafeAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + emailInputTextField.setValidator(validator); + emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setEmail(newValue.trim()); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void addCurrenciesGrid(boolean isEditable) { + FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, + Res.get("payment.supportedCurrenciesForReceiver"), 20, 20).second; + + if (isEditable) { + flowPane.setId("flow-pane-checkboxes-bg"); + } else { + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + } + + paymentAccount.getSupportedCurrencies().forEach(currency -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getEmail()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), + account.getEmail()).second; + field.setMouseTransparent(false); + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && account.getEmail() != null + && validator.validate(account.getEmail()).isValid + && account.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java index 6ed91e956b..a5f7332c81 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java @@ -38,6 +38,7 @@ import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayPalAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountFactory; +import haveno.core.payment.PaysafeAccount; import haveno.core.payment.RevolutAccount; import haveno.core.payment.USPostalMoneyOrderAccount; import haveno.core.payment.VenmoAccount; @@ -103,6 +104,7 @@ import haveno.desktop.components.paymentmethods.PaxumForm; import haveno.desktop.components.paymentmethods.PayByMailForm; import haveno.desktop.components.paymentmethods.PayPalForm; import haveno.desktop.components.paymentmethods.PaymentMethodForm; +import haveno.desktop.components.paymentmethods.PaysafeForm; import haveno.desktop.components.paymentmethods.PayseraForm; import haveno.desktop.components.paymentmethods.PaytmForm; import haveno.desktop.components.paymentmethods.PerfectMoneyForm; @@ -400,6 +402,13 @@ public class TraditionalAccountsView extends PaymentAccountsView doSaveNewAccount(paymentAccount)) .show(); + } else if (paymentAccount instanceof PaysafeAccount) { + new Popup().warning(Res.get("payment.paysafe.info")) + .width(700) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iUnderstand")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); } else { doSaveNewAccount(paymentAccount); } @@ -677,6 +686,8 @@ public class TraditionalAccountsView extends PaymentAccountsView Date: Tue, 4 Feb 2025 09:52:44 -0500 Subject: [PATCH 117/371] remove HRK currency from paysafe --- core/src/main/java/haveno/core/payment/PaysafeAccount.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/haveno/core/payment/PaysafeAccount.java b/core/src/main/java/haveno/core/payment/PaysafeAccount.java index 7d756ca362..531aa998b6 100644 --- a/core/src/main/java/haveno/core/payment/PaysafeAccount.java +++ b/core/src/main/java/haveno/core/payment/PaysafeAccount.java @@ -53,7 +53,6 @@ public final class PaysafeAccount extends PaymentAccount { new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("GEL"), - new TraditionalCurrency("HRK"), new TraditionalCurrency("HUF"), new TraditionalCurrency("ILS"), new TraditionalCurrency("INR"), From b48dbc2fb35eb3a71160ec69005eb14578b86868 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 5 Feb 2025 08:06:01 -0500 Subject: [PATCH 118/371] fix broken link to report issues --- .../src/main/java/haveno/desktop/main/overlays/Overlay.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java b/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java index f937fb4add..4d0eb57fea 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java @@ -394,7 +394,7 @@ public abstract class Overlay> { public T useReportBugButton() { this.closeButtonText = Res.get("shared.reportBug"); - this.closeHandlerOptional = Optional.of(() -> GUIUtil.openWebPage("https://haveno.exchange/source/haveno/issues")); + this.closeHandlerOptional = Optional.of(() -> GUIUtil.openWebPage("https://github.com/haveno-dex/haveno/issues")); return cast(); } @@ -939,7 +939,7 @@ public abstract class Overlay> { gitHubButton.setOnAction(event -> { if (message != null) Utilities.copyToClipboard(message); - GUIUtil.openWebPage("https://haveno.exchange/source/haveno/issues"); + GUIUtil.openWebPage("https://github.com/haveno-dex/haveno/issues"); hide(); }); } From f35c7f8544693658bed8bffacddf98c4af692bdf Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 5 Feb 2025 09:29:48 -0500 Subject: [PATCH 119/371] remove account info from crypto offer view --- .../desktop/main/offer/offerbook/OfferBookViewModel.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index 9990573155..d98098e99c 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -643,9 +643,10 @@ abstract class OfferBookViewModel extends ActivatableViewModel { public boolean hasSelectionAccountSigning() { if (showAllTradeCurrenciesProperty.get()) { - if (!isShowAllEntry(selectedPaymentMethod.getId())) { + if (isShowAllEntry(selectedPaymentMethod.getId())) + return !(this instanceof CryptoOfferBookViewModel); + else return PaymentMethod.hasChargebackRisk(selectedPaymentMethod); - } } else { if (isShowAllEntry(selectedPaymentMethod.getId())) return CurrencyUtil.getMatureMarketCurrencies().stream() @@ -653,7 +654,6 @@ abstract class OfferBookViewModel extends ActivatableViewModel { else return PaymentMethod.hasChargebackRisk(selectedPaymentMethod, tradeCurrencyCode.get()); } - return true; } private static String getDirectionWithCodeDetailed(OfferDirection direction, String currencyCode) { From 728cf22578112a7814c8bd3394930d8367b3366e Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 5 Feb 2025 08:44:23 -0500 Subject: [PATCH 120/371] replace 'mediator or arbitrator' with 'arbitrator' for some translations --- core/src/main/resources/i18n/displayStrings.properties | 6 +++--- core/src/main/resources/i18n/displayStrings_fa.properties | 3 --- core/src/main/resources/i18n/displayStrings_it.properties | 3 --- .../src/main/resources/i18n/displayStrings_pt-br.properties | 3 --- core/src/main/resources/i18n/displayStrings_pt.properties | 3 --- core/src/main/resources/i18n/displayStrings_ru.properties | 2 -- core/src/main/resources/i18n/displayStrings_th.properties | 3 --- core/src/main/resources/i18n/displayStrings_vi.properties | 3 --- 8 files changed, 3 insertions(+), 23 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index b6c5671ae2..2e48f78d65 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2980,8 +2980,8 @@ payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on - XMR buyers must send the USPMO to the XMR seller with Delivery Confirmation.\n\ \n\ In the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Haveno 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\n\ -Failure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\n\ -In all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\n\ +Failure to provide the required information to the arbitrator will result in losing the dispute case.\n\n\ +In all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the arbitrator.\n\n\ If you do not understand these requirements, do not trade using USPMO on Haveno. payment.payByMail.info=Trading using Pay by Mail on Haveno requires that you understand the following:\n\ @@ -3040,7 +3040,7 @@ payment.f2f.info='Face to Face' trades have different rules and come with differ ● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n\ ● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n\ ● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n\ - ● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence \ + ● In case of a dispute the arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence \ of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to \ an agreement.\n\n\ To be sure you fully understand the differences with 'Face to Face' trades please read the instructions and \ diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index af5737106d..eff59f853e 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1995,8 +1995,6 @@ payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the cou payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR 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- XMR buyers must send the USPMO to the XMR 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 Haveno 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 Haveno. - payment.payByMail.contact=اطلاعات تماس payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=اطلاعات تماس @@ -2009,7 +2007,6 @@ payment.shared.extraInfo.offer=اطلاعات اضافی پیشنهاد payment.shared.extraInfo.prompt.paymentAccount=هرگونه اصطلاحات، شرایط یا جزئیات خاصی که می‌خواهید همراه با پیشنهادات شما برای این حساب پرداخت نمایش داده شود را تعریف کنید (کاربران قبل از پذیرش پیشنهادات این اطلاعات را مشاهده خواهند کرد). payment.shared.extraInfo.prompt.offer=هر اصطلاح، شرایط یا جزئیات خاصی که مایلید همراه با پیشنهاد خود نمایش داده شود را تعریف کنید. payment.shared.extraInfo.noDeposit=جزئیات تماس و شرایط پیشنهاد -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=باز کردن صفحه وب payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} payment.shared.extraInfo.tooltip=اطلاعات اضافی: {0} diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 093b8ca26b..e38006bc67 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1998,8 +1998,6 @@ payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the cou payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR 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- XMR buyers must send the USPMO to the XMR 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 Haveno 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 Haveno. - payment.payByMail.contact=Informazioni di contatto payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Informazioni di contatto @@ -2012,7 +2010,6 @@ payment.shared.extraInfo.offer=Informazioni aggiuntive sull'offerta payment.shared.extraInfo.prompt.paymentAccount=Definisci eventuali termini, condizioni o dettagli speciali che desideri vengano visualizzati con le tue offerte per questo account di pagamento (gli utenti vedranno queste informazioni prima di accettare le offerte). payment.shared.extraInfo.prompt.offer=Definisci eventuali termini, condizioni o dettagli speciali che desideri mostrare con la tua offerta. payment.shared.extraInfo.noDeposit=Dettagli di contatto e termini dell'offerta -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Apri sito web payment.f2f.offerbook.tooltip.countryAndCity=Paese e città: {0} / {1} payment.shared.extraInfo.tooltip=Ulteriori informazioni: {0} diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 35fdafd993..52877fd254 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -2005,8 +2005,6 @@ payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the cou payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR 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- XMR buyers must send the USPMO to the XMR 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 Haveno 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 Haveno. - payment.payByMail.contact=Informações para contato payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Informações para contato @@ -2019,7 +2017,6 @@ payment.shared.extraInfo.offer=Informações adicionais sobre a oferta payment.shared.extraInfo.prompt.paymentAccount=Defina quaisquer termos, condições ou detalhes especiais que você gostaria que fossem exibidos com suas ofertas para esta conta de pagamento (os usuários verão estas informações antes de aceitar as ofertas). payment.shared.extraInfo.prompt.offer=Defina quaisquer termos, condições ou detalhes especiais que você gostaria de exibir com sua oferta. payment.shared.extraInfo.noDeposit=Detalhes de contato e termos da oferta -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Abrir site payment.f2f.offerbook.tooltip.countryAndCity=País e cidade: {0} / {1} payment.shared.extraInfo.tooltip=Informações adicionais: {0} diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 1878af937e..c690f722d4 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1995,8 +1995,6 @@ payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the cou payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR 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- XMR buyers must send the USPMO to the XMR 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 Haveno 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 Haveno. - payment.payByMail.contact=Informação de contacto payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Informação de contacto @@ -2009,7 +2007,6 @@ payment.shared.extraInfo.offer=Informações adicionais sobre a oferta payment.shared.extraInfo.prompt.paymentAccount=Defina quaisquer termos especiais, condições ou detalhes que você gostaria de exibir com suas ofertas para esta conta de pagamento (os usuários verão essas informações antes de aceitar as ofertas). payment.shared.extraInfo.prompt.offer=Defina quaisquer termos, condições ou detalhes especiais que você gostaria de exibir com sua oferta. payment.shared.extraInfo.noDeposit=Detalhes de contato e termos da oferta -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Abrir página web payment.f2f.offerbook.tooltip.countryAndCity=País e cidade: {0} / {1} payment.shared.extraInfo.tooltip=Informação adicional: {0} diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index f94de3c4da..6043564648 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1996,8 +1996,6 @@ payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the cou payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR 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- XMR buyers must send the USPMO to the XMR 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 Haveno 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 Haveno. - payment.payByMail.contact=Контактная информация payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Контактная информация diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index f77cea2f45..76a0847bc2 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1996,8 +1996,6 @@ payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the cou payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR 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- XMR buyers must send the USPMO to the XMR 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 Haveno 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 Haveno. - payment.payByMail.contact=ข้อมูลติดต่อ payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=ข้อมูลติดต่อ @@ -2010,7 +2008,6 @@ payment.shared.extraInfo.offer=ข้อมูลเพิ่มเติมเ payment.shared.extraInfo.prompt.paymentAccount=กำหนดคำศัพท์ เงื่อนไข หรือรายละเอียดพิเศษใดๆ ที่คุณต้องการให้แสดงพร้อมข้อเสนอของคุณสำหรับบัญชีการชำระเงินนี้ (ผู้ใช้จะเห็นข้อมูลนี้ก่อนที่จะยอมรับข้อเสนอ) payment.shared.extraInfo.prompt.offer=กำหนดเงื่อนไขพิเศษ ข้อกำหนด หรือรายละเอียดใด ๆ ที่คุณต้องการแสดงพร้อมกับข้อเสนอของคุณ payment.shared.extraInfo.noDeposit=รายละเอียดการติดต่อและเงื่อนไขข้อเสนอ -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=เปิดหน้าเว็บ payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} payment.shared.extraInfo.tooltip=ข้อมูลเพิ่มเติม: {0} diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 7c6605d7e2..3929731346 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1998,8 +1998,6 @@ payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the cou payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR 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- XMR buyers must send the USPMO to the XMR 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 Haveno 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 Haveno. - payment.payByMail.contact=thông tin liên hệ payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=thông tin liên hệ @@ -2012,7 +2010,6 @@ payment.shared.extraInfo.offer=Thông tin bổ sung về ưu đãi payment.shared.extraInfo.prompt.paymentAccount=Xác định bất kỳ điều khoản, điều kiện hoặc chi tiết đặc biệt nào bạn muốn hiển thị cùng với các ưu đãi của mình cho tài khoản thanh toán này (người dùng sẽ thấy thông tin này trước khi chấp nhận các ưu đãi). payment.shared.extraInfo.prompt.offer=Xác định bất kỳ thuật ngữ, điều kiện hoặc chi tiết đặc biệt nào bạn muốn hiển thị cùng với đề nghị của mình. payment.shared.extraInfo.noDeposit=Chi tiết liên hệ và điều khoản ưu đãi -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Mở trang web payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} payment.shared.extraInfo.tooltip=Thông tin thêm: {0} From afa95f1b159f3efae3301b3221adbf9fcae0110a Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 6 Feb 2025 09:50:35 -0500 Subject: [PATCH 121/371] fix mismatch between payment sent message state and trade state --- core/src/main/java/haveno/core/trade/Trade.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 069b257dd5..6ad8e7aef3 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -732,7 +732,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { xmrWalletService.addWalletListener(idlePayoutSyncer); } - // TODO: buyer's payment sent message state property can become unsynced (after improper shut down?) + // TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed in v1.0.19 so this check can be removed? if (isBuyer()) { MessageState expectedState = getPaymentSentMessageState(); if (expectedState != null && expectedState != processModel.getPaymentSentMessageStateProperty().get()) { @@ -2020,12 +2020,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (processModel.getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; switch (state) { case BUYER_SENT_PAYMENT_SENT_MSG: - case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: return MessageState.SENT; + case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: + return MessageState.ARRIVED; case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG: return MessageState.STORED_IN_MAILBOX; case SELLER_RECEIVED_PAYMENT_SENT_MSG: - return MessageState.ARRIVED; + return MessageState.ACKNOWLEDGED; case BUYER_SEND_FAILED_PAYMENT_SENT_MSG: return MessageState.FAILED; default: From 5f8cf97d1653bc491e89b1130cf565de690022e0 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 8 Feb 2025 08:40:51 -0500 Subject: [PATCH 122/371] replace throwing Error with RuntimeException --- common/src/main/java/haveno/common/crypto/KeyRing.java | 2 +- .../java/haveno/core/xmr/wallet/XmrWalletService.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/haveno/common/crypto/KeyRing.java b/common/src/main/java/haveno/common/crypto/KeyRing.java index 89552c7c74..580b3b285a 100644 --- a/common/src/main/java/haveno/common/crypto/KeyRing.java +++ b/common/src/main/java/haveno/common/crypto/KeyRing.java @@ -110,7 +110,7 @@ public final class KeyRing { * @param password The password to unlock the keys or to generate new keys, nullable. */ public void generateKeys(String password) { - if (isUnlocked()) throw new Error("Current keyring must be closed to generate new keys"); + if (isUnlocked()) throw new IllegalStateException("Current keyring must be closed to generate new keys"); symmetricKey = Encryption.generateSecretKey(256); signatureKeyPair = Sig.generateKeyPair(); encryptionKeyPair = Encryption.generateKeyPair(); diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 8432da2aed..5f047ee7f6 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -415,7 +415,7 @@ public class XmrWalletService extends XmrWalletBase { public void deleteWallet(String walletName) { assertNotPath(walletName); log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName); - if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName); + if (!walletExists(walletName)) throw new RuntimeException("Wallet does not exist at path: " + walletName); String path = walletDir.toString() + File.separator + walletName; if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet cache file: " + path); if (!new File(path + KEYS_FILE_POSTFIX).delete()) throw new RuntimeException("Failed to delete wallet keys file: " + path + KEYS_FILE_POSTFIX); @@ -755,7 +755,7 @@ public class XmrWalletService extends XmrWalletBase { if (keyImages != null) { Set txKeyImages = new HashSet(); for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex()); - if (!txKeyImages.equals(new HashSet(keyImages))) throw new Error("Tx inputs do not match claimed key images"); + if (!txKeyImages.equals(new HashSet(keyImages))) throw new RuntimeException("Tx inputs do not match claimed key images"); } // verify unlock height @@ -764,7 +764,7 @@ public class XmrWalletService extends XmrWalletBase { // verify miner fee BigInteger minerFeeEstimate = getElevatedFeeEstimate(tx.getWeight()); double minerFeeDiff = tx.getFee().subtract(minerFeeEstimate).abs().doubleValue() / minerFeeEstimate.doubleValue(); - if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new Error("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee()); + if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee()); log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff); // verify proof to fee address @@ -1739,7 +1739,7 @@ public class XmrWalletService extends XmrWalletBase { private MoneroWalletRpc startWalletRpcInstance(Integer port, boolean applyProxyUri) { // check if monero-wallet-rpc exists - if (!new File(MONERO_WALLET_RPC_PATH).exists()) throw new Error("monero-wallet-rpc executable doesn't exist at path " + MONERO_WALLET_RPC_PATH + if (!new File(MONERO_WALLET_RPC_PATH).exists()) throw new RuntimeException("monero-wallet-rpc executable doesn't exist at path " + MONERO_WALLET_RPC_PATH + "; copy monero-wallet-rpc to the project root or set WalletConfig.java MONERO_WALLET_RPC_PATH for your system"); // build command to start monero-wallet-rpc From c26974610cbea8ff83e09cd7b2a0764525d1cb5d Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 9 Feb 2025 09:18:54 -0500 Subject: [PATCH 123/371] always copy monero binaries to resources folder on build --- build.gradle | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index c8a80a9075..e93d74c1d4 100644 --- a/build.gradle +++ b/build.gradle @@ -506,16 +506,16 @@ configure(project(':core')) { } else { ext.extractArchiveTarGz(moneroArchiveFile, localnetDir) } + } - // add the current platform's monero dependencies into the resources folder for installation - copy { - from "${monerodFile}" - into "${project(':core').projectDir}/src/main/resources/bin" - } - copy { - from "${moneroRpcFile}" - into "${project(':core').projectDir}/src/main/resources/bin" - } + // add the current platform's monero dependencies into the resources folder for installation + copy { + from "${monerodFile}" + into "${project(':core').projectDir}/src/main/resources/bin" + } + copy { + from "${moneroRpcFile}" + into "${project(':core').projectDir}/src/main/resources/bin" } } From bffcf7c7c0e002af02f9b774e0ee45bb8f9ec80d Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 12 Feb 2025 06:20:17 -0500 Subject: [PATCH 124/371] fix hyperlinks to payment methods --- .../main/resources/i18n/displayStrings.properties | 12 ++++++------ .../main/resources/i18n/displayStrings_cs.properties | 12 ++++++------ .../main/resources/i18n/displayStrings_de.properties | 4 ++-- .../main/resources/i18n/displayStrings_es.properties | 4 ++-- .../main/resources/i18n/displayStrings_fa.properties | 4 ++-- .../main/resources/i18n/displayStrings_fr.properties | 4 ++-- .../main/resources/i18n/displayStrings_it.properties | 4 ++-- .../main/resources/i18n/displayStrings_ja.properties | 4 ++-- .../resources/i18n/displayStrings_pt-br.properties | 4 ++-- .../main/resources/i18n/displayStrings_pt.properties | 4 ++-- .../main/resources/i18n/displayStrings_ru.properties | 4 ++-- .../main/resources/i18n/displayStrings_th.properties | 4 ++-- .../main/resources/i18n/displayStrings_tr.properties | 12 ++++++------ .../main/resources/i18n/displayStrings_vi.properties | 4 ++-- .../resources/i18n/displayStrings_zh-hans.properties | 4 ++-- .../resources/i18n/displayStrings_zh-hant.properties | 4 ++-- 16 files changed, 44 insertions(+), 44 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 2e48f78d65..d8111b7b9c 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -687,7 +687,7 @@ portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, le portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees.swift=Make sure to use the SHA (shared fee model) to send the SWIFT payment. \ - See more details at [HYPERLINK:https://haveno.exchange/wiki/SWIFT#Use_the_correct_fee_option]. + See more details at [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT#Use_the_correct_fee_option]. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Please transfer from your external {0} wallet\n{1} to the XMR seller.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -707,7 +707,7 @@ portfolio.pending.step2_buyer.postal=Please send {0} by \"US Postal Money Order\ # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. \ Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. \ - See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n + See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2720,20 +2720,20 @@ payment.swift.info.account=Carefully review the core guidelines for using SWIFT - buyer must use the shared fee model (SHA) \n\ - buyer and seller may incur fees, so they should check their bank's fee schedules beforehand \n\ \n\ -SWIFT is more sophisticated than other payment methods, so please take a moment to review full guidance on the wiki [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +SWIFT is more sophisticated than other payment methods, so please take a moment to review full guidance on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.swift.info.buyer=To buy monero with SWIFT, you must:\n\ \n\ - send payment in the currency specified by the offer maker \n\ - use the shared fee model (SHA) to send payment\n\ \n\ -Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.swift.info.seller=SWIFT senders are required to use the shared fee model (SHA) to send payments.\n\ \n\ If you receive a SWIFT payment that does not use SHA, open a mediation ticket.\n\ \n\ -Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.imps.info.account=Please make sure to include your:\n\n\ ● Account owner full name\n\ @@ -3061,7 +3061,7 @@ payment.payid.info=A PayID like a phone number, email address or an Australian B bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. \ Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\n\ - Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\n\ + Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\n\ Three important notes:\n\ - try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n\ - try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat \ diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 5b64d1434a..5a2a065535 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -687,7 +687,7 @@ portfolio.pending.step2_buyer.refTextWarn=Důležité: když vyplňujete platebn portfolio.pending.step2_buyer.fees=Pokud vaše banka účtuje poplatky za převod, musíte tyto poplatky uhradit vy. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees.swift=Ujistěte se, že k odeslání platby SWIFT používáte model SHA (model sdílených poplatků). \ - Více detailů [HYPERLINK:https://haveno.exchange/wiki/SWIFT#Use_the_correct_fee_option]. + Více detailů [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT#Use_the_correct_fee_option]. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Převeďte prosím z vaší externí {0} peněženky\n{1} prodejci XMR.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -707,7 +707,7 @@ portfolio.pending.step2_buyer.postal=Zašlete prosím {0} prodejci XMR pomocí \ # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Zašlete prosím {0} prodejci XMR v poštovní zásilce (\"Hotovost poštou\"). \ Konkrétní instrukce naleznete v obchodní smlouvě. V případě pochybností se můžete zeptat protistrany pomocí obchodního chatu. \ - Více informací naleznete na Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n + Více informací naleznete na Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Prosím uhraďte {0} pomocí zvolené platební metody prodejci XMR. V dalším kroku naleznete detaily o účtu prodejce.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2720,20 +2720,20 @@ payment.swift.info.account=Pečlivě si prostudujte základní pokyny pro použ - kupující použije u platby model sdílených poplatků (SHA) \n\ - kupující a prodejce mohou platit poplatky, proto by se měli nejprve seznámit s poplatky své banky \n\ \n\ -SWIFT je složitější než jiné platební metody, proto si prosím přečtěte kompletní pokyny na wiki [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +SWIFT je složitější než jiné platební metody, proto si prosím přečtěte kompletní pokyny na wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.swift.info.buyer=Pro nákup monero pomocí SWIFT, musíte:\n\ \n\ - odeslat platbu v měně, stanovené tvůrcem nabídky \n\ - použít u platby model sdílených poplatků (SHA) \n\ \n\ -Přečtěte si další pokyny na wiki, abyste se vyhnuli sankcím a zajistili hladký průběh obchodů [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +Přečtěte si další pokyny na wiki, abyste se vyhnuli sankcím a zajistili hladký průběh obchodů [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.swift.info.seller=Odesílatelé SWIFT musí při odesílání plateb používat model sdílených poplatků (SHA).\n\ \n\ Pokud obdržíte platbu SWIFT, která nepoužívá SHA, otevřete mediační požadavek.\n\ \n\ -Přečtěte si další pokyny na wiki, abyste se vyhnuli sankcím a zajistili hladký průběh obchodů [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +Přečtěte si další pokyny na wiki, abyste se vyhnuli sankcím a zajistili hladký průběh obchodů [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.imps.info.account=Prosím, nezapomeňte uvést své:\n\n\ ● Celé jméno majitele účtu\n\ @@ -3059,7 +3059,7 @@ payment.payid.info=PayID jako telefonní číslo, e-mailová adresa nebo austral bankou, družstevní záložnou nebo účtem stavební spořitelny. Musíte mít již vytvořený PayID u své australské finanční instituce. \ Odesílající i přijímající finanční instituce musí podporovat PayID. Další informace najdete na [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=Chcete-li platit dárkovou kartou Amazon eGift, budete muset prodejci XMR poslat kartu Amazon eGift přes svůj účet Amazon.\n\n\ - Podívejte se do wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] pro podrobnosti a rady.\n\n\ + Podívejte se do wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] pro podrobnosti a rady.\n\n\ Zde jsou tři důležité poznámky:\n\ - Preferujte dárkové karty v hodnotě do 100 USD, protože Amazon může považovat nákupy karet s vyššími částkami jako podezřelé a zablokovat je.\n\ - Na kartě do zprávy pro příjemce můžete přidat i vlastní originální text (např. "Happy birthday Susan!") spolu s ID obchodu (v takovém případě \ diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index a4ff4da840..c19f183f47 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -640,7 +640,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=WICHTIGE VORAUSSETZUNG: \nNachd # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Bitte senden Sie {0} per \"US Postal Money Order\" an den XMR-Verkäufer.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Bitte schicken Sie {0} Bargeld per Post an den XMR Verkäufer. Genaue Anweisungen finden Sie im Handelsvertrag, oder Sie stellen über den Handels-Chat Fragen, wenn etwas unklar ist. Weitere Informationen über \"Bargeld per Post\" finden Sie im Haveno-Wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Bitte schicken Sie {0} Bargeld per Post an den XMR Verkäufer. Genaue Anweisungen finden Sie im Handelsvertrag, oder Sie stellen über den Handels-Chat Fragen, wenn etwas unklar ist. Weitere Informationen über \"Bargeld per Post\" finden Sie im Haveno-Wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Bitte zahlen Sie {0} mit der gewählten Zahlungsmethode an den XMR Verkäufer. Sie finden die Konto Details des Verkäufers im nächsten Fenster.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2046,7 +2046,7 @@ payment.japan.recipient=Name payment.australia.payid=PayID payment.payid=PayIDs wie E-Mail Adressen oder Telefonnummern die mit Finanzinstitutionen verbunden sind. payment.payid.info=Eine PayID wie eine Telefonnummer, E-Mail Adresse oder Australische Business Number (ABN) mit der Sie sicher Ihre Bank, Kreditgenossenschaft oder Bausparkassenkonto verlinken können. Sie müssen bereits eine PayID mit Ihrer Australischen Finanzinstitution erstellt haben. Beide Institutionen, die die sendet und die die empfängt, müssen PayID unterstützen. Weitere informationen finden Sie unter [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=Um mit einer Amazon eGift Geschenkkarte zu bezahlen, müssen Sie eine Amazon eGift Geschenkkarte über Ihr Amazon-Konto an den XMR-Verkäufer senden. \n\nHaveno zeigt die E-Mail-Adresse oder Telefonnummer des XMR-Verkäufers an, an die die Geschenkkarte gesendet werden soll, und Sie müssen die Handels-ID in das Nachrichtenfeld der Geschenkkarte eintragen. Bitte lesen Sie das Wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] für weitere Details und empfohlene Vorgehensweisen. \n\nDrei wichtige Hinweise:\n- Versuchen Sie Geschenkkarten mit Beträgen von 100 USD oder weniger zu versenden, weil Amazon größere Geschenkkarten gerne als betrügerisch kennzeichnet\n- Versuchen Sie einen kreativen, glaubwürdigen Text für die Nachricht der Geschenkkarten zu verwenden (z.B. "Alles Gute zum Geburtstag Susi!"), zusammen mit der Handels-ID (und verwenden Sie den Handels-Chat, um Ihrem Handelspartner den von Ihnen gewählten Referenztext mitzuteilen, damit er Ihre Zahlung überprüfen kann)\n- Amazon Geschenkkarten können nur auf der Amazon-Website eingelöst werden, auf der sie gekauft wurden (z. B. kann eine auf amazon.it gekaufte Geschenkkarte nur auf amazon.it eingelöst werden) +payment.amazonGiftCard.info=Um mit einer Amazon eGift Geschenkkarte zu bezahlen, müssen Sie eine Amazon eGift Geschenkkarte über Ihr Amazon-Konto an den XMR-Verkäufer senden. \n\nHaveno zeigt die E-Mail-Adresse oder Telefonnummer des XMR-Verkäufers an, an die die Geschenkkarte gesendet werden soll, und Sie müssen die Handels-ID in das Nachrichtenfeld der Geschenkkarte eintragen. Bitte lesen Sie das Wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] für weitere Details und empfohlene Vorgehensweisen. \n\nDrei wichtige Hinweise:\n- Versuchen Sie Geschenkkarten mit Beträgen von 100 USD oder weniger zu versenden, weil Amazon größere Geschenkkarten gerne als betrügerisch kennzeichnet\n- Versuchen Sie einen kreativen, glaubwürdigen Text für die Nachricht der Geschenkkarten zu verwenden (z.B. "Alles Gute zum Geburtstag Susi!"), zusammen mit der Handels-ID (und verwenden Sie den Handels-Chat, um Ihrem Handelspartner den von Ihnen gewählten Referenztext mitzuteilen, damit er Ihre Zahlung überprüfen kann)\n- Amazon Geschenkkarten können nur auf der Amazon-Website eingelöst werden, auf der sie gekauft wurden (z. B. kann eine auf amazon.it gekaufte Geschenkkarte nur auf amazon.it eingelöst werden) payment.paysafe.info=Zum Schutz Ihrer Sicherheit raten wir dringend davon ab, Paysafecard-PINs für Zahlungen zu verwenden.\n\n\ Transaktionen, die über PINs durchgeführt werden, können nicht unabhängig zur Streitbeilegung überprüft werden. Wenn ein Problem auftritt, kann die Rückerstattung von Geldern möglicherweise nicht möglich sein.\n\n\ Um die Transaktionssicherheit mit Streitbeilegung zu gewährleisten, verwenden Sie immer Zahlungsmethoden, die überprüfbare Aufzeichnungen bieten. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index d0248368a2..5174d8b022 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -640,7 +640,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=REQUERIMIENTO IMPORTANTE:\nDesp # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Por favor envíe {0} mediante \"US Postal Money Order\" a el vendedor de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Por favor envíe {0} usando \"Efectivo por Correo\" al vendedor. Las instrucciones específicas están en el contrato de intercambio, y si no queda claro, pregunte a través del chat de intercambio.\nVea más detalles acerca de Efectivo por Correo en la wiki de Haveno [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Por favor envíe {0} usando \"Efectivo por Correo\" al vendedor. Las instrucciones específicas están en el contrato de intercambio, y si no queda claro, pregunte a través del chat de intercambio.\nVea más detalles acerca de Efectivo por Correo en la wiki de Haveno [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Por favor pague {0} a través del método de pago especificado al vendedor XMR. Encontrará los detalles de la cuenta del vendedor en la siguiente pantalla.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2047,7 +2047,7 @@ payment.japan.recipient=Nombre payment.australia.payid=PayID payment.payid=PayID conectado a una institución financiera. Como la dirección email o el número de móvil. payment.payid.info=Un PayID como un número de teléfono, dirección email o Australian Business Number (ABN), que puede conectar con seguridad a su banco, unión de crédito o cuenta de construcción de sociedad. Necesita haber creado una PayID con su institución financiera australiana. Tanto para enviar y recibir las instituciones financieras deben soportar PayID. Para más información por favor compruebe [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=Para pagar con Tarjeta eGift Amazon. necesitará enviar una Tarjeta eGift Amazon al vendedor XMR a través de su cuenta Amazon.\n\nHaveno mostrará la dirección e-mail del vendedor de XMR o el número de teléfono donde la tarjeta de regalo deberá enviarse. Por favor vea la wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] para más detalles y mejores prácticas.\n\nNotas importantes:\n- Pruebe a enviar las tarjetas regalo en cantidades de 100USD o menores, ya que Amazon está señalando tarjetas regalo mayores como fraudulentas.\n- Intente usar textos para el mensaje de la tarjeta regalo creíbles y creativos ("Feliz cumpleaños!").\n- Las tarjetas Amazon eGift pueden ser redimidas únicamente en la web de Amazon en la que se compraron (por ejemplo, una tarjeta comprada en amazon.it solo puede ser redimida en amazon.it) +payment.amazonGiftCard.info=Para pagar con Tarjeta eGift Amazon. necesitará enviar una Tarjeta eGift Amazon al vendedor XMR a través de su cuenta Amazon.\n\nHaveno mostrará la dirección e-mail del vendedor de XMR o el número de teléfono donde la tarjeta de regalo deberá enviarse. Por favor vea la wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] para más detalles y mejores prácticas.\n\nNotas importantes:\n- Pruebe a enviar las tarjetas regalo en cantidades de 100USD o menores, ya que Amazon está señalando tarjetas regalo mayores como fraudulentas.\n- Intente usar textos para el mensaje de la tarjeta regalo creíbles y creativos ("Feliz cumpleaños!").\n- Las tarjetas Amazon eGift pueden ser redimidas únicamente en la web de Amazon en la que se compraron (por ejemplo, una tarjeta comprada en amazon.it solo puede ser redimida en amazon.it) payment.paysafe.info=Por su protección, desaconsejamos encarecidamente el uso de PINs de Paysafecard para pagos.\n\n\ Las transacciones realizadas mediante PINs no pueden ser verificadas de forma independiente para la resolución de disputas. Si surge un problema, recuperar los fondos puede no ser posible.\n\n\ Para garantizar la seguridad de las transacciones con resolución de disputas, utilice siempre métodos de pago que proporcionen registros verificables. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index eff59f853e..d6e00ed380 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -639,7 +639,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=مورد الزامی مهم:\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=لطفاً {0} را توسط \"US Postal Money Order\" به فروشنده‌ی بیتکوین پرداخت کنید.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2018,7 +2018,7 @@ payment.japan.recipient=نام payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=برای حفاظت از شما، به شدت از استفاده از پین‌های Paysafecard برای پرداخت جلوگیری می‌کنیم.\n\n\ تراکنش‌های انجام شده از طریق پین‌ها نمی‌توانند به طور مستقل برای حل اختلاف تأیید شوند. اگر مشکلی پیش آید، بازیابی وجوه ممکن است غیرممکن باشد.\n\n\ برای اطمینان از امنیت تراکنش و حل اختلاف، همیشه از روش‌های پرداختی استفاده کنید که سوابق قابل تاییدی ارائه می‌دهند. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 0c1abbd22d..2025ed4d66 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -640,7 +640,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=CONDITIONS REQUISES:\nAprès av # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Merci d''envoyer {0} par \"US Postal Money Order\" au vendeur de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Veuillez envoyer {0} en utlisant \"Pay by Mail\" au vendeur de XMR. Les instructions spécifiques sont dans le contrat de trade, ou si ce n'est pas clair, vous pouvez poser des questions via le chat des trader. Pour plus de détails sur Pay by Mail, allez sur le wiki Haveno \n[LIEN:https://haveno.exchange/wiki/Cash_by_Mail]\n +portfolio.pending.step2_buyer.payByMail=Veuillez envoyer {0} en utlisant \"Pay by Mail\" au vendeur de XMR. Les instructions spécifiques sont dans le contrat de trade, ou si ce n'est pas clair, vous pouvez poser des questions via le chat des trader. Pour plus de détails sur Pay by Mail, allez sur le wiki Haveno \n[LIEN:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail]\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Veuillez payer {0} via la méthode de paiement spécifiée par le vendeur de XMR. Vous trouverez les informations du compte du vendeur à l'écran suivant.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2050,7 +2050,7 @@ payment.japan.recipient=Nom payment.australia.payid=ID de paiement payment.payid=ID de paiement lié à une institution financière. Comme l'addresse email ou le téléphone portable. payment.payid.info=Un PayID, tel qu'un numéro de téléphone, une adresse électronique ou un numéro d'entreprise australien (ABN), que vous pouvez lier en toute sécurité à votre compte bancaire, votre crédit mutuel ou votre société de crédit immobilier. Vous devez avoir déjà créé un PayID auprès de votre institution financière australienne. Les institutions financières émettrices et réceptrices doivent toutes deux prendre en charge PayID. Pour plus d'informations, veuillez consulter [LIEN:https://payid.com.au/faqs/]. -payment.amazonGiftCard.info=Pour payer avec une carte cadeau Amazon eGift Card, vous devrez envoyer une carte cadeau Amazon eGift Card au vendeur de XMR via votre compte Amazon. \n\nHaveno indiquera l'adresse e-mail ou le numéro de téléphone du vendeur XMR où la carte cadeau doit être envoyée, et vous devrez inclure l'ID du trade dans le champ de messagerie de la carte cadeau. Veuillez consulter le wiki [LIEN:https://haveno.exchange/wiki/Amazon_eGift_card] pour plus de détails et pour les meilleures pratiques à adopter. \n\nTrois remarques importantes :\n- essayez d'envoyer des cartes-cadeaux d'un montant inférieur ou égal à 100 USD, car Amazon est connu pour signaler les cartes-cadeaux plus importantes comme frauduleuses\n- essayez d'utiliser un texte créatif et crédible pour le message de la carte cadeau (par exemple, "Joyeux anniversaire Susan !") ainsi que l'ID du trade (et utilisez le chat du trader pour indiquer à votre pair de trading le texte de référence que vous avez choisi afin qu'il puisse vérifier votre paiement).\n- Les cartes cadeaux électroniques Amazon ne peuvent être échangées que sur le site Amazon où elles ont été achetées (par exemple, une carte cadeau achetée sur amazon.it ne peut être échangée que sur amazon.it). +payment.amazonGiftCard.info=Pour payer avec une carte cadeau Amazon eGift Card, vous devrez envoyer une carte cadeau Amazon eGift Card au vendeur de XMR via votre compte Amazon. \n\nHaveno indiquera l'adresse e-mail ou le numéro de téléphone du vendeur XMR où la carte cadeau doit être envoyée, et vous devrez inclure l'ID du trade dans le champ de messagerie de la carte cadeau. Veuillez consulter le wiki [LIEN:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] pour plus de détails et pour les meilleures pratiques à adopter. \n\nTrois remarques importantes :\n- essayez d'envoyer des cartes-cadeaux d'un montant inférieur ou égal à 100 USD, car Amazon est connu pour signaler les cartes-cadeaux plus importantes comme frauduleuses\n- essayez d'utiliser un texte créatif et crédible pour le message de la carte cadeau (par exemple, "Joyeux anniversaire Susan !") ainsi que l'ID du trade (et utilisez le chat du trader pour indiquer à votre pair de trading le texte de référence que vous avez choisi afin qu'il puisse vérifier votre paiement).\n- Les cartes cadeaux électroniques Amazon ne peuvent être échangées que sur le site Amazon où elles ont été achetées (par exemple, une carte cadeau achetée sur amazon.it ne peut être échangée que sur amazon.it). payment.paysafe.info=Pour votre protection, nous déconseillons fortement d'utiliser les PINs Paysafecard pour les paiements.\n\n\ Les transactions effectuées via des PINs ne peuvent pas être vérifiées de manière indépendante pour la résolution des litiges. En cas de problème, la récupération des fonds peut ne pas être possible.\n\n\ Pour garantir la sécurité des transactions et la résolution des litiges, utilisez toujours des méthodes de paiement qui fournissent des preuves vérifiables. diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index e38006bc67..f482f5b9c7 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -639,7 +639,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=REQUISITO IMPORTANTE:\nDopo ave # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Invia {0} tramite \"Vaglia Postale Statunitense\" al venditore XMR.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2021,7 +2021,7 @@ payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Per la tua protezione, sconsigliamo vivamente di utilizzare i PIN di Paysafecard per i pagamenti.\n\n\ Le transazioni effettuate tramite PIN non possono essere verificate in modo indipendente per la risoluzione delle controversie. Se si verifica un problema, il recupero dei fondi potrebbe non essere possibile.\n\n\ Per garantire la sicurezza delle transazioni con risoluzione delle controversie, utilizza sempre metodi di pagamento che forniscono registrazioni verificabili. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 10090e7ca5..83f078a7c3 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -640,7 +640,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=重要な要件: \n支払いが # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal={0}を「米国の郵便為替」でXMRの売り手に送付してください。\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=\"郵送で現金\"で、{0}をXMR売り手に送って下さい。詳細な指示はトレード契約書に書いてあります、そして分からない点があれば取引者チャットで質問できます。「郵送で現金」について詳しくはHavenoのWikiを参照:[HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail]\n +portfolio.pending.step2_buyer.payByMail=\"郵送で現金\"で、{0}をXMR売り手に送って下さい。詳細な指示はトレード契約書に書いてあります、そして分からない点があれば取引者チャットで質問できます。「郵送で現金」について詳しくはHavenoのWikiを参照:[HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail]\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=特定された支払い方法で{0}をXMRの売り手に支払ってお願いします。売り手のアカウント詳細は次の画面に表示されます。\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2046,7 +2046,7 @@ payment.japan.recipient=名義 payment.australia.payid=PayID payment.payid=金融機関と繋がっているPayID。例えばEメールアドレスそれとも携帯電話番号。 payment.payid.info=銀行、信用金庫、あるいは住宅金融組合アカウントと安全に繋がれるPayIDとして使われる電話番号、Eメールアドレス、それともオーストラリア企業番号(ABN)。すでにオーストラリアの金融機関とPayIDを作った必要があります。送金と受取の金融機関は両方PayIDをサポートする必要があります。詳しくは以下を訪れて下さい [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=アマゾンeGiftカードで支払うには、アマゾンアカウントを使ってeGiftカードをXMR売り手に送る必要があります。\n\nHavenoはeGiftカードの送り先になるXMR売り手のメールアドレスそれとも電話番号を表示します。そしてeGiftカードのメッセージフィールドに、必ずトレードIDを入力して下さい。最良の慣行について詳しくはWikiを参照して下さい:[HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card]\n\n3つの注意点:\n- 可能であれば、100米ドル価格以下のeGiftカードを送って下さい。それ以上の価格はアマゾンに不正な取引というフラグが立てられることがあります。\n- eGiftカードのメッセージフィールドに、トレードIDと一緒に信ぴょう性のあるメッセージを入力して下さい。(例えば隆さん、「お誕生日おめでとう!」)。(そして確認のため、取引者チャットでトレードピアにメッセージの内容を伝えて下さい)。\n- アマゾンeGiftカードは買われたサイトのみに交換できます(例えば、amazon.jpから買われたカードはamazon.jpのみに交換できます)。 +payment.amazonGiftCard.info=アマゾンeGiftカードで支払うには、アマゾンアカウントを使ってeGiftカードをXMR売り手に送る必要があります。\n\nHavenoはeGiftカードの送り先になるXMR売り手のメールアドレスそれとも電話番号を表示します。そしてeGiftカードのメッセージフィールドに、必ずトレードIDを入力して下さい。最良の慣行について詳しくはWikiを参照して下さい:[HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card]\n\n3つの注意点:\n- 可能であれば、100米ドル価格以下のeGiftカードを送って下さい。それ以上の価格はアマゾンに不正な取引というフラグが立てられることがあります。\n- eGiftカードのメッセージフィールドに、トレードIDと一緒に信ぴょう性のあるメッセージを入力して下さい。(例えば隆さん、「お誕生日おめでとう!」)。(そして確認のため、取引者チャットでトレードピアにメッセージの内容を伝えて下さい)。\n- アマゾンeGiftカードは買われたサイトのみに交換できます(例えば、amazon.jpから買われたカードはamazon.jpのみに交換できます)。 payment.paysafe.info=あなたの保護のため、支払いにPaysafecard PINの使用は強くお勧めしません。\n\n\ PINを使用した取引は、紛争解決のために独立して確認することができません。問題が発生した場合、資金の回収が不可能になることがあります。\n\n\ 取引の安全性と紛争解決を確保するため、常に確認可能な記録を提供する支払い方法を使用してください。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 52877fd254..767e0b8999 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -642,7 +642,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANTE:\nApós ter feito o # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Envie {0} através de \"US Postal Money Order\" para o vendedor de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2028,7 +2028,7 @@ payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Para sua proteção, desaconselhamos fortemente o uso de PINs do Paysafecard para pagamento.\n\n\ Transações feitas por PINs não podem ser verificadas de forma independente para resolução de disputas. Se ocorrer um problema, a recuperação de fundos pode não ser possível.\n\n\ Para garantir a segurança das transações com resolução de disputas, sempre utilize métodos de pagamento que forneçam registros verificáveis. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index c690f722d4..cba08c5769 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -639,7 +639,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=REQUISITO IMPORTANTE:\nDepois d # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Por favor envie {0} por \"US Postal Money Order\" para o vendedor de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2018,7 +2018,7 @@ payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Para sua proteção, desaconselhamos fortemente o uso de PINs do Paysafecard para pagamento.\n\n\ Transações feitas por PINs não podem ser verificadas de forma independente para resolução de disputas. Se ocorrer um problema, a recuperação dos fundos pode não ser possível.\n\n\ Para garantir a segurança das transações com resolução de disputas, sempre use métodos de pagamento que forneçam registros verificáveis. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 6043564648..956e124eae 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -639,7 +639,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=ВАЖНОЕ ТРЕБОВАНИ # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Отправьте {0} \«Почтовым денежным переводом США\» продавцу XMR.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2020,7 +2020,7 @@ payment.japan.recipient=Имя payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Для вашей защиты мы настоятельно не рекомендуем использовать PIN-коды Paysafecard для платежей.\n\n\ Транзакции, выполненные с помощью PIN-кодов, не могут быть независимо подтверждены для разрешения споров. В случае возникновения проблемы возврат средств может быть невозможен.\n\n\ Чтобы обеспечить безопасность транзакций с возможностью разрешения споров, всегда используйте методы оплаты, предоставляющие проверяемые записи. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 76a0847bc2..5205776fe2 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -639,7 +639,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=ข้อกำหนดที # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=โปรดส่ง {0} โดยธนาณัติ \"US Postal Money Order \" ไปยังผู้ขาย XMR\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2019,7 +2019,7 @@ payment.japan.recipient=ชื่อ payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=เพื่อความปลอดภัยของคุณ เราขอแนะนำอย่างยิ่งให้หลีกเลี่ยงการใช้ Paysafecard PINs ในการชำระเงิน\n\n\ ธุรกรรมที่ดำเนินการผ่าน PIN ไม่สามารถตรวจสอบได้อย่างอิสระสำหรับการระงับข้อพิพาท หากเกิดปัญหา อาจไม่สามารถกู้คืนเงินได้\n\n\ เพื่อความปลอดภัยของธุรกรรมและรองรับการระงับข้อพิพาท โปรดใช้วิธีการชำระเงินที่มีบันทึกการทำธุรกรรมที่ตรวจสอบได้ diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index e46f5a3e10..5d0720412b 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -684,7 +684,7 @@ portfolio.pending.step2_buyer.refTextWarn=Önemli: ödeme yaparken, \"ödeme ned portfolio.pending.step2_buyer.fees=Bankanız transfer yapmak için sizden herhangi bir ücret alıyorsa, bu ücretleri ödemekten siz sorumlusunuz. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees.swift=SWIFT ödemesini göndermek için SHA (paylaşılan ücret modeli) kullanmanız gerekmektedir. \ - Daha fazla ayrıntı için [HYPERLINK:https://haveno.exchange/wiki/SWIFT#Use_the_correct_fee_option] adresine bakınız. + Daha fazla ayrıntı için [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT#Use_the_correct_fee_option] adresine bakınız. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Lütfen dış {0} cüzdanınızdan\n{1} XMR satıcısına transfer yapın.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -704,7 +704,7 @@ portfolio.pending.step2_buyer.postal=Lütfen "US Postal Money Order" kullanarak # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Lütfen "Pay by Mail" kullanarak {0} tutarını XMR satıcısına gönderin. \ Belirli talimatlar işlem sözleşmesinde bulunmaktadır, veya belirsizse trader sohbeti aracılığıyla sorular sorabilirsiniz. \ - Pay by Mail hakkında daha fazla ayrıntı için Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail] adresine bakın.\n\n + Pay by Mail hakkında daha fazla ayrıntı için Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail] adresine bakın.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Lütfen belirtilen ödeme yöntemini kullanarak {0} tutarını XMR satıcısına ödeyin. Satıcının hesap bilgilerini bir sonraki ekranda bulacaksınız.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2707,20 +2707,20 @@ payment.swift.info.account=Haveno'da SWIFT kullanımı için temel yönergeleri - alıcı, paylaşılan ücret modeli (SHA) kullanarak ödeme yapmalıdır \n\ - alıcı ve satıcı ücretlerle karşılaşabilir, bu yüzden bankalarının ücret tarifelerini önceden kontrol etmelidirler \n\ \n\ -SWIFT, diğer ödeme yöntemlerinden daha karmaşıktır, bu yüzden lütfen wiki'deki tam rehberi inceleyin [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +SWIFT, diğer ödeme yöntemlerinden daha karmaşıktır, bu yüzden lütfen wiki'deki tam rehberi inceleyin [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.swift.info.buyer=SWIFT ile monero satın almak için şunları yapmalısınız:\n\ \n\ - ödeme yapanın belirttiği para biriminde ödeme yapın \n\ - ödeme göndermek için paylaşılan ücret modeli (SHA) kullanın\n\ \n\ -Ceza almamak ve sorunsuz ticaretler yapmak için lütfen wiki'deki daha fazla rehberi inceleyin [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +Ceza almamak ve sorunsuz ticaretler yapmak için lütfen wiki'deki daha fazla rehberi inceleyin [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.swift.info.seller=SWIFT gönderenler, ödemeleri göndermek için paylaşılan ücret modeli (SHA) kullanmak zorundadır.\n\ \n\ SHA kullanmayan bir SWIFT ödemesi alırsanız, bir arabuluculuk bileti açın.\n\ \n\ -Ceza almamak ve sorunsuz ticaretler yapmak için lütfen wiki'deki daha fazla rehberi inceleyin [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +Ceza almamak ve sorunsuz ticaretler yapmak için lütfen wiki'deki daha fazla rehberi inceleyin [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.imps.info.account=Lütfen şunları dahil ettiğinizden emin olun:\n\n\ ● Hesap sahibi tam adı\n\ @@ -3046,7 +3046,7 @@ payment.payid.info=PayID, telefon numarası, e-posta adresi veya Avustralya İş toplum hesabınıza güvenli bir şekilde bağlayabileceğiniz bir kimliktir. Avustralya finans kurumunuzla zaten bir PayID oluşturmuş \ olmanız gerekmektedir. Hem gönderici hem de alıcı finans kurumlarının PayID'yi desteklemesi gerekmektedir. payment.amazonGiftCard.info=Amazon eGift Kart ile ödeme yapmak için, Amazon hesabınız aracılığıyla bir Amazon eGift Kartı'nı XMR satıcısına göndermeniz gerekecek. \n\n\ - Daha fazla ayrıntı ve en iyi uygulamalar için lütfen wiki'ye bakın: [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] \n\n\ + Daha fazla ayrıntı ve en iyi uygulamalar için lütfen wiki'ye bakın: [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] \n\n\ Üç önemli not:\n\ - Amazon'un daha büyük hediye kartlarını sahtekarlık olarak işaretlediği bilindiğinden, 100 USD veya daha küçük tutarlarda hediye kartları göndermeye çalışın\n\ - hediye kartının mesajı için yaratıcı, inanılabilir bir metin kullanmaya çalışın (örneğin, "Doğum günün kutlu olsun Metin Torun!") ve ticaret \ diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 3929731346..4b6ba937e4 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -639,7 +639,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=YÊU CẦU QUAN TRỌNG:\nSau k # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Hãy gửi {0} bằng \"Phiếu chuyển tiền US\" cho người bán XMR.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2021,7 +2021,7 @@ payment.japan.recipient=Tên payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Vì sự bảo vệ của bạn, chúng tôi khuyến cáo không nên sử dụng mã PIN Paysafecard để thanh toán.\n\n\ Các giao dịch được thực hiện bằng mã PIN không thể được xác minh độc lập để giải quyết tranh chấp. Nếu có vấn đề xảy ra, có thể không thể khôi phục số tiền đã mất.\n\n\ Để đảm bảo an toàn giao dịch và có thể giải quyết tranh chấp, hãy luôn sử dụng các phương thức thanh toán có hồ sơ xác minh được. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 3376029d07..b9cd9a21c5 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -640,7 +640,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=重要要求:\n完成支付 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=请用“美国邮政汇票”发送 {0} 给 XMR 卖家。\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2031,7 +2031,7 @@ payment.japan.recipient=名称 payment.australia.payid=PayID payment.payid=PayID 需链接至金融机构。例如电子邮件地址或手机。 payment.payid.info=PayID,如电话号码、电子邮件地址或澳大利亚商业号码(ABN),您可以安全地连接到您的银行、信用合作社或建立社会帐户。你需要在你的澳大利亚金融机构创建一个 PayID。发送和接收金融机构都必须支持 PayID。更多信息请查看[HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=为了保障您的安全,我们强烈不建议使用 Paysafecard PIN 进行支付。\n\n\ 通过 PIN 进行的交易无法被独立验证以解决争议。如果出现问题,资金可能无法追回。\n\n\ 为确保交易安全并支持争议解决,请始终使用提供可验证记录的支付方式。 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 2bba3084ef..e5bf5ec76b 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -640,7 +640,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=重要要求:\n完成支付 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=請用“美國郵政匯票”發送 {0} 給 XMR 賣家。\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -2025,7 +2025,7 @@ payment.japan.recipient=名稱 payment.australia.payid=PayID payment.payid=PayID 需鏈接至金融機構。例如電子郵件地址或手機。 payment.payid.info=PayID,如電話號碼、電子郵件地址或澳大利亞商業號碼(ABN),您可以安全地連接到您的銀行、信用合作社或建立社會帳户。你需要在你的澳大利亞金融機構創建一個 PayID。發送和接收金融機構都必須支持 PayID。更多信息請查看[HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=為了保護您的安全,我們強烈不建議使用 Paysafecard PIN 進行付款。\n\n\ 透過 PIN 進行的交易無法獨立驗證以進行爭議解決。如果發生問題,可能無法追回資金。\n\n\ 為確保交易安全並支持爭議解決,請始終使用可驗證記錄的付款方式。 From bd3fffada409c6356f7baa38fb29d91a419367da Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 12 Feb 2025 06:34:42 -0500 Subject: [PATCH 125/371] add hyperlink to dispute resolution in TAC window --- .../desktop/main/overlays/windows/TacWindow.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TacWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TacWindow.java index 755e814076..18fe8fc4b4 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TacWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TacWindow.java @@ -20,11 +20,16 @@ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.desktop.app.HavenoApp; +import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.main.overlays.Overlay; +import javafx.geometry.Insets; import javafx.geometry.Rectangle2D; +import javafx.scene.layout.GridPane; import javafx.stage.Screen; import lombok.extern.slf4j.Slf4j; +import static haveno.desktop.util.FormBuilder.addHyperlinkWithIcon; + @Slf4j public class TacWindow extends Overlay { @@ -91,11 +96,10 @@ public class TacWindow extends Overlay { String fontStyleClass = smallScreen ? "small-text" : "normal-text"; messageTextArea.getStyleClass().add(fontStyleClass); - // TODO: link to the wiki - // HyperlinkWithIcon hyperlinkWithIcon = addHyperlinkWithIcon(gridPane, ++rowIndex, Res.get("tacWindow.arbitrationSystem"), - // "https://haveno.exchange/wiki/Dispute_resolution"); - // hyperlinkWithIcon.getStyleClass().add(fontStyleClass); - // GridPane.setMargin(hyperlinkWithIcon, new Insets(-6, 0, -20, -4)); + HyperlinkWithIcon hyperlinkWithIcon = addHyperlinkWithIcon(gridPane, ++rowIndex, Res.get("tacWindow.arbitrationSystem"), + "https://docs.haveno.exchange/the-project/dispute-resolution"); + hyperlinkWithIcon.getStyleClass().add(fontStyleClass); + GridPane.setMargin(hyperlinkWithIcon, new Insets(-6, 0, -20, -4)); } @Override From cd71bcdde7ca7a65a19583c3aec88811af1b8e3d Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 13 Feb 2025 06:56:38 -0500 Subject: [PATCH 126/371] update bounties link --- docs/bounties.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bounties.md b/docs/bounties.md index e8de0828bb..ee52d69ed2 100644 --- a/docs/bounties.md +++ b/docs/bounties.md @@ -1,6 +1,6 @@ ## Bounties -We use bounties to incentivize development and reward contributors. All issues available for a bounty are listed [on the Kanban board](https://github.com/orgs/haveno-dex/projects/2). It's possible to list on each repository the issues with a bounty on them, by searching issues with the '💰bounty' label. +We use bounties to incentivize development and reward contributors. All issues available for a bounty are listed [here](https://github.com/haveno-dex/haveno/issues?q=is%3Aissue%20state%3Aopen%20label%3A%F0%9F%92%B0bounty). It's possible to list on each repository the issues with a bounty on them, by searching issues with the '💰bounty' label. To receive a bounty, you agree to these conditions: From b72159fcf8e9fdc7199f6bafcff2118707b43be0 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 12 Feb 2025 07:34:32 -0500 Subject: [PATCH 127/371] synchronize access to pending trades data model Co-authored-by: XMRZombie --- .../pendingtrades/PendingTradesDataModel.java | 36 +++++++++++-------- .../pendingtrades/PendingTradesView.java | 28 +++++++++------ 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index e4fc8b1bdc..7649274aa1 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -332,15 +332,17 @@ public class PendingTradesDataModel extends ActivatableDataModel { } // add shown trades to list - list.clear(); - list.addAll(tradeManager.getObservableList().stream() - .filter(trade -> isTradeShown(trade)) - .map(trade -> new PendingTradesListItem(trade, btcFormatter)) - .collect(Collectors.toList())); - } + synchronized (list) { + list.clear(); + list.addAll(tradeManager.getObservableList().stream() + .filter(trade -> isTradeShown(trade)) + .map(trade -> new PendingTradesListItem(trade, btcFormatter)) + .collect(Collectors.toList())); - // we sort by date, earliest first - list.sort((o1, o2) -> o2.getTrade().getDate().compareTo(o1.getTrade().getDate())); + // we sort by date, earliest first + list.sort((o1, o2) -> o2.getTrade().getDate().compareTo(o1.getTrade().getDate())); + } + } selectBestItem(); } @@ -350,17 +352,21 @@ public class PendingTradesDataModel extends ActivatableDataModel { } private void selectBestItem() { - if (list.size() == 1) - doSelectItem(list.get(0)); - else if (list.size() > 1 && (selectedItemProperty.get() == null || !list.contains(selectedItemProperty.get()))) - doSelectItem(list.get(0)); - else if (list.size() == 0) - doSelectItem(null); + synchronized (list) { + if (list.size() == 1) + doSelectItem(list.get(0)); + else if (list.size() > 1 && (selectedItemProperty.get() == null || !list.contains(selectedItemProperty.get()))) + doSelectItem(list.get(0)); + else if (list.size() == 0) + doSelectItem(null); + } } private void selectItemByTradeId(String tradeId) { if (activated) { - list.stream().filter(e -> e.getTrade().getId().equals(tradeId)).findAny().ifPresent(this::doSelectItem); + synchronized (list) { + list.stream().filter(e -> e.getTrade().getId().equals(tradeId)).findAny().ifPresent(this::doSelectItem); + } } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 8c31e3d8e6..645fbb5014 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -363,7 +363,11 @@ public class PendingTradesView extends ActivatableViewAndModel moveTradeToFailedColumn.setVisible(model.dataModel.list.stream().anyMatch(item -> isMaybeInvalidTrade(item.getTrade())))); + UserThread.execute(() -> { + synchronized (model.dataModel.list) { + moveTradeToFailedColumn.setVisible(model.dataModel.list.stream().anyMatch(item -> isMaybeInvalidTrade(item.getTrade()))); + } + }); } private boolean isMaybeInvalidTrade(Trade trade) { @@ -420,16 +424,18 @@ public class PendingTradesView extends ActivatableViewAndModel { - Trade trade = t.getTrade(); - synchronized (trade.getChatMessages()) { - newChatMessagesByTradeMap.put(trade.getId(), - trade.getChatMessages().stream() - .filter(m -> !m.isWasDisplayed()) - .filter(m -> !m.isSystemMessage()) - .count()); - } - }); + synchronized (model.dataModel.list) { + model.dataModel.list.forEach(t -> { + Trade trade = t.getTrade(); + synchronized (trade.getChatMessages()) { + newChatMessagesByTradeMap.put(trade.getId(), + trade.getChatMessages().stream() + .filter(m -> !m.isWasDisplayed()) + .filter(m -> !m.isSystemMessage()) + .count()); + } + }); + } } private void openChat(Trade trade) { From 4a82c695072887362995ef8877ede4cf19cb7508 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 13 Feb 2025 10:30:15 -0500 Subject: [PATCH 128/371] use default priority for trade transactions --- build.gradle | 18 +++++++++--------- .../core/xmr/wallet/XmrWalletService.java | 18 +++++++++++++----- gradle/verification-metadata.xml | 6 +++--- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index e93d74c1d4..74e776cb2b 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ configure(subprojects) { gsonVersion = '2.8.5' guavaVersion = '32.1.1-jre' guiceVersion = '7.0.0' - moneroJavaVersion = '0.8.35' + moneroJavaVersion = '0.8.36' httpclient5Version = '5.0' hamcrestVersion = '2.2' httpclientVersion = '4.5.12' @@ -457,14 +457,14 @@ configure(project(':core')) { doLast { // get monero binaries download url Map moneroBinaries = [ - 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-linux-x86_64.tar.gz', - 'linux-x86_64-sha256' : '0810808292fd5ad595a46a7fcc8ecb28d251d80f8d75c0e7a7d51afbeb413b68', - 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-linux-aarch64.tar.gz', - 'linux-aarch64-sha256' : '61222ee8e2021aaf59ab8813543afc5548f484190ee9360bc9cfa8fdf21cc1de', - 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-mac.tar.gz', - 'mac-sha256' : '5debb8d8d8dd63809e8351368a11aa85c47987f1a8a8f2dcca343e60bcff3287', - 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-windows.zip', - 'windows-sha256' : 'd7c14f029db37ae2a8bc6b74c35f572283257df5fbcc8cc97b704d1a97be9888' + 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-linux-x86_64.tar.gz', + 'linux-x86_64-sha256' : '92003b6d9104e8fe3c4dff292b782ed9b82b157aaff95200fda35e5c3dcb733a', + 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-linux-aarch64.tar.gz', + 'linux-aarch64-sha256' : '18b069c6c474ce18efea261c875a4d54022520e888712b2a56524d9c92f133b1', + 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-mac.tar.gz', + 'mac-sha256' : 'd308352191cd5a9e5e3932ad15869e033e22e209de459f4fd6460b111377dae2', + 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-windows.zip', + 'windows-sha256' : '9c9e1994d4738e2a89ca28bef343bcad460ea6c06e0dd40de8278ab3033bd6c7' ] String osKey diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 5f047ee7f6..0592fbdd59 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -112,7 +112,7 @@ public class XmrWalletService extends XmrWalletBase { public static final String MONERO_WALLET_RPC_NAME = Utilities.isWindows() ? "monero-wallet-rpc.exe" : "monero-wallet-rpc"; public static final String MONERO_WALLET_RPC_PATH = MONERO_BINS_DIR + File.separator + MONERO_WALLET_RPC_NAME; public static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee - public static final MoneroTxPriority PROTOCOL_FEE_PRIORITY = MoneroTxPriority.ELEVATED; + public static final MoneroTxPriority PROTOCOL_FEE_PRIORITY = MoneroTxPriority.DEFAULT; public static final int MONERO_LOG_LEVEL = -1; // monero library log level, -1 to disable private static final MoneroNetworkType MONERO_NETWORK_TYPE = getMoneroNetworkType(); private static final MoneroWalletRpcManager MONERO_WALLET_RPC_MANAGER = new MoneroWalletRpcManager(); @@ -762,9 +762,9 @@ public class XmrWalletService extends XmrWalletBase { if (!BigInteger.ZERO.equals(tx.getUnlockTime())) throw new RuntimeException("Unlock height must be 0"); // verify miner fee - BigInteger minerFeeEstimate = getElevatedFeeEstimate(tx.getWeight()); + BigInteger minerFeeEstimate = getFeeEstimate(tx.getWeight()); double minerFeeDiff = tx.getFee().subtract(minerFeeEstimate).abs().doubleValue() / minerFeeEstimate.doubleValue(); - if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee()); + if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee() + ", diff%=" + minerFeeDiff); log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff); // verify proof to fee address @@ -824,11 +824,19 @@ public class XmrWalletService extends XmrWalletBase { * @param txWeight - the tx weight * @return the tx fee estimate */ - private BigInteger getElevatedFeeEstimate(long txWeight) { + private BigInteger getFeeEstimate(long txWeight) { + + // get fee priority + MoneroTxPriority priority; + if (PROTOCOL_FEE_PRIORITY == MoneroTxPriority.DEFAULT) { + priority = wallet.getDefaultFeePriority(); + } else { + priority = PROTOCOL_FEE_PRIORITY; + } // get fee estimates per kB from daemon MoneroFeeEstimate feeEstimates = getDaemon().getFeeEstimate(); - BigInteger baseFeeEstimate = feeEstimates.getFees().get(2); // get elevated fee per kB + BigInteger baseFeeEstimate = feeEstimates.getFees().get(priority.ordinal() - 1); BigInteger qmask = feeEstimates.getQuantizationMask(); log.info("Monero base fee estimate={}, qmask={}", baseFeeEstimate, qmask); diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 97ff8bba43..069e08177b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -815,9 +815,9 @@ - - - + + + From 290a3738b709d67472cb5667d3324d1b19ef2426 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 14 Feb 2025 11:20:57 -0500 Subject: [PATCH 129/371] re-enable triggered offers if within trigger price again --- .../java/haveno/core/offer/OpenOffer.java | 23 +++++++++++-- .../haveno/core/offer/OpenOfferManager.java | 4 ++- .../core/offer/TriggerPriceService.java | 34 ++++++++++++------- .../openoffer/OpenOffersDataModel.java | 6 ++-- .../portfolio/openoffer/OpenOffersView.java | 6 ++-- proto/src/main/proto/pb.proto | 1 + 6 files changed, 52 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index 1f177959f3..fc4365ecba 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -110,6 +110,10 @@ public final class OpenOffer implements Tradable { @Getter @Setter transient int numProcessingAttempts = 0; + @Getter + @Setter + private boolean deactivatedByTrigger; + public OpenOffer(Offer offer) { this(offer, 0, false); } @@ -141,6 +145,7 @@ public final class OpenOffer implements Tradable { this.reserveTxHex = openOffer.reserveTxHex; this.reserveTxKey = openOffer.reserveTxKey; this.challenge = openOffer.challenge; + this.deactivatedByTrigger = openOffer.deactivatedByTrigger; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -158,7 +163,8 @@ public final class OpenOffer implements Tradable { @Nullable String reserveTxHash, @Nullable String reserveTxHex, @Nullable String reserveTxKey, - @Nullable String challenge) { + @Nullable String challenge, + boolean deactivatedByTrigger) { this.offer = offer; this.state = state; this.triggerPrice = triggerPrice; @@ -170,6 +176,7 @@ public final class OpenOffer implements Tradable { this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; this.challenge = challenge; + this.deactivatedByTrigger = deactivatedByTrigger; // reset reserved state to available if (this.state == State.RESERVED) setState(State.AVAILABLE); @@ -182,7 +189,8 @@ public final class OpenOffer implements Tradable { .setTriggerPrice(triggerPrice) .setState(protobuf.OpenOffer.State.valueOf(state.name())) .setSplitOutputTxFee(splitOutputTxFee) - .setReserveExactAmount(reserveExactAmount); + .setReserveExactAmount(reserveExactAmount) + .setDeactivatedByTrigger(deactivatedByTrigger); Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount)); Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes)); @@ -207,7 +215,8 @@ public final class OpenOffer implements Tradable { ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), - ProtoUtil.stringOrNullFromProto(proto.getChallenge())); + ProtoUtil.stringOrNullFromProto(proto.getChallenge()), + proto.getDeactivatedByTrigger()); return openOffer; } @@ -234,6 +243,14 @@ public final class OpenOffer implements Tradable { public void setState(State state) { this.state = state; stateProperty.set(state); + if (state == State.AVAILABLE) { + deactivatedByTrigger = false; + } + } + + public void deactivate(boolean deactivatedByTrigger) { + this.deactivatedByTrigger = deactivatedByTrigger; + setState(State.DEACTIVATED); } public ReadOnlyObjectProperty stateProperty() { diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 0920c036d7..5bb127f840 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -604,13 +604,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } public void deactivateOpenOffer(OpenOffer openOffer, + boolean deactivatedByTrigger, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Offer offer = openOffer.getOffer(); if (openOffer.isAvailable()) { offerBookService.deactivateOffer(offer.getOfferPayload(), () -> { - openOffer.setState(OpenOffer.State.DEACTIVATED); + openOffer.deactivate(deactivatedByTrigger); requestPersistence(); log.debug("deactivateOpenOffer, offerId={}", offer.getId()); resultHandler.handleResult(); @@ -661,6 +662,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (openOffer.isAvailable()) { deactivateOpenOffer(openOffer, + false, resultHandler, errorMessage -> { offersToBeEdited.remove(openOffer.getId()); diff --git a/core/src/main/java/haveno/core/offer/TriggerPriceService.java b/core/src/main/java/haveno/core/offer/TriggerPriceService.java index 59705de064..18527b774c 100644 --- a/core/src/main/java/haveno/core/offer/TriggerPriceService.java +++ b/core/src/main/java/haveno/core/offer/TriggerPriceService.java @@ -92,12 +92,11 @@ public class TriggerPriceService { .filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode())) .forEach(marketPrice -> { openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream() - .filter(openOffer -> !openOffer.isDeactivated()) .forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer)); }); } - public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) { + public static boolean isTriggered(MarketPrice marketPrice, OpenOffer openOffer) { Price price = openOffer.getOffer().getPrice(); if (price == null || marketPrice == null) { return false; @@ -125,13 +124,12 @@ public class TriggerPriceService { } private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) { - if (wasTriggered(marketPrice, openOffer)) { - String currencyCode = openOffer.getOffer().getCurrencyCode(); - int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ? - TraditionalMoney.SMALLEST_UNIT_EXPONENT : - CryptoMoney.SMALLEST_UNIT_EXPONENT; - long triggerPrice = openOffer.getTriggerPrice(); + String currencyCode = openOffer.getOffer().getCurrencyCode(); + int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ? + TraditionalMoney.SMALLEST_UNIT_EXPONENT : + CryptoMoney.SMALLEST_UNIT_EXPONENT; + if (openOffer.getState() == OpenOffer.State.AVAILABLE && isTriggered(marketPrice, openOffer)) { log.info("Market price exceeded the trigger price of the open offer.\n" + "We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" + "Market price: {};\nTrigger price: {}", @@ -139,14 +137,26 @@ public class TriggerPriceService { currencyCode, openOffer.getOffer().getDirection(), marketPrice.getPrice(), - MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent) + MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent) ); - openOfferManager.deactivateOpenOffer(openOffer, () -> { + openOfferManager.deactivateOpenOffer(openOffer, true, () -> { + }, errorMessage -> { + }); + } else if (openOffer.getState() == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger() && !isTriggered(marketPrice, openOffer)) { + log.info("Market price is back within the trigger price of the open offer.\n" + + "We reactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" + + "Market price: {};\nTrigger price: {}", + openOffer.getOffer().getShortId(), + currencyCode, + openOffer.getOffer().getDirection(), + marketPrice.getPrice(), + MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent) + ); + + openOfferManager.activateOpenOffer(openOffer, () -> { }, errorMessage -> { }); - } else if (openOffer.getState() == OpenOffer.State.AVAILABLE) { - // TODO: check if open offer's reserve tx is failed or double spend seen } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersDataModel.java index c1f255d96d..5a178f6dce 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersDataModel.java @@ -69,7 +69,7 @@ class OpenOffersDataModel extends ActivatableDataModel { } void onDeactivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - openOfferManager.deactivateOpenOffer(openOffer, resultHandler, errorMessageHandler); + openOfferManager.deactivateOpenOffer(openOffer, false, resultHandler, errorMessageHandler); } void onRemoveOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -94,7 +94,7 @@ class OpenOffersDataModel extends ActivatableDataModel { list.sort((o1, o2) -> o2.getOffer().getDate().compareTo(o1.getOffer().getDate())); } - boolean wasTriggered(OpenOffer openOffer) { - return TriggerPriceService.wasTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer); + boolean isTriggered(OpenOffer openOffer) { + return TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer); } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java index c1f5566398..575cc1d2b1 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -368,7 +368,7 @@ public class OpenOffersView extends ActivatableViewAndModel log.debug("Activate offer was successful"), (message) -> { @@ -720,7 +720,7 @@ public class OpenOffersView extends ActivatableViewAndModel { if (openOffer.isDeactivated()) { onActivateOpenOffer(openOffer); @@ -798,7 +798,7 @@ public class OpenOffersView extends ActivatableViewAndModel 0; button.setVisible(triggerPriceSet); - if (model.dataModel.wasTriggered(item.getOpenOffer())) { + if (model.dataModel.isTriggered(item.getOpenOffer())) { button.getGraphic().getStyleClass().add("warning"); button.setTooltip(new Tooltip(Res.get("openOffer.triggered"))); } else { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 290edb668f..04c32ee8dc 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1421,6 +1421,7 @@ message OpenOffer { string reserve_tx_hex = 10; string reserve_tx_key = 11; string challenge = 12; + bool deactivated_by_trigger = 13; } message Tradable { From f675588a2deaa123258470df55b6df1b6b4e56e8 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 14 Feb 2025 16:34:08 -0500 Subject: [PATCH 130/371] allow offer trigger price outside of current price --- .../haveno/core/offer/OpenOfferManager.java | 15 +++++++++++- .../main/java/haveno/core/util/PriceUtil.java | 24 ++----------------- .../portfolio/openoffer/OpenOffersView.java | 2 +- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 5bb127f840..5df08a38ba 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -595,6 +595,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe offerBookService.activateOffer(offer, () -> { openOffer.setState(OpenOffer.State.AVAILABLE); + applyTriggerState(openOffer); requestPersistence(); log.debug("activateOpenOffer, offerId={}", offer.getId()); resultHandler.handleResult(); @@ -603,6 +604,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } + private void applyTriggerState(OpenOffer openOffer) { + if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; + if (TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer)) { + openOffer.deactivate(true); + } + } + public void deactivateOpenOffer(OpenOffer openOffer, boolean deactivatedByTrigger, ResultHandler resultHandler, @@ -688,7 +696,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe removeOpenOffer(openOffer); OpenOffer editedOpenOffer = new OpenOffer(editedOffer, triggerPrice, openOffer); - editedOpenOffer.setState(originalState); + if (originalState == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger()) { + editedOpenOffer.setState(OpenOffer.State.AVAILABLE); + applyTriggerState(editedOpenOffer); + } else { + editedOpenOffer.setState(originalState); + } addOpenOffer(editedOpenOffer); diff --git a/core/src/main/java/haveno/core/util/PriceUtil.java b/core/src/main/java/haveno/core/util/PriceUtil.java index 35a6b0bec8..a91b13c85c 100644 --- a/core/src/main/java/haveno/core/util/PriceUtil.java +++ b/core/src/main/java/haveno/core/util/PriceUtil.java @@ -22,7 +22,6 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; -import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; @@ -69,27 +68,8 @@ public class PriceUtil { if (!result.isValid) { return result; } - - long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, marketPrice.getCurrencyCode()); - long marketPriceAsLong = PriceUtil.getMarketPriceAsLong("" + marketPrice.getPrice(), marketPrice.getCurrencyCode()); - String marketPriceAsString = FormattingUtils.formatMarketPrice(marketPrice.getPrice(), marketPrice.getCurrencyCode()); - - boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); - if ((isSellOffer && !isCryptoCurrency) || (!isSellOffer && isCryptoCurrency)) { - if (triggerPriceAsLong >= marketPriceAsLong) { - return new InputValidator.ValidationResult(false, - Res.get("createOffer.triggerPrice.invalid.tooHigh", marketPriceAsString)); - } else { - return new InputValidator.ValidationResult(true); - } - } else { - if (triggerPriceAsLong <= marketPriceAsLong) { - return new InputValidator.ValidationResult(false, - Res.get("createOffer.triggerPrice.invalid.tooLow", marketPriceAsString)); - } else { - return new InputValidator.ValidationResult(true); - } - } + + return new InputValidator.ValidationResult(true); } public static Price marketPriceToPrice(MarketPrice marketPrice) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java index 575cc1d2b1..7c41eef890 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -692,7 +692,7 @@ public class OpenOffersView extends ActivatableViewAndModel Date: Sat, 15 Feb 2025 07:59:38 -0500 Subject: [PATCH 131/371] add usdt-trc20 as main crypto currency --- core/src/main/java/haveno/core/locale/CurrencyUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java index bd66667da5..2c0acfba43 100644 --- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java +++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java @@ -201,6 +201,7 @@ public class CurrencyUtil { result.add(new CryptoCurrency("ETH", "Ether")); result.add(new CryptoCurrency("LTC", "Litecoin")); result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)")); + result.add(new CryptoCurrency("USDT-TRC20", "Tether USD (TRC20)")); result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)")); result.sort(TradeCurrency::compareTo); return result; From 667f0c8fb5d3da9ad7231e7c5575ccb14a84f71b Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 16 Feb 2025 09:10:23 -0500 Subject: [PATCH 132/371] rename currency code base util --- core/src/main/java/haveno/core/locale/CurrencyUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java index 2c0acfba43..db6ee6722b 100644 --- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java +++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java @@ -298,7 +298,7 @@ public class CurrencyUtil { if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) { return isCryptoCurrencyMap.get(currencyCode.toUpperCase()); } - if (isCryptoCurrencyBase(currencyCode)) { + if (isCryptoCurrencyCodeBase(currencyCode)) { return true; } @@ -327,7 +327,7 @@ public class CurrencyUtil { return isCryptoCurrency; } - private static boolean isCryptoCurrencyBase(String currencyCode) { + private static boolean isCryptoCurrencyCodeBase(String currencyCode) { if (currencyCode == null) return false; currencyCode = currencyCode.toUpperCase(); return currencyCode.equals("USDT") || currencyCode.equals("USDC"); From 024e59a9826e87781a33adcfce72d74f7f9a46d4 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 16 Feb 2025 08:30:26 -0500 Subject: [PATCH 133/371] support DAI Stablecoin (ERC20) --- assets/src/main/java/haveno/asset/package-info.java | 2 +- .../tokens/{DaiStablecoin.java => DaiStablecoinERC20.java} | 6 +++--- .../src/main/resources/META-INF/services/haveno.asset.Asset | 3 ++- core/src/main/java/haveno/core/locale/CurrencyUtil.java | 4 +++- 4 files changed, 9 insertions(+), 6 deletions(-) rename assets/src/main/java/haveno/asset/tokens/{DaiStablecoin.java => DaiStablecoinERC20.java} (85%) diff --git a/assets/src/main/java/haveno/asset/package-info.java b/assets/src/main/java/haveno/asset/package-info.java index a50b986115..6eeaf4095f 100644 --- a/assets/src/main/java/haveno/asset/package-info.java +++ b/assets/src/main/java/haveno/asset/package-info.java @@ -21,7 +21,7 @@ * {@link haveno.asset.Token} and {@link haveno.asset.Erc20Token}, as well as concrete * implementations of each, such as {@link haveno.asset.coins.Bitcoin} itself, cryptos like * {@link haveno.asset.coins.Litecoin} and {@link haveno.asset.coins.Ether} and tokens like - * {@link haveno.asset.tokens.DaiStablecoin}. + * {@link haveno.asset.tokens.DaiStablecoinERC20}. *

    * The purpose of this package is to provide everything necessary for registering * ("listing") new assets and managing / accessing those assets within, e.g. the Haveno diff --git a/assets/src/main/java/haveno/asset/tokens/DaiStablecoin.java b/assets/src/main/java/haveno/asset/tokens/DaiStablecoinERC20.java similarity index 85% rename from assets/src/main/java/haveno/asset/tokens/DaiStablecoin.java rename to assets/src/main/java/haveno/asset/tokens/DaiStablecoinERC20.java index e9cc01f74f..8c1e84e871 100644 --- a/assets/src/main/java/haveno/asset/tokens/DaiStablecoin.java +++ b/assets/src/main/java/haveno/asset/tokens/DaiStablecoinERC20.java @@ -19,9 +19,9 @@ package haveno.asset.tokens; import haveno.asset.Erc20Token; -public class DaiStablecoin extends Erc20Token { +public class DaiStablecoinERC20 extends Erc20Token { - public DaiStablecoin() { - super("Dai Stablecoin", "DAI"); + public DaiStablecoinERC20() { + super("Dai Stablecoin", "DAI-ERC20"); } } diff --git a/assets/src/main/resources/META-INF/services/haveno.asset.Asset b/assets/src/main/resources/META-INF/services/haveno.asset.Asset index 709b951227..80b9cd036d 100644 --- a/assets/src/main/resources/META-INF/services/haveno.asset.Asset +++ b/assets/src/main/resources/META-INF/services/haveno.asset.Asset @@ -9,4 +9,5 @@ haveno.asset.coins.Litecoin haveno.asset.coins.Monero haveno.asset.tokens.TetherUSDERC20 haveno.asset.tokens.TetherUSDTRC20 -haveno.asset.tokens.USDCoinERC20 \ No newline at end of file +haveno.asset.tokens.USDCoinERC20 +haveno.asset.tokens.DaiStablecoinERC20 \ No newline at end of file diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java index db6ee6722b..6ed42b6234 100644 --- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java +++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java @@ -200,6 +200,7 @@ public class CurrencyUtil { result.add(new CryptoCurrency("BCH", "Bitcoin Cash")); result.add(new CryptoCurrency("ETH", "Ether")); result.add(new CryptoCurrency("LTC", "Litecoin")); + result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin (ERC20)")); result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)")); result.add(new CryptoCurrency("USDT-TRC20", "Tether USD (TRC20)")); result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)")); @@ -330,7 +331,7 @@ public class CurrencyUtil { private static boolean isCryptoCurrencyCodeBase(String currencyCode) { if (currencyCode == null) return false; currencyCode = currencyCode.toUpperCase(); - return currencyCode.equals("USDT") || currencyCode.equals("USDC"); + return currencyCode.equals("USDT") || currencyCode.equals("USDC") || currencyCode.equals("DAI"); } public static String getCurrencyCodeBase(String currencyCode) { @@ -338,6 +339,7 @@ public class CurrencyUtil { currencyCode = currencyCode.toUpperCase(); if (currencyCode.contains("USDT")) return "USDT"; if (currencyCode.contains("USDC")) return "USDC"; + if (currencyCode.contains("DAI")) return "DAI"; return currencyCode; } From c3b7289943d6d29eafa5371b55db37c114128ee9 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 16 Feb 2025 20:56:31 -0500 Subject: [PATCH 134/371] update chat message ack state on main thread --- .../main/java/haveno/core/support/SupportManager.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/support/SupportManager.java b/core/src/main/java/haveno/core/support/SupportManager.java index 10cbfdafaf..4bb8e86d82 100644 --- a/core/src/main/java/haveno/core/support/SupportManager.java +++ b/core/src/main/java/haveno/core/support/SupportManager.java @@ -232,10 +232,12 @@ public abstract class SupportManager { getAllChatMessages(ackMessage.getSourceId()).stream() .filter(msg -> msg.getUid().equals(ackMessage.getSourceUid())) .forEach(msg -> { - if (ackMessage.isSuccess()) - msg.setAcknowledged(true); - else - msg.setAckError(ackMessage.getErrorMessage()); + UserThread.execute(() -> { + if (ackMessage.isSuccess()) + msg.setAcknowledged(true); + else + msg.setAckError(ackMessage.getErrorMessage()); + }); }); requestPersistence(); } From e4fa5f520d7319e5fd57f32857507fee51d2be93 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 16 Feb 2025 20:59:36 -0500 Subject: [PATCH 135/371] synchronize access to get closed trade by id --- core/src/main/java/haveno/core/api/CoreTradesService.java | 4 +--- .../main/java/haveno/core/trade/ClosedTradableManager.java | 4 ++-- core/src/main/java/haveno/core/trade/TradeManager.java | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java index 14f10b4f00..9854167c71 100644 --- a/core/src/main/java/haveno/core/api/CoreTradesService.java +++ b/core/src/main/java/haveno/core/api/CoreTradesService.java @@ -47,7 +47,6 @@ import haveno.core.support.messages.ChatMessage; import haveno.core.support.traderchat.TradeChatSession; import haveno.core.support.traderchat.TraderChatManager; import haveno.core.trade.ClosedTradableManager; -import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.TradeUtil; @@ -223,8 +222,7 @@ class CoreTradesService { } private Optional getClosedTrade(String tradeId) { - Optional tradable = closedTradableManager.getTradeById(tradeId); - return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value); + return closedTradableManager.getTradeById(tradeId); } List getTrades() { diff --git a/core/src/main/java/haveno/core/trade/ClosedTradableManager.java b/core/src/main/java/haveno/core/trade/ClosedTradableManager.java index 0a48fc5188..cac8e9e261 100644 --- a/core/src/main/java/haveno/core/trade/ClosedTradableManager.java +++ b/core/src/main/java/haveno/core/trade/ClosedTradableManager.java @@ -150,9 +150,9 @@ public class ClosedTradableManager implements PersistedDataHost { } } - public Optional getTradeById(String id) { + public Optional getTradeById(String id) { synchronized (closedTradables) { - return closedTradables.stream().filter(e -> e instanceof Trade && e.getId().equals(id)).findFirst(); + return getClosedTrades().stream().filter(e -> e.getId().equals(id)).findFirst(); } } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index a3cca84912..caabdb384a 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -1315,7 +1315,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public Optional getClosedTrade(String tradeId) { - return closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(tradeId)).findFirst(); + return closedTradableManager.getTradeById(tradeId); } public Optional getFailedTrade(String tradeId) { From 4d765fa5d969e4a5aa5079ed370a61db1e726fbe Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 18 Feb 2025 08:16:13 -0500 Subject: [PATCH 136/371] resend deposit confirmed and payment sent messages more often until ack --- .../protocol/tasks/BuyerSendPaymentSentMessage.java | 13 +++++++++++-- .../tasks/SendDepositsConfirmedMessage.java | 6 +++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java index 42206c8de0..f060effe78 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java @@ -66,7 +66,7 @@ import lombok.extern.slf4j.Slf4j; public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { private ChangeListener listener; private Timer timer; - private static final int MAX_RESEND_ATTEMPTS = 10; + private static final int MAX_RESEND_ATTEMPTS = 20; private int delayInMin = 10; private int resendCounter = 0; @@ -198,7 +198,16 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask onMessageStateChange(processModel.getPaymentSentMessageStateProperty().get()); } - delayInMin = delayInMin * 2; + // first re-send is after 2 minutes, then increase the delay exponentially + if (resendCounter == 0) { + int shortDelay = 2; + log.info("We will send the message again to the peer after a delay of {} min.", shortDelay); + timer = UserThread.runAfter(this::run, shortDelay, TimeUnit.MINUTES); + } else { + log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); + timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); + delayInMin = (int) ((double) delayInMin * 1.5); + } resendCounter++; } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java index 8be8f00da5..fb17d60d4d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java @@ -37,7 +37,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTask { private Timer timer; - private static final int MAX_RESEND_ATTEMPTS = 10; + private static final int MAX_RESEND_ATTEMPTS = 20; private int delayInMin = 10; private int resendCounter = 0; @@ -137,7 +137,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas timer.stop(); } - // first re-send is after 2 minutes, then double the delay each iteration + // first re-send is after 2 minutes, then increase the delay exponentially if (resendCounter == 0) { int shortDelay = 2; log.info("We will send the message again to the peer after a delay of {} min.", shortDelay); @@ -145,7 +145,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas } else { log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); - delayInMin = delayInMin * 2; + delayInMin = (int) ((double) delayInMin * 1.5); } resendCounter++; } From b9381f7f9ff5a78432bb2c41d22c45a7d4faafd3 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 18 Feb 2025 08:53:22 -0500 Subject: [PATCH 137/371] increase max connections per ip --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 51d55af341..ac3d9b9a70 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,7 @@ monerod1-local: --log-level 0 \ --add-exclusive-node 127.0.0.1:48080 \ --add-exclusive-node 127.0.0.1:58080 \ + --max-connections-per-ip 10 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ @@ -88,6 +89,7 @@ monerod2-local: --confirm-external-bind \ --add-exclusive-node 127.0.0.1:28080 \ --add-exclusive-node 127.0.0.1:58080 \ + --max-connections-per-ip 10 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ @@ -106,6 +108,7 @@ monerod3-local: --confirm-external-bind \ --add-exclusive-node 127.0.0.1:28080 \ --add-exclusive-node 127.0.0.1:48080 \ + --max-connections-per-ip 10 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ From 5d457d62c50a12cf8430ffa7b90312ac6fd0bb89 Mon Sep 17 00:00:00 2001 From: jermanuts <109705802+jermanuts@users.noreply.github.com> Date: Sat, 22 Feb 2025 15:44:56 +0200 Subject: [PATCH 138/371] fix support link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c439f9a55..9e3c5b9fa1 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ See the [developer guide](docs/developer-guide.md) to get started developing for See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for our styling guides. -If you are not able to contribute code and want to contribute development resources, [donations](#support) fund development bounties. +If you are not able to contribute code and want to contribute development resources, [donations](#support-and-sponsorships) fund development bounties. ## Bounties From 8a01a07ac21b467d454ce6dde7f15acf3e758d5b Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 22 Feb 2025 09:58:23 -0500 Subject: [PATCH 139/371] update link to ui poc in developer guide --- docs/developer-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 565ee4886e..dd799a9a9e 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -8,7 +8,7 @@ This document is a guide for Haveno development. ## Run the UI proof of concept -Follow [instructions](https://github.com/haveno-dex/haveno-ts#run-in-a-browser) to run Haveno's UI proof of concept in a browser. +Follow [instructions](https://github.com/haveno-dex/haveno-ui-poc) to run Haveno's UI proof of concept in a browser. This proof of concept demonstrates using Haveno's gRPC server with a web frontend (react and typescript) instead of Haveno's JFX application. From 40924a6f7bce01f382710d899f0cd33e5c43b087 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 21 Feb 2025 18:27:48 -0500 Subject: [PATCH 140/371] prevent wallet backup on windows due to file lock --- .../java/haveno/core/xmr/wallet/XmrWalletService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 0592fbdd59..95669e9e0b 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -247,7 +247,11 @@ public class XmrWalletService extends XmrWalletBase { @Override public void saveWallet() { - saveWallet(!(Utilities.isWindows() && wallet != null)); + saveWallet(shouldBackup(wallet)); + } + + private boolean shouldBackup(MoneroWallet wallet) { + return wallet != null && !Utilities.isWindows(); // TODO: cannot backup on windows because file is locked } public void saveWallet(boolean backup) { @@ -389,7 +393,7 @@ public class XmrWalletService extends XmrWalletBase { MoneroError err = null; String path = wallet.getPath(); try { - if (save) saveWallet(wallet, true); + if (save) saveWallet(wallet, shouldBackup(wallet)); wallet.close(); } catch (MoneroError e) { err = e; From 964c71ed1be2ef32392595fe49b1c4856b666009 Mon Sep 17 00:00:00 2001 From: boldsuck Date: Wed, 26 Feb 2025 19:26:47 +0100 Subject: [PATCH 141/371] Fix broken 'create-mainnet link' --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e3c5b9fa1..5b1d7e3db6 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Haveno can be installed on Linux, macOS, and Windows by using a third party inst A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the test network. -Alternatively, you can [create your own mainnet network](create-mainnet.md). +Alternatively, you can [create your own mainnet network](https://github.com/haveno-dex/haveno/blob/master/docs/create-mainnet.md). Note that Haveno is being actively developed. If you find issues or bugs, please let us know. From 28d2bc891f6764f239c8a16d0ada68ff9c7521a3 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 27 Feb 2025 09:18:07 -0500 Subject: [PATCH 142/371] update install instructions for MSYS2 --- docs/installing.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installing.md b/docs/installing.md index 29a15b54f5..951f752ea8 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -19,9 +19,9 @@ On Windows, first install MSYS2: 4. Update pacman: `pacman -Syy` 5. Install dependencies. During installation, use default=all by leaving the input blank and pressing enter. - 64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git` + 64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git zip unzip` - 32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git` + 32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git zip unzip` On all platforms, install Java JDK 21: @@ -30,6 +30,8 @@ curl -s "https://get.sdkman.io" | bash sdk install java 21.0.2.fx-librca ``` +Restart the terminal for the changes to take effect. + ## Build Haveno If it's the first time you are building Haveno, run the following commands to download the repository, the needed dependencies, and build the latest release. If using a third party network, replace the repository URL with theirs: From 816d273956d968d6d47b384d9d86fe81ba1c6002 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 27 Feb 2025 14:01:22 -0500 Subject: [PATCH 143/371] add demo video to readme, from @Minecon724 in #1352 Co-authored-by: Minecon724 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 5b1d7e3db6..4e90b06ce3 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ Main features: See the [FAQ on our website](https://haveno.exchange/faq/) for more information. +## Haveno Demo + +https://github.com/user-attachments/assets/eb6b3af0-78ce-46a7-bfa1-2aacd8649d47 + ## Installing Haveno Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network. From 998b893cc3131edfc50d07ced8e42f48711f430e Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 28 Feb 2025 09:01:10 -0500 Subject: [PATCH 144/371] update pgp public key for commit verification --- gpg_keys/woodser.asc | 66 ++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/gpg_keys/woodser.asc b/gpg_keys/woodser.asc index 2dcc3f3a7a..4bbc755edc 100644 --- a/gpg_keys/woodser.asc +++ b/gpg_keys/woodser.asc @@ -1,5 +1,4 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- -Comment: GPGTools - https://gpgtools.org mQINBFpYwMsBEACpSn/AxDOGCELE9lmYPfvBzgw2+1xS3TX7kYdlvVDQf+8eCgGz 8ZpBY3lXdga/yMZZBoDknGzjlyaiG/vi7NljMQmWd5eGyhyfkWpeDXYLbiB5HlKe @@ -25,29 +24,42 @@ zA6zmydMyNeUOYKjqnimQUuHBhxuUl5FlokoWaXnUavJvOjVfsoTcNxCcvMHnhFN R5TmNLOLPXrXwdU0V86nDmHstXl+E02SWFTgZ8Vxg318ZLpIw3rb65zUALTfZwpl 32XhIUhBBnN0zRl3scGW+oj6ks8WgErQ7o6dYdTu17AIggNdpHXO3XXVnW0mS6tz IeCvDkEQxegoL/B83B+9LI//U9sc5iSCQOEZQ1YLUdEkNSFgr7HU9GPllop52HUB -GffqGoz4F7MXl3g2ZrkCDQRaWMDLARAAxCZjAQT2bz3wqyz0vDMzwbtPRtHo3DY+ -r1n6CLxXDXu8u6DWGvRX3M2Z7jrbJe4e/cYDSDfNVj9E2YIVyD8pUbv9AUYh5VBq -hQU5C+3aeReO1js2iS1Xk6IAJ60aqp/JsrnRyOQfpAnGQaZlvqomdbbrzZaAaOXv -dgbHyBRj2eHZtSfYkhndfstpkE28etoZhNZP2h0e5DVLmfniwgMmMuZoiJNzEAGG -e9kAxdkvKgRp9HDrj6mGkHmbw6bam87DVrveNTPp662H7gLpIcUUJxzV7LttZDJa -k1/JxCQVbPoy0Frmp3TxXhmSJlV1vGVX8SFucaxrSS8ARhCSBrf+hGypbDGm+Tg5 -+oa1gdUSw24FODk7ut6LNwEgJ4n9ubs/8EP7/9rReiVLjJsW46ZueS1EjFTneZM1 -VyeAqBKqbwj21H9KxTghogCxpPHe4tqTr3J8eFjVYoNZDoFO3b00kjhXWOWicbCt -aT4SYUsRZP5WuBwgQu8W4AGgQpCFv6kJ37ctYfeSduDfGsMK0EJxpxutaDZC2940 -VfUA38LORFbwzPaNAGV8e7mViqEEmDE4g6fT0vyGodCsAM5EIbP/Q4u6ftNfE7Mf -mmp2CLnqHsfVLUvGbH8GbMLqoS1bajy8t4HEU0OZ7N12IQ1hnfnKHrLKpfGKXfl4 -1jkrL2gnuyUAEQEAAYkCNgQYAQoAIBYhBFL9fAGHfKloyXEY0FWhDdSK3uXvBQJa -WMDLAhsMAAoJEFWhDdSK3uXvf3wQAJyXitW8l+D2AaaszKmm4VXYkqf+azrVmRLp -nqUMvIaxhJTY4J2H5bT6JAAEU3/Dp6/ghYvqGbz25r94PUkDPKZ/23MvBMFab8bi -I//pT+jJwQFXKrXEIWhuBNFvqKhL8OxMi1kqys3E456quueohQzZbKyzTAYrEBQX -8/fNf/qaGuWIzcrdWqAO1OxnO/LBTZIh4Jrn1spBh3nW/U6k3LLSsXsPkBv9EIHx -R680R8cstT9cLaxUzqBhXX+iKPq8MqWXD5hZKKBCylWybdfhGc4FF+OszduWDP4n -VahNGD7pFX9hCMi6K5uIRj8bMtVahN7bBiwZMp3nQRAGCO5upqowMaGJv7A9zQ14 -lPKEEOf+3kQUj2XUw4juRmViU91hpIRy4Hf/4Wry3AhqICf9mMgkm/tI1ez+moWQ -RhopYZ4WTNbIhQrSUtaEOQHBcJFinKuR4SXxxmrFHpZ37It3SZZ5zJyZHrLypT9r -y0xrm7JWF++wQVofqvzTmVtIiwbYADuL/fDvyolo85rSeoDSdZVGnvY2tipMhr0+ -qBDrOi3tSaFzU+pmd0/hBmeNxS1ciYnxA6Ei+w0v79mbgKywngMTq+wQDynXrIHe -Np1oXqGvFU9bQ6BhDDKS54pPHm0ZlEg80+vealNXpXIVtjSM2PlRpsTlmqs3YcIa -mqKdaDoa -=bRX1 ------END PGP PUBLIC KEY BLOCK----- +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 +=4SFl +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file From 48501a6572068f1495e9fa360faff74d2fb2b999 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:06:46 -0500 Subject: [PATCH 145/371] direct bind tor node uses configured socks5 proxy --- .../java/haveno/network/p2p/NetworkNodeProvider.java | 6 ++++-- .../p2p/network/TorNetworkNodeDirectBind.java | 12 +++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java b/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java index 798d162357..fa1aa61470 100644 --- a/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java +++ b/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java @@ -22,6 +22,7 @@ import com.google.inject.Provider; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.proto.network.NetworkProtoResolver; +import haveno.network.Socks5ProxyProvider; import haveno.network.p2p.network.BanFilter; import haveno.network.p2p.network.BridgeAddressProvider; import haveno.network.p2p.network.LocalhostNetworkNode; @@ -55,7 +56,8 @@ public class NetworkNodeProvider implements Provider { @Named(Config.TOR_CONTROL_PASSWORD) String password, @Nullable @Named(Config.TOR_CONTROL_COOKIE_FILE) File cookieFile, @Named(Config.TOR_STREAM_ISOLATION) boolean streamIsolation, - @Named(Config.TOR_CONTROL_USE_SAFE_COOKIE_AUTH) boolean useSafeCookieAuthentication) { + @Named(Config.TOR_CONTROL_USE_SAFE_COOKIE_AUTH) boolean useSafeCookieAuthentication, + Socks5ProxyProvider socks5ProxyProvider) { if (useLocalhostForP2P) { networkNode = new LocalhostNetworkNode(port, networkProtoResolver, banFilter, maxConnections); } else { @@ -72,7 +74,7 @@ public class NetworkNodeProvider implements Provider { if (torMode instanceof NewTor || torMode instanceof RunningTor) { networkNode = new TorNetworkNodeNetlayer(port, networkProtoResolver, torMode, banFilter, maxConnections, streamIsolation, controlHost); } else { - networkNode = new TorNetworkNodeDirectBind(port, networkProtoResolver, banFilter, maxConnections, hiddenServiceAddress); + networkNode = new TorNetworkNodeDirectBind(port, networkProtoResolver, banFilter, maxConnections, hiddenServiceAddress, socks5ProxyProvider); } } } diff --git a/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeDirectBind.java b/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeDirectBind.java index 8333af3f8b..6ccd5b1314 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeDirectBind.java +++ b/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeDirectBind.java @@ -1,6 +1,7 @@ package haveno.network.p2p.network; import haveno.common.util.Hex; +import haveno.network.Socks5ProxyProvider; import haveno.network.p2p.NodeAddress; import haveno.common.UserThread; @@ -9,7 +10,6 @@ import haveno.common.proto.network.NetworkProtoResolver; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import java.net.Socket; -import java.net.InetAddress; import java.net.ServerSocket; import java.io.IOException; @@ -25,16 +25,18 @@ import static com.google.common.base.Preconditions.checkArgument; @Slf4j public class TorNetworkNodeDirectBind extends TorNetworkNode { - private static final int TOR_DATA_PORT = 9050; // TODO: config option? private final String serviceAddress; + private final Socks5ProxyProvider socks5ProxyProvider; public TorNetworkNodeDirectBind(int servicePort, NetworkProtoResolver networkProtoResolver, @Nullable BanFilter banFilter, int maxConnections, - String hiddenServiceAddress) { + String hiddenServiceAddress, + Socks5ProxyProvider socks5ProxyProvider) { super(servicePort, networkProtoResolver, banFilter, maxConnections); this.serviceAddress = hiddenServiceAddress; + this.socks5ProxyProvider = socks5ProxyProvider; } @Override @@ -47,7 +49,7 @@ public class TorNetworkNodeDirectBind extends TorNetworkNode { @Override public Socks5Proxy getSocksProxy() { - Socks5Proxy proxy = new Socks5Proxy(InetAddress.getLoopbackAddress(), TOR_DATA_PORT); + Socks5Proxy proxy = new Socks5Proxy(socks5ProxyProvider.getSocks5Proxy().getInetAddress(), socks5ProxyProvider.getSocks5Proxy().getPort()); // TODO: can/should we return the same socks5 proxy directly? proxy.resolveAddrLocally(false); return proxy; } @@ -57,7 +59,7 @@ public class TorNetworkNodeDirectBind extends TorNetworkNode { // https://datatracker.ietf.org/doc/html/rfc1928 SOCKS5 Protocol try { checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address"); - Socket sock = new Socket(InetAddress.getLoopbackAddress(), TOR_DATA_PORT); + Socket sock = new Socket(getSocksProxy().getInetAddress(), getSocksProxy().getPort()); sock.getOutputStream().write(Hex.decode("050100")); String response = Hex.encode(sock.getInputStream().readNBytes(2)); if (!response.equalsIgnoreCase("0500")) { From 31b0edca2255170edf702a7b740f782e4fa29790 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 2 Mar 2025 10:44:38 -0500 Subject: [PATCH 146/371] do not ignore local node if configured --- .../java/haveno/core/api/XmrLocalNode.java | 23 +++++++++++++++++-- .../haveno/core/user/PreferencesTest.java | 6 +++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrLocalNode.java b/core/src/main/java/haveno/core/api/XmrLocalNode.java index 5a424dad38..583ae8b390 100644 --- a/core/src/main/java/haveno/core/api/XmrLocalNode.java +++ b/core/src/main/java/haveno/core/api/XmrLocalNode.java @@ -25,6 +25,8 @@ import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.xmr.XmrNodeSettings; import haveno.core.xmr.nodes.XmrNodes; +import haveno.core.xmr.nodes.XmrNodes.XmrNode; +import haveno.core.xmr.nodes.XmrNodesSetupPreferences; import haveno.core.xmr.wallet.XmrWalletService; import java.io.File; @@ -55,6 +57,7 @@ public class XmrLocalNode { private MoneroConnectionManager connectionManager; private final Config config; private final Preferences preferences; + private final XmrNodes xmrNodes; private final List listeners = new ArrayList<>(); // required arguments @@ -69,9 +72,12 @@ public class XmrLocalNode { } @Inject - public XmrLocalNode(Config config, Preferences preferences) { + public XmrLocalNode(Config config, + Preferences preferences, + XmrNodes xmrNodes) { this.config = config; this.preferences = preferences; + this.xmrNodes = xmrNodes; this.daemon = new MoneroDaemonRpc(getUri()); // initialize connection manager to listen to local connection @@ -101,7 +107,20 @@ public class XmrLocalNode { * Returns whether Haveno should ignore a local Monero node even if it is usable. */ public boolean shouldBeIgnored() { - return config.ignoreLocalXmrNode || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; + if (config.ignoreLocalXmrNode) return true; + + // determine if local node is configured + boolean hasConfiguredLocalNode = false; + for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { + String prefix = node.getAddress().startsWith("http") ? "" : "http://"; + if (equalsUri(prefix + node.getAddress() + ":" + node.getPort())) { + hasConfiguredLocalNode = true; + break; + } + } + if (!hasConfiguredLocalNode) return true; + + return false; } public void addListener(XmrLocalNodeListener listener) { diff --git a/core/src/test/java/haveno/core/user/PreferencesTest.java b/core/src/test/java/haveno/core/user/PreferencesTest.java index 365d54732b..0639ba11af 100644 --- a/core/src/test/java/haveno/core/user/PreferencesTest.java +++ b/core/src/test/java/haveno/core/user/PreferencesTest.java @@ -24,6 +24,7 @@ import haveno.core.locale.CountryUtil; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TraditionalCurrency; +import haveno.core.xmr.nodes.XmrNodes; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import javafx.collections.ObservableList; @@ -45,6 +46,7 @@ public class PreferencesTest { private Preferences preferences; private PersistenceManager persistenceManager; + private XmrNodes xmrNodes; @BeforeEach public void setUp() { @@ -53,12 +55,12 @@ public class PreferencesTest { GlobalSettings.setLocale(en_US); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); - persistenceManager = mock(PersistenceManager.class); Config config = new Config(); - XmrLocalNode xmrLocalNode = new XmrLocalNode(config, preferences); preferences = new Preferences( persistenceManager, config, null, null); + xmrNodes = new XmrNodes(); + XmrLocalNode xmrLocalNode = new XmrLocalNode(config, preferences, xmrNodes); } @Test From fff0fa0186bf2dbf264b4af50d0eb2562514d1fa Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 5 Mar 2025 08:20:24 -0500 Subject: [PATCH 147/371] add short name for paysafe --- core/src/main/resources/i18n/displayStrings.properties | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index d8111b7b9c..b499950537 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -3201,14 +3201,16 @@ DOMESTIC_WIRE_TRANSFER=Domestic Wire Transfer # suppress inspection "UnusedProperty" BSQ_SWAP=BSQ Swap -# Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo +# suppress inspection "UnusedProperty" PAYPAL=PayPal +# suppress inspection "UnusedProperty" +PAYSAFE=Paysafe # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold @@ -3304,9 +3306,10 @@ OK_PAY_SHORT=OKPay CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo +# suppress inspection "UnusedProperty" PAYPAL_SHORT=PayPal # suppress inspection "UnusedProperty" -PAYSAFE=Paysafe +PAYSAFE_SHORT=Paysafe #################################################################### From 5720ee74b041e1378740706e322046c5c6774299 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 5 Mar 2025 08:39:03 -0500 Subject: [PATCH 148/371] run trade charts view listeners on user thread to fix npe --- .../main/market/trades/TradesChartsView.java | 73 ++++++++++--------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java index dee3591bc8..97fcce06d1 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java @@ -208,29 +208,29 @@ public class TradesChartsView extends ActivatableViewAndModel { + timeUnitChangeListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { if (newValue != null) { model.setTickUnit((TradesChartsViewModel.TickUnit) newValue.getUserData()); priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); } - }; - priceAxisYWidthListener = (observable, oldValue, newValue) -> { + }); + priceAxisYWidthListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { priceAxisYWidth = (double) newValue; layoutChart(); - }; - volumeAxisYWidthListener = (observable, oldValue, newValue) -> { + }); + volumeAxisYWidthListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { volumeAxisYWidth = (double) newValue; layoutChart(); - }; - tradeStatisticsByCurrencyListener = c -> { + }); + tradeStatisticsByCurrencyListener = c -> UserThread.execute(() -> { nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); fillList(); - }; - parentHeightListener = (observable, oldValue, newValue) -> layout(); + }); + parentHeightListener = (observable, oldValue, newValue) -> UserThread.execute(this::layout); - priceColumnLabelListener = (o, oldVal, newVal) -> priceColumn.setGraphic(new AutoTooltipLabel(newVal)); + priceColumnLabelListener = (o, oldVal, newVal) -> UserThread.execute(() -> priceColumn.setGraphic(new AutoTooltipLabel(newVal))); // Need to render on next frame as otherwise there are issues in the chart rendering itemsChangeListener = c -> UserThread.execute(this::updateChartData); @@ -238,31 +238,34 @@ public class TradesChartsView extends ActivatableViewAndModel { - priceChart.setVisible(!showAll); - priceChart.setManaged(!showAll); - priceColumn.setSortable(!showAll); + UserThread.execute(() -> { + priceChart.setVisible(!showAll); + priceChart.setManaged(!showAll); + priceColumn.setSortable(!showAll); - if (showAll) { - volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amount"))); - priceColumnLabel.set(Res.get("shared.price")); - if (!tableView.getColumns().contains(marketColumn)) - tableView.getColumns().add(1, marketColumn); + if (showAll) { + volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amount"))); + priceColumnLabel.set(Res.get("shared.price")); + if (!tableView.getColumns().contains(marketColumn)) + tableView.getColumns().add(1, marketColumn); + + volumeChart.setPrefHeight(volumeChart.getMaxHeight()); + volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMaxHeight()); + } else { + volumeChart.setPrefHeight(volumeChart.getMinHeight()); + volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMinHeight()); + priceSeries.setName(selectedTradeCurrency.getName()); + String code = selectedTradeCurrency.getCode(); + volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", code))); + + priceColumnLabel.set(CurrencyUtil.getPriceWithCurrencyCode(code)); + + tableView.getColumns().remove(marketColumn); + } + + layout(); + }); - volumeChart.setPrefHeight(volumeChart.getMaxHeight()); - volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMaxHeight()); - } else { - volumeChart.setPrefHeight(volumeChart.getMinHeight()); - volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMinHeight()); - priceSeries.setName(selectedTradeCurrency.getName()); - String code = selectedTradeCurrency.getCode(); - volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", code))); - - priceColumnLabel.set(CurrencyUtil.getPriceWithCurrencyCode(code)); - - tableView.getColumns().remove(marketColumn); - } - - layout(); return null; }); } @@ -286,14 +289,14 @@ public class TradesChartsView extends ActivatableViewAndModel { + currencyComboBox.setOnChangeConfirmed(e -> UserThread.execute(() -> { if (currencyComboBox.getEditor().getText().isEmpty()) currencyComboBox.getSelectionModel().select(SHOW_ALL); CurrencyListItem selectedItem = currencyComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { model.onSetTradeCurrency(selectedItem.tradeCurrency); } - }); + })); toggleGroup.getToggles().get(model.tickUnit.ordinal()).setSelected(true); From 580e5b672cc4b9238391294ba15a592d29d96f7c Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 5 Mar 2025 08:39:31 -0500 Subject: [PATCH 149/371] add lock to submit tx to pool for verification and sync on shut down --- .../java/haveno/core/xmr/wallet/XmrWalletService.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 95669e9e0b..045897ed37 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -68,7 +68,6 @@ import java.util.stream.Stream; import javafx.beans.property.LongProperty; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.value.ChangeListener; -import lombok.Getter; import monero.common.MoneroError; import monero.common.MoneroRpcConnection; import monero.common.MoneroRpcError; @@ -145,8 +144,7 @@ public class XmrWalletService extends XmrWalletBase { private TradeManager tradeManager; private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type - @Getter - public final Object lock = new Object(); + private final Object lock = new Object(); private TaskLooper pollLooper; private boolean pollInProgress; private Long pollPeriodMs; @@ -740,7 +738,7 @@ public class XmrWalletService extends XmrWalletBase { MoneroDaemonRpc daemon = getDaemon(); MoneroWallet wallet = getWallet(); MoneroTx tx = null; - synchronized (daemon) { + synchronized (lock) { try { // verify tx not submitted to pool @@ -926,7 +924,7 @@ public class XmrWalletService extends XmrWalletBase { } // shut down threads - synchronized (getLock()) { + synchronized (lock) { List shutDownThreads = new ArrayList<>(); shutDownThreads.add(() -> ThreadUtils.shutDown(THREAD_ID)); ThreadUtils.awaitTasks(shutDownThreads); From 52bf1edf79a3981f6ffb896a82d4fff1fde825fe Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:19:41 -0500 Subject: [PATCH 150/371] Revert "direct bind tor node uses configured socks5 proxy" (#1635) This reverts commit fc42f6314eb593b129ff0bb028c585ce677e0e6d. --- .../java/haveno/network/p2p/NetworkNodeProvider.java | 6 ++---- .../p2p/network/TorNetworkNodeDirectBind.java | 12 +++++------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java b/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java index fa1aa61470..798d162357 100644 --- a/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java +++ b/p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java @@ -22,7 +22,6 @@ import com.google.inject.Provider; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.proto.network.NetworkProtoResolver; -import haveno.network.Socks5ProxyProvider; import haveno.network.p2p.network.BanFilter; import haveno.network.p2p.network.BridgeAddressProvider; import haveno.network.p2p.network.LocalhostNetworkNode; @@ -56,8 +55,7 @@ public class NetworkNodeProvider implements Provider { @Named(Config.TOR_CONTROL_PASSWORD) String password, @Nullable @Named(Config.TOR_CONTROL_COOKIE_FILE) File cookieFile, @Named(Config.TOR_STREAM_ISOLATION) boolean streamIsolation, - @Named(Config.TOR_CONTROL_USE_SAFE_COOKIE_AUTH) boolean useSafeCookieAuthentication, - Socks5ProxyProvider socks5ProxyProvider) { + @Named(Config.TOR_CONTROL_USE_SAFE_COOKIE_AUTH) boolean useSafeCookieAuthentication) { if (useLocalhostForP2P) { networkNode = new LocalhostNetworkNode(port, networkProtoResolver, banFilter, maxConnections); } else { @@ -74,7 +72,7 @@ public class NetworkNodeProvider implements Provider { if (torMode instanceof NewTor || torMode instanceof RunningTor) { networkNode = new TorNetworkNodeNetlayer(port, networkProtoResolver, torMode, banFilter, maxConnections, streamIsolation, controlHost); } else { - networkNode = new TorNetworkNodeDirectBind(port, networkProtoResolver, banFilter, maxConnections, hiddenServiceAddress, socks5ProxyProvider); + networkNode = new TorNetworkNodeDirectBind(port, networkProtoResolver, banFilter, maxConnections, hiddenServiceAddress); } } } diff --git a/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeDirectBind.java b/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeDirectBind.java index 6ccd5b1314..8333af3f8b 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeDirectBind.java +++ b/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeDirectBind.java @@ -1,7 +1,6 @@ package haveno.network.p2p.network; import haveno.common.util.Hex; -import haveno.network.Socks5ProxyProvider; import haveno.network.p2p.NodeAddress; import haveno.common.UserThread; @@ -10,6 +9,7 @@ import haveno.common.proto.network.NetworkProtoResolver; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import java.net.Socket; +import java.net.InetAddress; import java.net.ServerSocket; import java.io.IOException; @@ -25,18 +25,16 @@ import static com.google.common.base.Preconditions.checkArgument; @Slf4j public class TorNetworkNodeDirectBind extends TorNetworkNode { + private static final int TOR_DATA_PORT = 9050; // TODO: config option? private final String serviceAddress; - private final Socks5ProxyProvider socks5ProxyProvider; public TorNetworkNodeDirectBind(int servicePort, NetworkProtoResolver networkProtoResolver, @Nullable BanFilter banFilter, int maxConnections, - String hiddenServiceAddress, - Socks5ProxyProvider socks5ProxyProvider) { + String hiddenServiceAddress) { super(servicePort, networkProtoResolver, banFilter, maxConnections); this.serviceAddress = hiddenServiceAddress; - this.socks5ProxyProvider = socks5ProxyProvider; } @Override @@ -49,7 +47,7 @@ public class TorNetworkNodeDirectBind extends TorNetworkNode { @Override public Socks5Proxy getSocksProxy() { - Socks5Proxy proxy = new Socks5Proxy(socks5ProxyProvider.getSocks5Proxy().getInetAddress(), socks5ProxyProvider.getSocks5Proxy().getPort()); // TODO: can/should we return the same socks5 proxy directly? + Socks5Proxy proxy = new Socks5Proxy(InetAddress.getLoopbackAddress(), TOR_DATA_PORT); proxy.resolveAddrLocally(false); return proxy; } @@ -59,7 +57,7 @@ public class TorNetworkNodeDirectBind extends TorNetworkNode { // https://datatracker.ietf.org/doc/html/rfc1928 SOCKS5 Protocol try { checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address"); - Socket sock = new Socket(getSocksProxy().getInetAddress(), getSocksProxy().getPort()); + Socket sock = new Socket(InetAddress.getLoopbackAddress(), TOR_DATA_PORT); sock.getOutputStream().write(Hex.decode("050100")); String response = Hex.encode(sock.getInputStream().readNBytes(2)); if (!response.equalsIgnoreCase("0500")) { From 060d9fa4f138ca07f596386972265782e5ec7b7a Mon Sep 17 00:00:00 2001 From: U65535F <132809543+U65535F@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:42:22 +0530 Subject: [PATCH 151/371] Serialize lists to comma delimited string in PaymentAccount.toJson() (#1620) --- .../haveno/core/payment/PaymentAccount.java | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/haveno/core/payment/PaymentAccount.java b/core/src/main/java/haveno/core/payment/PaymentAccount.java index 8dda8fdedd..14dd88482b 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccount.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccount.java @@ -36,6 +36,7 @@ package haveno.core.payment; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.util.Utilities; @@ -341,12 +342,29 @@ public abstract class PaymentAccount implements PersistablePayload { // ---------------------------- SERIALIZATION ----------------------------- public String toJson() { - Map jsonMap = new HashMap(); - if (paymentAccountPayload != null) jsonMap.putAll(gsonBuilder.create().fromJson(paymentAccountPayload.toJson(), (Type) Object.class)); + Gson gson = gsonBuilder.create(); + Map jsonMap = new HashMap<>(); + + if (paymentAccountPayload != null) { + String payloadJson = paymentAccountPayload.toJson(); + Map payloadMap = gson.fromJson(payloadJson, new TypeToken>() {}.getType()); + + for (Map.Entry entry : payloadMap.entrySet()) { + Object value = entry.getValue(); + if (value instanceof List) { + List list = (List) value; + String joinedString = list.stream().map(Object::toString).collect(Collectors.joining(",")); + entry.setValue(joinedString); + } + } + + jsonMap.putAll(payloadMap); + } + jsonMap.put("accountName", getAccountName()); jsonMap.put("accountId", getId()); if (paymentAccountPayload != null) jsonMap.put("salt", getSaltAsHex()); - return gsonBuilder.create().toJson(jsonMap); + return gson.toJson(jsonMap); } /** @@ -388,12 +406,7 @@ public abstract class PaymentAccount implements PersistablePayload { PaymentAccountForm form = new PaymentAccountForm(PaymentAccountForm.FormId.valueOf(paymentMethod.getId())); for (PaymentAccountFormField.FieldId fieldId : getInputFieldIds()) { PaymentAccountFormField field = getEmptyFormField(fieldId); - Object value = jsonMap.get(HavenoUtils.toCamelCase(field.getId().toString())); - if (value instanceof List) { // TODO: list should already be serialized to comma delimited string in PaymentAccount.toJson() (PaymentAccountTypeAdapter?) - field.setValue(String.join(",", (List) value)); - } else { - field.setValue((String) value); - } + field.setValue((String) jsonMap.get(HavenoUtils.toCamelCase(field.getId().toString()))); form.getFields().add(field); } return form; From 67d0589e7bf84c6223cfa2c91c01ba5595ca3d8e Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:11:10 -0500 Subject: [PATCH 152/371] fix hyperlinks to add new API functions (#1641) --- docs/developer-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide.md b/docs/developer-guide.md index dd799a9a9e..e09b953df7 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -28,7 +28,7 @@ Follow [instructions](https://github.com/haveno-dex/haveno-ts#run-tests) to run 2. Define the new service or message in Haveno's [protobuf definition](../proto/src/main/proto/grpc.proto). 3. Clean and build Haveno after modifying the protobuf definition: `make clean && make` 4. Implement the new service in Haveno's backend, following existing patterns.
    - For example, the gRPC function to get offers is implemented by [`GrpcServer`](https://github.com/haveno-dex/haveno/blob/master/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java) > [`GrpcOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java#L104) > [`CoreApi.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/api/CoreApi.java#L128) > [`CoreOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/api/CoreOffersService.java#L126) > [`OfferBookService.getOffers()`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/offer/OfferBookService.java#L193). + For example, the gRPC function to get offers is implemented by [`GrpcServer`](https://github.com/haveno-dex/haveno/blob/master/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java) > [`GrpcOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java#L102) > [`CoreApi.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/api/CoreApi.java#L403) > [`CoreOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/api/CoreOffersService.java#L131) > [`OfferBookService.getOffers()`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/offer/OfferBookService.java#L248). 5. Build Haveno: `make` 6. Update the gRPC client in haveno-ts: `npm install` 7. Add the corresponding typescript method(s) to [HavenoClient.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/HavenoClient.ts) with clear and concise documentation. From e24b1c2461342b9cbf331437156c99396d43c7bc Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:40:49 -0500 Subject: [PATCH 153/371] remove rino and melo.tools from public xmr nodes --- core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java index 97217b0fe4..a8fa1ade26 100644 --- a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java +++ b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java @@ -75,8 +75,6 @@ public class XmrNodes { new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 38081, 1, "@local"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 39081, 1, "@local"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "45.63.8.26", 38081, 2, "@haveno"), - new XmrNode(MoneroNodesOption.PROVIDED, null, null, "stagenet.community.rino.io", 38081, 3, "@RINOwallet"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "stagenet.melo.tools", 38081, 3, null), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.sethforprivacy.com", 38089, 3, null), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node2.sethforprivacy.com", 38089, 3, null), new XmrNode(MoneroNodesOption.PUBLIC, null, "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion", null, 38089, 3, null) @@ -85,7 +83,6 @@ public class XmrNodes { return Arrays.asList( new XmrNode(MoneroNodesOption.PUBLIC, null, null, "127.0.0.1", 18081, 1, "@local"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "xmr-node.cakewallet.com", 18081, 2, "@cakewallet"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.community.rino.io", 18081, 2, "@RINOwallet"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "nodes.hashvault.pro", 18080, 2, "@HashVault"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "p2pmd.xmrvsbeast.com", 18080, 2, "@xmrvsbeast"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.monerodevs.org", 18089, 2, "@monerodevs.org"), From d67d259b2cde1cb35b989452ac8d5e449f4774ee Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 7 Mar 2025 07:06:03 -0500 Subject: [PATCH 154/371] update stagenet faucet link --- docs/installing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installing.md b/docs/installing.md index 951f752ea8..eefd844ce6 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -77,7 +77,7 @@ Steps: 1. Run `make user1-desktop-stagenet` to start the application. 2. Click on the "Funds" tab in the top menu and copy the generated XMR address. -3. Go to the [stagenet faucet](https://community.rino.io/faucet/stagenet/) and paste the address above in the "Get XMR" field. Submit and see the stagenet coins being sent to your Haveno instance. +3. Go to the [stagenet faucet](https://stagenet-faucet.xmr-tw.org) and paste the address above in the "Get XMR" field. Submit and see the stagenet coins being sent to your Haveno instance. 4. While you wait the 10 confirmations (20 minutes) needed for your funds to be spendable, create a fiat account by clicking on "Account" in the top menu, select the "National currency accounts" tab, then add a new account. For simplicity, we suggest to test using a Revolut account with a random ID. 5. Now pick up an existing offer or open a new one. Fund your trade and wait 10 blocks for your deposit to be unlocked. 6. Now if you are taking a trade you'll be asked to confirm you have sent the payment outside Haveno. Confirm in the app and wait for the confirmation of received payment from the other trader. From 68f7067125fa6decc474194b232dec25703b4cac Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 7 Mar 2025 07:11:49 -0500 Subject: [PATCH 155/371] fix translation to wait for confirmations when deposits published --- core/src/main/resources/i18n/displayStrings.properties | 3 ++- core/src/main/resources/i18n/displayStrings_cs.properties | 3 ++- core/src/main/resources/i18n/displayStrings_de.properties | 3 ++- core/src/main/resources/i18n/displayStrings_es.properties | 3 ++- core/src/main/resources/i18n/displayStrings_fa.properties | 3 ++- core/src/main/resources/i18n/displayStrings_fr.properties | 3 ++- core/src/main/resources/i18n/displayStrings_it.properties | 3 ++- core/src/main/resources/i18n/displayStrings_ja.properties | 3 ++- core/src/main/resources/i18n/displayStrings_pt-br.properties | 3 ++- core/src/main/resources/i18n/displayStrings_pt.properties | 3 ++- core/src/main/resources/i18n/displayStrings_ru.properties | 3 ++- core/src/main/resources/i18n/displayStrings_th.properties | 3 ++- core/src/main/resources/i18n/displayStrings_tr.properties | 3 ++- core/src/main/resources/i18n/displayStrings_vi.properties | 3 ++- core/src/main/resources/i18n/displayStrings_zh-hans.properties | 3 ++- core/src/main/resources/i18n/displayStrings_zh-hant.properties | 3 ++- .../portfolio/pendingtrades/steps/buyer/BuyerStep1View.java | 2 +- .../portfolio/pendingtrades/steps/seller/SellerStep1View.java | 2 +- 18 files changed, 34 insertions(+), 18 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index b499950537..cb6a632270 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -672,7 +672,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" 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 10 confirmations (about 20 minutes) before the payment can start. +portfolio.pending.step1.info.you=Deposit transaction has been published.\nYou need to wait for 10 confirmations (about 20 minutes) before the payment can start. +portfolio.pending.step1.info.buyer=Deposit transaction has been published.\nThe XMR buyer needs to wait for 10 confirmations (about 20 minutes) before the payment can start. portfolio.pending.step1.warn=The deposit transaction is not confirmed yet. This usually takes about 20 minutes, but could be more if the network is congested. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. \ If you have been waiting for much longer than 20 minutes, contact Haveno support. diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 5a2a065535..25b796e403 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -672,7 +672,8 @@ portfolio.pending.autoConf.state.ERROR=Došlo k chybě při požadavku na služb # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Služba se vrátila se selháním. Není možné automatické potvrzení. -portfolio.pending.step1.info=Vkladová transakce byla zveřejněna.\n{0} před zahájením platby musíte počkat na alespoň jedno potvrzení na blockchainu. +portfolio.pending.step1.info.you=Transakce vkladu byla publikována.\nMusíte počkat na 10 potvrzení (přibližně 20 minut), než bude platba zahájena. +portfolio.pending.step1.info.buyer=Transakce vkladu byla publikována.\nKupující XMR musí počkat na 10 potvrzení (asi 20 minut), než bude platba zahájena. portfolio.pending.step1.warn=Vkladová transakce není stále potvrzena. K tomu někdy dochází ve vzácných případech, kdy byl poplatek za financování jednoho obchodníka z externí peněženky příliš nízký. portfolio.pending.step1.openForDispute=Vkladová transakce není stále potvrzena. \ Pokud jste čekali mnohem déle než 20 minut, můžete poádat o pomoc podporu Haveno. diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index c19f183f47..5cf9e07618 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -615,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An einer Service-Abfrage ist ein Fehler a # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Eine Service-Abfrage ist ausgefallen. Eine Automatische Bestätigung ist nicht mehr möglich. -portfolio.pending.step1.info=Die Kautionstransaktion wurde veröffentlicht.\n{0} muss auf wenigstens eine Blockchain-Bestätigung warten, bevor die Zahlung beginnt. +portfolio.pending.step1.info.you=Die Einzahlungstransaktion wurde veröffentlicht.\nSie müssen 10 Bestätigungen abwarten (etwa 20 Minuten), bevor die Zahlung beginnen kann. +portfolio.pending.step1.info.buyer=Die Einzahlungstransaktion wurde veröffentlicht.\nDer XMR-Käufer muss 10 Bestätigungen abwarten (ca. 20 Minuten), bevor die Zahlung gestartet werden kann. portfolio.pending.step1.warn=Die Kautionstransaktion ist noch nicht bestätigt. Dies geschieht manchmal in seltenen Fällen, wenn die Finanzierungsgebühr aus der externen Wallet eines Traders zu niedrig war. portfolio.pending.step1.openForDispute=Die Kautionstransaktion ist noch nicht bestätigt. Sie können länger warten oder den Vermittler um Hilfe bitten. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 5174d8b022..8a6f4fb02a 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -615,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=Ocurrió un error en el servicio solicit # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Un servicio volvió con algún fallo. No es posible la autoconfirmación. -portfolio.pending.step1.info=La transacción de depósito ha sido publicada.\n{0} tiene que esperar al menos una confirmación en la cadena de bloques antes de comenzar el pago. +portfolio.pending.step1.info.you=La transacción de depósito ha sido publicada.\nNecesitas esperar 10 confirmaciones (aproximadamente 20 minutos) antes de que el pago pueda comenzar. +portfolio.pending.step1.info.buyer=La transacción de depósito ha sido publicada.\nEl comprador de XMR necesita esperar 10 confirmaciones (aproximadamente 20 minutos) antes de que el pago pueda comenzar. portfolio.pending.step1.warn=La transacción del depósito aún no se ha confirmado.\nEsto puede suceder en raras ocasiones cuando la tasa de depósito de un comerciante desde una cartera externa es demasiado baja. portfolio.pending.step1.openForDispute=La transacción de depósito aún no ha sido confirmada. Puede esperar más o contactar con el mediador para obtener asistencia. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index d6e00ed380..c8a52bbc51 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -614,7 +614,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=تراکنش سپرده منتشر شده است.\nباید برای حداقل یک تأییدیه بلاک چین قبل از آغاز پرداخت، {0} صبر کنید. +portfolio.pending.step1.info.you=تراکنش واریز منتشر شده است.\nشما باید منتظر 10 تاییدیه (حدود 20 دقیقه) باشید تا پرداخت آغاز شود. +portfolio.pending.step1.info.buyer=تراکنش واریز منتشر شده است.\nخریدار XMR باید منتظر ۱۰ تاییدیه (حدود ۲۰ دقیقه) باشد تا پرداخت آغاز شود. 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. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 2025ed4d66..e6d353b1c7 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -615,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=Une erreur lors de la demande du service # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Un service a retourné un échec. L'auto-confirmation n'est pas possible. -portfolio.pending.step1.info=La transaction de dépôt à été publiée.\n{0} devez attendre au moins une confirmation de la blockchain avant d''initier le paiement. +portfolio.pending.step1.info.you=La transaction de dépôt a été publiée.\nVous devez attendre 10 confirmations (environ 20 minutes) avant que le paiement ne puisse commencer. +portfolio.pending.step1.info.buyer=La transaction de dépôt a été publiée.\nL'acheteur XMR doit attendre 10 confirmations (environ 20 minutes) avant que le paiement puisse commencer. portfolio.pending.step1.warn=La transaction de dépôt n'est toujours pas confirmée. Cela se produit parfois dans de rares occasions lorsque les frais de financement d'un trader en provenance d'un portefeuille externe sont trop bas. portfolio.pending.step1.openForDispute=La transaction de dépôt n'est toujours pas confirmée. Vous pouvez attendre plus longtemps ou contacter le médiateur pour obtenir de l'aide. diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index f482f5b9c7..9c037a7da7 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -614,7 +614,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=La transazione di deposito è stata pubblicata.\n {0} deve attendere almeno una conferma dalla blockchain prima di avviare il pagamento. +portfolio.pending.step1.info.you=La transazione di deposito è stata pubblicata.\nDevi aspettare 10 conferme (circa 20 minuti) prima che il pagamento possa iniziare. +portfolio.pending.step1.info.buyer=La transazione di deposito è stata pubblicata.\nL'acquirente XMR deve aspettare 10 conferme (circa 20 minuti) prima che il pagamento possa iniziare. portfolio.pending.step1.warn=La transazione di deposito non è ancora confermata. Questo accade raramente e nel caso in cui la commissione di transazione di un trader proveniente da un portafoglio esterno è troppo bassa. portfolio.pending.step1.openForDispute=La transazione di deposito non è ancora confermata. Puoi attendere più a lungo o contattare il mediatore per ricevere assistenza. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 83f078a7c3..566ebf68ce 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -615,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=サービスリクエストにはエラ # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=サービスは失敗を返しました。自動確認できません。 -portfolio.pending.step1.info=デポジットトランザクションが発行されました。\n{0}は、支払いを開始する前に少なくとも1つのブロックチェーンの承認を待つ必要があります。 +portfolio.pending.step1.info.you=入金トランザクションが公開されました。\n支払いが開始されるまで、10回の確認(約20分)を待つ必要があります。 +portfolio.pending.step1.info.buyer=入金トランザクションが公開されました。\nXMRの購入者は、支払いを開始する前に10回の確認(約20分)を待つ必要があります。 portfolio.pending.step1.warn=デポジットトランザクションがまだ承認されていません。外部ウォレットからの取引者の資金調達手数料が低すぎるときには、例外的なケースで起こるかもしれません。 portfolio.pending.step1.openForDispute=デポジットトランザクションがまだ承認されていません。もう少し待つか、助けを求めて調停人に連絡できます。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 767e0b8999..5a46618da3 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -617,7 +617,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=A transação de depósito foi publicada\n{0} precisa esperar ao menos uma confirmação da blockchain antes de iniciar o pagamento. +portfolio.pending.step1.info.you=A transação de depósito foi publicada.\nVocê precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa começar. +portfolio.pending.step1.info.buyer=A transação de depósito foi publicada.\nO comprador de XMR precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa ser iniciado. portfolio.pending.step1.warn=A transação do depósito ainda não foi confirmada.\nIsto pode ocorrer em casos raros em que a taxa de financiamento de um dos negociadores enviada a partir de uma carteira externa foi muito baixa. portfolio.pending.step1.openForDispute=A transação de depósito ainda não foi confirmada. Você pode aguardar um pouco mais ou entrar em contato com o mediador para pedir assistência. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index cba08c5769..e5e9f02508 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -614,7 +614,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=A transação de depósito foi publicada.\n{0} precisa aguardar pelo menos uma confirmação da blockchain antes de iniciar o pagamento. +portfolio.pending.step1.info.you=A transação de depósito foi publicada.\nVocê precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa começar. +portfolio.pending.step1.info.buyer=A transação de depósito foi publicada.\nO comprador de XMR precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa ser iniciado. portfolio.pending.step1.warn=A transação de depósito ainda não foi confirmada. Isso pode acontecer em casos raros, quando a taxa de financiamento de um negociador proveniente de uma carteira externa foi muito baixa. portfolio.pending.step1.openForDispute=A transação de depósito ainda não foi confirmada. Você pode esperar mais tempo ou entrar em contato com o mediador para obter assistência. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 956e124eae..eb76a4f78b 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -614,7 +614,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=Депозитная транзакция опубликована.\n{0} должен дождаться хотя бы одного подтверждения в блокчейне перед началом платежа. +portfolio.pending.step1.info.you=Транзакция депозита была опубликована.\nВам нужно дождаться 10 подтверждений (около 20 минут), прежде чем платеж сможет начаться. +portfolio.pending.step1.info.buyer=Транзакция депозита была опубликована.\nПокупатель XMR должен подождать 10 подтверждений (около 20 минут), прежде чем платеж может быть начат. 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. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 5205776fe2..3ba44f304b 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -614,7 +614,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=ธุรกรรมเงินฝากได้รับการเผยแพร่แล้ว\n{0} ต้องรอการยืนยันของบล็อกเชนอย่างน้อยหนึ่งครั้งก่อนที่จะเริ่มการชำระเงิน +portfolio.pending.step1.info.you=ธุรกรรมการฝากเงินได้รับการเผยแพร่แล้ว\nคุณต้องรอ 10 คอนเฟิร์ม (ประมาณ 20 นาที) ก่อนที่การชำระเงินจะเริ่มต้น +portfolio.pending.step1.info.buyer=การทำธุรกรรมฝากเงินได้รับการเผยแพร่แล้ว。\nผู้ซื้อ XMR จำเป็นต้องรอการยืนยัน 10 ครั้ง (ประมาณ 20 นาที) ก่อนที่การชำระเงินจะเริ่มต้นได้ 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. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 5d0720412b..bdc840da6e 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -669,7 +669,8 @@ portfolio.pending.autoConf.state.ERROR=Bir hizmet talebinde hata oluştu. Otomat # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Bir hizmet başarısızlıkla sonuçlandı. Otomatik onay mümkün değil. -portfolio.pending.step1.info=Yatırım işlemi yayımlandı.\n{0} ödemeye başlamadan önce 10 onay (yaklaşık 20 dakika) beklemeniz gerekiyor. +portfolio.pending.step1.info.you=Depozito işlemi yayımlandı.\nÖdemenin başlayabilmesi için 10 onay beklemeniz gerekiyor (yaklaşık 20 dakika). +portfolio.pending.step1.info.buyer=Depozito işlemi yayınlandı.\nXMR alıcısının ödeme işlemine başlanmadan önce 10 onay beklemesi gerekiyor (yaklaşık 20 dakika). portfolio.pending.step1.warn=Yatırım işlemi henüz onaylanmadı. Bu genellikle yaklaşık 20 dakika sürer, ancak ağ yoğunsa daha uzun sürebilir. portfolio.pending.step1.openForDispute=Yatırım işlemi hala onaylanmadı. \ 20 dakikadan çok daha uzun süre beklediyseniz, Haveno desteği ile iletişime geçin. diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 4b6ba937e4..e6bfad14ac 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -614,7 +614,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=Giao dịch đặt cọc đã được công bố.\n{0} Bạn cần đợi ít nhất một xác nhận blockchain trước khi bắt đầu thanh toán. +portfolio.pending.step1.info.you=Giao dịch nạp tiền đã được công bố.\nBạn cần đợi 10 xác nhận (khoảng 20 phút) trước khi thanh toán có thể bắt đầu. +portfolio.pending.step1.info.buyer=Giao dịch gửi tiền đã được công bố.\nNgười mua XMR cần chờ 10 xác nhận (khoảng 20 phút) trước khi thanh toán có thể bắt đầu. 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. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index b9cd9a21c5..96b1de98de 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -615,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=您请求的服务发生了错误。没 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=服务返回失败。没有自动确认。 -portfolio.pending.step1.info=存款交易已经发布。\n开始付款之前,{0} 需要等待至少一个区块链确认。 +portfolio.pending.step1.info.you=存款交易已发布。\n您需要等待 10 次确认(约 20 分钟)后付款才能开始。 +portfolio.pending.step1.info.buyer=存款交易已发布。\nXMR 买家需要等待 10 次确认(大约 20 分钟),然后才能开始付款。 portfolio.pending.step1.warn=保证金交易仍未得到确认。这种情况可能会发生在外部钱包转账时使用的交易手续费用较低造成的。 portfolio.pending.step1.openForDispute=保证金交易仍未得到确认。请联系调解员协助。 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index e5bf5ec76b..39823b2329 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -615,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=您請求的服務發生了錯誤。沒 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=服務返回失敗。沒有自動確認。 -portfolio.pending.step1.info=存款交易已經發布。\n開始付款之前,{0} 需要等待至少一個區塊鏈確認。 +portfolio.pending.step1.info.you=存款交易已發布。\n您需要等待 10 次確認(約 20 分鐘)後,付款才能開始。 +portfolio.pending.step1.info.buyer=存款交易已發佈。\nXMR 購買者需要等待 10 次確認(大約 20 分鐘)才能開始付款。 portfolio.pending.step1.warn=保證金交易仍未得到確認。這種情況可能會發生在外部錢包轉賬時使用的交易手續費用較低造成的。 portfolio.pending.step1.openForDispute=保證金交易仍未得到確認。請聯繫調解員協助。 diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java index 804a6dd741..4888d5b4a8 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java @@ -51,7 +51,7 @@ public class BuyerStep1View extends TradeStepView { @Override protected String getInfoText() { - return Res.get("portfolio.pending.step1.info", Res.get("shared.You")); + return Res.get("portfolio.pending.step1.info.you"); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java index f641ddf3f4..2ca41f1d42 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java @@ -48,7 +48,7 @@ public class SellerStep1View extends TradeStepView { @Override protected String getInfoText() { - return Res.get("portfolio.pending.step1.info", Res.get("shared.TheXMRBuyer")); + return Res.get("portfolio.pending.step1.info.buyer"); } /////////////////////////////////////////////////////////////////////////////////////////// From c9350e123e24a5513dde15dc01959429248c1387 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 7 Mar 2025 07:12:02 -0500 Subject: [PATCH 156/371] fix npe with xmrNodes with onion address --- core/src/main/java/haveno/core/api/XmrLocalNode.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrLocalNode.java b/core/src/main/java/haveno/core/api/XmrLocalNode.java index 583ae8b390..7295202c64 100644 --- a/core/src/main/java/haveno/core/api/XmrLocalNode.java +++ b/core/src/main/java/haveno/core/api/XmrLocalNode.java @@ -112,8 +112,7 @@ public class XmrLocalNode { // determine if local node is configured boolean hasConfiguredLocalNode = false; for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { - String prefix = node.getAddress().startsWith("http") ? "" : "http://"; - if (equalsUri(prefix + node.getAddress() + ":" + node.getPort())) { + if (node.getAddress() != null && equalsUri("http://" + node.getAddress() + ":" + node.getPort())) { hasConfiguredLocalNode = true; break; } @@ -139,7 +138,11 @@ public class XmrLocalNode { } public boolean equalsUri(String uri) { - return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort(); + try { + return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort(); + } catch (Exception e) { + return false; + } } /** From 6b567b94f2c5c146df4314a5da08c542df9257ec Mon Sep 17 00:00:00 2001 From: PromptPunksFauxCough <200402670+PromptPunksFauxCough@users.noreply.github.com> Date: Fri, 7 Mar 2025 12:30:58 +0000 Subject: [PATCH 157/371] Document external tor usage (#1627) --- docs/external-tor-usage.md | 460 +++++++++++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100644 docs/external-tor-usage.md diff --git a/docs/external-tor-usage.md b/docs/external-tor-usage.md new file mode 100644 index 0000000000..0a2a7300c6 --- /dev/null +++ b/docs/external-tor-usage.md @@ -0,0 +1,460 @@ +# **Using External `tor` with `Haveno`** +## *[How to Install little-t-`tor` for Your Platform?](https://support.torproject.org/little-t-tor/#little-t-tor_install-little-t-tor)* + +The following `tor` installation instructions have are presented here for convenience. + +* **For the most complete, up-to-date & authoritative steps, readers are encouraged to refer the [Tor Project's Official Homepage](https://www.torproject.org) linked in the header** + +* **Notes:** + + For optimum compatibility with `Haveno` the running `tor` version should match that of the internal `Haveno` `tor` version + + For best results, use a version of `tor` which supports the [Onion Service Proof of Work](https://onionservices.torproject.org/technology/security/pow) (`PoW`) mechanism + * (IE: `GNU` build of `tor`) + +--- + +* **Note Regarding Admin Access:** + + To install `tor` you need root privileges. Below all commands that need to be run as `root` user like `apt` and `dpkg` are prepended with `#`, while commands to be run as user with `$` resembling the standard prompt in a terminal. + +### macOS +#### Install a Package Manager + Two of the most popular package managers for `macOS` are: + + [`Homebrew`](https://brew.sh) + + and + + [`Macports`](https://www.macports.org) + + (You can use the package manager of your choice) + + + Install [`Homebrew`](https://brew.sh) + + Follow the instructions on [brew.sh](https://brew.sh) + + + Install [`Macports`](https://www.macports.org) + + Follow the instructions on [macports.org](https://www.macports.org) + +#### Package Installation +##### [`Homebrew`](https://brew.sh) + ```shell + # brew update && brew install tor + ``` + +##### [`Macports`](https://www.macports.org) + ```shell + # port sync && port install tor + ``` + +### Debian / Ubuntu +* *Do **not** use the packages in Ubuntu's universe. In the past they have not reliably been updated. That means you could be missing stability and security fixes.* + +* Configure the [Official `Tor` Package Repository](https://deb.torproject.org/torproject.org) + + Enable the [Official `Tor` Package Repository](https://deb.torproject.org/torproject.org) following these [instructions](https://support.torproject.org/apt/tor-deb-repo/) + +#### Package Installation +```shell +# apt update && apt install tor +``` + +### Fedora + * Configure the [Official `Tor` Package Repository](https://rpm.torproject.org/fedora) + + Enable the [Official `Tor` Package Repository](https://rpm.torproject.org/fedora) by following these [instructions](https://support.torproject.org/rpm/tor-rpm-install) + +#### Package Installation +``` +# dnf update && dnf install tor +``` + +### Arch Linux +#### Package Installation +```shell +# pacman -Fy && pacman -Syu tor +``` + +### Installing `tor` from source +#### Download Latest Release & Dependencies +The latest release of `tor` can be found on the [download](https://www.torproject.org/download/tor) page + +* When building from source: + + *First* install `libevent`,`openssl` & `zlib` + + *(Including the -devel packages when applicable)* + +#### Install `tor` +```shell +$ tar -xzf tor-.tar.gz; cd tor- +``` + +* Replace \ with the latest version of `tor` + + > For example, `tor-0.4.8.14` + +```shell +$ ./configure && make +``` + +* Now you can run `tor` (0.4.3.x and Later) locally like this: + +```shell +$ ./src/app/tor +``` + +Or, you can run `make install` (as `root` if necessary) to install it globally into `/usr/local/` + +* Now you can run `tor` directly without absolute path like this: + +```shell +$ tor +``` + +### Windows +#### Download +* Download the `Windows Expert Bundle` from the [Official `Tor` Project's Download page](https://www.torproject.org/download/tor) + +#### Extract +* Extract Archive to Disk + +#### Open Terminal +* Open PowerShell with Admin Privileges + +#### Change to Location of Extracted Archive +* Navigate to `Tor` Directory + +#### Package Installation +* v10 +```powershell +PS C:\Tor\> tor.exe –-service install +``` + +* v11 +```powershell +PS C:\Tor\> tor.exe –-service install +``` + +#### Create Service +```powershell +PS C:\Tor\> sc create tor start=auto binPath="\Tor\tor.exe -nt-service" +``` + +#### Start Service +```powershell +PS C:\Tor\> sc start tor +``` + +### ***Configuring `tor`via `torrc`*** +#### [I'm supposed to "edit my torrc". What does that mean?](https://support.torproject.org/tbb/tbb-editing-torrc/) +* Per the [Official Tor Project's support page](https://support.torproject.org/tbb/tbb-editing-torrc/): + * **WARNING:** Do **NOT** follow random advice instructing you to edit your torrc! Doing so can allow an attacker to compromise your security and anonymity through malicious configuration of your torrc. + + **Note:** + + The `torrc` location will ***not*** match those stated in the documentation linked above and will vary across each platform. + +#### [Sample `torrc`](https://gitlab.torproject.org/tpo/core/tor/-/blob/HEAD/src/config/torrc.sample.in) +Users are ***strongly*** encouraged to review both the [Official Tor Project's support page](https://support.torproject.org/tbb/tbb-editing-torrc/) as well as the [sample `torrc`](https://gitlab.torproject.org/tpo/core/tor/-/blob/HEAD/src/config/torrc.sample.in) before proceeding. + +#### Enable `torControlPort` in `torrc` +In order for `Haveno` to use the `--torControlPort` option, it must be enabled and accessible. The most common way to do so is to edit the `torrc` fiel with a text editor to ensure that an entry for `ControlPort` followed by port number to listen on is present in the `torrc` file. + +#### [Authentication](https://spec.torproject.org/control-spec/implementation-notes.html#authentication) +Per the [Tor Control Protocol - Implementation Notes](https://spec.torproject.org/control-spec/implementation-notes.html): + + * ***"If the control port is open and no authentication operation is enabled, `tor` trusts any local user that connects to the control port. This is generally a poor idea."*** + +##### `CookieAuthentication` +If the `CookieAuthentication` option is true, `tor` writes a *"magic cookie"* file named `control_auth_cookie` into its data directory (or to another file specified in the `CookieAuthFile` option). + +##### Example: +```shell +ControlPort 9051 +CookieAuthentication 1 +``` + +##### `HashedControlPassword` +If the `HashedControlPassword` option is set, it must contain the salted hash of a secret password. The salted hash is computed according to the S2K algorithm in `RFC 2440` of `OpenPGP`, and prefixed with the s2k specifier. This is then encoded in hexadecimal, prefixed by the indicator sequence "16:". + +* `HashedControlPassword` can be generated like so: + ```shell + $ tor --hash-password + ``` + +###### Example: +```shell +ControlPort 9051 +HashedControlPassword 16:C01147DC5F4DA2346056668DD23522558D0E0C8B5CC88FE72EEBC51967 +``` + +##### Restart `tor` +`tor` must be restarted for changes to `torrc` to be applied. + +### \* ***Optional*** \* +#### [Set Up Your Onion Service](https://community.torproject.org/onion-services/setup) + +While not a *strict* requirement for use with `Haveno`, some users may wish to configure an [Onion Service](https://community.torproject.org/onion-services) + + * ***Only Required When Using The `Haveno` `--hiddenServiceAddress` Option*** + +Please see the [Official `Tor` Project's Documentation](https://community.torproject.org/onion-services/setup) for more information about configuration and usage of these services + +--- + +## *`Haveno`'s `tor` Aware Options* + +`Haveno` is a natively `tor` aware application and offers **many** flexible configuration options for use by privacy conscious users. + +While some are mutually exclusive, many are cross-applicable. + +Users are encouraged to experiment with options before use to determine which options best fit their personal threat profile. + +### Options +#### `--hiddenServiceAddress` +* Function: + + This option configures a *static* Hidden Service Address to listen on + +* Expected Input Format: + + `` + + (`ed25519`) + +* Acceptable Values + + `` + +* Default value: + + `null` + +#### `--socks5ProxyXmrAddress` +* Function: + + A proxy address to be used for `monero` network + +* Expected Input Format: + + `` + +* Acceptable Values + + `` + +* Default value: + + `null` + +#### `--torrcFile` +* Function: + + An existing `torrc`-file to be sourced for `tor` + + **Note:** + + `torrc`-entries which are critical to `Haveno`'s flawless operation (`torrc` options line, `torrc` option, ...) **can not** be overwritten + +* Expected Input Format: + + `` + +* Acceptable Values + + `` + +* Default value: + + `null` + +#### `--torrcOptions` +* Function: + + A list of `torrc`-entries to amend to `Haveno`'s `torrc` + + **Note:** + + *`torrc`-entries which are critical to `Haveno`'s flawless operation (`torrc` options line, `torrc` option, ...) can **not** be overwritten* + +* Expected Input Format: + + `` + +* Acceptable Values + + `<^([^\s,]+\s[^,]+,?\s*)+$>` + +* Default value: + + `null` + +#### `--torControlHost` ++ Function + + The control `hostname` or `IP` of an already running `tor` service to be used by `Haveno` + +* Expected Input Format + + `` + + (`hostname`, `IPv4` or `IPv6`) + +* Acceptable Values + + `` + +* Default Value + + `null` + +#### `--torControlPort` ++ Function + + The control port of an already running `tor` service to be used by `Haveno` + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `-1` + +#### `--torControlPassword` ++ Function + + The password for controlling the already running `tor` service + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `null` + +#### `--torControlCookieFile` ++ Function + + The cookie file for authenticating against the already running `tor` service + * Used in conjunction with `--torControlUseSafeCookieAuth` option + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `null` + +#### `--torControlUseSafeCookieAuth` ++ Function + + Use the `SafeCookie` method when authenticating to the already running `tor` service + +* Expected Input Format + + `null` + +* Acceptable Values + + `none` + +* Default Value + + `off` + +#### `--torStreamIsolation` ++ Function + + Use stream isolation for Tor + * This option is currently considered ***experimental*** + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `off` + +#### `--useTorForXmr` ++ Function + + Configure `tor` for `monero` connections with ***either***: + + * after_sync + + **or** + + * off + + **or** + + * on + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `AFTER_SYNC` + +#### `--socks5DiscoverMode` ++ Function + + Specify discovery mode for `monero` nodes + +* Expected Input Format + + `` + +* Acceptable Values + + `ADDR, DNS, ONION, ALL` + + One or more comma separated. + + *(Will be **OR**'d together)* + +* Default Value + + `ALL` + +--- + +## *Starting `Haveno` Using Externally Available `tor`* +### Dynamic Onion Assignment via `--torControlPort` +```shell +$ /opt/haveno/bin/Haveno --torControlPort='9051' --torControlCookieFile='/var/run/tor/control.authcookie' --torControlUseSafeCookieAuth --useTorForXmr='on' --socks5ProxyXmrAddress='127.0.0.1:9050' +``` + +### Static Onion Assignment via `--hiddenServiceAddress` +```shell +$ /opt/haveno/bin/Haveno --socks5ProxyXmrAddress='127.0.0.1:9050' --useTorForXmr='on' --hiddenServiceAddress='2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion' +``` From a53026be8ac238c74ddbcfc1ba7a6595b72fa6bc Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 7 Mar 2025 07:45:17 -0500 Subject: [PATCH 158/371] cleanup external tor docs --- docs/external-tor-usage.md | 46 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/external-tor-usage.md b/docs/external-tor-usage.md index 0a2a7300c6..ace4a2817a 100644 --- a/docs/external-tor-usage.md +++ b/docs/external-tor-usage.md @@ -1,13 +1,13 @@ -# **Using External `tor` with `Haveno`** -## *[How to Install little-t-`tor` for Your Platform?](https://support.torproject.org/little-t-tor/#little-t-tor_install-little-t-tor)* +# **Using External `tor` with Haveno** +## [How to Install little-t-`tor` for Your Platform](https://support.torproject.org/little-t-tor/#little-t-tor_install-little-t-tor) -The following `tor` installation instructions have are presented here for convenience. +The following `tor` installation instructions are presented here for convenience. -* **For the most complete, up-to-date & authoritative steps, readers are encouraged to refer the [Tor Project's Official Homepage](https://www.torproject.org) linked in the header** +* **For the most complete, up-to-date & authoritative steps, readers are encouraged to refer to the [Tor Project's Official Homepage](https://www.torproject.org) linked in the header** * **Notes:** - For optimum compatibility with `Haveno` the running `tor` version should match that of the internal `Haveno` `tor` version + For optimum compatibility with Haveno the running `tor` version should match that of the internal Haveno `tor` version For best results, use a version of `tor` which supports the [Onion Service Proof of Work](https://onionservices.torproject.org/technology/security/pow) (`PoW`) mechanism * (IE: `GNU` build of `tor`) @@ -52,9 +52,9 @@ The following `tor` installation instructions have are presented here for conven ### Debian / Ubuntu * *Do **not** use the packages in Ubuntu's universe. In the past they have not reliably been updated. That means you could be missing stability and security fixes.* -* Configure the [Official `Tor` Package Repository](https://deb.torproject.org/torproject.org) +* Configure the [Official Tor Package Repository](https://deb.torproject.org/torproject.org) - Enable the [Official `Tor` Package Repository](https://deb.torproject.org/torproject.org) following these [instructions](https://support.torproject.org/apt/tor-deb-repo/) + Enable the [Official Tor Package Repository](https://deb.torproject.org/torproject.org) following these [instructions](https://support.torproject.org/apt/tor-deb-repo/) #### Package Installation ```shell @@ -62,9 +62,9 @@ The following `tor` installation instructions have are presented here for conven ``` ### Fedora - * Configure the [Official `Tor` Package Repository](https://rpm.torproject.org/fedora) + * Configure the [Official Tor Package Repository](https://rpm.torproject.org/fedora) - Enable the [Official `Tor` Package Repository](https://rpm.torproject.org/fedora) by following these [instructions](https://support.torproject.org/rpm/tor-rpm-install) + Enable the [Official Tor Package Repository](https://rpm.torproject.org/fedora) by following these [instructions](https://support.torproject.org/rpm/tor-rpm-install) #### Package Installation ``` @@ -116,7 +116,7 @@ $ tor ### Windows #### Download -* Download the `Windows Expert Bundle` from the [Official `Tor` Project's Download page](https://www.torproject.org/download/tor) +* Download the `Windows Expert Bundle` from the [Official Tor Project's Download page](https://www.torproject.org/download/tor) #### Extract * Extract Archive to Disk @@ -148,7 +148,7 @@ PS C:\Tor\> sc create tor start=auto binPath="\Tor\tor.exe -nt-service" PS C:\Tor\> sc start tor ``` -### ***Configuring `tor`via `torrc`*** +## Configuring `tor` via `torrc` #### [I'm supposed to "edit my torrc". What does that mean?](https://support.torproject.org/tbb/tbb-editing-torrc/) * Per the [Official Tor Project's support page](https://support.torproject.org/tbb/tbb-editing-torrc/): * **WARNING:** Do **NOT** follow random advice instructing you to edit your torrc! Doing so can allow an attacker to compromise your security and anonymity through malicious configuration of your torrc. @@ -161,7 +161,7 @@ PS C:\Tor\> sc start tor Users are ***strongly*** encouraged to review both the [Official Tor Project's support page](https://support.torproject.org/tbb/tbb-editing-torrc/) as well as the [sample `torrc`](https://gitlab.torproject.org/tpo/core/tor/-/blob/HEAD/src/config/torrc.sample.in) before proceeding. #### Enable `torControlPort` in `torrc` -In order for `Haveno` to use the `--torControlPort` option, it must be enabled and accessible. The most common way to do so is to edit the `torrc` fiel with a text editor to ensure that an entry for `ControlPort` followed by port number to listen on is present in the `torrc` file. +In order for Haveno to use the `--torControlPort` option, it must be enabled and accessible. The most common way to do so is to edit the `torrc` fiel with a text editor to ensure that an entry for `ControlPort` followed by port number to listen on is present in the `torrc` file. #### [Authentication](https://spec.torproject.org/control-spec/implementation-notes.html#authentication) Per the [Tor Control Protocol - Implementation Notes](https://spec.torproject.org/control-spec/implementation-notes.html): @@ -197,17 +197,17 @@ HashedControlPassword 16:C01147DC5F4DA2346056668DD23522558D0E0C8B5CC88FE72EEBC51 ### \* ***Optional*** \* #### [Set Up Your Onion Service](https://community.torproject.org/onion-services/setup) -While not a *strict* requirement for use with `Haveno`, some users may wish to configure an [Onion Service](https://community.torproject.org/onion-services) +While not a *strict* requirement for use with Haveno, some users may wish to configure an [Onion Service](https://community.torproject.org/onion-services) - * ***Only Required When Using The `Haveno` `--hiddenServiceAddress` Option*** + * ***Only Required When Using The Haveno `--hiddenServiceAddress` Option*** -Please see the [Official `Tor` Project's Documentation](https://community.torproject.org/onion-services/setup) for more information about configuration and usage of these services +Please see the [Official Tor Project's Documentation](https://community.torproject.org/onion-services/setup) for more information about configuration and usage of these services --- -## *`Haveno`'s `tor` Aware Options* +## Haveno's `tor` Aware Options -`Haveno` is a natively `tor` aware application and offers **many** flexible configuration options for use by privacy conscious users. +Haveno is a natively `tor` aware application and offers **many** flexible configuration options for use by privacy conscious users. While some are mutually exclusive, many are cross-applicable. @@ -257,7 +257,7 @@ Users are encouraged to experiment with options before use to determine which op **Note:** - `torrc`-entries which are critical to `Haveno`'s flawless operation (`torrc` options line, `torrc` option, ...) **can not** be overwritten + `torrc`-entries which are critical to Haveno's flawless operation (`torrc` options line, `torrc` option, ...) **can not** be overwritten * Expected Input Format: @@ -274,11 +274,11 @@ Users are encouraged to experiment with options before use to determine which op #### `--torrcOptions` * Function: - A list of `torrc`-entries to amend to `Haveno`'s `torrc` + A list of `torrc`-entries to amend to Haveno's `torrc` **Note:** - *`torrc`-entries which are critical to `Haveno`'s flawless operation (`torrc` options line, `torrc` option, ...) can **not** be overwritten* + *`torrc`-entries which are critical to Haveno's flawless operation (`torrc` options line, `torrc` option, ...) can **not** be overwritten* * Expected Input Format: @@ -295,7 +295,7 @@ Users are encouraged to experiment with options before use to determine which op #### `--torControlHost` + Function - The control `hostname` or `IP` of an already running `tor` service to be used by `Haveno` + The control `hostname` or `IP` of an already running `tor` service to be used by Haveno * Expected Input Format @@ -314,7 +314,7 @@ Users are encouraged to experiment with options before use to determine which op #### `--torControlPort` + Function - The control port of an already running `tor` service to be used by `Haveno` + The control port of an already running `tor` service to be used by Haveno * Expected Input Format @@ -448,7 +448,7 @@ Users are encouraged to experiment with options before use to determine which op --- -## *Starting `Haveno` Using Externally Available `tor`* +## Starting Haveno Using Externally Available `tor` ### Dynamic Onion Assignment via `--torControlPort` ```shell $ /opt/haveno/bin/Haveno --torControlPort='9051' --torControlCookieFile='/var/run/tor/control.authcookie' --torControlUseSafeCookieAuth --useTorForXmr='on' --socks5ProxyXmrAddress='127.0.0.1:9050' From 61a62a1d942babbdb57da9eedc966376a29b0fdd Mon Sep 17 00:00:00 2001 From: boldsuck Date: Sun, 9 Mar 2025 01:42:53 +0100 Subject: [PATCH 159/371] Update tor-upgrade.md docu (#1645) --- docs/tor-upgrade.md | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/docs/tor-upgrade.md b/docs/tor-upgrade.md index 990cc5d161..cf7548207f 100644 --- a/docs/tor-upgrade.md +++ b/docs/tor-upgrade.md @@ -10,7 +10,7 @@ As per the project's authors, `netlayer` is _"essentially a wrapper around the o easy use and convenient integration into Kotlin/Java projects"_. Similarly, `tor-binary` is _"[the] Tor binary packaged in a way that can be used for java projects"_. The project -unpacks the tor browser binaries to extract and repackage the tor binaries themselves. +unpacks the Tor Browser binaries to extract and repackage the tor binaries themselves. Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts. @@ -22,8 +22,8 @@ Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts. - Find out which tor version Haveno currently uses - Find out the current `netlayer` version (see `netlayerVersion` in `haveno/build.gradle`) - - Find that release on the project's [releases page][3] - - The release description says which tor version it includes + - Find that tag on the project's [Tags page][3] + - The tag description says which tor version it includes - Find out the latest available tor release - See the [official tor changelog][4] @@ -32,23 +32,24 @@ Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts. During this update, you will need to keep track of: - - the new tor browser version + - the new Tor Browser version - the new tor binary version Create a PR for the `master` branch of [tor-binary][2] with the following changes: - - Decide which tor browser version contains the desired tor binary version - - The official tor browser releases are here: https://dist.torproject.org/torbrowser/ - - For the chosen tor browser version, get the list of SHA256 checksums and its signature - - For example, for tor browser 10.0.12: - - https://dist.torproject.org/torbrowser/10.0.12/sha256sums-signed-build.txt - - https://dist.torproject.org/torbrowser/10.0.12/sha256sums-signed-build.txt.asc + - Decide which Tor Browser version contains the desired tor binary version + - The latest official Tor Browser releases are here: https://dist.torproject.org/torbrowser/ + - All official Tor Browser releases are here: https://archive.torproject.org/tor-package-archive/torbrowser/ + - For the chosen Tor Browser version, get the list of SHA256 checksums and its signature + - For example, for Tor Browser 14.0.7: + - https://dist.torproject.org/torbrowser/14.0.7/sha256sums-signed-build.txt + - https://dist.torproject.org/torbrowser/14.0.7/sha256sums-signed-build.txt.asc - Verify the signature of the checksums list (see [instructions][5]) - Update the `tor-binary` checksums - For each file present in `tor-binary/tor-binary-resources/checksums`: - - Rename the file such that it reflects the new tor browser version, but preserves the naming scheme + - Rename the file such that it reflects the new Tor Browser version, but preserves the naming scheme - Update the contents of the file with the corresponding SHA256 checksum from the list - - Update `torbrowser.version` to the new tor browser version in: + - Update `torbrowser.version` to the new Tor Browser version in: - `tor-binary/build.xml` - `tor-binary/pom.xml` - Update `version` to the new tor binary version in: @@ -72,7 +73,7 @@ next. ### 3. Update `netlayer` -Create a PR for the `externaltor` branch of [netlayer][1] with the following changes: +Create a PR for the `master` branch of [netlayer][1] with the following changes: - In `netlayer/pom.xml`: - Update `tor-binary.version` to the `tor-binary` commit ID from above (e.g. `a4b868a`) @@ -82,13 +83,13 @@ Create a PR for the `externaltor` branch of [netlayer][1] with the following cha - `netlayer/tor.external/pom.xml` - `netlayer/tor.native/pom.xml` -Once the PR is merged, make a note of the commit ID in the `externaltor` branch (for example `32779ac`), as it will be +Once the PR is merged, make a note of the commit ID in the `master` branch (for example `32779ac`), as it will be needed next. Create a tag for the new artefact version, having the new tor binary version as description, for example: ``` -# Create tag locally for new netlayer release, on the externaltor branch +# Create tag locally for new netlayer release, on the master branch git tag -s 0.7.0 -m"tor 0.4.5.6" # Push it to netlayer repo @@ -105,8 +106,6 @@ Create a Haveno PR with the following changes: - See instructions in `haveno/gradle/witness/gradle-witness.gradle` - - ## Credits Thanks to freimair, JesusMcCloud, mrosseel, sschuberth and cedricwalter for their work on the original @@ -115,8 +114,8 @@ Thanks to freimair, JesusMcCloud, mrosseel, sschuberth and cedricwalter for thei -[1]: https://github.com/bisq-network/netlayer "netlayer" -[2]: https://github.com/bisq-network/tor-binary "tor-binary" -[3]: https://github.com/bisq-network/netlayer/releases "netlayer releases" +[1]: https://github.com/haveno-dex/netlayer "netlayer" +[2]: https://github.com/haveno-dex/tor-binary "tor-binary" +[3]: https://github.com/haveno-dex/netlayer/tags "netlayer Tags" [4]: https://gitweb.torproject.org/tor.git/plain/ChangeLog "tor changelog" [5]: https://support.torproject.org/tbb/how-to-verify-signature/ "verify tor signature" From 03a1132c2f9e7a11a7af2a119e22e7976c00ddc3 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 8 Mar 2025 06:23:55 -0500 Subject: [PATCH 160/371] copy monero payment uri to clipboard in qr code window --- .../java/haveno/desktop/main/overlays/Overlay.java | 9 ++++++--- .../desktop/main/overlays/windows/QRCodeWindow.java | 10 +++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java b/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java index 4d0eb57fea..e216b14ed9 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java @@ -765,9 +765,7 @@ public abstract class Overlay> { FormBuilder.getIconForLabel(AwesomeIcon.COPY, copyIcon, "1.1em"); copyIcon.addEventHandler(MOUSE_CLICKED, mouseEvent -> { if (message != null) { - String forClipboard = headLineLabel.getText() + System.lineSeparator() + message - + System.lineSeparator() + (messageHyperlinks == null ? "" : messageHyperlinks.toString()); - Utilities.copyToClipboard(forClipboard); + Utilities.copyToClipboard(getClipboardText()); Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); Node node = (Node) mouseEvent.getSource(); UserThread.runAfter(() -> tp.hide(), 1); @@ -1083,6 +1081,11 @@ public abstract class Overlay> { return isDisplayed; } + public String getClipboardText() { + return headLineLabel.getText() + System.lineSeparator() + message + + System.lineSeparator() + (messageHyperlinks == null ? "" : messageHyperlinks.toString()); + } + @Override public String toString() { return "Popup{" + diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java index 223933e5c1..8bca62d143 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java @@ -37,10 +37,10 @@ import java.io.ByteArrayInputStream; public class QRCodeWindow extends Overlay { private static final Logger log = LoggerFactory.getLogger(QRCodeWindow.class); private final ImageView qrCodeImageView; - private final String bitcoinURI; + private final String moneroUri; public QRCodeWindow(String bitcoinURI) { - this.bitcoinURI = bitcoinURI; + this.moneroUri = bitcoinURI; final byte[] imageBytes = QRCode .from(bitcoinURI) .withSize(300, 300) @@ -70,7 +70,7 @@ public class QRCodeWindow extends Overlay { GridPane.setHalignment(qrCodeImageView, HPos.CENTER); gridPane.getChildren().add(qrCodeImageView); - String request = bitcoinURI.replace("%20", " ").replace("?", "\n?").replace("&", "\n&"); + String request = moneroUri.replace("%20", " ").replace("?", "\n?").replace("&", "\n&"); Label infoLabel = new AutoTooltipLabel(Res.get("qRCodeWindow.request", request)); infoLabel.setMouseTransparent(true); infoLabel.setWrapText(true); @@ -87,4 +87,8 @@ public class QRCodeWindow extends Overlay { applyStyles(); display(); } + + public String getClipboardText() { + return moneroUri; + } } From e5f729d12f8389a50a0a3b6113ec23f56ac3ed44 Mon Sep 17 00:00:00 2001 From: boldsuck Date: Sun, 9 Mar 2025 19:32:32 +0100 Subject: [PATCH 161/371] Update Tor Browser version: 14.0.7 and tor binary version: 0.4.8.14 (#1650) --- build.gradle | 2 +- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/build.gradle b/build.gradle index 74e776cb2b..e083e8ec08 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ configure(subprojects) { loggingVersion = '1.2' lombokVersion = '1.18.30' mockitoVersion = '5.10.0' - netlayerVersion = '700ec94f0f' // Tor browser version 14.0.3 and tor binary version: 0.4.8.13 + netlayerVersion = 'd4f9d0ce24' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14 protobufVersion = '3.19.1' protocVersion = protobufVersion pushyVersion = '0.13.2' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 069e08177b..cbfb839d9f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -205,44 +205,44 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From c853c4ffcb0579625f630ef83f0af4c1957e8e97 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 9 Mar 2025 16:54:28 -0400 Subject: [PATCH 162/371] bump version to 1.0.19 --- build.gradle | 2 +- common/src/main/java/haveno/common/app/Version.java | 2 +- desktop/package/linux/exchange.haveno.Haveno.metainfo.xml | 2 +- desktop/package/macosx/Info.plist | 4 ++-- docs/deployment-guide.md | 2 +- seednode/src/main/java/haveno/seednode/SeedNodeMain.java | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index e083e8ec08..3c3d95d074 100644 --- a/build.gradle +++ b/build.gradle @@ -610,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.18-SNAPSHOT' + version = '1.0.19-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index d39016dc31..74f3ceb9d6 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.0.18"; + public static final String VERSION = "1.0.19"; /** * Holds a list of the tagged resource files for optimizing the getData requests. diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index a298688669..fc5f50c2b5 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -60,6 +60,6 @@ - + diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index 7693e124b7..a24f430c12 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.18 + 1.0.19 CFBundleShortVersionString - 1.0.18 + 1.0.19 CFBundleExecutable Haveno diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index 42f89b115b..67b5236d33 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -270,7 +270,7 @@ Then follow these instructions: https://github.com/haveno-dex/haveno/blob/master Set the mandatory minimum version for trading (optional) -If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.18) in the field labeled "Min. version required for trading". +If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.19) in the field labeled "Min. version required for trading". Send update alert diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index 281e138c0b..35d4bbbe17 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.0.18"; + private static final String VERSION = "1.0.19"; private SeedNode seedNode; private Timer checkConnectionLossTime; From 9acd7ad584c62c4123bc8f47ccf7ca7e32ce734f Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 9 Mar 2025 17:02:15 -0400 Subject: [PATCH 163/371] rename config handler from btc to xmr --- core/src/main/java/haveno/core/app/HavenoHeadlessApp.java | 2 +- core/src/main/java/haveno/core/app/HavenoSetup.java | 4 ++-- core/src/main/java/haveno/core/app/WalletAppSetup.java | 6 +++--- .../src/main/java/haveno/desktop/main/MainViewModel.java | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java index 0cf18224ba..fc6eb2d75c 100644 --- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java @@ -86,7 +86,7 @@ public class HavenoHeadlessApp implements HeadlessApp { havenoSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler")); havenoSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg)); havenoSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage)); - havenoSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler")); + havenoSetup.setShowPopupIfInvalidXmrConfigHandler(() -> log.error("onShowPopupIfInvalidXmrConfigHandler")); havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList)); havenoSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler")); havenoSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler")); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index eee13f3eec..6637511298 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -176,7 +176,7 @@ public class HavenoSetup { private Consumer displayPrivateNotificationHandler; @Setter @Nullable - private Runnable showPopupIfInvalidBtcConfigHandler; + private Runnable showPopupIfInvalidXmrConfigHandler; @Setter @Nullable private Consumer> revolutAccountsUpdateHandler; @@ -461,7 +461,7 @@ public class HavenoSetup { havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet); walletAppSetup.init(chainFileLockedExceptionHandler, showFirstPopupIfResyncSPVRequestedHandler, - showPopupIfInvalidBtcConfigHandler, + showPopupIfInvalidXmrConfigHandler, () -> {}, () -> {}); } diff --git a/core/src/main/java/haveno/core/app/WalletAppSetup.java b/core/src/main/java/haveno/core/app/WalletAppSetup.java index d17c7ba366..7d37372afd 100644 --- a/core/src/main/java/haveno/core/app/WalletAppSetup.java +++ b/core/src/main/java/haveno/core/app/WalletAppSetup.java @@ -117,7 +117,7 @@ public class WalletAppSetup { void init(@Nullable Consumer chainFileLockedExceptionHandler, @Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler, - @Nullable Runnable showPopupIfInvalidBtcConfigHandler, + @Nullable Runnable showPopupIfInvalidXmrConfigHandler, Runnable downloadCompleteHandler, Runnable walletInitializedHandler) { log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion()); @@ -199,8 +199,8 @@ public class WalletAppSetup { walletInitializedHandler.run(); }, exception -> { - if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) { - showPopupIfInvalidBtcConfigHandler.run(); + if (exception instanceof InvalidHostException && showPopupIfInvalidXmrConfigHandler != null) { + showPopupIfInvalidXmrConfigHandler.run(); } else { walletServiceException.set(exception); } diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index f120b794e7..670543a7aa 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -420,7 +420,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener havenoSetup.setRejectedTxErrorMessageHandler(msg -> new Popup().width(850).warning(msg).show()); - havenoSetup.setShowPopupIfInvalidBtcConfigHandler(this::showPopupIfInvalidBtcConfig); + havenoSetup.setShowPopupIfInvalidXmrConfigHandler(this::showPopupIfInvalidXmrConfig); havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> { // We copy the array as we will mutate it later @@ -536,7 +536,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener }); } - private void showPopupIfInvalidBtcConfig() { + private void showPopupIfInvalidXmrConfig() { preferences.setMoneroNodesOptionOrdinal(0); new Popup().warning(Res.get("settings.net.warn.invalidXmrConfig")) .hideCloseButton() From 2d46b2ab7c72db58a1ca8be8a3ce288d5a961d06 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:16:23 -0500 Subject: [PATCH 164/371] log warning on error taking offer from ui --- .../haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java index f6b947954d..1a548f1b1f 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -284,6 +284,7 @@ class TakeOfferDataModel extends OfferDataModel { // handle error if (errorMsg != null) { new Popup().warning(errorMsg).show(); + log.warn("Error taking offer " + offer.getId() + ": " + errorMsg); errorMessageHandler.handleErrorMessage(errorMsg); } } From 8b1d2aa203d4a680fe69ce502955232f9947ebcd Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:53:18 -0500 Subject: [PATCH 165/371] fix bug to delete scheduled failed trade after restart --- core/src/main/java/haveno/core/trade/Trade.java | 13 +++++++++---- .../main/java/haveno/core/trade/TradeManager.java | 8 ++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 6ad8e7aef3..ed4a8ce8b0 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1618,15 +1618,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // done if wallet already deleted if (!walletExists()) return; - // move to failed trades - processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this); - // set error height if (processModel.getTradeProtocolErrorHeight() == 0) { log.warn("Scheduling to remove trade if unfunded for {} {} from height {}", getClass().getSimpleName(), getId(), xmrConnectionService.getLastInfo().getHeight()); - processModel.setTradeProtocolErrorHeight(xmrConnectionService.getLastInfo().getHeight()); + processModel.setTradeProtocolErrorHeight(xmrConnectionService.getLastInfo().getHeight()); // height denotes scheduled error handling } + // move to failed trades + processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this); + requestPersistence(); + // listen for deposits published to restore trade protocolErrorStateSubscription = EasyBind.subscribe(stateProperty(), state -> { if (isDepositsPublished()) { @@ -1680,6 +1681,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { }); } + public boolean isProtocolErrorHandlingScheduled() { + return processModel.getTradeProtocolErrorHeight() > 0; + } + private void restoreDepositsPublishedTrade() { // close open offer diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index caabdb384a..8e5b5d9dd8 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -450,8 +450,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } - // skip if marked as failed - if (failedTradesManager.getObservableList().contains(trade)) { + // skip if failed and error handling not scheduled + if (failedTradesManager.getObservableList().contains(trade) && !trade.isProtocolErrorHandlingScheduled()) { log.warn("Skipping initialization of failed trade {} {}", trade.getClass().getSimpleName(), trade.getId()); tradesToSkip.add(trade); return; @@ -460,8 +460,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // initialize trade initPersistedTrade(trade); - // remove trade if protocol didn't initialize - if (getOpenTradeByUid(trade.getUid()).isPresent() && !trade.isDepositsPublished()) { + // record if protocol didn't initialize + if (!trade.isDepositsPublished()) { uninitializedTrades.add(trade); } } catch (Exception e) { From bf97fbc7eacd344567bb3302633de67de6658f21 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 7 Mar 2025 09:43:29 -0500 Subject: [PATCH 166/371] skip reset address entries when failed trade is scheduled for deletion --- core/src/main/java/haveno/core/trade/TradeManager.java | 8 +++++++- .../java/haveno/core/xmr/wallet/XmrWalletService.java | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 8e5b5d9dd8..41135e62f1 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -923,8 +923,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi requestPersistence(); }, errorMessage -> { log.warn("Taker error during trade initialization: " + errorMessage); - xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move to maybe remove on error trade.onProtocolError(); + xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move this into protocol error handling errorMessageHandler.handleErrorMessage(errorMessage); }); @@ -1285,6 +1285,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } + public boolean hasFailedScheduledTrade(String offerId) { + synchronized (failedTradesManager) { + return failedTradesManager.getTradeById(offerId).isPresent() && failedTradesManager.getTradeById(offerId).get().isProtocolErrorHandlingScheduled(); + } + } + public Optional getOpenTradeByUid(String tradeUid) { synchronized (tradableList) { return tradableList.stream().filter(e -> e.getUid().equals(tradeUid)).findFirst(); diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 045897ed37..23e4f74550 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1016,6 +1016,13 @@ public class XmrWalletService extends XmrWalletBase { public synchronized void resetAddressEntriesForOpenOffer(String offerId) { log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); + + // skip if failed trade is scheduled for processing // TODO: do not call this function in this case? + if (tradeManager.hasFailedScheduledTrade(offerId)) { + log.warn("Refusing to reset address entries because trade is scheduled for deletion with offerId={}", offerId); + return; + } + swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.OFFER_FUNDING); // swap trade payout to available if applicable From b0e9627c10ae0a9499d2b802639caba73a30a103 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 8 Mar 2025 07:55:06 -0500 Subject: [PATCH 167/371] rename openOfferManager.getOpenOffer(id) --- .../java/haveno/core/api/CoreOffersService.java | 2 +- .../java/haveno/core/offer/OpenOfferManager.java | 14 +++++++------- .../dispute/mediation/MediationManager.java | 2 +- .../core/support/dispute/refund/RefundManager.java | 4 ++-- core/src/main/java/haveno/core/trade/Trade.java | 4 ++-- .../main/java/haveno/core/trade/TradeManager.java | 2 +- .../tasks/MaybeSendSignContractRequest.java | 2 +- .../haveno/core/xmr/wallet/XmrWalletService.java | 2 +- .../desktop/main/funds/locked/LockedView.java | 4 ++-- .../desktop/main/funds/reserved/ReservedView.java | 4 ++-- .../desktop/main/offer/MutableOfferViewModel.java | 2 +- .../main/offer/offerbook/OfferBookViewModel.java | 2 +- .../main/overlays/windows/OfferDetailsWindow.java | 2 +- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index a66388c040..f036fb13ea 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -159,7 +159,7 @@ public class CoreOffersService { } OpenOffer getMyOffer(String id) { - return openOfferManager.getOpenOfferById(id) + return openOfferManager.getOpenOffer(id) .filter(open -> open.getOffer().isMyOffer(keyRing)) .orElseThrow(() -> new IllegalStateException(format("openoffer with id '%s' not found", id))); diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 5df08a38ba..7c71aa9a90 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -236,7 +236,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe public void onAdded(Offer offer) { // cancel offer if reserved funds spent - Optional openOfferOptional = getOpenOfferById(offer.getId()); + Optional openOfferOptional = getOpenOffer(offer.getId()); if (openOfferOptional.isPresent() && openOfferOptional.get().getState() != OpenOffer.State.RESERVED && offer.isReservedFundsSpent()) { log.warn("Canceling open offer because reserved funds have been spent, offerId={}, state={}", offer.getId(), openOfferOptional.get().getState()); cancelOpenOffer(openOfferOptional.get(), null, null); @@ -573,7 +573,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // Remove from offerbook public void removeOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - Optional openOfferOptional = getOpenOfferById(offer.getId()); + Optional openOfferOptional = getOpenOffer(offer.getId()); if (openOfferOptional.isPresent()) { cancelOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler); } else { @@ -686,7 +686,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe OpenOffer.State originalState, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - Optional openOfferOptional = getOpenOfferById(editedOffer.getId()); + Optional openOfferOptional = getOpenOffer(editedOffer.getId()); if (openOfferOptional.isPresent()) { OpenOffer openOffer = openOfferOptional.get(); @@ -750,7 +750,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // close open offer after key images spent public void closeOpenOffer(Offer offer) { - getOpenOfferById(offer.getId()).ifPresent(openOffer -> { + getOpenOffer(offer.getId()).ifPresent(openOffer -> { removeOpenOffer(openOffer); openOffer.setState(OpenOffer.State.CLOSED); xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId()); @@ -813,14 +813,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return openOffers.getObservableList(); } - public Optional getOpenOfferById(String offerId) { + public Optional getOpenOffer(String offerId) { synchronized (openOffers) { return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst(); } } public boolean hasOpenOffer(String offerId) { - return getOpenOfferById(offerId).isPresent(); + return getOpenOffer(offerId).isPresent(); } public Optional getSignedOfferById(String offerId) { @@ -1575,7 +1575,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } try { - Optional openOfferOptional = getOpenOfferById(request.offerId); + Optional openOfferOptional = getOpenOffer(request.offerId); AvailabilityResult availabilityResult; byte[] makerSignature = null; if (openOfferOptional.isPresent()) { diff --git a/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java index b7fa902b83..56686faa61 100644 --- a/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java @@ -196,7 +196,7 @@ public final class MediationManager extends DisputeManager tradeManager.requestPersistence(); } } else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); } sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); diff --git a/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java b/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java index fa3503f625..034eac6d5a 100644 --- a/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java @@ -196,7 +196,7 @@ public final class RefundManager extends DisputeManager { tradeManager.requestPersistence(); } } else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); } sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); @@ -205,7 +205,7 @@ public final class RefundManager extends DisputeManager { if (tradeManager.getOpenTrade(tradeId).isPresent()) { tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED); } else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); } diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index ed4a8ce8b0..0bc6105049 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1604,7 +1604,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // unreserve maker's open offer - Optional openOffer = processModel.getOpenOfferManager().getOpenOfferById(this.getId()); + Optional openOffer = processModel.getOpenOfferManager().getOpenOffer(this.getId()); if (this instanceof MakerTrade && openOffer.isPresent()) { processModel.getOpenOfferManager().unreserveOpenOffer(openOffer.get()); } @@ -1688,7 +1688,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private void restoreDepositsPublishedTrade() { // close open offer - if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOfferById(getId()).isPresent()) { + if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOffer(getId()).isPresent()) { log.info("Closing open offer because {} {} was restored after protocol error", getClass().getSimpleName(), getShortId()); processModel.getOpenOfferManager().closeOpenOffer(checkNotNull(getOffer())); } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 41135e62f1..88db590c42 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -556,7 +556,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (request.getMakerNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) { // get open offer - Optional openOfferOptional = openOfferManager.getOpenOfferById(request.getOfferId()); + Optional openOfferOptional = openOfferManager.getOpenOffer(request.getOfferId()); if (!openOfferOptional.isPresent()) return; OpenOffer openOffer = openOfferOptional.get(); if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index e1c4cce5cc..1d2170cb53 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -87,7 +87,7 @@ public class MaybeSendSignContractRequest extends TradeTask { Integer subaddressIndex = null; boolean reserveExactAmount = false; if (trade instanceof MakerTrade) { - reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount(); + reserveExactAmount = processModel.getOpenOfferManager().getOpenOffer(trade.getId()).get().isReserveExactAmount(); if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex(); } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 23e4f74550..97aef9545f 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1171,7 +1171,7 @@ public class XmrWalletService extends XmrWalletBase { public Stream getAddressEntriesForAvailableBalanceStream() { Stream available = getFundedAvailableAddressEntries().stream(); available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream()); - available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOfferById(entry.getOfferId()).isPresent())); + available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOffer(entry.getOfferId()).isPresent())); available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutUnlocked())); return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0); } diff --git a/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java b/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java index 0a986b5505..8cf5700cc6 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java @@ -225,8 +225,8 @@ public class LockedView extends ActivatableView { Optional tradeOptional = tradeManager.getOpenTrade(offerId); if (tradeOptional.isPresent()) { return Optional.of(tradeOptional.get()); - } else if (openOfferManager.getOpenOfferById(offerId).isPresent()) { - return Optional.of(openOfferManager.getOpenOfferById(offerId).get()); + } else if (openOfferManager.getOpenOffer(offerId).isPresent()) { + return Optional.of(openOfferManager.getOpenOffer(offerId).get()); } else { return Optional.empty(); } diff --git a/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java b/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java index bfa8bd26b0..bcef7e6488 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java @@ -224,8 +224,8 @@ public class ReservedView extends ActivatableView { Optional tradeOptional = tradeManager.getOpenTrade(offerId); if (tradeOptional.isPresent()) { return Optional.of(tradeOptional.get()); - } else if (openOfferManager.getOpenOfferById(offerId).isPresent()) { - return Optional.of(openOfferManager.getOpenOfferById(offerId).get()); + } else if (openOfferManager.getOpenOffer(offerId).isPresent()) { + return Optional.of(openOfferManager.getOpenOffer(offerId).get()); } else { return Optional.empty(); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index 5588bb53c0..e32869afe2 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -665,7 +665,7 @@ public abstract class MutableOfferViewModel ext createOfferRequested = false; createOfferCanceled = true; OpenOfferManager openOfferManager = HavenoUtils.openOfferManager; - Optional openOffer = openOfferManager.getOpenOfferById(offer.getId()); + Optional openOffer = openOfferManager.getOpenOffer(offer.getId()); if (openOffer.isPresent()) { openOfferManager.cancelOpenOffer(openOffer.get(), () -> { UserThread.execute(() -> { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index d98098e99c..c93dfa8d43 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -711,6 +711,6 @@ abstract class OfferBookViewModel extends ActivatableViewModel { abstract String getCurrencyCodeFromPreferences(OfferDirection direction); public OpenOffer getOpenOffer(Offer offer) { - return openOfferManager.getOpenOfferById(offer.getId()).orElse(null); + return openOfferManager.getOpenOffer(offer.getId()).orElse(null); } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index 3ef8ca521b..2139bf2813 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -342,7 +342,7 @@ public class OfferDetailsWindow extends Overlay { BigInteger reservedAmount = isMyOffer ? offer.getReservedAmount() : null; // get offer challenge - OpenOffer myOpenOffer = HavenoUtils.openOfferManager.getOpenOfferById(offer.getId()).orElse(null); + OpenOffer myOpenOffer = HavenoUtils.openOfferManager.getOpenOffer(offer.getId()).orElse(null); String offerChallenge = myOpenOffer == null ? null : myOpenOffer.getChallenge(); rows = 3; From bedd38748ea282161d8a33f903cd15940a6d4a59 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 8 Mar 2025 17:30:31 -0500 Subject: [PATCH 168/371] sign and post offer directly if reserve amount = available balance --- .../haveno/core/offer/OpenOfferManager.java | 67 ++++++++++++------- .../tasks/MakerReserveOfferFunds.java | 3 + .../tasks/MakerSendSignOfferRequest.java | 2 +- .../tasks/TakerReserveTradeFunds.java | 3 + 4 files changed, 49 insertions(+), 26 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 7c71aa9a90..a475691736 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -987,26 +987,16 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe setSplitOutputTx(openOffer, splitOutputTx); } - // if not found, create tx to split exact output - if (splitOutputTx == null) { - if (openOffer.getSplitOutputTxHash() != null) { - log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash()); - setSplitOutputTx(openOffer, null); - } - try { - splitOrSchedule(openOffers, openOffer, amountNeeded); - } catch (Exception e) { - log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage()); - openOffer.getOffer().setState(Offer.State.INVALID); - errorMessageHandler.handleErrorMessage(e.getMessage()); - return; - } - } else if (!splitOutputTx.isLocked()) { - - // otherwise sign and post offer if split output available - signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); + // if wallet has exact available balance, try to sign and post directly + if (xmrWalletService.getAvailableBalance().equals(amountNeeded)) { + signAndPostOffer(openOffer, true, resultHandler, (errorMessage) -> { + splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler); + }); return; + } else { + splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler); } + } else { // sign and post offer if enough funds @@ -1017,11 +1007,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } else if (openOffer.getScheduledTxHashes() == null) { scheduleWithEarliestTxs(openOffers, openOffer); + resultHandler.handleResult(null); + return; } } - - // handle result - resultHandler.handleResult(null); } catch (Exception e) { if (!openOffer.isCanceled()) log.error("Error processing pending offer: {}\n", e.getMessage(), e); errorMessageHandler.handleErrorMessage(e.getMessage()); @@ -1087,13 +1076,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (output.isSpent() || output.isFrozen()) removeTxs.add(tx); } } - if (!hasExactAmount(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx); + if (!hasExactOutput(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx); } splitOutputTxs.removeAll(removeTxs); return splitOutputTxs; } - private boolean hasExactAmount(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) { + private boolean hasExactOutput(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) { boolean hasExactOutput = (tx.getOutputsWallet(new MoneroOutputQuery() .setAccountIndex(0) .setSubaddressIndex(preferredSubaddressIndex) @@ -1115,7 +1104,35 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return earliestUnscheduledTx; } - private void splitOrSchedule(List openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) { + // if split tx not found and cannot reserve exact amount directly, create tx to split or reserve exact output + private void splitOrSchedule(MoneroTxWallet splitOutputTx, List openOffers, OpenOffer openOffer, BigInteger amountNeeded, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + if (splitOutputTx == null) { + if (openOffer.getSplitOutputTxHash() != null) { + log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash()); + setSplitOutputTx(openOffer, null); + } + try { + splitOrScheduleAux(openOffers, openOffer, amountNeeded); + resultHandler.handleResult(null); + return; + } catch (Exception e) { + log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage()); + openOffer.getOffer().setState(Offer.State.INVALID); + errorMessageHandler.handleErrorMessage(e.getMessage()); + return; + } + } else if (!splitOutputTx.isLocked()) { + + // otherwise sign and post offer if split output available + signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); + return; + } else { + resultHandler.handleResult(null); + return; + } + } + + private void splitOrScheduleAux(List openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) { // handle sufficient available balance to split output boolean sufficientAvailableBalance = xmrWalletService.getAvailableBalance().compareTo(offerReserveAmount) >= 0; @@ -1299,13 +1316,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe openOffer.setScheduledAmount(null); requestPersistence(); - resultHandler.handleResult(transaction); if (!stopped) { startPeriodicRepublishOffersTimer(); startPeriodicRefreshOffersTimer(); } else { log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call."); } + resultHandler.handleResult(transaction); }, errorMessageHandler); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index 0d9271a41e..e873d1e561 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -87,6 +87,9 @@ public class MakerReserveOfferFunds extends Task { try { //if (true) throw new RuntimeException("Pretend error"); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); + } catch (IllegalStateException e) { + log.warn("Illegal state creating reserve tx, offerId={}, error={}", openOffer.getShortId(), i + 1, e.getMessage()); + throw e; } catch (Exception e) { log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); model.getXmrWalletService().handleWalletError(e, sourceConnection); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java index 2037b51d09..3644492735 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java @@ -77,7 +77,7 @@ public class MakerSendSignOfferRequest extends Task { offer.getOfferPayload().getReserveTxKeyImages(), returnAddress); - // send request to least used arbitrators until success + // send request to random arbitrators until success sendSignOfferRequests(request, () -> { complete(); }, (errorMessage) -> { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java index e6c71032f4..aa0fc9dfe9 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -70,6 +70,9 @@ public class TakerReserveTradeFunds extends TradeTask { MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); try { reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); + } catch (IllegalStateException e) { + log.warn("Illegal state creating reserve tx, offerId={}, error={}", trade.getShortId(), i + 1, e.getMessage()); + throw e; } catch (Exception e) { log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); trade.getXmrWalletService().handleWalletError(e, sourceConnection); From 251a973fd6296912e8109d16805991265c35a326 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 9 Mar 2025 09:44:20 -0400 Subject: [PATCH 169/371] do not refresh or republish offers if disconnected from xmr node --- .../haveno/core/offer/OpenOfferManager.java | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index a475691736..b4b8dd80fb 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1978,6 +1978,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } private boolean preventedFromPublishing(OpenOffer openOffer) { + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return true; return openOffer.isDeactivated() || openOffer.isCanceled() || openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null; } @@ -2000,25 +2001,27 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (periodicRefreshOffersTimer == null) periodicRefreshOffersTimer = UserThread.runPeriodically(() -> { if (!stopped) { - int size = openOffers.size(); - //we clone our list as openOffers might change during our delayed call - final ArrayList openOffersList = new ArrayList<>(openOffers.getList()); - for (int i = 0; i < size; i++) { - // we delay to avoid reaching throttle limits - // roughly 4 offers per second - - long delay = 300; - final long minDelay = (i + 1) * delay; - final long maxDelay = (i + 2) * delay; - final OpenOffer openOffer = openOffersList.get(i); - UserThread.runAfterRandomDelay(() -> { - // we need to check if in the meantime the offer has been removed - boolean contained = false; - synchronized (openOffers) { - contained = openOffers.contains(openOffer); - } - if (contained) maybeRefreshOffer(openOffer, 0, 1); - }, minDelay, maxDelay, TimeUnit.MILLISECONDS); + synchronized (openOffers) { + int size = openOffers.size(); + //we clone our list as openOffers might change during our delayed call + final ArrayList openOffersList = new ArrayList<>(openOffers.getList()); + for (int i = 0; i < size; i++) { + // we delay to avoid reaching throttle limits + // roughly 4 offers per second + + long delay = 300; + final long minDelay = (i + 1) * delay; + final long maxDelay = (i + 2) * delay; + final OpenOffer openOffer = openOffersList.get(i); + UserThread.runAfterRandomDelay(() -> { + // we need to check if in the meantime the offer has been removed + boolean contained = false; + synchronized (openOffers) { + contained = openOffers.contains(openOffer); + } + if (contained) maybeRefreshOffer(openOffer, 0, 1); + }, minDelay, maxDelay, TimeUnit.MILLISECONDS); + } } } else { log.debug("We have stopped already. We ignore that periodicRefreshOffersTimer.run call."); From 00a2a7c2b7981310f657cc8dad4edc7456339690 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 9 Mar 2025 09:50:20 -0400 Subject: [PATCH 170/371] nack offer availability request if disconnected from xmr node --- .../src/main/java/haveno/core/offer/OpenOfferManager.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index b4b8dd80fb..32a1507f77 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1574,6 +1574,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } + // Don't allow trade start if not connected to Monero node + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) { + errorMessage = "We got a handleOfferAvailabilityRequest but we are not connected to a Monero node."; + log.info(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + if (stopped) { errorMessage = "We have stopped already. We ignore that handleOfferAvailabilityRequest call."; log.debug(errorMessage); From 84d8a17ab492d5c173a2e5bf4fc6bdc23e226f37 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 3 Mar 2025 06:56:20 -0500 Subject: [PATCH 171/371] rename payment sent message state property for seller --- .../main/java/haveno/core/trade/Trade.java | 8 +++--- .../core/trade/protocol/ProcessModel.java | 26 ++++++++++++------- .../core/trade/protocol/TradeProtocol.java | 3 ++- .../tasks/BuyerSendPaymentSentMessage.java | 6 ++--- ...yerSendPaymentSentMessageToArbitrator.java | 3 +-- .../BuyerSendPaymentSentMessageToSeller.java | 10 +++---- .../pendingtrades/PendingTradesViewModel.java | 10 +++---- .../steps/buyer/BuyerStep3View.java | 6 ++--- proto/src/main/proto/pb.proto | 2 +- 9 files changed, 41 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 0bc6105049..6ae5c9fcc4 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -735,9 +735,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed in v1.0.19 so this check can be removed? if (isBuyer()) { MessageState expectedState = getPaymentSentMessageState(); - if (expectedState != null && expectedState != processModel.getPaymentSentMessageStateProperty().get()) { - log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStateProperty().get()); - processModel.getPaymentSentMessageStateProperty().set(expectedState); + if (expectedState != null && expectedState != processModel.getPaymentSentMessageStatePropertySeller().get()) { + log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStatePropertySeller().get()); + processModel.getPaymentSentMessageStatePropertySeller().set(expectedState); } } @@ -2022,7 +2022,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public MessageState getPaymentSentMessageState() { if (isPaymentReceived()) return MessageState.ACKNOWLEDGED; - if (processModel.getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; + if (processModel.getPaymentSentMessageStatePropertySeller().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; switch (state) { case BUYER_SENT_PAYMENT_SENT_MSG: return MessageState.SENT; diff --git a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java index 54aaa65d37..d81c4f476a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -163,7 +163,7 @@ public class ProcessModel implements Model, PersistablePayload { // PaymentSentMessage. As well we do an automatic re-send in case it was not ACKed yet. // To enable that even after restart we persist the state. @Setter - private ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + private ObjectProperty paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED); @Setter private ObjectProperty paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); private ObjectProperty paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false); @@ -203,7 +203,7 @@ public class ProcessModel implements Model, PersistablePayload { .setPubKeyRing(pubKeyRing.toProtoMessage()) .setUseSavingsWallet(useSavingsWallet) .setFundsNeededForTrade(fundsNeededForTrade) - .setPaymentSentMessageState(paymentSentMessageStateProperty.get().name()) + .setPaymentSentMessageStateSeller(paymentSentMessageStatePropertySeller.get().name()) .setPaymentSentMessageStateArbitrator(paymentSentMessageStatePropertyArbitrator.get().name()) .setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation) .setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation) @@ -240,9 +240,9 @@ public class ProcessModel implements Model, PersistablePayload { processModel.setTradeFeeAddress(ProtoUtil.stringOrNullFromProto(proto.getTradeFeeAddress())); processModel.setMultisigAddress(ProtoUtil.stringOrNullFromProto(proto.getMultisigAddress())); - String paymentSentMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageState()); - MessageState paymentSentMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateString); - processModel.setPaymentSentMessageState(paymentSentMessageState); + String paymentSentMessageStateSellerString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateSeller()); + MessageState paymentSentMessageStateSeller = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateSellerString); + processModel.setPaymentSentMessageStateSeller(paymentSentMessageStateSeller); String paymentSentMessageStateArbitratorString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateArbitrator()); MessageState paymentSentMessageStateArbitrator = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateArbitratorString); @@ -274,11 +274,11 @@ public class ProcessModel implements Model, PersistablePayload { return getP2PService().getAddress(); } - void setPaymentSentAckMessage(AckMessage ackMessage) { + void setPaymentSentAckMessageSeller(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : MessageState.FAILED; - setPaymentSentMessageState(messageState); + setPaymentSentMessageStateSeller(messageState); } void setPaymentSentAckMessageArbitrator(AckMessage ackMessage) { @@ -288,8 +288,8 @@ public class ProcessModel implements Model, PersistablePayload { setPaymentSentMessageStateArbitrator(messageState); } - public void setPaymentSentMessageState(MessageState paymentSentMessageStateProperty) { - this.paymentSentMessageStateProperty.set(paymentSentMessageStateProperty); + public void setPaymentSentMessageStateSeller(MessageState paymentSentMessageStateProperty) { + this.paymentSentMessageStatePropertySeller.set(paymentSentMessageStateProperty); if (tradeManager != null) { tradeManager.requestPersistence(); } @@ -302,6 +302,14 @@ public class ProcessModel implements Model, PersistablePayload { } } + public boolean isPaymentSentMessageAckedBySeller() { + return paymentSentMessageStatePropertySeller.get() == MessageState.ACKNOWLEDGED; + } + + public boolean isPaymentSentMessageAckedByArbitrator() { + return paymentSentMessageStatePropertyArbitrator.get() == MessageState.ACKNOWLEDGED; + } + void setDepositTxSentAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 684dfb7c62..6a158bea7a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -652,11 +652,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // handle ack for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) { if (trade.getTradePeer(sender) == trade.getSeller()) { - processModel.setPaymentSentAckMessage(ackMessage); + processModel.setPaymentSentAckMessageSeller(ackMessage); trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); processModel.getTradeManager().requestPersistence(); } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { processModel.setPaymentSentAckMessageArbitrator(ackMessage); + processModel.getTradeManager().requestPersistence(); } else if (!ackMessage.isSuccess()) { String err = "Received AckMessage with error state for " + ackMessage.getSourceMsgClassName() + " from "+ sender + " with tradeId " + trade.getId() + " and errorMessage=" + ackMessage.getErrorMessage(); log.warn(err); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java index f060effe78..bc064399a3 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java @@ -170,7 +170,7 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask timer.stop(); } if (listener != null) { - processModel.getPaymentSentMessageStateProperty().removeListener(listener); + processModel.getPaymentSentMessageStatePropertySeller().removeListener(listener); } } @@ -194,8 +194,8 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask if (resendCounter == 0) { listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); - processModel.getPaymentSentMessageStateProperty().addListener(listener); - onMessageStateChange(processModel.getPaymentSentMessageStateProperty().get()); + processModel.getPaymentSentMessageStatePropertySeller().addListener(listener); + onMessageStateChange(processModel.getPaymentSentMessageStatePropertySeller().get()); } // first re-send is after 2 minutes, then increase the delay exponentially diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java index cc4113e342..cd3098737a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java @@ -18,7 +18,6 @@ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; -import haveno.core.network.MessageState; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import lombok.EqualsAndHashCode; @@ -59,6 +58,6 @@ public class BuyerSendPaymentSentMessageToArbitrator extends BuyerSendPaymentSen @Override protected boolean isAckedByReceiver() { - return trade.getProcessModel().getPaymentSentMessageStatePropertyArbitrator().get() == MessageState.ACKNOWLEDGED; + return trade.getProcessModel().isPaymentSentMessageAckedByArbitrator(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java index caf402be0a..825220d5b4 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java @@ -40,25 +40,25 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes @Override protected void setStateSent() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.SENT); + trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.SENT); super.setStateSent(); } @Override protected void setStateArrived() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.ARRIVED); + trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.ARRIVED); super.setStateArrived(); } @Override protected void setStateStoredInMailbox() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.STORED_IN_MAILBOX); + trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.STORED_IN_MAILBOX); super.setStateStoredInMailbox(); } @Override protected void setStateFault() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.FAILED); + trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.FAILED); super.setStateFault(); } @@ -72,6 +72,6 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes @Override protected boolean isAckedByReceiver() { - return trade.getState().ordinal() >= Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal(); + return trade.getProcessModel().isPaymentSentMessageAckedBySeller(); } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index 41881c58be..b9936236c6 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -100,7 +100,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel buyerState = new SimpleObjectProperty<>(); private final ObjectProperty sellerState = new SimpleObjectProperty<>(); @Getter - private final ObjectProperty messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + private final ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); private Subscription tradeStateSubscription; private Subscription paymentAccountDecryptedSubscription; private Subscription payoutStateSubscription; @@ -186,7 +186,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { onPayoutStateChanged(state); }); - messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentSentMessageStateProperty(), this::onMessageStateChanged); + messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentSentMessageStatePropertySeller(), this::onPaymentSentMessageStateChanged); } } } @@ -215,8 +215,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel Date: Mon, 3 Mar 2025 07:55:57 -0500 Subject: [PATCH 172/371] save payment received message immediately for reprocessing --- .../haveno/core/trade/protocol/TradeProtocol.java | 13 +++++++++++++ .../tasks/ProcessPaymentReceivedMessage.java | 3 --- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 6a158bea7a..117c16cc05 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -536,6 +536,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private void handle(PaymentReceivedMessage message, NodeAddress peer, boolean reprocessOnError) { System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + + // validate signature + try { + HavenoUtils.verifyPaymentReceivedMessage(trade, message); + } catch (Throwable t) { + log.warn("Ignoring PaymentReceivedMessage with invalid signature for {} {}, error={}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage()); + return; + } + + // save message for reprocessing + trade.getSeller().setPaymentReceivedMessage(message); + trade.requestPersistence(); + if (!trade.isInitialized() || trade.isShutDown()) return; ThreadUtils.execute(() -> { if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java index 2ce29828a4..f1016f3c61 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java @@ -80,9 +80,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask { return; } - // save message for reprocessing - trade.getSeller().setPaymentReceivedMessage(message); - // set state trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getBuyer().setAccountAgeWitness(message.getBuyerAccountAgeWitness()); From fb2b4a0c6ab20a4d8d451d0e7b6e2d641458c9c5 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 3 Mar 2025 08:51:32 -0500 Subject: [PATCH 173/371] save and reprocess payment sent message --- .../main/java/haveno/core/trade/Trade.java | 5 +- .../core/trade/protocol/TradeProtocol.java | 49 ++++++++++++++++++- .../tasks/ProcessPaymentSentMessage.java | 1 - 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 6ae5c9fcc4..dc3a1bc7a2 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2441,8 +2441,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!wasWalletSynced) trySyncWallet(true); updatePollPeriod(); - // reprocess pending payout messages - this.getProtocol().maybeReprocessPaymentReceivedMessage(false); + // reprocess pending messages + getProtocol().maybeReprocessPaymentSentMessage(false); + getProtocol().maybeReprocessPaymentReceivedMessage(false); HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); startPolling(); diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 117c16cc05..6400e69f6a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -106,6 +106,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected ErrorMessageHandler errorMessageHandler; private boolean depositsConfirmedTasksCalled; + private int reprocessPaymentSentMessageCount; private int reprocessPaymentReceivedMessageCount; /////////////////////////////////////////////////////////////////////////////////////////// @@ -279,6 +280,22 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D }, trade.getId()); } + public void maybeReprocessPaymentSentMessage(boolean reprocessOnError) { + if (trade.isShutDownStarted()) return; + ThreadUtils.execute(() -> { + synchronized (trade.getLock()) { + + // skip if no need to reprocess + if (trade.isBuyer() || trade.getBuyer().getPaymentSentMessage() == null || trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) { + return; + } + + log.warn("Reprocessing payment sent message for {} {}", trade.getClass().getSimpleName(), trade.getId()); + handle(trade.getBuyer().getPaymentSentMessage(), trade.getBuyer().getPaymentSentMessage().getSenderNodeAddress(), reprocessOnError); + } + }, trade.getId()); + } + public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { @@ -481,7 +498,25 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // received by seller and arbitrator protected void handle(PaymentSentMessage message, NodeAddress peer) { + handle(message, peer, true); + } + + // received by seller and arbitrator + protected void handle(PaymentSentMessage message, NodeAddress peer, boolean reprocessOnError) { System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + + // validate signature + try { + HavenoUtils.verifyPaymentSentMessage(trade, message); + } catch (Throwable t) { + log.warn("Ignoring PaymentSentMessage with invalid signature for {} {}, error={}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage()); + return; + } + + // save message for reprocessing + trade.getBuyer().setPaymentSentMessage(message); + trade.requestPersistence(); + if (!trade.isInitialized() || trade.isShutDown()) return; if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); @@ -521,7 +556,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D handleTaskRunnerSuccess(peer, message); }, (errorMessage) -> { - handleTaskRunnerFault(peer, message, errorMessage); + log.warn("Error processing payment sent message: " + errorMessage); + processModel.getTradeManager().requestPersistence(); + + // schedule to reprocess message unless deleted + if (trade.getBuyer().getPaymentSentMessage() != null) { + UserThread.runAfter(() -> { + reprocessPaymentSentMessageCount++; + maybeReprocessPaymentSentMessage(reprocessOnError); + }, trade.getReprocessDelayInSeconds(reprocessPaymentSentMessageCount)); + } else { + handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack + } + unlatchTrade(); }))) .executeTasks(true); awaitTradeLatch(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java index 93d1ce520a..de7d949ade 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java @@ -48,7 +48,6 @@ public class ProcessPaymentSentMessage extends TradeTask { trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress()); // update state from message - trade.getBuyer().setPaymentSentMessage(message); trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness()); String counterCurrencyTxId = message.getCounterCurrencyTxId(); From 1510e6f18d49ef9ff6610b9b6f550b12300ee228 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:27:50 -0500 Subject: [PATCH 174/371] logging cleanup --- core/src/main/java/haveno/core/trade/Trade.java | 2 +- core/src/main/java/haveno/core/trade/TradeManager.java | 4 ++-- .../java/haveno/core/xmr/setup/MoneroWalletRpcManager.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index dc3a1bc7a2..9466355f34 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1089,10 +1089,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { + log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); handleWalletError(e, sourceConnection); doPollWallet(); if (isPayoutPublished()) break; - log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 88db590c42..c98978ae4c 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -747,7 +747,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); + log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -766,7 +766,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void handleSignContractRequest(SignContractRequest request, NodeAddress sender) { - log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); + log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); diff --git a/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java b/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java index c993ebd181..1bb1e3500e 100644 --- a/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java +++ b/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java @@ -135,7 +135,7 @@ public class MoneroWalletRpcManager { // stop process String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid()); - log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}", path, port, pid); + log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}, force={}", path, port, pid, force); walletRpc.stopProcess(force); } From a55daf803efca28662af7aaebdbe26fd28d64632 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:23:25 -0500 Subject: [PATCH 175/371] call trade message handling off trade thread --- .../main/java/haveno/core/trade/protocol/TradeProtocol.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 6400e69f6a..caeb654813 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -125,12 +125,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getOfferId(), message.getUid()); - ThreadUtils.execute(() -> handle(message, peerNodeAddress), trade.getId()); + handle(message, peerNodeAddress); } protected void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getOfferId(), message.getUid()); - ThreadUtils.execute(() -> handle(message, peerNodeAddress), trade.getId()); + handle(message, peerNodeAddress); } private void handle(TradeMessage message, NodeAddress peerNodeAddress) { From 46734459d497ce2b2c9190897c78e2d146719711 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 4 Mar 2025 06:57:41 -0500 Subject: [PATCH 176/371] highlight logs for handling trade protocol messages --- .../core/trade/protocol/ArbitratorProtocol.java | 4 ++-- .../haveno/core/trade/protocol/BuyerProtocol.java | 2 +- .../core/trade/protocol/SellerProtocol.java | 2 +- .../haveno/core/trade/protocol/TradeProtocol.java | 15 ++++++++------- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java b/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java index 98b3f1ab0d..b25c6c68d1 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java @@ -43,7 +43,7 @@ public class ArbitratorProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - System.out.println("ArbitratorProtocol.handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -78,7 +78,7 @@ public class ArbitratorProtocol extends DisputeProtocol { } public void handleDepositRequest(DepositRequest request, NodeAddress sender) { - System.out.println("ArbitratorProtocol.handleDepositRequest() " + trade.getId()); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleDepositRequest() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java index 06e1eead7c..4302f6db6f 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java @@ -119,7 +119,7 @@ public class BuyerProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println("BuyerProtocol.onPaymentSent()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "BuyerProtocol.onPaymentSent() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java index 4de8fa0d6a..2b7f45877a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -115,7 +115,7 @@ public class SellerProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - log.info("SellerProtocol.onPaymentReceived()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "SellerProtocol.onPaymentReceived() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index caeb654813..08c99570f6 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -96,6 +96,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private static final String TIMEOUT_REACHED = "Timeout reached."; public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other wallet functions public static final long REPROCESS_DELAY_MS = 5000; + public static final String LOG_HIGHLIGHT = "\u001B[0m"; // terminal default protected final ProcessModel processModel; protected final Trade trade; @@ -313,7 +314,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -350,7 +351,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -393,7 +394,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -439,7 +440,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -469,7 +470,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handle(DepositsConfirmedMessage message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender + " for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handle(DepositsConfirmedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); if (!trade.isInitialized() || trade.isShutDown()) return; ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -503,7 +504,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // received by seller and arbitrator protected void handle(PaymentSentMessage message, NodeAddress peer, boolean reprocessOnError) { - System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); // validate signature try { @@ -582,7 +583,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } private void handle(PaymentReceivedMessage message, NodeAddress peer, boolean reprocessOnError) { - System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); // validate signature try { From cb69d0646883490b75ae01060e1c6aca23b0ed30 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 4 Mar 2025 15:12:57 -0500 Subject: [PATCH 177/371] increase grpc rate limits for testnet --- .../java/haveno/daemon/grpc/GrpcOffersService.java | 2 +- .../java/haveno/daemon/grpc/GrpcTradesService.java | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index 84443da4d5..485ff38ca8 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -208,7 +208,7 @@ class GrpcOffersService extends OffersImplBase { put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, SECONDS)); put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java index 286fccf51a..0a5d2d39d2 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java @@ -252,14 +252,14 @@ class GrpcTradesService extends TradesImplBase { .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 1, SECONDS)); - put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, SECONDS)); - put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 1, SECONDS)); + put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(3, MINUTES)); - put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } From d4eb30bb979c855f71ad9e408ecf14c2b274c9df Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:20:58 -0500 Subject: [PATCH 178/371] schedule import multisig hex on deposit confirmation msg --- .../main/java/haveno/core/trade/Trade.java | 41 ++++++++++++++++--- .../core/trade/protocol/ProcessModel.java | 7 +++- .../ProcessDepositsConfirmedMessage.java | 13 +----- proto/src/main/proto/pb.proto | 1 + 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 9466355f34..13594cc3a1 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -143,6 +143,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS; + private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 10; protected final Object pollLock = new Object(); protected static final Object importMultisigLock = new Object(); private boolean pollInProgress; @@ -741,6 +742,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + // handle confirmations + walletHeight.addListener((observable, oldValue, newValue) -> { + importMultisigHexIfScheduled(); + }); + // trade is initialized isInitialized = true; @@ -1077,6 +1083,26 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + public void scheduleImportMultisigHex() { + processModel.setImportMultisigHexScheduled(true); + requestPersistence(); + } + + private void importMultisigHexIfScheduled() { + if (!isInitialized || isShutDownStarted) return; + if (!isDepositsConfirmed() || getMaker().getDepositTx() == null) return; + if (walletHeight.get() - getMaker().getDepositTx().getHeight() < NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT) return; + ThreadUtils.execute(() -> { + if (!isInitialized || isShutDownStarted) return; + synchronized (getLock()) { + if (processModel.isImportMultisigHexScheduled()) { + processModel.setImportMultisigHexScheduled(false); + ThreadUtils.submitToPool(() -> importMultisigHex()); + } + } + }, getId()); + } + public void importMultisigHex() { synchronized (walletLock) { synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh @@ -1141,6 +1167,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (removed) wallet.importMultisigHex(multisigHexes.toArray(new String[0])); if (wallet.isMultisigImportNeeded()) throw new IllegalStateException(errorMessage); } + + // remove scheduled import + processModel.setImportMultisigHexScheduled(false); } catch (MoneroError e) { // import multisig hex individually if one is invalid @@ -2350,7 +2379,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return tradeAmountTransferred(); } - public boolean tradeAmountTransferred() { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean tradeAmountTransferred() { return isPaymentReceived() || (getDisputeResult() != null && getDisputeResult().getWinner() == DisputeResult.Winner.SELLER); } @@ -2366,11 +2400,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Private - /////////////////////////////////////////////////////////////////////////////////////////// - // lazy initialization private ObjectProperty getAmountProperty() { if (tradeAmountProperty == null) diff --git a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java index d81c4f476a..8521174ca8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -158,6 +158,9 @@ public class ProcessModel implements Model, PersistablePayload { @Getter @Setter private long tradeProtocolErrorHeight; + @Getter + @Setter + private boolean importMultisigHexScheduled; // We want to indicate the user the state of the message delivery of the // PaymentSentMessage. As well we do an automatic re-send in case it was not ACKed yet. @@ -207,7 +210,8 @@ public class ProcessModel implements Model, PersistablePayload { .setPaymentSentMessageStateArbitrator(paymentSentMessageStatePropertyArbitrator.get().name()) .setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation) .setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation) - .setTradeProtocolErrorHeight(tradeProtocolErrorHeight); + .setTradeProtocolErrorHeight(tradeProtocolErrorHeight) + .setImportMultisigHexScheduled(importMultisigHexScheduled); Optional.ofNullable(maker).ifPresent(e -> builder.setMaker((protobuf.TradePeer) maker.toProtoMessage())); Optional.ofNullable(taker).ifPresent(e -> builder.setTaker((protobuf.TradePeer) taker.toProtoMessage())); Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator((protobuf.TradePeer) arbitrator.toProtoMessage())); @@ -231,6 +235,7 @@ public class ProcessModel implements Model, PersistablePayload { processModel.setBuyerPayoutAmountFromMediation(proto.getBuyerPayoutAmountFromMediation()); processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation()); processModel.setTradeProtocolErrorHeight(proto.getTradeProtocolErrorHeight()); + processModel.setImportMultisigHexScheduled(proto.getImportMultisigHexScheduled()); // nullable processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature())); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java index c11df74fae..7e0c85af2d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java @@ -18,7 +18,6 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.ThreadUtils; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositsConfirmedMessage; @@ -63,17 +62,7 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { // update multisig hex if (sender.getUpdatedMultisigHex() == null) { sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); - - // try to import multisig hex (retry later) - if (!trade.isPayoutPublished()) { - ThreadUtils.submitToPool(() -> { - try { - trade.importMultisigHex(); - } catch (Exception e) { - log.warn("Error importing multisig hex on deposits confirmed for trade " + trade.getId() + ": " + e.getMessage() + "\n", e); - } - }); - } + trade.scheduleImportMultisigHex(); } // persist diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 6436052333..9bca09b668 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1581,6 +1581,7 @@ message ProcessModel { int64 seller_payout_amount_from_mediation = 17; int64 trade_protocol_error_height = 18; string trade_fee_address = 19; + bool import_multisig_hex_scheduled = 20; } message TradePeer { From 63917fe8ccd8a40e75d921960c01da9e1b44c00c Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 11 Mar 2025 13:42:10 -0400 Subject: [PATCH 179/371] replace sys.outs with log.info in buyer/seller protocols --- .../java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java | 4 ++-- .../java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java | 4 ++-- .../haveno/core/trade/protocol/SellerAsMakerProtocol.java | 2 +- .../haveno/core/trade/protocol/SellerAsTakerProtocol.java | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java index 160e1bee6c..9dc1b64405 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java @@ -60,8 +60,8 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); - ThreadUtils.execute(() -> { + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); + ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); this.errorMessageHandler = errorMessageHandler; diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java index 7a5a899e87..927997e611 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java @@ -68,7 +68,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol @Override public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "onTakerOffer for {} {}", getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -99,7 +99,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol @Override public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java index 15d92ff785..9219d0ad7d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java @@ -65,7 +65,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java index 2332ca2003..f4914efe60 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java @@ -68,7 +68,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc @Override public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "onTakerOffer for {} {}", getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -99,7 +99,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc @Override public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); From 34458cf3dfd2aea691c1eacccace8eb2014d950c Mon Sep 17 00:00:00 2001 From: PromptPunksFauxCough <200402670+PromptPunksFauxCough@users.noreply.github.com> Date: Sun, 16 Mar 2025 14:11:17 +0000 Subject: [PATCH 180/371] Install Haveno on Whonix + Qubes (#1628) --- scripts/install_whonix_qubes/INSTALL.md | 401 ++++++++++++++++++ scripts/install_whonix_qubes/README.md | 75 ++++ .../scripts/0-dom0/0.0-dom0.sh | 6 + .../scripts/0-dom0/0.1-dom0.sh | 6 + .../scripts/0-dom0/0.2-dom0.sh | 7 + .../scripts/0-dom0/0.3-dom0.sh | 6 + .../1-TemplateVM/1.0-haveno-templatevm.sh | 185 ++++++++ .../scripts/2-NetVM/2.0-haveno-netvm.sh | 30 ++ .../scripts/3-AppVM/3.0-haveno-appvm.sh | 61 +++ 9 files changed, 777 insertions(+) create mode 100644 scripts/install_whonix_qubes/INSTALL.md create mode 100644 scripts/install_whonix_qubes/README.md create mode 100644 scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh create mode 100644 scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh create mode 100644 scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh create mode 100644 scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh create mode 100644 scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh create mode 100644 scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh create mode 100644 scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh diff --git a/scripts/install_whonix_qubes/INSTALL.md b/scripts/install_whonix_qubes/INSTALL.md new file mode 100644 index 0000000000..c56b35cacd --- /dev/null +++ b/scripts/install_whonix_qubes/INSTALL.md @@ -0,0 +1,401 @@ +# Haveno on Qubes/Whonix + +## **Conventions:** + ++ \# – Requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command + ++ $ or % – Requires given linux commands to be executed as a regular non-privileged user + ++ \ – Used to indicate user supplied variable + +--- + +## **Installation - Scripted & Manual (GUI + CLI):** +### *Acquire release files:* +#### In `dispXXXX` AppVM: +##### Clone repository +```shell +% git clone --depth=1 https://github.com/haveno-dex/haveno +``` + +--- + +### **Create TemplateVM, NetVM & AppVM:** +#### Scripted +##### In `dispXXXX` AppVM: +###### Prepare files for transfer to `dom0` +```shell +% tar -C haveno/scripts/install_qubes/scripts/0-dom0 -zcvf /tmp/haveno.tgz . +``` + +##### In `dom0`: +###### Copy files to `dom0` +```shell +$ mkdir -p /tmp/haveno && qvm-run -p dispXXXX 'cat /tmp/haveno.tgz' > /tmp/haveno.tgz && tar -C /tmp/haveno -zxfv /tmp/haveno.tgz +$ bash /tmp/haveno/0.0-dom0.sh && bash /tmp/haveno/0.1-dom0.sh && bash /tmp/haveno/0.2-dom0.sh +``` + +#### GUI +##### TemplateVM +###### Via `Qubes Manager`: + ++ Locate & highlight whonix-workstation-17 (TemplateVM) + ++ Right-Click "whonix-workstation-17" and select "Clone qube" from Drop-Down + ++ Enter "haveno-template" in "Name" + ++ Click OK Button + +##### NetVM +###### Via `Qubes Manager`: + ++ Click "New qube" Button + ++ Enter "sys-haveno" for "Name and label" + ++ Click the Button Beside "Name and label" and Select "orange" + ++ Select "whonix-gateway-17" from "Template" Drop-Down + ++ Select "sys-firewall" from "Networking" Drop-Down + ++ Tick "Launch settings after creation" Radio-Box + ++ Click OK + ++ Click "Advanced" Tab + ++ Enter "512" for "Initial memory" + +

    (Within reason, can adjust to personal preference)

    + ++ Enter "512" for "Max memory" + +

    (Within reason, can adjust to personal preference)

    + ++ Tick "Provides network" Radio-Box + ++ Click "Apply" Button + ++ Click "OK" Button + +##### AppVM +###### Via `Qubes Manager`: + ++ Click "New qube" Button + ++ Enter "haveno" for "Name and label" + ++ Click the Button Beside "Name and label" and Select "orange" + ++ Select "haveno-template" from "Template" Drop-Down + ++ Select "sys-haveno" from "Networking" Drop-Down + ++ Tick "Launch settings after creation" Radio-Box + ++ Click OK + ++ Click "Advanced" Tab + ++ Enter "2048" for "Initial memory" + +

    (Within reason, can adjust to personal preference)

    + ++ Enter "4096" for "Max memory" + +

    (Within reason, can adjust to personal preference)

    + ++ Click "Apply" Button + ++ Click "OK" Button + + +#### CLI +##### TemplateVM +###### In `dom0`: +```shell +$ qvm-clone whonix-workstation-17 haveno-template +``` + +##### NetVM +##### In `dom0`: +```shell +$ qvm-create --template whonix-gateway-17 --class AppVM --label=orange --property memory=512 --property maxmem=512 --property netvm=sys-firewall sys-haveno && qvm-prefs --set sys-haveno provides_network True +``` + +#### AppVM +##### In `dom0`: +```shell +$ qvm-create --template haveno-template --class AppVM --label=orange --property memory=2048 --property maxmem=4096 --property netvm=sys-haveno haveno +$ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno +``` + +--- + +### **Build TemplateVM, NetVM & AppVM:** +#### *TemplateVM Using Precompiled Package via `git` Repository (Scripted)* +##### In `dispXXXX` AppVM: +```shell +% qvm-copy haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh +``` + ++ Select "haveno-template" for "Target" of Pop-Up + ++ Click OK + +##### In `haveno-template` TemplateVM: +```shell +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" +``` + +

    Example:

    + +```shell +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno-linux-deb.zip" "ABAF11C65A2970B130ABE3C479BE3E4300411886" +``` + +#### *TemplateVM Using Precompiled Package From `git` Repository (CLI)* +##### In `haveno-template` TemplateVM: +###### Download & Import Project PGP Key +

    For Whonix On Qubes OS:

    + +```shell +# export https_proxy=http://127.0.0.1:8082 +# export KEY_SEARCH="" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + +

    Example:

    + +```shell +# export https_proxy=http://127.0.0.1:8082 +# export KEY_SEARCH="ABAF11C65A2970B130ABE3C479BE3E4300411886" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + +

    For Whonix On Anything Other Than Qubes OS:

    + +```shell +# export KEY_SEARCH="" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + +

    Example:

    + +```shell +# export KEY_SEARCH="ABAF11C65A2970B130ABE3C479BE3E4300411886" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + + +###### Download Release Files +

    For Whonix On Qubes OS:

    + +```shell +# export https_proxy=http://127.0.0.1:8082 +# curl -sSLo /tmp/hashes.txt https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt +# curl -sSLo /tmp/hashes.txt.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt.sig +# curl -sSLo /tmp/haveno.zip https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip +# curl -sSLo /tmp/haveno.zip.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip.sig +``` + +

    Note:

    +

    Above are dummy URLS which MUST be replaced with actual working URLs

    + +

    For Whonix On Anything Other Than Qubes OS:

    + +```shell +# curl -sSLo /tmp/hashes.txt https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt +# curl -sSLo /tmp/hashes.txt.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt.sig +# curl -sSLo /tmp/haveno.zip https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip +# curl -sSLo /tmp/haveno.zip.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip.sig +``` + +

    Note:

    +

    Above are dummy URLS which MUST be replaced with actual working URLs

    + +###### Verify Release Files +```shell +# if gpg --digest-algo SHA256 --verify /tmp/hashes.txt.sig >/dev/null 2>&1; then printf $'SHASUM file has a VALID signature!\n'; else printf $'SHASUMS failed signature check\n' && sleep 5 && exit 1; fi +``` + +###### Verify Hash, Unpack & Install Package +```shell +# if [[ $(cat /tmp/hashes.txt) =~ $(sha512sum /tmp/haveno*.zip | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && mkdir -p /usr/share/desktop-directories && cd /tmp && unzip /tmp/haveno*.zip && apt install -y /tmp/haveno*.deb; else printf $'WARNING: Bad Hash!\n' && exit; fi +``` + +###### Verify Jar +```shell +# if [[ $(cat /tmp/desktop*.SHA-256) =~ $(sha256sum /opt/haveno/lib/app/desktop*.jar | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && printf 'Happy trading!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi +``` + +#### *TemplateVM Building From Source via `git` Repository (Scripted)* +##### In `dispXXXX` AppVM: +```shell +% bash haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh "" "" "" +``` + +

    Example:

    + +```shell +% bash haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh "https://download.bell-sw.com/java/21.0.6+10/bellsoft-jdk21.0.6+10-linux-amd64.deb" "a5e3fd9f5323de5fc188180c91e0caa777863b5b" "https://github.com/haveno-dex/haveno" +``` ++ Upon Successful Compilation & Packaging, A `Filecopy` Confirmation Will Be Presented + ++ Select "haveno-template" for "Target" of Pop-Up + ++ Click OK + +##### In `haveno-template` TemplateVM: +```shell +% sudo apt install -y ./QubesIncoming/dispXXXX/haveno.deb +``` + +#### *NetVM (Scripted)* +##### In `dispXXXX` AppVM: +```shell +$ qvm-copy haveno/scripts/install_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh +``` + ++ Select "sys-haveno" for "Target" Within Pop-Up + ++ Click "OK" Button + +##### In `sys-haveno` NetVM: +(Allow bootstrap process to complete) +```shell +% sudo zsh QubesIncoming/dispXXXX/2.0-haveno-netvm.sh +``` + +#### *NetVM (CLI)* +##### In `sys-haveno` NetVM: +###### Add `onion-grater` Profile +```shell +# onion-grater-add 40_haveno +``` + +###### Restart `onion-grater` Service +```shell +# systemctl restart onion-grater.service +# poweroff +``` + +#### *AppVM (Scripted)* +##### In `dispXXXX` AppVM: +```shell +$ qvm-copy haveno/scripts/install_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh +``` + ++ Select "haveno" for "Target" of Pop-Up + ++ Click OK + +##### In `haveno` AppVM: +```shell +% sudo zsh QubesIncoming/dispXXXX/3.0-haveno-appvm.sh +``` + +#### *AppVM (CLI)* +##### In `haveno` AppVM: +###### Adjust `sdwdate` Configuration +```shell +# mkdir /usr/local/etc/sdwdate-gui.d +# printf "gateway=sys-haveno\n" > /usr/local/etc/sdwdate-gui.d/50_user.conf +# systemctl restart sdwdate +``` + +###### Prepare Firewall Settings via `/rw/config/rc.local` +```shell +# printf "\n# Prepare Local FW Settings\nmkdir -p /usr/local/etc/whonix_firewall.d\n" >> /rw/config/rc.local +# printf "\n# Poke FW\nprintf \"EXTERNAL_OPEN_PORTS+=\\\\\" 9999 \\\\\"\\\n\" | tee /usr/local/etc/whonix_firewall.d/50_user.conf\n" >> /rw/config/rc.local +# printf "\n# Restart FW\nwhonix_firewall\n\n" >> /rw/config/rc.local +``` + +###### View & Verify Change +```shell +# tail /rw/config/rc.local +``` + +

    Confirm output contains:

    + +> # Poke FW +> printf "EXTERNAL_OPEN_PORTS+=\" 9999 \"\n" | tee /usr/local/etc/whonix_firewall.d/50_user.conf +> +> # Restart FW +> whonix_firewall + +###### Restart `whonix_firewall` +```shell +# whonix_firewall +``` + +###### Create `haveno-Haveno.desktop` +```shell +# mkdir -p /home/$(ls /home)/\.local/share/applications +# sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +# chown -R $(ls /home):$(ls /home) /home/$(ls /home)/.local/share/applications +``` + +###### View & Verify Change +```shell +# tail /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +``` + +

    Confirm output contains:

    + +> [Desktop Entry] +> Name=Haveno +> Comment=Haveno +> Exec=/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on +> Icon=/opt/haveno/lib/Haveno.png +> Terminal=false +> Type=Application +> Categories=Network +> MimeType= + +###### Poweroff +```shell +# poweroff +``` + +### **Remove TemplateVM, NetVM & AppVM:** +#### Scripted +##### In `dom0`: +```shell +$ bash /tmp/haveno/0.3-dom0.sh +``` + +#### GUI +##### Via `Qubes Manager`: + ++ Highlight "haveno" (AppVM) + ++ Click "Delete qube" + ++ Enter "haveno" + ++ Click "OK" Button + ++ Highlight "haveno-template" (TemplateVM) + ++ Click "Delete qube" + ++ Enter "haveno-template" + ++ Click "OK" Button + ++ Highlight "sys-haveno" (NetVM) + ++ Click "Delete qube" + ++ Enter "sys-haveno" + ++ Click "OK" Button + +#### CLI +##### In `dom0`: +```shell +$ qvm-shutdown --force --quiet haveno haveno-template sys-haveno && qvm-remove --force --quiet haveno haveno-template sys-haveno +``` diff --git a/scripts/install_whonix_qubes/README.md b/scripts/install_whonix_qubes/README.md new file mode 100644 index 0000000000..5bfda7419e --- /dev/null +++ b/scripts/install_whonix_qubes/README.md @@ -0,0 +1,75 @@ +# Install Haveno on Qubes/Whonix + + +After you already have [`Qubes`](https://www.qubes-os.org/downloads) or [`Whonix`](https://www.whonix.org/wiki/Download) installed: + +1. Download [scripts](https://github.com/haveno-dex/haveno/tree/master/install_whonix_qubes/scripts/install_whonix_qubes/scripts). +2. Move script(s) to their respective destination (`0.*-dom0.sh` -> `dom0`, `1.0-haveno-templatevm.sh` -> `haveno-template`, etc.). +3. Consecutively execute the following commands in their respective destinations. + +--- + +## **Create VMs** +[`Qubes`](https://www.qubes-os.org/downloads) +### **In `dom0`:** + +```shell +$ bash 0.0-dom0.sh && bash 0.1-dom0.sh && bash 0.2-dom0.sh +``` + +[`Whonix`](https://www.whonix.org/wiki/Download) On Anything Other Than [`Qubes`](https://www.qubes-os.org/downloads) + +- Clone `Whonix Workstation` To VM Named `haveno-template` +- Clone `Whonix Gateway` To VM Named `sys-haveno` +- Create New Linked VM Clone Based On `haveno-template` Named `haveno` + + +## **Build TemplateVM** +### *Via Binary Archive* +#### **In `haveno-template` `TemplateVM`:** + +```shell +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" +``` + +

    Example:

    + +```shell +% sudo bash 1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno-linux-deb.zip" "ABAF11C65A2970B130ABE3C479BE3E4300411886" +``` + +### *Via Source* +#### **In `dispXXXX` `AppVM`:** +```shell +% bash 1.0-haveno-templatevm.sh "" "" "" +``` + +

    Example:

    + +```shell +% bash 1.0-haveno-templatevm.sh "https://download.bell-sw.com/java/21.0.6+10/bellsoft-jdk21.0.6+10-linux-amd64.deb" "a5e3fd9f5323de5fc188180c91e0caa777863b5b" "https://github.com/haveno-dex/haveno" +``` + +#### **In `haveno-template` `TemplateVM`:** + +```shell +% sudo apt install -y haveno.deb +``` + +## **Build NetVM** +### **In `sys-haveno` `NetVM`:** + +```shell +% sudo zsh 3.0-haveno-appvm.sh +``` + +## **Build AppVM** +### **In `haveno` `AppVM`:** + +```shell +% sudo zsh 3.0-haveno-appvm.sh +``` + +--- + +Complete Documentation Can Be Found [Here](https://github.com/haveno-dex/haveno/blob/master/install_whonix_qubes/scripts/install_whonix_qubes/INSTALL.md). diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh new file mode 100644 index 0000000000..5618cf1e12 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh @@ -0,0 +1,6 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.0-dom0.sh + +## Create Haveno TemplateVM: +qvm-clone whonix-workstation-17 haveno-template + diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh new file mode 100644 index 0000000000..befa8b6702 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh @@ -0,0 +1,6 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.1-dom0.sh + +## Create Haveno NetVM: +qvm-create --template whonix-gateway-17 --class AppVM --label=orange --property memory=512 --property maxmem=512 --property netvm=sys-firewall sys-haveno && qvm-prefs --set sys-haveno provides_network True + diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh new file mode 100644 index 0000000000..6f52637632 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.2-dom0.sh + +## Create Haveno AppVM: +qvm-create --template haveno-template --class AppVM --label=orange --property memory=2048 --property maxmem=4096 --property netvm=sys-haveno haveno +printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist - haveno + diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh new file mode 100644 index 0000000000..4bdae35533 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh @@ -0,0 +1,6 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.3-dom0.sh + +## Remove Haveno GuestVMs +qvm-shutdown --force --quiet haveno haveno-template sys-haveno && qvm-remove --force --quiet haveno haveno-template sys-haveno + diff --git a/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh b/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh new file mode 100644 index 0000000000..f1ab43ae1b --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh @@ -0,0 +1,185 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/1.1-haveno-templatevm_maker.sh + + +function remote { + if [[ -z $PRECOMPILED_URL || -z $FINGERPRINT ]]; then + printf "\nNo arguments provided!\n\nThis script requires two arguments to be provided:\nBinary URL & PGP Fingerprint\n\nPlease review documentation and try again.\n\nExiting now ...\n" + exit 1 + fi + ## Update & Upgrade + apt update && apt upgrade -y + + + ## Install wget + apt install -y wget + + + ## Function to print messages in blue: + echo_blue() { + echo -e "\033[1;34m$1\033[0m" + } + + + # Function to print error messages in red: + echo_red() { + echo -e "\033[0;31m$1\033[0m" + } + + + ## Sweep for old release files + rm *.asc desktop-*-SNAPSHOT-all.jar.SHA-256 haveno* + + + ## Define URL & PGP Fingerprint etc. vars: + user_url=$PRECOMPILED_URL + base_url=$(printf ${user_url} | awk -F'/' -v OFS='/' '{$NF=""}1') + expected_fingerprint=$FINGERPRINT + binary_filename=$(awk -F'/' '{ print $NF }' <<< "$user_url") + package_filename="haveno.deb" + signature_filename="${binary_filename}.sig" + key_filename="$(printf "$expected_fingerprint" | tr -d ' ' | sed -E 's/.*(................)/\1/' )".asc + wget_flags="--tries=10 --timeout=10 --waitretry=5 --retry-connrefused --show-progress" + + + ## Debug: + printf "\nUser URL=$user_url\n" + printf "\nBase URL=$base_url\n" + printf "\nFingerprint=$expected_fingerprint\n" + printf "\nBinary Name=$binary_filename\n" + printf "\nPackage Name=$package_filename\n" + printf "\nSig Filename=$signature_filename\n" + printf "\nKey Filename=$key_filename\n" + + + ## Configure for tinyproxy: + export https_proxy=http://127.0.0.1:8082 + + + ## Download Haveno binary: + echo_blue "Downloading Haveno from URL provided ..." + wget "${wget_flags}" -cq "${user_url}" || { echo_red "Failed to download Haveno binary."; exit 1; } + + + ## Download Haveno signature file: + echo_blue "Downloading Haveno signature ..." + wget "${wget_flags}" -cq "${base_url}""${signature_filename}" || { echo_red "Failed to download Haveno signature."; exit 1; } + + + ## Download the GPG key: + echo_blue "Downloading signing GPG key ..." + wget "${wget_flags}" -cqO "${key_filename}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(echo "$expected_fingerprint" | tr -d ' ')" || { echo_red "Failed to download GPG key."; exit 1; } + + + ## Import the GPG key: + echo_blue "Importing the GPG key ..." + gpg --import "${key_filename}" || { echo_red "Failed to import GPG key."; exit 1; } + + + ## Extract imported fingerprints: + imported_fingerprints=$(gpg --with-colons --fingerprint | grep -A 1 'pub' | grep 'fpr' | cut -d: -f10 | tr -d '\n') + + + ## Remove spaces from the expected fingerprint for comparison: + formatted_expected_fingerprint=$(echo "${expected_fingerprint}" | tr -d ' ') + + + ## Check if the expected fingerprint is in the list of imported fingerprints: + if [[ ! "${imported_fingerprints}" =~ "${formatted_expected_fingerprint}" ]]; then + echo_red "The imported GPG key fingerprint does not match the expected fingerprint." + exit 1 + fi + + + ## Verify the downloaded binary with the signature: + echo_blue "Verifying the signature of the downloaded file ..." + if gpg --digest-algo SHA256 --verify "${signature_filename}" >/dev/null 2>&1; then + 7z x "${binary_filename}" && mv haveno*.deb "${package_filename}"; + else echo_red "Verification failed!" && sleep 5 + exit 1; + fi + + + echo_blue "Haveno binaries have been successfully verified." + + + # Install Haveno: + echo_blue "Installing Haveno ..." + apt install -y ./"${package_filename}" || { echo_red "Failed to install Haveno."; exit 1; } + + ## Finalize + echo_blue "Haveno TemplateVM installation and configuration complete." + echo_blue "\nHappy Trading\!\n" + printf "%s \n" "Press [ENTER] to complete ..." + read ans + #exit + poweroff +} + + +function build { + if [[ -z $JAVA_URL || -z $JAVA_SHA1 || -z $SOURCE_URL ]]; then + printf "\nNo arguments provided!\n\nThis script requires three argument to be provided:\n\nURL for Java 21 JDK Debian Package\n\nSHA1 Hash for Java 21 JDK Debian Package\n\nURL for Remote Git Source Repository\n\nPlease review documentation and try again.\n\nExiting now ...\n" + exit 1 + fi + # Dependancies + sudo apt install -y make git expect fakeroot binutils + + # Java + curl -fsSLo jdk21.deb ${JAVA_URL} + if [[ $(shasum ./jdk21.deb | awk '{ print $1 }') == ${JAVA_SHA1} ]] ; then printf $'SHA Hash IS valid!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi + sudo apt install -y ./jdk21.deb + + # Build + git clone --depth=1 $SOURCE_URL + GIT_DIR=$(awk -F'/' '{ print $NF }' <<< "$SOURCE_URL") + cd ${GIT_DIR} + git checkout master + sed -i 's|XMR_STAGENET|XMR_MAINNET|g' desktop/package/package.gradle + ./gradlew clean build --refresh-keys --refresh-dependencies + + # Package + # Expect + cat <> /tmp/haveno_package_deb.exp +set send_slow {1 .1} +proc send {ignore arg} { + sleep 1.1 + exp_send -s -- \$arg +} +set timeout -1 +spawn ./gradlew packageInstallers --console=plain +match_max 100000 +expect -exact "" +send -- "y\r" +expect -exact "" +send -- "y\r" +expect -exact "" +send -- "y\r" +expect -exact "app-image" +send -- \x03 +expect eof +DONE + + # Package + expect -f /tmp/haveno_package_deb.exp && find ./ -name '*.deb' -exec qvm-copy {} \; + printf "\nHappy Trading!\n" + +} + +if ! [[ $# -eq 2 || $# -eq 3 ]] ; then + printf "\nFor this script to function, user supplied arguments are required.\n\n" + printf "\nPlease review documentation and try again.\n\n" +fi + +if [[ $# -eq 2 ]] ; then + PRECOMPILED_URL=$1 + FINGERPRINT=$2 + remote +fi + +if [[ $# -eq 3 ]] ; then + JAVA_URL=$1 + JAVA_SHA1=$2 + SOURCE_URL=$3 + build +fi diff --git a/scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh b/scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh new file mode 100644 index 0000000000..d29e61dcf5 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh @@ -0,0 +1,30 @@ +#!/bin/zsh +## ./haveno-on-qubes/scripts/2.0-haveno-netvm_taker.sh + +## Function to print messages in blue: +echo_blue() { + echo -e "\033[1;34m$1\033[0m" +} + + +# Function to print error messages in red: +echo_red() { + echo -e "\033[0;31m$1\033[0m" +} + + +## onion-grater +# Add onion-grater Profile +echo_blue "\nAdding onion-grater Profile ..." +onion-grater-add 40_haveno + + +# Restart onion-grater +echo_blue "\nRestarting onion-grater Service ..." +systemctl restart onion-grater.service +echo_blue "Haveno NetVM configuration complete." +printf "%s \n" "Press [ENTER] to complete ..." +read ans +#exit +poweroff + diff --git a/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh b/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh new file mode 100644 index 0000000000..11582a8314 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh @@ -0,0 +1,61 @@ +#!/bin/zsh +## ./haveno-on-qubes/scripts/3.0-haveno-appvm_taker.sh + +## Function to print messages in blue: +echo_blue() { + echo -e "\033[1;34m$1\033[0m" +} + + +# Function to print error messages in red: +echo_red() { + echo -e "\033[0;31m$1\033[0m" +} + + +## Adjust sdwdate Configuration +mkdir -p /usr/local/etc/sdwdate-gui.d +printf "gateway=sys-haveno\n" > /usr/local/etc/sdwdate-gui.d/50_user.conf +systemctl restart sdwdate + + +## Prepare Firewall Settings +echo_blue "\nConfiguring FW ..." +printf "\n# Prepare Local FW Settings\nmkdir -p /usr/local/etc/whonix_firewall.d\n" >> /rw/config/rc.local +printf "\n# Poke FW\nprintf \"EXTERNAL_OPEN_PORTS+=\\\\\" 9999 \\\\\"\\\n\" | tee /usr/local/etc/whonix_firewall.d/50_user.conf\n" >> /rw/config/rc.local +printf "\n# Restart FW\nwhonix_firewall\n\n" >> /rw/config/rc.local + + +## View & Verify Change +echo_blue "\nReview the following output and be certain in matches documentation!\n" +tail /rw/config/rc.local +printf "%s \n" "Press [ENTER] to continue ..." +read ans +: + + +## Restart FW +echo_blue "\nRestarting Whonix FW ..." +whonix_firewall + + +### Create Desktop Launcher: +echo_blue "Creating desktop launcher ..." +mkdir -p /home/$(ls /home)/\.local/share/applications +sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --torControlUseSafeCookieAuth --torControlCookieFile=/var/run/tor/control.authcookie --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +chown -R $(ls /home):$(ls /home) /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop + + +## View & Verify Change +echo_blue "\nReview the following output and be certain in matches documentation!\n" +tail /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +printf "%s \n" "Press [ENTER] to continue ..." +read ans +: + +echo_blue "Haveno AppVM configuration complete." +echo_blue "Refresh applications via Qubes Manager GUI now." +printf "%s \n" "Press [ENTER] to complete ..." +read ans +#exit +poweroff From b7c9dea5185b410af4b5fb494ae3a102027078a7 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 16 Mar 2025 10:21:58 -0400 Subject: [PATCH 181/371] fix links in whonix instructions --- scripts/install_whonix_qubes/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/install_whonix_qubes/README.md b/scripts/install_whonix_qubes/README.md index 5bfda7419e..72670e41ca 100644 --- a/scripts/install_whonix_qubes/README.md +++ b/scripts/install_whonix_qubes/README.md @@ -3,7 +3,7 @@ After you already have [`Qubes`](https://www.qubes-os.org/downloads) or [`Whonix`](https://www.whonix.org/wiki/Download) installed: -1. Download [scripts](https://github.com/haveno-dex/haveno/tree/master/install_whonix_qubes/scripts/install_whonix_qubes/scripts). +1. Download [scripts](https://github.com/haveno-dex/haveno/tree/master/scripts/install_whonix_qubes/scripts). 2. Move script(s) to their respective destination (`0.*-dom0.sh` -> `dom0`, `1.0-haveno-templatevm.sh` -> `haveno-template`, etc.). 3. Consecutively execute the following commands in their respective destinations. @@ -72,4 +72,4 @@ $ bash 0.0-dom0.sh && bash 0.1-dom0.sh && bash 0.2-dom0.sh --- -Complete Documentation Can Be Found [Here](https://github.com/haveno-dex/haveno/blob/master/install_whonix_qubes/scripts/install_whonix_qubes/INSTALL.md). +Complete Documentation Can Be Found [Here](https://github.com/haveno-dex/haveno/blob/master/scripts/install_whonix_qubes/INSTALL.md). From cb25a23779855d64b15de5ac9fedb4ce136f35e6 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 14 Mar 2025 08:55:47 -0400 Subject: [PATCH 182/371] refactor message resending, reprocessing, and ack handling --- .../main/java/haveno/core/trade/Trade.java | 77 +++++-------- .../core/trade/protocol/ProcessModel.java | 84 ++++++-------- .../core/trade/protocol/SellerProtocol.java | 49 ++++---- .../haveno/core/trade/protocol/TradePeer.java | 100 +++++++++++++++- .../core/trade/protocol/TradeProtocol.java | 108 ++++++++++++------ .../tasks/BuyerSendPaymentSentMessage.java | 23 ++-- ...yerSendPaymentSentMessageToArbitrator.java | 21 +--- .../BuyerSendPaymentSentMessageToSeller.java | 14 +-- .../SellerSendPaymentReceivedMessage.java | 85 +++++++++++++- ...llerSendPaymentReceivedMessageToBuyer.java | 24 ++++ .../tasks/SendDepositsConfirmedMessage.java | 27 +++-- ...dDepositsConfirmedMessageToArbitrator.java | 12 +- .../SendDepositsConfirmedMessageToBuyer.java | 12 +- .../SendDepositsConfirmedMessageToSeller.java | 12 +- .../pendingtrades/PendingTradesViewModel.java | 3 +- .../steps/seller/SellerStep3View.java | 1 + proto/src/main/proto/pb.proto | 10 +- 17 files changed, 419 insertions(+), 243 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 13594cc3a1..72d77de7d6 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -46,7 +46,6 @@ import haveno.common.taskrunner.Model; import haveno.common.util.Utilities; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; -import haveno.core.network.MessageState; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; @@ -195,7 +194,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { SELLER_SENT_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), - SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED); + SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), + BUYER_RECEIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED); @NotNull public Phase getPhase() { @@ -603,12 +603,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - // notified from TradeProtocol of ack messages - public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { + // notified from TradeProtocol of ack messages + public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { for (TradeListener listener : new ArrayList(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception listener.onAckMessage(ackMessage, sender); } - } + } /////////////////////////////////////////////////////////////////////////////////////////// @@ -618,8 +618,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public void initialize(ProcessModelServiceProvider serviceProvider) { if (isInitialized) throw new IllegalStateException(getClass().getSimpleName() + " " + getId() + " is already initialized"); - // done if payout unlocked and marked complete - if (isPayoutUnlocked() && isCompleted()) { + // skip initialization if trade is complete + // starting in v1.0.19, seller resends payment received message until acked or stored in mailbox + if (isPayoutUnlocked() && isCompleted() && !getProtocol().needsToResendPaymentReceivedMessages()) { clearAndShutDown(); return; } @@ -733,15 +734,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { xmrWalletService.addWalletListener(idlePayoutSyncer); } - // TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed in v1.0.19 so this check can be removed? - if (isBuyer()) { - MessageState expectedState = getPaymentSentMessageState(); - if (expectedState != null && expectedState != processModel.getPaymentSentMessageStatePropertySeller().get()) { - log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStatePropertySeller().get()); - processModel.getPaymentSentMessageStatePropertySeller().set(expectedState); - } - } - // handle confirmations walletHeight.addListener((observable, oldValue, newValue) -> { importMultisigHexIfScheduled(); @@ -771,11 +763,20 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - // start polling if deposit requested - if (isDepositRequested()) tryInitPolling(); + // init syncing if deposit requested + if (isDepositRequested()) { + tryInitSyncing(); + } isFullyInitialized = true; } + public void reprocessApplicableMessages() { + if (!isDepositRequested() || isPayoutUnlocked() || isCompleted()) return; + getProtocol().maybeReprocessPaymentSentMessage(false); + getProtocol().maybeReprocessPaymentReceivedMessage(false); + HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); + } + public void awaitInitialized() { while (!isFullyInitialized) HavenoUtils.waitFor(100); // TODO: use proper notification and refactor isInitialized, fullyInitialized, and arbitrator idling } @@ -1535,7 +1536,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { peer.setUpdatedMultisigHex(null); peer.setDisputeClosedMessage(null); peer.setPaymentSentMessage(null); - peer.setPaymentReceivedMessage(null); + if (peer.isPaymentReceivedMessageReceived()) peer.setPaymentReceivedMessage(null); } } @@ -2049,25 +2050,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { throw new IllegalArgumentException("Trade is not buyer, seller, or arbitrator"); } - public MessageState getPaymentSentMessageState() { - if (isPaymentReceived()) return MessageState.ACKNOWLEDGED; - if (processModel.getPaymentSentMessageStatePropertySeller().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; - switch (state) { - case BUYER_SENT_PAYMENT_SENT_MSG: - return MessageState.SENT; - case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: - return MessageState.ARRIVED; - case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG: - return MessageState.STORED_IN_MAILBOX; - case SELLER_RECEIVED_PAYMENT_SENT_MSG: - return MessageState.ACKNOWLEDGED; - case BUYER_SEND_FAILED_PAYMENT_SENT_MSG: - return MessageState.FAILED; - default: - return null; - } - } - public String getPeerRole(TradePeer peer) { if (peer == getBuyer()) return "Buyer"; if (peer == getSeller()) return "Seller"; @@ -2444,11 +2426,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // sync and reprocess messages on new thread if (isInitialized && connection != null && !Boolean.FALSE.equals(xmrConnectionService.isConnected())) { - ThreadUtils.execute(() -> tryInitPolling(), getId()); + ThreadUtils.execute(() -> tryInitSyncing(), getId()); } } } - private void tryInitPolling() { + + private void tryInitSyncing() { if (isShutDownStarted) return; // set known deposit txs @@ -2457,24 +2440,18 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // start polling if (!isIdling()) { - tryInitPollingAux(); + doTryInitSyncing(); } else { long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getPollPeriod()); // random time to start polling UserThread.runAfter(() -> ThreadUtils.execute(() -> { - if (!isShutDownStarted) tryInitPollingAux(); + if (!isShutDownStarted) doTryInitSyncing(); }, getId()), startSyncingInMs / 1000l); } } - private void tryInitPollingAux() { + private void doTryInitSyncing() { if (!wasWalletSynced) trySyncWallet(true); updatePollPeriod(); - - // reprocess pending messages - getProtocol().maybeReprocessPaymentSentMessage(false); - getProtocol().maybeReprocessPaymentReceivedMessage(false); - HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); - startPolling(); } @@ -2825,7 +2802,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!isShutDownStarted) wallet = getWallet(); restartInProgress = false; pollWallet(); - if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId()); + if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitSyncing(), getId()); } private void setStateDepositsSeen() { diff --git a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java index 8521174ca8..209e28afd5 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -44,6 +44,7 @@ import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.network.MessageState; import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; @@ -73,6 +74,9 @@ import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import javax.annotation.Nullable; + +import java.util.Arrays; +import java.util.List; import java.util.Optional; // Fields marked as transient are only used during protocol execution which are based on directMessages so we do not @@ -161,15 +165,11 @@ public class ProcessModel implements Model, PersistablePayload { @Getter @Setter private boolean importMultisigHexScheduled; - - // We want to indicate the user the state of the message delivery of the - // PaymentSentMessage. As well we do an automatic re-send in case it was not ACKed yet. - // To enable that even after restart we persist the state. - @Setter - private ObjectProperty paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED); - @Setter - private ObjectProperty paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); private ObjectProperty paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false); + @Deprecated + private ObjectProperty paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED); + @Deprecated + private ObjectProperty paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) { this(offerId, accountId, pubKeyRing, new TradePeer(), new TradePeer(), new TradePeer()); @@ -191,6 +191,31 @@ public class ProcessModel implements Model, PersistablePayload { this.offer = offer; this.provider = provider; this.tradeManager = tradeManager; + for (TradePeer peer : getTradePeers()) { + peer.applyTransient(tradeManager); + } + + // migrate deprecated fields to new model for v1.0.19 + if (paymentSentMessageStatePropertySeller.get() != MessageState.UNDEFINED && getSeller().getPaymentSentMessageStateProperty().get() == MessageState.UNDEFINED) { + getSeller().getPaymentSentMessageStateProperty().set(paymentSentMessageStatePropertySeller.get()); + tradeManager.requestPersistence(); + } + if (paymentSentMessageStatePropertyArbitrator.get() != MessageState.UNDEFINED && getArbitrator().getPaymentSentMessageStateProperty().get() == MessageState.UNDEFINED) { + getArbitrator().getPaymentSentMessageStateProperty().set(paymentSentMessageStatePropertyArbitrator.get()); + tradeManager.requestPersistence(); + } + } + + private List getTradePeers() { + return Arrays.asList(maker, taker, arbitrator); + } + + private TradePeer getBuyer() { + return offer.getDirection() == OfferDirection.BUY ? maker : taker; + } + + private TradePeer getSeller() { + return offer.getDirection() == OfferDirection.BUY ? taker : maker; } @@ -245,14 +270,13 @@ public class ProcessModel implements Model, PersistablePayload { processModel.setTradeFeeAddress(ProtoUtil.stringOrNullFromProto(proto.getTradeFeeAddress())); processModel.setMultisigAddress(ProtoUtil.stringOrNullFromProto(proto.getMultisigAddress())); + // deprecated fields need to be read in order to migrate to new fields String paymentSentMessageStateSellerString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateSeller()); MessageState paymentSentMessageStateSeller = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateSellerString); - processModel.setPaymentSentMessageStateSeller(paymentSentMessageStateSeller); - + processModel.paymentSentMessageStatePropertySeller.set(paymentSentMessageStateSeller); String paymentSentMessageStateArbitratorString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateArbitrator()); MessageState paymentSentMessageStateArbitrator = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateArbitratorString); - processModel.setPaymentSentMessageStateArbitrator(paymentSentMessageStateArbitrator); - + processModel.paymentSentMessageStatePropertyArbitrator.set(paymentSentMessageStateArbitrator); return processModel; } @@ -279,40 +303,8 @@ public class ProcessModel implements Model, PersistablePayload { return getP2PService().getAddress(); } - void setPaymentSentAckMessageSeller(AckMessage ackMessage) { - MessageState messageState = ackMessage.isSuccess() ? - MessageState.ACKNOWLEDGED : - MessageState.FAILED; - setPaymentSentMessageStateSeller(messageState); - } - - void setPaymentSentAckMessageArbitrator(AckMessage ackMessage) { - MessageState messageState = ackMessage.isSuccess() ? - MessageState.ACKNOWLEDGED : - MessageState.FAILED; - setPaymentSentMessageStateArbitrator(messageState); - } - - public void setPaymentSentMessageStateSeller(MessageState paymentSentMessageStateProperty) { - this.paymentSentMessageStatePropertySeller.set(paymentSentMessageStateProperty); - if (tradeManager != null) { - tradeManager.requestPersistence(); - } - } - - public void setPaymentSentMessageStateArbitrator(MessageState paymentSentMessageStateProperty) { - this.paymentSentMessageStatePropertyArbitrator.set(paymentSentMessageStateProperty); - if (tradeManager != null) { - tradeManager.requestPersistence(); - } - } - - public boolean isPaymentSentMessageAckedBySeller() { - return paymentSentMessageStatePropertySeller.get() == MessageState.ACKNOWLEDGED; - } - - public boolean isPaymentSentMessageAckedByArbitrator() { - return paymentSentMessageStatePropertyArbitrator.get() == MessageState.ACKNOWLEDGED; + public boolean isPaymentReceivedMessagesReceived() { + return getArbitrator().isPaymentReceivedMessageReceived() && getBuyer().isPaymentReceivedMessageReceived(); } void setDepositTxSentAckMessage(AckMessage ackMessage) { diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java index 2b7f45877a..4daa800229 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -53,6 +53,9 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SellerProtocol extends DisputeProtocol { + + private static final long RESEND_PAYMENT_RECEIVED_MSGS_AFTER = 1741629525730L; // Mar 10 2025 17:58 UTC + enum SellerEvent implements FluentProtocol.Event { STARTUP, DEPOSIT_TXS_CONFIRMED, @@ -69,31 +72,37 @@ public class SellerProtocol extends DisputeProtocol { // re-send payment received message if payout not published ThreadUtils.execute(() -> { - if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; + if (!needsToResendPaymentReceivedMessages()) return; synchronized (trade.getLock()) { - if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; - if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.isPayoutPublished()) { - latchTrade(); - given(anyPhase(Trade.Phase.PAYMENT_RECEIVED) - .with(SellerEvent.STARTUP)) - .setup(tasks( - SellerSendPaymentReceivedMessageToBuyer.class, - SellerSendPaymentReceivedMessageToArbitrator.class) - .using(new TradeTaskRunner(trade, - () -> { - unlatchTrade(); - }, - (errorMessage) -> { - log.warn("Error sending PaymentReceivedMessage on startup: " + errorMessage); - unlatchTrade(); - }))) - .executeTasks(); - awaitTradeLatch(); - } + if (!needsToResendPaymentReceivedMessages()) return; + latchTrade(); + given(anyPhase(Trade.Phase.PAYMENT_RECEIVED) + .with(SellerEvent.STARTUP)) + .setup(tasks( + SellerSendPaymentReceivedMessageToBuyer.class, + SellerSendPaymentReceivedMessageToArbitrator.class) + .using(new TradeTaskRunner(trade, + () -> { + unlatchTrade(); + }, + (errorMessage) -> { + log.warn("Error sending PaymentReceivedMessage on startup: " + errorMessage); + unlatchTrade(); + }))) + .executeTasks(); + awaitTradeLatch(); } }, trade.getId()); } + public boolean needsToResendPaymentReceivedMessages() { + return !trade.isShutDownStarted() && trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.getProcessModel().isPaymentReceivedMessagesReceived() && resendPaymentReceivedMessagesEnabled(); + } + + private boolean resendPaymentReceivedMessagesEnabled() { + return trade.getTakeOfferDate().getTime() > RESEND_PAYMENT_RECEIVED_MSGS_AFTER; + } + @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); diff --git a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java index eeef2d4daf..11c035a329 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java @@ -24,12 +24,17 @@ import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.core.account.witness.AccountAgeWitness; +import haveno.core.network.MessageState; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.dispute.messages.DisputeClosedMessage; +import haveno.core.trade.TradeManager; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; +import haveno.network.p2p.AckMessage; import haveno.network.p2p.NodeAddress; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -57,6 +62,7 @@ public final class TradePeer implements PersistablePayload { @Nullable transient private byte[] preparedDepositTx; transient private MoneroTxWallet depositTx; + transient private TradeManager tradeManager; // Persistable mutable @Nullable @@ -96,7 +102,6 @@ public final class TradePeer implements PersistablePayload { @Getter private DisputeClosedMessage disputeClosedMessage; - // added in v 0.6 @Nullable private byte[] accountAgeWitnessNonce; @@ -142,13 +147,32 @@ public final class TradePeer implements PersistablePayload { private long payoutAmount; @Nullable private String updatedMultisigHex; - @Getter + @Deprecated + private boolean depositsConfirmedMessageAcked; + + // We want to indicate the user the state of the message delivery of the payment + // confirmation messages. We do an automatic re-send in case it was not ACKed yet. + // To enable that even after restart we persist the state. @Setter - boolean depositsConfirmedMessageAcked; + private ObjectProperty depositsConfirmedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + @Setter + private ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + @Setter + private ObjectProperty paymentReceivedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); public TradePeer() { } + public void applyTransient(TradeManager tradeManager) { + this.tradeManager = tradeManager; + + // migrate deprecated fields to new model for v1.0.19 + if (depositsConfirmedMessageAcked && depositsConfirmedMessageStateProperty.get() == MessageState.UNDEFINED) { + depositsConfirmedMessageStateProperty.set(MessageState.ACKNOWLEDGED); + tradeManager.requestPersistence(); + } + } + public BigInteger getDepositTxFee() { return BigInteger.valueOf(depositTxFee); } @@ -181,6 +205,60 @@ public final class TradePeer implements PersistablePayload { this.payoutAmount = payoutAmount.longValueExact(); } + void setDepositsConfirmedAckMessage(AckMessage ackMessage) { + MessageState messageState = ackMessage.isSuccess() ? + MessageState.ACKNOWLEDGED : + MessageState.FAILED; + setDepositsConfirmedMessageState(messageState); + } + + void setPaymentSentAckMessage(AckMessage ackMessage) { + MessageState messageState = ackMessage.isSuccess() ? + MessageState.ACKNOWLEDGED : + MessageState.FAILED; + setPaymentSentMessageState(messageState); + } + + void setPaymentReceivedAckMessage(AckMessage ackMessage) { + MessageState messageState = ackMessage.isSuccess() ? + MessageState.ACKNOWLEDGED : + MessageState.FAILED; + setPaymentReceivedMessageState(messageState); + } + + public void setDepositsConfirmedMessageState(MessageState depositsConfirmedMessageStateProperty) { + this.depositsConfirmedMessageStateProperty.set(depositsConfirmedMessageStateProperty); + if (tradeManager != null) { + tradeManager.requestPersistence(); + } + } + + public void setPaymentSentMessageState(MessageState paymentSentMessageStateProperty) { + this.paymentSentMessageStateProperty.set(paymentSentMessageStateProperty); + if (tradeManager != null) { + tradeManager.requestPersistence(); + } + } + + public void setPaymentReceivedMessageState(MessageState paymentReceivedMessageStateProperty) { + this.paymentReceivedMessageStateProperty.set(paymentReceivedMessageStateProperty); + if (tradeManager != null) { + tradeManager.requestPersistence(); + } + } + + public boolean isDepositsConfirmedMessageAcked() { + return depositsConfirmedMessageStateProperty.get() == MessageState.ACKNOWLEDGED; + } + + public boolean isPaymentSentMessageAcked() { + return paymentSentMessageStateProperty.get() == MessageState.ACKNOWLEDGED; + } + + public boolean isPaymentReceivedMessageReceived() { + return paymentReceivedMessageStateProperty.get() == MessageState.ACKNOWLEDGED || paymentReceivedMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX; + } + @Override public Message toProtoMessage() { final protobuf.TradePeer.Builder builder = protobuf.TradePeer.newBuilder(); @@ -221,6 +299,9 @@ public final class TradePeer implements PersistablePayload { Optional.ofNullable(payoutTxFee).ifPresent(e -> builder.setPayoutTxFee(payoutTxFee)); Optional.ofNullable(payoutAmount).ifPresent(e -> builder.setPayoutAmount(payoutAmount)); builder.setDepositsConfirmedMessageAcked(depositsConfirmedMessageAcked); + builder.setDepositsConfirmedMessageState(depositsConfirmedMessageStateProperty.get().name()); + builder.setPaymentSentMessageState(paymentSentMessageStateProperty.get().name()); + builder.setPaymentReceivedMessageState(paymentReceivedMessageStateProperty.get().name()); builder.setCurrentDate(currentDate); return builder.build(); @@ -270,6 +351,19 @@ public final class TradePeer implements PersistablePayload { tradePeer.setUnsignedPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex())); tradePeer.setPayoutTxFee(BigInteger.valueOf(proto.getPayoutTxFee())); tradePeer.setPayoutAmount(BigInteger.valueOf(proto.getPayoutAmount())); + + String depositsConfirmedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getDepositsConfirmedMessageState()); + MessageState depositsConfirmedMessageState = ProtoUtil.enumFromProto(MessageState.class, depositsConfirmedMessageStateString); + tradePeer.setDepositsConfirmedMessageState(depositsConfirmedMessageState); + + String paymentSentMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageState()); + MessageState paymentSentMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateString); + tradePeer.setPaymentSentMessageState(paymentSentMessageState); + + String paymentReceivedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentReceivedMessageState()); + MessageState paymentReceivedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentReceivedMessageStateString); + tradePeer.setPaymentReceivedMessageState(paymentReceivedMessageState); + return tradePeer; } } diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 08c99570f6..512db73983 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -252,6 +252,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService(); if (!trade.isCompleted()) mailboxMessageService.addDecryptedMailboxListener(this); handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages()); + + // reprocess applicable messages + trade.reprocessApplicableMessages(); } // send deposits confirmed message if applicable @@ -281,6 +284,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D }, trade.getId()); } + public boolean needsToResendPaymentReceivedMessages() { + return false; // seller protocol overrides + } + public void maybeReprocessPaymentSentMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { @@ -291,7 +298,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D return; } - log.warn("Reprocessing payment sent message for {} {}", trade.getClass().getSimpleName(), trade.getId()); + log.warn("Reprocessing PaymentSentMessage for {} {}", trade.getClass().getSimpleName(), trade.getId()); handle(trade.getBuyer().getPaymentSentMessage(), trade.getBuyer().getPaymentSentMessage().getSenderNodeAddress(), reprocessOnError); } }, trade.getId()); @@ -307,7 +314,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D return; } - log.warn("Reprocessing payment received message for {} {}", trade.getClass().getSimpleName(), trade.getId()); + log.warn("Reprocessing PaymentReceivedMessage for {} {}", trade.getClass().getSimpleName(), trade.getId()); handle(trade.getSeller().getPaymentReceivedMessage(), trade.getSeller().getPaymentReceivedMessage().getSenderNodeAddress(), reprocessOnError); } }, trade.getId()); @@ -710,47 +717,76 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private void onAckMessage(AckMessage ackMessage, NodeAddress sender) { - // handle ack for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time - if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) { - if (trade.getTradePeer(sender) == trade.getSeller()) { - processModel.setPaymentSentAckMessageSeller(ackMessage); - trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); - processModel.getTradeManager().requestPersistence(); - } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { - processModel.setPaymentSentAckMessageArbitrator(ackMessage); - processModel.getTradeManager().requestPersistence(); - } else if (!ackMessage.isSuccess()) { - String err = "Received AckMessage with error state for " + ackMessage.getSourceMsgClassName() + " from "+ sender + " with tradeId " + trade.getId() + " and errorMessage=" + ackMessage.getErrorMessage(); - log.warn(err); - return; // log error and ignore nack if not seller - } + // get trade peer + TradePeer peer = trade.getTradePeer(sender); + if (peer == null) { + if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getArbitrator().getNodeAddress()))) peer = trade.getArbitrator(); + else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getMaker().getNodeAddress()))) peer = trade.getMaker(); + else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getTaker().getNodeAddress()))) peer = trade.getTaker(); + } + if (peer == null) { + if (ackMessage.isSuccess()) log.warn("Received AckMessage from unknown peer for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); + else log.warn("Received AckMessage with error state from unknown peer for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); + return; } - if (ackMessage.isSuccess()) { - log.info("Received AckMessage for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); + // update sender's node address + if (!peer.getNodeAddress().equals(sender)) { + log.info("Updating peer's node address from {} to {} using ACK message to {}", peer.getNodeAddress(), sender, ackMessage.getSourceMsgClassName()); + peer.setNodeAddress(sender); + } - // handle ack for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time - if (ackMessage.getSourceMsgClassName().equals(DepositsConfirmedMessage.class.getSimpleName())) { - TradePeer peer = trade.getTradePeer(sender); - if (peer == null) { - - // get the applicable peer based on the sourceUid - if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getArbitrator().getNodeAddress()))) peer = trade.getArbitrator(); - else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getMaker().getNodeAddress()))) peer = trade.getMaker(); - else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getTaker().getNodeAddress()))) peer = trade.getTaker(); - } - if (peer == null) log.warn("Received AckMesage for DepositsConfirmedMessage for unknown peer: " + sender); - else peer.setDepositsConfirmedMessageAcked(true); - } - } else { - log.warn("Received AckMessage with error state for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); - - // set trade state on deposit request nack - if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) { + // set trade state on deposit request nack + if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) { + if (!ackMessage.isSuccess()) { trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); processModel.getTradeManager().requestPersistence(); } + } + // handle ack for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time + if (ackMessage.getSourceMsgClassName().equals(DepositsConfirmedMessage.class.getSimpleName())) { + peer.setDepositsConfirmedAckMessage(ackMessage); + processModel.getTradeManager().requestPersistence(); + } + + // handle ack for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time + if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) { + if (trade.getTradePeer(sender) == trade.getSeller()) { + trade.getSeller().setPaymentSentAckMessage(ackMessage); + if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); + else trade.setState(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); + processModel.getTradeManager().requestPersistence(); + } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { + trade.getArbitrator().setPaymentSentAckMessage(ackMessage); + processModel.getTradeManager().requestPersistence(); + } else { + log.warn("Received AckMessage from unexpected peer for {}, sender={}, trade={} {}, messageUid={}, success={}, errorMsg={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.isSuccess(), ackMessage.getErrorMessage()); + return; + } + } + + // handle ack for PaymentReceivedMessage, which automatically re-sends if not ACKed in a certain time + if (ackMessage.getSourceMsgClassName().equals(PaymentReceivedMessage.class.getSimpleName())) { + if (trade.getTradePeer(sender) == trade.getBuyer()) { + trade.getBuyer().setPaymentReceivedAckMessage(ackMessage); + if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG); + else trade.setState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); + processModel.getTradeManager().requestPersistence(); + } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { + trade.getArbitrator().setPaymentReceivedAckMessage(ackMessage); + processModel.getTradeManager().requestPersistence(); + } else { + log.warn("Received AckMessage from unexpected peer for {}, sender={}, trade={} {}, messageUid={}, success={}, errorMsg={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.isSuccess(), ackMessage.getErrorMessage()); + return; + } + } + + // generic handling + if (ackMessage.isSuccess()) { + log.info("Received AckMessage for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); + } else { + log.warn("Received AckMessage with error state for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); handleError(ackMessage.getErrorMessage()); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java index bc064399a3..86bb957577 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java @@ -142,26 +142,26 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask @Override protected void setStateSent() { - if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.SENT); tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.ARRIVED); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateStoredInMailbox() { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.STORED_IN_MAILBOX); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateFault() { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.FAILED); processModel.getTradeManager().requestPersistence(); } @@ -170,7 +170,7 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask timer.stop(); } if (listener != null) { - processModel.getPaymentSentMessageStatePropertySeller().removeListener(listener); + trade.getSeller().getPaymentReceivedMessageStateProperty().removeListener(listener); } } @@ -185,7 +185,6 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask return; } - log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); if (timer != null) { timer.stop(); } @@ -194,8 +193,8 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask if (resendCounter == 0) { listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); - processModel.getPaymentSentMessageStatePropertySeller().addListener(listener); - onMessageStateChange(processModel.getPaymentSentMessageStatePropertySeller().get()); + getReceiver().getPaymentSentMessageStateProperty().addListener(listener); + onMessageStateChange(getReceiver().getPaymentSentMessageStateProperty().get()); } // first re-send is after 2 minutes, then increase the delay exponentially @@ -212,12 +211,12 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask } private void onMessageStateChange(MessageState newValue) { - if (newValue == MessageState.ACKNOWLEDGED) { - trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); - processModel.getTradeManager().requestPersistence(); + if (isAckedByReceiver()) { cleanup(); } } - protected abstract boolean isAckedByReceiver(); + protected boolean isAckedByReceiver() { + return getReceiver().isPaymentSentMessageAcked(); + } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java index cd3098737a..9fea701200 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java @@ -38,26 +38,7 @@ public class BuyerSendPaymentSentMessageToArbitrator extends BuyerSendPaymentSen @Override protected void setStateSent() { + super.setStateSent(); complete(); // don't wait for message to arbitrator } - - @Override - protected void setStateFault() { - // state only updated on seller message - } - - @Override - protected void setStateStoredInMailbox() { - // state only updated on seller message - } - - @Override - protected void setStateArrived() { - // state only updated on seller message - } - - @Override - protected boolean isAckedByReceiver() { - return trade.getProcessModel().isPaymentSentMessageAckedByArbitrator(); - } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java index 825220d5b4..57ca170455 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java @@ -18,7 +18,6 @@ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; -import haveno.core.network.MessageState; import haveno.core.trade.Trade; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.TradePeer; @@ -40,25 +39,25 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes @Override protected void setStateSent() { - trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.SENT); + if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); super.setStateSent(); } @Override protected void setStateArrived() { - trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.ARRIVED); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG); super.setStateArrived(); } @Override protected void setStateStoredInMailbox() { - trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.STORED_IN_MAILBOX); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG); super.setStateStoredInMailbox(); } @Override protected void setStateFault() { - trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.FAILED); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); super.setStateFault(); } @@ -69,9 +68,4 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); complete(); } - - @Override - protected boolean isAckedByReceiver() { - return trade.getProcessModel().isPaymentSentMessageAckedBySeller(); - } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java index f08fe87946..202d4c8c79 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java @@ -35,11 +35,15 @@ package haveno.core.trade.protocol.tasks; import com.google.common.base.Charsets; + +import haveno.common.Timer; +import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; import haveno.common.taskrunner.TaskRunner; import haveno.core.account.sign.SignedWitness; import haveno.core.account.witness.AccountAgeWitnessService; +import haveno.core.network.MessageState; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.PaymentReceivedMessage; @@ -47,15 +51,23 @@ import haveno.core.trade.messages.TradeMailboxMessage; import haveno.core.trade.protocol.TradePeer; import haveno.core.util.JsonUtil; import haveno.network.p2p.NodeAddress; +import javafx.beans.value.ChangeListener; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkArgument; +import java.util.concurrent.TimeUnit; + @Slf4j @EqualsAndHashCode(callSuper = true) public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask { - SignedWitness signedWitness = null; + private SignedWitness signedWitness = null; + private ChangeListener listener; + private Timer timer; + private static final int MAX_RESEND_ATTEMPTS = 20; + private int delayInMin = 10; + private int resendCounter = 0; public SellerSendPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); @@ -77,6 +89,13 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag protected void run() { try { runInterceptHook(); + + // skip if already received + if (isReceived()) { + if (!isCompleted()) complete(); + return; + } + super.run(); } catch (Throwable t) { failed(t); @@ -134,29 +153,85 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag @Override protected void setStateSent() { - trade.advanceState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); log.info("{} sent: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.SENT); + tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateFault() { - trade.advanceState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); log.error("{} failed: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.FAILED); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateStoredInMailbox() { - trade.advanceState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG); log.info("{} stored in mailbox: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.STORED_IN_MAILBOX); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { - trade.advanceState(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG); log.info("{} arrived: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.ARRIVED); processModel.getTradeManager().requestPersistence(); } + + private void cleanup() { + if (timer != null) { + timer.stop(); + } + if (listener != null) { + trade.getBuyer().getPaymentReceivedMessageStateProperty().removeListener(listener); + } + } + + private void tryToSendAgainLater() { + + // skip if already received + if (isReceived()) return; + + if (resendCounter >= MAX_RESEND_ATTEMPTS) { + cleanup(); + log.warn("We never received an ACK message when sending the PaymentReceivedMessage to the peer. We stop trying to send the message."); + return; + } + + if (timer != null) { + timer.stop(); + } + + timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); + + if (resendCounter == 0) { + listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); + getReceiver().getPaymentReceivedMessageStateProperty().addListener(listener); + onMessageStateChange(getReceiver().getPaymentReceivedMessageStateProperty().get()); + } + + // first re-send is after 2 minutes, then increase the delay exponentially + if (resendCounter == 0) { + int shortDelay = 2; + log.info("We will send the message again to the peer after a delay of {} min.", shortDelay); + timer = UserThread.runAfter(this::run, shortDelay, TimeUnit.MINUTES); + } else { + log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); + timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); + delayInMin = (int) ((double) delayInMin * 1.5); + } + resendCounter++; + } + + private void onMessageStateChange(MessageState newValue) { + if (isReceived()) { + cleanup(); + } + } + + protected boolean isReceived() { + return getReceiver().isPaymentReceivedMessageReceived(); + } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java index 7228b40307..212dcb22f4 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java @@ -37,6 +37,30 @@ public class SellerSendPaymentReceivedMessageToBuyer extends SellerSendPaymentRe return trade.getBuyer(); } + @Override + protected void setStateSent() { + trade.advanceState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); + super.setStateSent(); + } + + @Override + protected void setStateFault() { + trade.advanceState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); + super.setStateFault(); + } + + @Override + protected void setStateStoredInMailbox() { + trade.advanceState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG); + super.setStateStoredInMailbox(); + } + + @Override + protected void setStateArrived() { + trade.advanceState(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG); + super.setStateArrived(); + } + // continue execution on fault so payment received message is sent to arbitrator @Override protected void onFault(String errorMessage, TradeMessage message) { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java index fb17d60d4d..ba20d74351 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java @@ -23,6 +23,7 @@ import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; +import haveno.core.network.MessageState; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositsConfirmedMessage; @@ -52,8 +53,8 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas try { runInterceptHook(); - // skip if already acked by receiver - if (isAckedByReceiver()) { + // skip if already acked or payout published + if (isAckedByReceiver() || trade.isPayoutPublished()) { if (!isCompleted()) complete(); return; } @@ -64,11 +65,17 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas } } - @Override - protected abstract NodeAddress getReceiverNodeAddress(); + protected abstract TradePeer getReceiver(); @Override - protected abstract PubKeyRing getReceiverPubKeyRing(); + protected NodeAddress getReceiverNodeAddress() { + return getReceiver().getNodeAddress(); + } + + @Override + protected PubKeyRing getReceiverPubKeyRing() { + return getReceiver().getPubKeyRing(); + } @Override protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { @@ -97,23 +104,24 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas @Override protected void setStateSent() { + getReceiver().setDepositsConfirmedMessageState(MessageState.SENT); tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { - // no additional handling + getReceiver().setDepositsConfirmedMessageState(MessageState.ARRIVED); } @Override protected void setStateStoredInMailbox() { - // no additional handling + getReceiver().setDepositsConfirmedMessageState(MessageState.STORED_IN_MAILBOX); } @Override protected void setStateFault() { - // no additional handling + getReceiver().setDepositsConfirmedMessageState(MessageState.FAILED); } private void cleanup() { @@ -151,7 +159,6 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas } private boolean isAckedByReceiver() { - TradePeer peer = trade.getTradePeer(getReceiverNodeAddress()); - return peer.isDepositsConfirmedMessageAcked(); + return getReceiver().isDepositsConfirmedMessageAcked(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java index baaa6ae987..ae8a171aa8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java @@ -17,10 +17,9 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; -import haveno.network.p2p.NodeAddress; +import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** @@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToArbitrator extends SendDepositsConfir } @Override - public NodeAddress getReceiverNodeAddress() { - return trade.getArbitrator().getNodeAddress(); - } - - @Override - public PubKeyRing getReceiverPubKeyRing() { - return trade.getArbitrator().getPubKeyRing(); + protected TradePeer getReceiver() { + return trade.getArbitrator(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java index 5795ce8947..bf1212c9a8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java @@ -17,10 +17,9 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; -import haveno.network.p2p.NodeAddress; +import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** @@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToBuyer extends SendDepositsConfirmedMe } @Override - public NodeAddress getReceiverNodeAddress() { - return trade.getBuyer().getNodeAddress(); - } - - @Override - public PubKeyRing getReceiverPubKeyRing() { - return trade.getBuyer().getPubKeyRing(); + protected TradePeer getReceiver() { + return trade.getBuyer(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java index efdf9a99cd..4ea097fd2b 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java @@ -17,10 +17,9 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; -import haveno.network.p2p.NodeAddress; +import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** @@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToSeller extends SendDepositsConfirmedM } @Override - public NodeAddress getReceiverNodeAddress() { - return trade.getSeller().getNodeAddress(); - } - - @Override - public PubKeyRing getReceiverPubKeyRing() { - return trade.getSeller().getPubKeyRing(); + protected TradePeer getReceiver() { + return trade.getSeller(); } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index b9936236c6..b19d6dcc26 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -200,7 +200,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { onPayoutStateChanged(state); }); - messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentSentMessageStatePropertySeller(), this::onPaymentSentMessageStateChanged); + messageStateSubscription = EasyBind.subscribe(trade.getSeller().getPaymentSentMessageStateProperty(), this::onPaymentSentMessageStateChanged); } } } @@ -425,6 +425,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel Date: Thu, 13 Mar 2025 09:08:37 -0400 Subject: [PATCH 183/371] automatically cancel offers with duplicate key images --- .../haveno/core/api/CoreOffersService.java | 1 + .../haveno/core/offer/OpenOfferManager.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index f036fb13ea..f9c450b825 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -265,6 +265,7 @@ public class CoreOffersService { if (!seenKeyImages.add(keyImage)) { for (Offer offer2 : offers) { if (offer == offer2) continue; + if (offer2.getOfferPayload().getReserveTxKeyImages() == null) continue; if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId()); duplicateFundedOffers.add(offer2); diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 32a1507f77..81d2b2b821 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -97,6 +97,7 @@ import haveno.network.p2p.peers.PeerManager; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -888,6 +889,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe List errorMessages = new ArrayList(); synchronized (processOffersLock) { List openOffers = getOpenOffers(); + removeOffersWithDuplicateKeyImages(openOffers); for (OpenOffer pendingOffer : openOffers) { if (pendingOffer.getState() != OpenOffer.State.PENDING) continue; if (skipOffersWithTooManyAttempts && pendingOffer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts @@ -919,6 +921,28 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }, THREAD_ID); } + private void removeOffersWithDuplicateKeyImages(List openOffers) { + + // collect offers with duplicate key images + Set keyImages = new HashSet<>(); + Set offersToRemove = new HashSet<>(); + for (OpenOffer openOffer : openOffers) { + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) continue; + if (Collections.disjoint(keyImages, openOffer.getOffer().getOfferPayload().getReserveTxKeyImages())) { + keyImages.addAll(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); + } else { + offersToRemove.add(openOffer); + } + } + + // remove offers with duplicate key images + for (OpenOffer offerToRemove : offersToRemove) { + log.warn("Removing open offer which has duplicate key images with other open offers: {}", offerToRemove.getId()); + doCancelOffer(offerToRemove); + openOffers.remove(offerToRemove); + } + } + private void processPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { // skip if already processing From 07fa0b35e40d72ff4142abd6d0a7249bba8d6308 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 13 Mar 2025 10:42:54 -0400 Subject: [PATCH 184/371] fix error message if arbitrator fails to publish deposit txs --- .../main/java/haveno/core/trade/protocol/ProcessModel.java | 1 + .../protocol/tasks/ArbitratorProcessDepositRequest.java | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java index 209e28afd5..ae6a27e7f0 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -94,6 +94,7 @@ public class ProcessModel implements Model, PersistablePayload { transient private ProcessModelServiceProvider provider; transient private TradeManager tradeManager; transient private Offer offer; + transient public Throwable error; // Added in v1.4.0 // MessageState of the last message sent from the seller to the buyer in the take offer process. diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java index be9d528f35..3a7fc7ace9 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java @@ -44,7 +44,6 @@ import java.util.UUID; @Slf4j public class ArbitratorProcessDepositRequest extends TradeTask { - private Throwable error; private boolean depositResponsesSent; @SuppressWarnings({"unused"}) @@ -68,7 +67,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { processDepositRequest(); complete(); } catch (Throwable t) { - this.error = t; + trade.getProcessModel().error = t; log.error("Error processing deposit request for trade {}: {}\n", trade.getId(), t.getMessage(), t); trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); failed(t); @@ -188,7 +187,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { trade.stateProperty().addListener((obs, oldState, newState) -> { if (oldState == newState) return; if (newState == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED) { - sendDepositResponsesOnce(error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : error.getMessage()); + sendDepositResponsesOnce(trade.getProcessModel().error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : trade.getProcessModel().error.getMessage()); } else if (newState.ordinal() >= Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS.ordinal()) { sendDepositResponsesOnce(null); } @@ -230,7 +229,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { } private void sendDepositResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, DepositResponse response) { - log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), error); + log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), trade.getProcessModel().error); processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() { @Override public void onArrived() { From b19724e33de95c465fe52acd6e68e7c9dd3ca19a Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 14 Mar 2025 08:26:11 -0400 Subject: [PATCH 185/371] fix summary info not populated on normal payout after dispute --- core/src/main/java/haveno/core/trade/Trade.java | 5 ++--- .../portfolio/pendingtrades/PendingTradesViewModel.java | 8 ++++++-- .../pendingtrades/steps/buyer/BuyerStep4View.java | 8 ++++---- .../pendingtrades/steps/seller/SellerStep4View.java | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 72d77de7d6..57767a5757 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1903,10 +1903,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { getSeller().setPayoutTxFee(splitTxFee); getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount())); getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee())); - } else if (getDisputeState().isClosed()) { + } else { DisputeResult disputeResult = getDisputeResult(); - if (disputeResult == null) log.warn("Dispute result is not set for {} {}", getClass().getSimpleName(), getId()); - else { + if (disputeResult != null) { BigInteger[] buyerSellerPayoutTxFees = ArbitrationManager.getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee()); getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]); getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index b19d6dcc26..1db4d944c0 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -416,8 +416,12 @@ public class PendingTradesViewModel extends ActivatableWithDataModel Date: Sat, 15 Mar 2025 06:45:40 -0400 Subject: [PATCH 186/371] share dispute opener's updated multisig info on dispute opened --- .../haveno/core/support/dispute/DisputeManager.java | 7 ++++--- .../support/dispute/messages/DisputeOpenedMessage.java | 10 +++++----- proto/src/main/proto/pb.proto | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index 64de6b3f2d..92512b56a8 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -578,8 +578,9 @@ public abstract class DisputeManager> extends Sup trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); } - // update multisig hex - if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex()); + // update opener's multisig hex + TradePeer opener = sender == trade.getArbitrator() ? trade.getTradePeer() : sender; + if (message.getOpenerUpdatedMultisigHex() != null) opener.setUpdatedMultisigHex(message.getOpenerUpdatedMultisigHex()); // add chat message with price info if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0); @@ -605,7 +606,7 @@ public abstract class DisputeManager> extends Sup if (trade.isArbitrator()) { TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker(); if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress()); - sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), trade.getSelf().getUpdatedMultisigHex()); + sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), opener.getUpdatedMultisigHex()); } tradeManager.requestPersistence(); errorMessage = null; diff --git a/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java b/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java index fb6fb2fc19..a11c6b1175 100644 --- a/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java +++ b/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java @@ -34,7 +34,7 @@ import java.util.Optional; public final class DisputeOpenedMessage extends DisputeMessage { private final Dispute dispute; private final NodeAddress senderNodeAddress; - private final String updatedMultisigHex; + private final String openerUpdatedMultisigHex; private final PaymentSentMessage paymentSentMessage; public DisputeOpenedMessage(Dispute dispute, @@ -67,7 +67,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { super(messageVersion, uid, supportType); this.dispute = dispute; this.senderNodeAddress = senderNodeAddress; - this.updatedMultisigHex = updatedMultisigHex; + this.openerUpdatedMultisigHex = updatedMultisigHex; this.paymentSentMessage = paymentSentMessage; } @@ -78,7 +78,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { .setDispute(dispute.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setType(SupportType.toProtoMessage(supportType)) - .setUpdatedMultisigHex(updatedMultisigHex); + .setOpenerUpdatedMultisigHex(openerUpdatedMultisigHex); Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage())); return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build(); } @@ -91,7 +91,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { proto.getUid(), messageVersion, SupportType.fromProto(proto.getType()), - ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), + ProtoUtil.stringOrNullFromProto(proto.getOpenerUpdatedMultisigHex()), proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null); } @@ -108,7 +108,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { ",\n DisputeOpenedMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + ",\n supportType=" + supportType + - ",\n updatedMultisigHex=" + updatedMultisigHex + + ",\n openerUpdatedMultisigHex=" + openerUpdatedMultisigHex + ",\n paymentSentMessage=" + paymentSentMessage + "\n} " + super.toString(); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index a814f71e4a..f919f4f1e7 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -385,7 +385,7 @@ message DisputeOpenedMessage { NodeAddress sender_node_address = 2; string uid = 3; SupportType type = 4; - string updated_multisig_hex = 5; + string opener_updated_multisig_hex = 5; PaymentSentMessage payment_sent_message = 6; } From 51fc4d0c41d625174939687b8596ed64b5717786 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 15 Mar 2025 06:44:34 -0400 Subject: [PATCH 187/371] do not export multisig info on dispute opened --- .../java/haveno/core/support/dispute/DisputeManager.java | 7 ------- .../portfolio/pendingtrades/PendingTradesDataModel.java | 7 ------- 2 files changed, 14 deletions(-) diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index 92512b56a8..047fe85456 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -399,13 +399,6 @@ public abstract class DisputeManager> extends Sup chatMessage.setSystemMessage(true); dispute.addAndPersistChatMessage(chatMessage); - // export latest multisig hex - try { - trade.exportMultisigHex(); - } catch (Exception e) { - log.error("Failed to export multisig hex", e); - } - // create dispute opened message NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 7649274aa1..ea93d145fb 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -553,13 +553,6 @@ public class PendingTradesDataModel extends ActivatableDataModel { disputeManager = arbitrationManager; Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket); - // export latest multisig hex - try { - trade.exportMultisigHex(); - } catch (Exception e) { - log.error("Failed to export multisig hex", e); - } - // send dispute opened message sendDisputeOpenedMessage(dispute, disputeManager); tradeManager.requestPersistence(); From d7be2885bdd5d866cccd5342e63c4eeb3db77665 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 20 Mar 2025 08:00:23 -0400 Subject: [PATCH 188/371] fix error fetching prices with --socks5ProxyXmrAddress config (#1658) --- p2p/src/main/java/haveno/network/Socks5ProxyProvider.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java b/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java index f9c498f08e..79317494bd 100644 --- a/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java +++ b/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java @@ -95,7 +95,9 @@ public class Socks5ProxyProvider { String[] tokens = socks5ProxyAddress.split(":"); if (tokens.length == 2) { try { - return new Socks5Proxy(tokens[0], Integer.valueOf(tokens[1])); + Socks5Proxy proxy = new Socks5Proxy(tokens[0], Integer.valueOf(tokens[1])); + proxy.resolveAddrLocally(false); + return proxy; } catch (UnknownHostException e) { log.error(ExceptionUtils.getStackTrace(e)); } From 5711aabad83e1cbcab0d494fba47aac52b841f94 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 18 Mar 2025 10:18:15 -0400 Subject: [PATCH 189/371] remove outdated code for v1.0.7 update --- .../support/dispute/arbitration/ArbitrationManager.java | 9 --------- .../protocol/tasks/ProcessPaymentReceivedMessage.java | 8 -------- .../tasks/SellerPreparePaymentReceivedMessage.java | 7 ------- 3 files changed, 24 deletions(-) diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 50be387c76..6ef9cd69ed 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -62,7 +62,6 @@ import haveno.core.support.dispute.messages.DisputeClosedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.messages.ChatMessage; import haveno.core.support.messages.SupportMessage; -import haveno.core.trade.BuyerTrade; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; @@ -464,14 +463,6 @@ public final class ArbitrationManager extends DisputeManager Date: Sun, 16 Mar 2025 18:18:48 -0400 Subject: [PATCH 190/371] do not process payment confirmation messages if shut down started --- .../haveno/core/trade/protocol/TradeProtocol.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 512db73983..372c26b5da 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -291,10 +291,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D public void maybeReprocessPaymentSentMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { + if (trade.isShutDownStarted()) return; synchronized (trade.getLock()) { // skip if no need to reprocess - if (trade.isBuyer() || trade.getBuyer().getPaymentSentMessage() == null || trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) { + if (trade.isShutDownStarted() || trade.isBuyer() || trade.getBuyer().getPaymentSentMessage() == null || trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) { return; } @@ -307,10 +308,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { + if (trade.isShutDownStarted()) return; synchronized (trade.getLock()) { // skip if no need to reprocess - if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) { + if (trade.isShutDownStarted() || trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) { return; } @@ -525,7 +527,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D trade.getBuyer().setPaymentSentMessage(message); trade.requestPersistence(); - if (!trade.isInitialized() || trade.isShutDown()) return; + if (!trade.isInitialized() || trade.isShutDownStarted()) return; if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); return; @@ -537,7 +539,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // TODO A better fix would be to add a listener for the wallet sync state and process // the mailbox msg once wallet is ready and trade state set. synchronized (trade.getLock()) { - if (!trade.isInitialized() || trade.isShutDown()) return; + if (!trade.isInitialized() || trade.isShutDownStarted()) return; if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) { log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); handleTaskRunnerSuccess(peer, message); @@ -604,14 +606,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D trade.getSeller().setPaymentReceivedMessage(message); trade.requestPersistence(); - if (!trade.isInitialized() || trade.isShutDown()) return; + if (!trade.isInitialized() || trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator"); return; } synchronized (trade.getLock()) { - if (!trade.isInitialized() || trade.isShutDown()) return; + if (!trade.isInitialized() || trade.isShutDownStarted()) return; latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); From 79aa214f2282e167c8b81346180ce4e4f66d4816 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 17 Mar 2025 07:47:39 -0400 Subject: [PATCH 191/371] check failed state to determine if payment sent or received --- core/src/main/java/haveno/core/trade/Trade.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 57767a5757..0f6606c91a 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2171,7 +2171,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isPaymentSent() { - return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal(); + return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal() && getState() != State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG; } public boolean hasPaymentReceivedMessage() { @@ -2189,7 +2189,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isPaymentReceived() { - return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal(); + return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal() && getState() != State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG; } public boolean isPayoutPublished() { From 5107c6ba578cd658f06b92e554eb0022d15944e0 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 17 Mar 2025 08:32:34 -0400 Subject: [PATCH 192/371] fixes to process payout tx and revert to payment sent state --- .../main/java/haveno/core/trade/Trade.java | 29 ++++++++++++++----- .../core/trade/protocol/SellerProtocol.java | 2 +- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 0f6606c91a..75ef6ead4c 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -644,7 +644,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // reset seller's payment received state if no ack receive if (this instanceof SellerTrade && getState().ordinal() >= Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) { log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); - setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + resetToPaymentSentState(); } // handle trade state events @@ -770,6 +770,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { isFullyInitialized = true; } + public void resetToPaymentSentState() { + setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + for (TradePeer peer : getAllPeers()) peer.setPaymentReceivedMessage(null); + setPayoutTxHex(null); + } + public void reprocessApplicableMessages() { if (!isDepositRequested() || isPayoutUnlocked() || isCompleted()) return; getProtocol().maybeReprocessPaymentSentMessage(false); @@ -1365,7 +1371,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0); - if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if not signed + if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if id currently unknown // verify payout tx has exactly 2 destinations if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations"); @@ -1396,6 +1402,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCostSplit); if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); + // update payout tx + updatePayout(payoutTx); + // check connection boolean doSign = sign && getPayoutTxHex() == null; if (doSign || publish) verifyDaemonConnection(); @@ -1404,19 +1413,15 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (doSign) { // sign tx + String signedPayoutTxHex; try { MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex); if (result.getSignedMultisigTxHex() == null) throw new IllegalArgumentException("Error signing payout tx, signed multisig hex is null"); - setPayoutTxHex(result.getSignedMultisigTxHex()); + signedPayoutTxHex = result.getSignedMultisigTxHex(); } catch (Exception e) { throw new IllegalStateException(e); } - // describe result - describedTxSet = wallet.describeMultisigTxSet(getPayoutTxHex()); - payoutTx = describedTxSet.getTxs().get(0); - updatePayout(payoutTx); - // verify fee is within tolerance by recreating payout tx // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getId()); @@ -1426,6 +1431,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal? if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee()); log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff); + + // set signed payout tx hex + setPayoutTxHex(signedPayoutTxHex); + + // describe result + describedTxSet = wallet.describeMultisigTxSet(getPayoutTxHex()); + payoutTx = describedTxSet.getTxs().get(0); + updatePayout(payoutTx); } // save trade state diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java index 4daa800229..021de47450 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -146,7 +146,7 @@ public class SellerProtocol extends DisputeProtocol { resultHandler.handleResult(); }, (errorMessage) -> { log.warn("Error confirming payment received, reverting state to {}, error={}", Trade.State.BUYER_SENT_PAYMENT_SENT_MSG, errorMessage); - trade.setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + trade.resetToPaymentSentState(); handleTaskRunnerFault(event, errorMessage); }))) .run(() -> trade.advanceState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT)) From 2af9019db0d5d8d1cfa4523b5dbd7faa8ae17fe0 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:24:16 -0400 Subject: [PATCH 193/371] do not reset payment states if payout published --- .../main/java/haveno/core/trade/Trade.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 75ef6ead4c..5a4ddeb2a4 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -635,16 +635,20 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ThreadUtils.execute(() -> onConnectionChanged(connection), getId()); }); - // reset buyer's payment sent state if no ack receive - if (this instanceof BuyerTrade && getState().ordinal() >= Trade.State.BUYER_CONFIRMED_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) { - log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); - setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); - } + // reset states if no ack receive + if (!isPayoutPublished()) { - // reset seller's payment received state if no ack receive - if (this instanceof SellerTrade && getState().ordinal() >= Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) { - log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); - resetToPaymentSentState(); + // reset buyer's payment sent state if no ack receive + if (this instanceof BuyerTrade && getState().ordinal() >= Trade.State.BUYER_CONFIRMED_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) { + log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); + setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); + } + + // reset seller's payment received state if no ack receive + if (this instanceof SellerTrade && getState().ordinal() >= Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) { + log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + resetToPaymentSentState(); + } } // handle trade state events From 26e3a153bc28ca34c222152ba9930b51075cc165 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 17 Mar 2025 10:14:33 -0400 Subject: [PATCH 194/371] process messages until trade is completely finished --- core/src/main/java/haveno/core/trade/Trade.java | 6 +++++- .../haveno/core/trade/protocol/TradeProtocol.java | 15 ++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 5a4ddeb2a4..19cab12662 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -620,7 +620,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // skip initialization if trade is complete // starting in v1.0.19, seller resends payment received message until acked or stored in mailbox - if (isPayoutUnlocked() && isCompleted() && !getProtocol().needsToResendPaymentReceivedMessages()) { + if (isFinished()) { clearAndShutDown(); return; } @@ -774,6 +774,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { isFullyInitialized = true; } + public boolean isFinished() { + return isPayoutUnlocked() && isCompleted() && !getProtocol().needsToResendPaymentReceivedMessages(); + } + public void resetToPaymentSentState() { setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); for (TradePeer peer : getAllPeers()) peer.setPaymentReceivedMessage(null); diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 372c26b5da..290e2cce6e 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -165,7 +165,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } else if (networkEnvelope instanceof AckMessage) { onAckMessage((AckMessage) networkEnvelope, peer); - trade.onAckMessage((AckMessage) networkEnvelope, peer); // notify trade listeners } } @@ -210,11 +209,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D onMailboxMessage(tradeMessage, mailboxMessage.getSenderNodeAddress()); } else if (mailboxMessage instanceof AckMessage) { AckMessage ackMessage = (AckMessage) mailboxMessage; - if (!trade.isCompleted()) { - // We only apply the msg if we have not already completed the trade - onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress()); - } - // In any case we remove the msg + onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress()); processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(ackMessage); log.info("Remove {} from the P2P network.", ackMessage.getClass().getSimpleName()); } @@ -242,7 +237,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected void onInitialized() { // listen for direct messages unless completed - if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this); + if (!trade.isFinished()) processModel.getP2PService().addDecryptedDirectMessageListener(this); // initialize trade synchronized (trade.getLock()) { @@ -719,6 +714,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private void onAckMessage(AckMessage ackMessage, NodeAddress sender) { + // ignore if trade is completely finished + if (trade.isFinished()) return; + // get trade peer TradePeer peer = trade.getTradePeer(sender); if (peer == null) { @@ -791,6 +789,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D log.warn("Received AckMessage with error state for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); handleError(ackMessage.getErrorMessage()); } + + // notify trade listeners + trade.onAckMessage(ackMessage, sender); } protected void sendAckMessage(NodeAddress peer, TradeMessage message, boolean result, @Nullable String errorMessage) { From bee86daff3b06636251becd9ed5230dee03d1833 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:40:11 -0400 Subject: [PATCH 195/371] increase grpc rate limit for offers and trades on testnet --- .../haveno/daemon/grpc/GrpcOffersService.java | 12 ++++++------ .../haveno/daemon/grpc/GrpcTradesService.java | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index 485ff38ca8..cbc6b6b2e3 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -203,12 +203,12 @@ class GrpcOffersService extends OffersImplBase { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ - put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, SECONDS)); - put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, SECONDS)); - put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, SECONDS)); - put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); + put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); + put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); + put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java index 0a5d2d39d2..2d650dfb53 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java @@ -251,15 +251,15 @@ class GrpcTradesService extends TradesImplBase { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ - put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 1, SECONDS)); - put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 1, SECONDS)); - put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 1, SECONDS)); + put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 1, SECONDS)); + put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(3, MINUTES)); - put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } From b5f9bc307baf2974f740b0134e476ef4074ed66c Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 20 Mar 2025 08:04:30 -0400 Subject: [PATCH 196/371] verify offer versions when signing --- .../main/java/haveno/common/app/Version.java | 19 +++++++++++++ .../haveno/core/app/DomainInitialisation.java | 7 ++--- .../haveno/core/filter/FilterManager.java | 4 +++ .../haveno/core/offer/OpenOfferManager.java | 28 ++++++++++++++++++- .../core/xmr/wallet/XmrWalletService.java | 4 +-- 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 74f3ceb9d6..1d7b99f747 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -72,6 +72,25 @@ public class Version { return false; } + public static int compare(String version1, String version2) { + if (version1.equals(version2)) + return 0; + else if (getMajorVersion(version1) > getMajorVersion(version2)) + return 1; + else if (getMajorVersion(version1) < getMajorVersion(version2)) + return -1; + else if (getMinorVersion(version1) > getMinorVersion(version2)) + return 1; + else if (getMinorVersion(version1) < getMinorVersion(version2)) + return -1; + else if (getPatchVersion(version1) > getPatchVersion(version2)) + return 1; + else if (getPatchVersion(version1) < getPatchVersion(version2)) + return -1; + else + return 0; + } + private static int getSubVersion(String version, int index) { final String[] split = version.split("\\."); checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version); diff --git a/core/src/main/java/haveno/core/app/DomainInitialisation.java b/core/src/main/java/haveno/core/app/DomainInitialisation.java index 646d80d9dd..220fb05040 100644 --- a/core/src/main/java/haveno/core/app/DomainInitialisation.java +++ b/core/src/main/java/haveno/core/app/DomainInitialisation.java @@ -178,6 +178,9 @@ public class DomainInitialisation { closedTradableManager.onAllServicesInitialized(); failedTradesManager.onAllServicesInitialized(); + filterManager.setFilterWarningHandler(filterWarningHandler); + filterManager.onAllServicesInitialized(); + openOfferManager.onAllServicesInitialized(); balances.onAllServicesInitialized(); @@ -199,10 +202,6 @@ public class DomainInitialisation { priceFeedService.setCurrencyCodeOnInit(); priceFeedService.startRequestingPrices(); - filterManager.setFilterWarningHandler(filterWarningHandler); - filterManager.onAllServicesInitialized(); - - mobileNotificationService.onAllServicesInitialized(); myOfferTakenEvents.onAllServicesInitialized(); tradeEvents.onAllServicesInitialized(); diff --git a/core/src/main/java/haveno/core/filter/FilterManager.java b/core/src/main/java/haveno/core/filter/FilterManager.java index cb7e0e9b21..2cebb66f3a 100644 --- a/core/src/main/java/haveno/core/filter/FilterManager.java +++ b/core/src/main/java/haveno/core/filter/FilterManager.java @@ -406,6 +406,10 @@ public class FilterManager { .anyMatch(e -> e.equals(address)); } + public String getDisableTradeBelowVersion() { + return getFilter() == null || getFilter().getDisableTradeBelowVersion() == null || getFilter().getDisableTradeBelowVersion().isEmpty() ? null : getFilter().getDisableTradeBelowVersion(); + } + public boolean requireUpdateToNewVersionForTrading() { if (getFilter() == null) { return false; diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 81d2b2b821..91e2284d46 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -737,7 +737,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe doCancelOffer(openOffer, true); } - // remove open offer which thaws its key images + // cancel open offer which thaws its key images private void doCancelOffer(@NotNull OpenOffer openOffer, boolean resetAddressEntries) { Offer offer = openOffer.getOffer(); offer.setState(Offer.State.REMOVED); @@ -1396,6 +1396,32 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } + // verify the trade protocol version + if (request.getOfferPayload().getProtocolVersion() != Version.TRADE_PROTOCOL_VERSION) { + errorMessage = "Unsupported protocol version: " + request.getOfferPayload().getProtocolVersion(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify the min version number + if (filterManager.getDisableTradeBelowVersion() != null) { + if (Version.compare(request.getOfferPayload().getVersionNr(), filterManager.getDisableTradeBelowVersion()) < 0) { + errorMessage = "Offer version number is too low: " + request.getOfferPayload().getVersionNr() + " < " + filterManager.getDisableTradeBelowVersion(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + } + + // verify the max version number + if (Version.compare(request.getOfferPayload().getVersionNr(), Version.VERSION) > 0) { + errorMessage = "Offer version number is too high: " + request.getOfferPayload().getVersionNr() + " > " + Version.VERSION; + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + // verify maker and taker fees boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0; if (hasBuyerAsTakerWithoutDeposit) { diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 97aef9545f..7bfc37f8e2 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -767,7 +767,7 @@ public class XmrWalletService extends XmrWalletBase { BigInteger minerFeeEstimate = getFeeEstimate(tx.getWeight()); double minerFeeDiff = tx.getFee().subtract(minerFeeEstimate).abs().doubleValue() / minerFeeEstimate.doubleValue(); if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee() + ", diff%=" + minerFeeDiff); - log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff); + log.info("Trade miner fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff); // verify proof to fee address BigInteger actualTradeFee = BigInteger.ZERO; @@ -785,7 +785,7 @@ public class XmrWalletService extends XmrWalletBase { // verify trade fee amount if (!actualTradeFee.equals(tradeFeeAmount)) { if (equalsWithinFractionError(actualTradeFee, tradeFeeAmount)) { - log.warn("Trade tx fee amount is within fraction error, expected " + tradeFeeAmount + " but was " + actualTradeFee); + log.warn("Trade fee amount is within fraction error, expected " + tradeFeeAmount + " but was " + actualTradeFee); } else { throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee); } From c95a26e043ce4c5c77c9a2515242a6128356e897 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:38:47 -0400 Subject: [PATCH 197/371] re-sign offers on protocol version update --- .../haveno/core/offer/OpenOfferManager.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 91e2284d46..6ae03a7042 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1876,27 +1876,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe originalOfferPayload.getChallengeHash(), updatedExtraDataMap, protocolVersion, - originalOfferPayload.getArbitratorSigner(), - originalOfferPayload.getArbitratorSignature(), - originalOfferPayload.getReserveTxKeyImages(), + null, + null, + null, originalOfferPayload.getExtraInfo()); - // Save states from original data to use for the updated - Offer.State originalOfferState = originalOffer.getState(); - OpenOffer.State originalOpenOfferState = originalOpenOffer.getState(); + // cancel old offer + log.info("Canceling outdated offer id={}", originalOffer.getId()); + doCancelOffer(originalOpenOffer, false); - // remove old offer - originalOffer.setState(Offer.State.REMOVED); - originalOpenOffer.setState(OpenOffer.State.CANCELED); - removeOpenOffer(originalOpenOffer); - - // Create new Offer + // create new offer Offer updatedOffer = new Offer(updatedPayload); updatedOffer.setPriceFeedService(priceFeedService); - updatedOffer.setState(originalOfferState); OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice()); - updatedOpenOffer.setState(originalOpenOfferState); addOpenOffer(updatedOpenOffer); requestPersistence(); From 028ced70212353fb3cafe46f5e4581f89f63b629 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:44:30 -0400 Subject: [PATCH 198/371] bump offer protocol version, do not verify miner fee for outdated trades --- .../main/java/haveno/common/app/Version.java | 5 +++-- .../main/java/haveno/core/trade/Trade.java | 22 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 1d7b99f747..3dc04889b8 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -110,8 +110,9 @@ public class Version { // For the switch to version 2, offers created with the old version will become invalid and have to be canceled. // For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening // the Haveno app. - // VERSION = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 - public static final int TRADE_PROTOCOL_VERSION = 1; + // Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 + // Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2 + public static final int TRADE_PROTOCOL_VERSION = 2; private static String p2pMessageVersion; public static String getP2PMessageVersion() { diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 19cab12662..eaec78a6b0 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1430,15 +1430,19 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { throw new IllegalStateException(e); } - // verify fee is within tolerance by recreating payout tx - // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? - log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getId()); - saveWallet(); // save wallet before creating fee estimate tx - MoneroTxWallet feeEstimateTx = createPayoutTx(); - BigInteger feeEstimate = feeEstimateTx.getFee(); - double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal? - if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee()); - log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff); + // verify miner fee is within tolerance unless outdated offer version + if (getOffer().getOfferPayload().getProtocolVersion() >= 2) { + + // verify fee is within tolerance by recreating payout tx + // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? + log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getId()); + saveWallet(); // save wallet before creating fee estimate tx + MoneroTxWallet feeEstimateTx = createPayoutTx(); + BigInteger feeEstimate = feeEstimateTx.getFee(); + double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal? + if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee()); + log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff); + } // set signed payout tx hex setPayoutTxHex(signedPayoutTxHex); From a7d8e4560f84d4f31460dacbb8ab6ab7fdace2e2 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 18 Mar 2025 19:10:56 -0400 Subject: [PATCH 199/371] enable resending payment received msgs based on offer protocol version --- .../main/java/haveno/core/trade/protocol/SellerProtocol.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java index 021de47450..2a01c5a2ca 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -54,8 +54,6 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SellerProtocol extends DisputeProtocol { - private static final long RESEND_PAYMENT_RECEIVED_MSGS_AFTER = 1741629525730L; // Mar 10 2025 17:58 UTC - enum SellerEvent implements FluentProtocol.Event { STARTUP, DEPOSIT_TXS_CONFIRMED, @@ -100,7 +98,7 @@ public class SellerProtocol extends DisputeProtocol { } private boolean resendPaymentReceivedMessagesEnabled() { - return trade.getTakeOfferDate().getTime() > RESEND_PAYMENT_RECEIVED_MSGS_AFTER; + return trade.getOffer().getOfferPayload().getProtocolVersion() >= 2; } @Override From ce27818f437963b441c9d15706e61217c38b08e2 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 20 Mar 2025 08:02:01 -0400 Subject: [PATCH 200/371] recover payment sent message state on startup if undefined --- .../main/java/haveno/core/trade/Trade.java | 29 +++++++++++++++++++ .../pendingtrades/PendingTradesViewModel.java | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index eaec78a6b0..114edcabe5 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -46,6 +46,7 @@ import haveno.common.taskrunner.Model; import haveno.common.util.Utilities; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; +import haveno.core.network.MessageState; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; @@ -738,6 +739,15 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { xmrWalletService.addWalletListener(idlePayoutSyncer); } + // TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed mismatch in v1.0.19, but can this check can be removed? + if (isBuyer()) { + MessageState expectedState = getPaymentSentMessageState(); + if (expectedState != null && expectedState != getSeller().getPaymentSentMessageStateProperty().get()) { + log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStatePropertySeller().get()); + getSeller().getPaymentSentMessageStateProperty().set(expectedState); + } + } + // handle confirmations walletHeight.addListener((observable, oldValue, newValue) -> { importMultisigHexIfScheduled(); @@ -2074,6 +2084,25 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { throw new IllegalArgumentException("Trade is not buyer, seller, or arbitrator"); } + private MessageState getPaymentSentMessageState() { + if (isPaymentReceived()) return MessageState.ACKNOWLEDGED; + if (getSeller().getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; + switch (state) { + case BUYER_SENT_PAYMENT_SENT_MSG: + return MessageState.SENT; + case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: + return MessageState.ARRIVED; + case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG: + return MessageState.STORED_IN_MAILBOX; + case SELLER_RECEIVED_PAYMENT_SENT_MSG: + return MessageState.ACKNOWLEDGED; + case BUYER_SEND_FAILED_PAYMENT_SENT_MSG: + return MessageState.FAILED; + default: + return null; + } + } + public String getPeerRole(TradePeer peer) { if (peer == getBuyer()) return "Buyer"; if (peer == getSeller()) return "Seller"; diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index 1db4d944c0..6a34504dad 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -424,7 +424,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel Date: Sat, 22 Mar 2025 07:57:31 -0400 Subject: [PATCH 201/371] limit offer extra info to 1500 characters --- .../haveno/core/offer/OpenOfferManager.java | 8 + .../haveno/core/xmr/wallet/Restrictions.java | 1 + .../resources/i18n/displayStrings.properties | 1 + .../desktop/components/InputTextArea.java | 140 ++++++++++++++++++ .../src/main/java/haveno/desktop/haveno.css | 6 +- .../desktop/main/offer/MutableOfferView.java | 14 +- .../main/offer/MutableOfferViewModel.java | 55 ++++--- 7 files changed, 195 insertions(+), 30 deletions(-) create mode 100644 desktop/src/main/java/haveno/desktop/components/InputTextArea.java diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 6ae03a7042..a08b767a47 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1396,6 +1396,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } + // verify max length of extra info + if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) { + errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.MAX_EXTRA_INFO_LENGTH + " but got " + offer.getOfferPayload().getExtraInfo().length(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + // verify the trade protocol version if (request.getOfferPayload().getProtocolVersion() != Version.TRADE_PROTOCOL_VERSION) { errorMessage = "Unsupported protocol version: " + request.getOfferPayload().getProtocolVersion(); diff --git a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java index b270762d3b..aefb92c41a 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java +++ b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java @@ -30,6 +30,7 @@ public class Restrictions { public static final double MAX_SECURITY_DEPOSIT_PCT = 0.5; public static BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.1); public static BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1); + public static int MAX_EXTRA_INFO_LENGTH = 1500; // At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the // mediated payout. For Refund agent cases we do not have that restriction. diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index cb6a632270..82a09919ba 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -495,6 +495,7 @@ createOffer.triggerPrice.tooltip=As protection against drastic price movements y deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} +createOffer.extraInfo.invalid.tooLong=Must not exceed {0} characters. # new entries createOffer.placeOfferButton=Review: Place offer to {0} monero diff --git a/desktop/src/main/java/haveno/desktop/components/InputTextArea.java b/desktop/src/main/java/haveno/desktop/components/InputTextArea.java new file mode 100644 index 0000000000..7bcd18de93 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/components/InputTextArea.java @@ -0,0 +1,140 @@ +/* + * This file is part of Haveno. + * + * Haveno 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. + * + * Haveno 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 Haveno. If not, see . + */ + +package haveno.desktop.components; + + +import com.jfoenix.controls.JFXTextArea; +import haveno.core.util.validation.InputValidator; +import haveno.desktop.util.validation.JFXInputValidator; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.control.Skin; + +/** + * TextArea with validation support. + * If validator is set it supports on focus out validation with that validator. If a more sophisticated validation is + * needed the validationResultProperty can be used for applying validation result done by external validation. + * In case the isValid property in validationResultProperty get set to false we display a red border and an error + * message within the errorMessageDisplay placed on the right of the text area. + * The errorMessageDisplay gets closed when the ValidatingTextArea instance gets removed from the scene graph or when + * hideErrorMessageDisplay() is called. + * There can be only 1 errorMessageDisplays at a time we use static field for it. + * The position is derived from the position of the textArea itself or if set from the layoutReference node. + */ +//TODO There are some rare situation where it behaves buggy. Needs further investigation and improvements. +public class InputTextArea extends JFXTextArea { + + private final ObjectProperty validationResult = new SimpleObjectProperty<> + (new InputValidator.ValidationResult(true)); + + private final JFXInputValidator jfxValidationWrapper = new JFXInputValidator(); + + private InputValidator validator; + private String errorMessage = null; + + + public InputValidator getValidator() { + return validator; + } + + public void setValidator(InputValidator validator) { + this.validator = validator; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public InputTextArea() { + super(); + + getValidators().add(jfxValidationWrapper); + + validationResult.addListener((ov, oldValue, newValue) -> { + if (newValue != null) { + jfxValidationWrapper.resetValidation(); + if (!newValue.isValid) { + if (!newValue.errorMessageEquals(oldValue)) { // avoid blinking + validate(); // ensure that the new error message replaces the old one + } + if (this.errorMessage != null) { + jfxValidationWrapper.applyErrorMessage(this.errorMessage); + } else { + jfxValidationWrapper.applyErrorMessage(newValue); + } + } + validate(); + } + }); + + textProperty().addListener((o, oldValue, newValue) -> { + refreshValidation(); + }); + + focusedProperty().addListener((o, oldValue, newValue) -> { + if (validator != null) { + if (!oldValue && newValue) { + this.validationResult.set(new InputValidator.ValidationResult(true)); + } else { + this.validationResult.set(validator.validate(getText())); + } + } + }); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + public void resetValidation() { + jfxValidationWrapper.resetValidation(); + + String input = getText(); + if (input.isEmpty()) { + validationResult.set(new InputValidator.ValidationResult(true)); + } else { + validationResult.set(validator.validate(input)); + } + } + + public void refreshValidation() { + if (validator != null) { + this.validationResult.set(validator.validate(getText())); + } + } + + public void setInvalid(String message) { + validationResult.set(new InputValidator.ValidationResult(false, message)); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public ObjectProperty validationResultProperty() { + return validationResult; + } + + protected Skin createDefaultSkin() { + return new JFXTextAreaSkinHavenoStyle(this); + } +} diff --git a/desktop/src/main/java/haveno/desktop/haveno.css b/desktop/src/main/java/haveno/desktop/haveno.css index fc92b1c308..2f50f8d0f3 100644 --- a/desktop/src/main/java/haveno/desktop/haveno.css +++ b/desktop/src/main/java/haveno/desktop/haveno.css @@ -501,15 +501,15 @@ tree-table-view:focused { -jfx-default-color: -bs-color-primary; } -.jfx-date-picker .jfx-text-field { +.jfx-date-picker .jfx-text-field .jfx-text-area { -fx-padding: 0.333333em 0em 0.333333em 0em; } -.jfx-date-picker .jfx-text-field > .input-line { +.jfx-date-picker .jfx-text-field .jfx-text-area > .input-line { -fx-translate-x: 0em; } -.jfx-date-picker .jfx-text-field > .input-focused-line { +.jfx-date-picker .jfx-text-field .jfx-text-area > .input-focused-line { -fx-translate-x: 0em; } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index e3f9132a84..1ed15ad845 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -44,8 +44,8 @@ import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.BalanceTextField; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.FundsTextField; -import haveno.desktop.components.HavenoTextArea; import haveno.desktop.components.InfoInputTextField; +import haveno.desktop.components.InputTextArea; import haveno.desktop.components.InputTextField; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.MainView; @@ -76,7 +76,6 @@ import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; -import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; @@ -140,7 +139,7 @@ public abstract class MutableOfferView> exten private BalanceTextField balanceTextField; private ToggleButton reserveExactAmountSlider; private ToggleButton buyerAsTakerWithoutDepositSlider; - protected TextArea extraInfoTextArea; + protected InputTextArea extraInfoTextArea; private FundsTextField totalToPayTextField; private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel, waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescriptionLabel, tradeFeeDescriptionLabel, @@ -211,7 +210,7 @@ public abstract class MutableOfferView> exten createListeners(); - balanceTextField.setFormatter(model.getBtcFormatter()); + balanceTextField.setFormatter(model.getXmrFormatter()); paymentAccountsComboBox.setConverter(GUIUtil.getPaymentAccountsComboBoxStringConverter()); paymentAccountsComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("shared.chooseTradingAccount"), @@ -592,6 +591,7 @@ public abstract class MutableOfferView> exten triggerPriceInputTextField.validationResultProperty().bind(model.triggerPriceValidationResult); volumeTextField.validationResultProperty().bind(model.volumeValidationResult); securityDepositInputTextField.validationResultProperty().bind(model.securityDepositValidationResult); + extraInfoTextArea.validationResultProperty().bind(model.extraInfoValidationResult); // funding fundingHBox.visibleProperty().bind(model.getDataModel().getIsXmrWalletFunded().not().and(model.showPayFundsScreenDisplayed)); @@ -713,7 +713,7 @@ public abstract class MutableOfferView> exten triggerPriceInputTextField.setText(model.triggerPrice.get()); }; extraInfoFocusedListener = (observable, oldValue, newValue) -> { - model.onFocusOutExtraInfoTextField(oldValue, newValue); + model.onFocusOutExtraInfoTextArea(oldValue, newValue); extraInfoTextArea.setText(model.extraInfo.get()); }; @@ -1097,7 +1097,7 @@ public abstract class MutableOfferView> exten Res.get("payment.shared.optionalExtra"), 25 + heightAdjustment); GridPane.setColumnSpan(extraInfoTitledGroupBg, 3); - extraInfoTextArea = new HavenoTextArea(); + extraInfoTextArea = new InputTextArea(); extraInfoTextArea.setPromptText(Res.get("payment.shared.extraInfo.prompt.offer")); extraInfoTextArea.getStyleClass().add("text-area"); extraInfoTextArea.setWrapText(true); @@ -1109,7 +1109,7 @@ public abstract class MutableOfferView> exten GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING); GridPane.setColumnIndex(extraInfoTextArea, 0); GridPane.setHalignment(extraInfoTextArea, HPos.LEFT); - GridPane.setMargin(extraInfoTextArea, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); + GridPane.setMargin(extraInfoTextArea, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 10, 0)); gridPane.getChildren().add(extraInfoTextArea); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index e32869afe2..6d087b27ea 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -99,7 +99,7 @@ public abstract class MutableOfferViewModel ext private final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final Preferences preferences; - protected final CoinFormatter btcFormatter; + protected final CoinFormatter xmrFormatter; private final FiatVolumeValidator fiatVolumeValidator; private final AmountValidator4Decimals amountValidator4Decimals; private final AmountValidator8Decimals amountValidator8Decimals; @@ -160,6 +160,7 @@ public abstract class MutableOfferViewModel ext final ObjectProperty triggerPriceValidationResult = new SimpleObjectProperty<>(new InputValidator.ValidationResult(true)); final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); final ObjectProperty securityDepositValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty extraInfoValidationResult = new SimpleObjectProperty<>(); private ChangeListener amountStringListener; private ChangeListener minAmountStringListener; @@ -195,26 +196,26 @@ public abstract class MutableOfferViewModel ext FiatVolumeValidator fiatVolumeValidator, AmountValidator4Decimals amountValidator4Decimals, AmountValidator8Decimals amountValidator8Decimals, - XmrValidator btcValidator, + XmrValidator xmrValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, Preferences preferences, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter, OfferUtil offerUtil) { super(dataModel); this.fiatVolumeValidator = fiatVolumeValidator; this.amountValidator4Decimals = amountValidator4Decimals; this.amountValidator8Decimals = amountValidator8Decimals; - this.xmrValidator = btcValidator; + this.xmrValidator = xmrValidator; this.securityDepositValidator = securityDepositValidator; this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.preferences = preferences; - this.btcFormatter = btcFormatter; + this.xmrFormatter = xmrFormatter; this.offerUtil = offerUtil; paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId); @@ -500,11 +501,7 @@ public abstract class MutableOfferViewModel ext }; extraInfoStringListener = (ov, oldValue, newValue) -> { - if (newValue != null) { - extraInfo.set(newValue); - } else { - extraInfo.set(""); - } + onExtraInfoTextAreaChanged(); }; isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); @@ -531,7 +528,7 @@ public abstract class MutableOfferViewModel ext tradeFee.set(HavenoUtils.formatXmr(makerFee)); tradeFeeInXmrWithFiat.set(OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getMaxMakerFee(), - btcFormatter)); + xmrFormatter)); } @@ -836,8 +833,16 @@ public abstract class MutableOfferViewModel ext } } - public void onFocusOutExtraInfoTextField(boolean oldValue, boolean newValue) { + public void onFocusOutExtraInfoTextArea(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { + onExtraInfoTextAreaChanged(); + } + } + + public void onExtraInfoTextAreaChanged() { + extraInfoValidationResult.set(getExtraInfoValidationResult()); + updateButtonDisableState(); + if (extraInfoValidationResult.get().isValid) { dataModel.setExtraInfo(extraInfo.get()); } } @@ -1045,8 +1050,8 @@ public abstract class MutableOfferViewModel ext .show(); } - CoinFormatter getBtcFormatter() { - return btcFormatter; + CoinFormatter getXmrFormatter() { + return xmrFormatter; } public boolean isShownAsBuyOffer() { @@ -1064,7 +1069,7 @@ public abstract class MutableOfferViewModel ext public String getTradeAmount() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getAmount().get(), - btcFormatter); + xmrFormatter); } public String getSecurityDepositLabel() { @@ -1084,7 +1089,7 @@ public abstract class MutableOfferViewModel ext return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getSecurityDeposit(), dataModel.getAmount().get(), - btcFormatter + xmrFormatter ); } @@ -1097,7 +1102,7 @@ public abstract class MutableOfferViewModel ext return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getMaxMakerFee(), dataModel.getAmount().get(), - btcFormatter); + xmrFormatter); } public String getMakerFeePercentage() { @@ -1108,7 +1113,7 @@ public abstract class MutableOfferViewModel ext public String getTotalToPayInfo() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.totalToPay.get(), - btcFormatter); + xmrFormatter); } public String getFundsStructure() { @@ -1181,7 +1186,7 @@ public abstract class MutableOfferViewModel ext private void setAmountToModel() { if (amount.get() != null && !amount.get().isEmpty()) { - BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), btcFormatter)); + BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), xmrFormatter)); long maxTradeLimit = dataModel.getMaxTradeLimit(); Price price = dataModel.getPrice().get(); @@ -1202,7 +1207,7 @@ public abstract class MutableOfferViewModel ext private void setMinAmountToModel() { if (minAmount.get() != null && !minAmount.get().isEmpty()) { - BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), btcFormatter)); + BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), xmrFormatter)); Price price = dataModel.getPrice().get(); long maxTradeLimit = dataModel.getMaxTradeLimit(); @@ -1343,10 +1348,20 @@ public abstract class MutableOfferViewModel ext inputDataValid = inputDataValid && securityDepositValidator.validate(securityDeposit.get()).isValid; } + inputDataValid = inputDataValid && getExtraInfoValidationResult().isValid; + isNextButtonDisabled.set(!inputDataValid); isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || !dataModel.getIsXmrWalletFunded().get()); } + private ValidationResult getExtraInfoValidationResult() { + if (extraInfo.get() != null && !extraInfo.get().isEmpty() && extraInfo.get().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) { + return new InputValidator.ValidationResult(false, Res.get("createOffer.extraInfo.invalid.tooLong", Restrictions.MAX_EXTRA_INFO_LENGTH)); + } else { + return new InputValidator.ValidationResult(true); + } + } + private void updateMarketPriceToManual() { final String currencyCode = dataModel.getTradeCurrencyCode().get(); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); From 82728aef6965e96203a4146af039e0372a16d466 Mon Sep 17 00:00:00 2001 From: boldsuck Date: Sat, 22 Mar 2025 12:58:05 +0100 Subject: [PATCH 202/371] Change http links to https in LICENSE (#1660) --- LICENSE | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 93a5d000f0..90fb6f4689 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Copyright (C) 2020 Haveno Dex Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -644,7 +644,7 @@ the "copyright" line and a pointer to where the full notice is found. GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. @@ -659,4 +659,4 @@ specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see -. +. From b3174518d9058701b7ce1f0837b6b9329ee7703e Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 22 Mar 2025 08:34:50 -0400 Subject: [PATCH 203/371] add vscode to IDE documentation --- docs/import-haveno.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/import-haveno.md b/docs/import-haveno.md index b05e8925a8..23f0a7e8c3 100644 --- a/docs/import-haveno.md +++ b/docs/import-haveno.md @@ -1,10 +1,16 @@ -## Importing Haveno into development environment +# Importing Haveno dev environment This document describes how to import Haveno into an integrated development environment (IDE). -## Importing Haveno into Eclipse IDE +First [install and run a Haveno test network](installing.md), then use the following instructions to import Haveno into an IDE. -These steps describe how to import Haveno into Eclipse IDE for development. You can also develop using [IntelliJ IDEA](#importing-haveno-into-intellij-idea) or VSCode if you prefer. +## Visual Studio Code (recommended) + +1. Download and open Visual Studio Code: https://code.visualstudio.com/. +2. File > Add folder to Workspace... +3. Browse to the `haveno` git project. + +## Eclipse IDE > Note: Use default values unless specified otherwise. @@ -26,7 +32,7 @@ These steps describe how to import Haveno into Eclipse IDE for development. You You are now ready to make, run, and test changes to the Haveno project! -## Importing Haveno into IntelliJ IDEA +## IntelliJ IDEA > Note: These instructions are outdated and for Haveno. From ad809ff20e331353976244f1e297395d2d49b060 Mon Sep 17 00:00:00 2001 From: boldsuck Date: Sun, 23 Mar 2025 13:00:26 +0100 Subject: [PATCH 204/371] Add price node (#1661) --- .../main/java/haveno/core/provider/ProvidersRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/provider/ProvidersRepository.java b/core/src/main/java/haveno/core/provider/ProvidersRepository.java index 8357bb7f08..08736e0f76 100644 --- a/core/src/main/java/haveno/core/provider/ProvidersRepository.java +++ b/core/src/main/java/haveno/core/provider/ProvidersRepository.java @@ -52,7 +52,8 @@ public class ProvidersRepository { private static final String DEFAULT_LOCAL_NODE = "http://localhost:8078/"; private static final List DEFAULT_NODES = Arrays.asList( "http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno - "http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/" // Cake + "http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/", // Cake + "http://2c6y3sqmknakl3fkuwh4tjhxb2q5isr53dnfcqs33vt3y7elujc6tyad.onion/" // boldsuck ); private final Config config; From 207ff5416c9e7be551779e43975804ce8d57a6a1 Mon Sep 17 00:00:00 2001 From: Brandon Trussell Date: Tue, 25 Mar 2025 10:49:26 +0000 Subject: [PATCH 205/371] Support linux aarch64 (#1665) --- build.gradle | 2 +- gradle/verification-metadata.xml | 53 +++++++++++++++++--------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/build.gradle b/build.gradle index 3c3d95d074..cb1cc172b2 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ configure(subprojects) { loggingVersion = '1.2' lombokVersion = '1.18.30' mockitoVersion = '5.10.0' - netlayerVersion = 'd4f9d0ce24' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14 + netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14 protobufVersion = '3.19.1' protocVersion = protobufVersion pushyVersion = '0.13.2' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index cbfb839d9f..15194cfc52 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -205,44 +205,49 @@
    - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + + From 29e6540234046c9d3b4627ed9fc5c6db44aa09f7 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 26 Mar 2025 09:42:00 -0400 Subject: [PATCH 206/371] fix missing edit icon on offers view --- desktop/src/main/java/haveno/desktop/images.css | 4 ---- .../haveno/desktop/main/offer/offerbook/OfferBookView.java | 5 +++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/images.css b/desktop/src/main/java/haveno/desktop/images.css index 39f0c7e806..aacd4c6b1b 100644 --- a/desktop/src/main/java/haveno/desktop/images.css +++ b/desktop/src/main/java/haveno/desktop/images.css @@ -39,10 +39,6 @@ -fx-image: url("../../images/remove.png"); } -#image-edit { - -fx-image: url("../../images/edit.png"); -} - #image-buy-white { -fx-image: url("../../images/buy_white.png"); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index b15f43db8a..af47d9bec5 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -20,6 +20,7 @@ package haveno.desktop.main.offer.offerbook; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.util.Tuple3; @@ -1082,7 +1083,7 @@ abstract public class OfferBookView onRemoveOpenOffer(offer)); - iconView2.setId("image-edit"); + iconView2.setSize("16px"); button2.updateText(Res.get("shared.edit")); button2.setOnAction(e -> onEditOpenOffer(offer)); button2.setManaged(true); From e699b427e27f376dbcea30d63e68e3b790f2b6d6 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 28 Mar 2025 13:31:28 -0400 Subject: [PATCH 207/371] apply log highlighter when running daemon --- daemon/src/main/resources/logback.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/daemon/src/main/resources/logback.xml b/daemon/src/main/resources/logback.xml index e6ba6a6327..f838048ece 100644 --- a/daemon/src/main/resources/logback.xml +++ b/daemon/src/main/resources/logback.xml @@ -1,8 +1,11 @@ + + + - %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + %hl2(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{40}: %msg %xEx%n) From df9daf99bf5000fdb0ac269264acf8dba607074c Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:22:24 -0400 Subject: [PATCH 208/371] change color of highlighted logs to cyan --- .../src/main/java/haveno/core/trade/protocol/TradeProtocol.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 290e2cce6e..b09584ae00 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -96,7 +96,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private static final String TIMEOUT_REACHED = "Timeout reached."; public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other wallet functions public static final long REPROCESS_DELAY_MS = 5000; - public static final String LOG_HIGHLIGHT = "\u001B[0m"; // terminal default + public static final String LOG_HIGHLIGHT = "\u001B[36m"; // cyan protected final ProcessModel processModel; protected final Trade trade; From a21971429cae56db3b4f95b1b6df8741995552cf Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:23:54 -0400 Subject: [PATCH 209/371] log standby mode as warning --- common/src/main/java/haveno/common/ClockWatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/haveno/common/ClockWatcher.java b/common/src/main/java/haveno/common/ClockWatcher.java index 7bd8454c78..ba77ca38ab 100644 --- a/common/src/main/java/haveno/common/ClockWatcher.java +++ b/common/src/main/java/haveno/common/ClockWatcher.java @@ -69,7 +69,7 @@ public class ClockWatcher { listeners.forEach(listener -> listener.onMissedSecondTick(missedMs)); if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) { - log.info("We have been in standby mode for {} sec", missedMs / 1000); + log.warn("We have been in standby mode for {} sec", missedMs / 1000); listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs)); } } From 93369c421176af56945221b74d26901e2202e8db Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 28 Mar 2025 14:51:19 -0400 Subject: [PATCH 210/371] prompt to fallback if last synced local node is offline on startup --- .../haveno/core/api/XmrConnectionService.java | 41 +++++++------------ .../java/haveno/core/app/HavenoSetup.java | 8 ++-- .../resources/i18n/displayStrings.properties | 3 +- .../i18n/displayStrings_cs.properties | 2 +- .../haveno/desktop/main/MainViewModel.java | 32 +++++++-------- 5 files changed, 36 insertions(+), 50 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 88d0117f8e..d686d64925 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -24,6 +24,7 @@ import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; +import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.xmr.model.EncryptedConnectionList; @@ -43,7 +44,6 @@ import java.util.Set; import org.apache.commons.lang3.exception.ExceptionUtils; -import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.ObjectProperty; @@ -51,7 +51,6 @@ import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleLongProperty; import javafx.beans.property.SimpleObjectProperty; @@ -91,7 +90,7 @@ public final class XmrConnectionService { private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter - private final BooleanProperty connectionServiceFallbackHandlerActive = new SimpleBooleanProperty(); + private final SimpleStringProperty connectionServiceFallbackHandler = new SimpleStringProperty(); @Getter private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final LongProperty numUpdates = new SimpleLongProperty(0); @@ -261,6 +260,7 @@ public final class XmrConnectionService { private MoneroRpcConnection getBestConnection(Collection ignoredConnections) { accountService.checkAccountOpen(); + if (!fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) return null; // user needs to explicitly allow fallback after syncing local node Set ignoredConnectionsSet = new HashSet<>(ignoredConnections); addLocalNodeIfIgnored(ignoredConnectionsSet); MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections @@ -604,9 +604,6 @@ public final class XmrConnectionService { if (coreContext.isApiUser()) connectionManager.setAutoSwitch(connectionList.getAutoSwitch()); else connectionManager.setAutoSwitch(true); // auto switch is always enabled on desktop ui - // start local node if applicable - maybeStartLocalNode(); - // update connection if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) { MoneroRpcConnection bestConnection = getBestConnection(); @@ -619,9 +616,6 @@ public final class XmrConnectionService { MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1); if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri()); connectionManager.setConnection(connection); - - // start local node if applicable - maybeStartLocalNode(); } // register connection listener @@ -634,20 +628,8 @@ public final class XmrConnectionService { onConnectionChanged(connectionManager.getConnection()); } - private void maybeStartLocalNode() { - - // skip if seed node - if (HavenoUtils.isSeedNode()) return; - - // start local node if offline and used as last connection - if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) { - try { - log.info("Starting local node"); - xmrLocalNode.start(); - } catch (Exception e) { - log.error("Unable to start local monero node, error={}\n", e.getMessage(), e); - } - } + private boolean lastUsedLocalSyncingNode() { + return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored(); } private void onConnectionChanged(MoneroRpcConnection currentConnection) { @@ -733,12 +715,17 @@ public final class XmrConnectionService { if (isShutDownStarted) return; // invoke fallback handling on startup error - boolean canFallback = isFixedConnection() || isCustomConnections(); + boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode(); if (lastInfo == null && canFallback) { - if (!connectionServiceFallbackHandlerActive.get() && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { - log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); + if (connectionServiceFallbackHandler.get() == null || connectionServiceFallbackHandler.equals("") && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { lastFallbackInvocation = System.currentTimeMillis(); - connectionServiceFallbackHandlerActive.set(true); + if (lastUsedLocalSyncingNode()) { + log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage()); + connectionServiceFallbackHandler.set(Res.get("connectionFallback.localNode")); + } else { + log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); + connectionServiceFallbackHandler.set(Res.get("connectionFallback.customNode")); + } } return; } diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index 6637511298..4ac7b23512 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -158,7 +158,7 @@ public class HavenoSetup { rejectedTxErrorMessageHandler; @Setter @Nullable - private Consumer displayMoneroConnectionFallbackHandler; + private Consumer displayMoneroConnectionFallbackHandler; @Setter @Nullable private Consumer displayTorNetworkSettingsHandler; @@ -430,7 +430,7 @@ public class HavenoSetup { getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); // listen for fallback handling - getConnectionServiceFallbackHandlerActive().addListener((observable, oldValue, newValue) -> { + getConnectionServiceFallbackHandler().addListener((observable, oldValue, newValue) -> { if (displayMoneroConnectionFallbackHandler == null) return; displayMoneroConnectionFallbackHandler.accept(newValue); }); @@ -734,8 +734,8 @@ public class HavenoSetup { return xmrConnectionService.getConnectionServiceErrorMsg(); } - public BooleanProperty getConnectionServiceFallbackHandlerActive() { - return xmrConnectionService.getConnectionServiceFallbackHandlerActive(); + public StringProperty getConnectionServiceFallbackHandler() { + return xmrConnectionService.getConnectionServiceFallbackHandler(); } public StringProperty getTopErrorMsg() { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 82a09919ba..c434edb6cf 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2058,7 +2058,8 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amoun walletPasswordWindow.headline=Enter password to unlock connectionFallback.headline=Connection error -connectionFallback.msg=Error connecting to your custom Monero node(s).\n\nDo you want to try the next best available Monero node? +connectionFallback.customNode=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? +connectionFallback.localNode=Error connecting to your last used local node.\n\nDo you want to use the next best available Monero node? torNetworkSettingWindow.header=Tor networks settings torNetworkSettingWindow.noBridges=Don't use bridges diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 25b796e403..a009d33060 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -2057,7 +2057,7 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} z celkového objemu walletPasswordWindow.headline=Pro odemknutí zadejte heslo connectionFallback.headline=Chyba připojení -connectionFallback.msg=Chyba při připojování k vlastním uzlům Monero.\n\nChcete vyzkoušet další nejlepší dostupný uzel Monero? +connectionFallback.customNode=Chyba při připojování k vlastním uzlům Monero.\n\nChcete vyzkoušet další nejlepší dostupný uzel Monero? torNetworkSettingWindow.header=Nastavení sítě Tor torNetworkSettingWindow.noBridges=Nepoužívat most (bridge) diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index 670543a7aa..8c36eaf179 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -335,25 +335,23 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener tacWindow.onAction(acceptedHandler::run).show(); }, 1)); - havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> { - if (moneroConnectionFallbackPopup == null) { + havenoSetup.setDisplayMoneroConnectionFallbackHandler(fallbackMsg -> { + if (fallbackMsg != null && !fallbackMsg.isEmpty()) { moneroConnectionFallbackPopup = new Popup() - .headLine(Res.get("connectionFallback.headline")) - .warning(Res.get("connectionFallback.msg")) - .closeButtonText(Res.get("shared.no")) - .actionButtonText(Res.get("shared.yes")) - .onAction(() -> { - havenoSetup.getConnectionServiceFallbackHandlerActive().set(false); - new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); - }) - .onClose(() -> { - log.warn("User has declined to fallback to the next best available Monero node."); - havenoSetup.getConnectionServiceFallbackHandlerActive().set(false); - }); - } - if (show) { + .headLine(Res.get("connectionFallback.headline")) + .warning(fallbackMsg) + .closeButtonText(Res.get("shared.no")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + havenoSetup.getConnectionServiceFallbackHandler().set(""); + new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + }) + .onClose(() -> { + log.warn("User has declined to fallback to the next best available Monero node."); + havenoSetup.getConnectionServiceFallbackHandler().set(""); + }); moneroConnectionFallbackPopup.show(); - } else if (moneroConnectionFallbackPopup.isDisplayed()) { + } else if (moneroConnectionFallbackPopup != null && moneroConnectionFallbackPopup.isDisplayed()) { moneroConnectionFallbackPopup.hide(); } }); From dc43e1c329fbbd83f96279f3361311fc6c12954a Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:45:37 -0400 Subject: [PATCH 211/371] re-sign offers on edit if applicable --- .../haveno/core/offer/OpenOfferManager.java | 235 +++++++++--------- .../offer/placeoffer/PlaceOfferProtocol.java | 4 +- ...fferBook.java => MaybeAddToOfferBook.java} | 32 ++- .../haveno/desktop/main/debug/DebugView.java | 4 +- .../editoffer/EditOfferDataModel.java | 8 +- .../portfolio/editoffer/EditOfferView.java | 3 +- 6 files changed, 153 insertions(+), 133 deletions(-) rename core/src/main/java/haveno/core/offer/placeoffer/tasks/{AddToOfferBook.java => MaybeAddToOfferBook.java} (58%) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index a08b767a47..e68f90e484 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -135,7 +135,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 30; private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(30); private static final long REFRESH_INTERVAL_MS = OfferPayload.TTL / 2; - private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process pending offer only on republish cycle after this many attempts + private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process offer only on republish cycle after this many attempts private final CoreContext coreContext; private final KeyRing keyRing; @@ -475,19 +475,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService) // .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); - // process pending offers - processPendingOffers(false, (transaction) -> {}, (errorMessage) -> { - log.warn("Error processing pending offers on bootstrap: " + errorMessage); + // processs offers + processOffers(false, (transaction) -> {}, (errorMessage) -> { + log.warn("Error processing offers on bootstrap: " + errorMessage); }); - // register to process pending offers on new block + // register to process offers on new block xmrWalletService.addWalletListener(new MoneroWalletListener() { @Override public void onNewBlock(long height) { - // process each pending offer on new block a few times, then rely on period republish - processPendingOffers(true, (transaction) -> {}, (errorMessage) -> { - log.warn("Error processing pending offers on new block {}: {}", height, errorMessage); + // process each offer on new block a few times, then rely on period republish + processOffers(true, (transaction) -> {}, (errorMessage) -> { + log.warn("Error processing offers on new block {}: {}", height, errorMessage); }); } }); @@ -555,13 +555,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); addOpenOffer(openOffer); - processPendingOffer(getOpenOffers(), openOffer, (transaction) -> { + processOffer(getOpenOffers(), openOffer, (transaction) -> { requestPersistence(); latch.countDown(); resultHandler.handleResult(transaction); }, (errorMessage) -> { if (!openOffer.isCanceled()) { - log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage); + log.warn("Error processing offer {}: {}", openOffer.getId(), errorMessage); doCancelOffer(openOffer, resetAddressEntriesOnError); } latch.countDown(); @@ -578,8 +578,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (openOfferOptional.isPresent()) { cancelOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler); } else { - log.warn("Offer was not found in our list of open offers. We still try to remove it from the offerbook."); - errorMessageHandler.handleErrorMessage("Offer was not found in our list of open offers. " + "We still try to remove it from the offerbook."); + String errorMsg = "Offer was not found in our list of open offers. We still try to remove it from the offerbook."; + log.warn(errorMsg); + errorMessageHandler.handleErrorMessage(errorMsg); offerBookService.removeOffer(offer.getOfferPayload(), () -> offer.setState(Offer.State.REMOVED), null); } } @@ -706,12 +707,21 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe addOpenOffer(editedOpenOffer); - if (editedOpenOffer.isAvailable()) - maybeRepublishOffer(editedOpenOffer); + // reset arbitrator signature if invalid + Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(editedOpenOffer.getOffer().getOfferPayload().getArbitratorSigner()); + if (arbitrator == null || !HavenoUtils.isArbitratorSignatureValid(editedOpenOffer.getOffer().getOfferPayload(), arbitrator)) { + editedOpenOffer.getOffer().getOfferPayload().setArbitratorSignature(null); + editedOpenOffer.getOffer().getOfferPayload().setArbitratorSigner(null); + } - offersToBeEdited.remove(openOffer.getId()); - requestPersistence(); - resultHandler.handleResult(); + // process offer which might sign and publish + processOffer(getOpenOffers(), editedOpenOffer, (transaction) -> { + offersToBeEdited.remove(openOffer.getId()); + requestPersistence(); + resultHandler.handleResult(); + }, (errorMsg) -> { + errorMessageHandler.handleErrorMessage(errorMsg); + }); } else { errorMessageHandler.handleErrorMessage("There is no offer with this id existing to be published."); } @@ -728,6 +738,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } else { resultHandler.handleResult(); } + requestPersistence(); } else { errorMessageHandler.handleErrorMessage("Editing of offer can't be canceled as it is not edited."); } @@ -882,7 +893,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe /////////////////////////////////////////////////////////////////////////////////////////// // Place offer helpers /////////////////////////////////////////////////////////////////////////////////////////// - private void processPendingOffers(boolean skipOffersWithTooManyAttempts, + private void processOffers(boolean skipOffersWithTooManyAttempts, TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler ErrorMessageHandler errorMessageHandler) { ThreadUtils.execute(() -> { @@ -890,23 +901,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe synchronized (processOffersLock) { List openOffers = getOpenOffers(); removeOffersWithDuplicateKeyImages(openOffers); - for (OpenOffer pendingOffer : openOffers) { - if (pendingOffer.getState() != OpenOffer.State.PENDING) continue; - if (skipOffersWithTooManyAttempts && pendingOffer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts + for (OpenOffer offer : openOffers) { + if (skipOffersWithTooManyAttempts && offer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts CountDownLatch latch = new CountDownLatch(1); - processPendingOffer(openOffers, pendingOffer, (transaction) -> { + processOffer(openOffers, offer, (transaction) -> { latch.countDown(); }, errorMessage -> { - if (!pendingOffer.isCanceled()) { - String warnMessage = "Error processing pending offer, offerId=" + pendingOffer.getId() + ", attempt=" + pendingOffer.getNumProcessingAttempts() + ": " + errorMessage; - errorMessages.add(warnMessage); - - // cancel offer if invalid - if (pendingOffer.getOffer().getState() == Offer.State.INVALID) { - log.warn("Canceling offer because it's invalid: {}", pendingOffer.getId()); - doCancelOffer(pendingOffer); - } - } + errorMessages.add(errorMessage); latch.countDown(); }); HavenoUtils.awaitLatch(latch); @@ -943,7 +944,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - private void processPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + private void processOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { // skip if already processing if (openOffer.isProcessing()) { @@ -953,23 +954,33 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // process offer openOffer.setProcessing(true); - doProcessPendingOffer(openOffers, openOffer, (transaction) -> { + doProcessOffer(openOffers, openOffer, (transaction) -> { openOffer.setProcessing(false); resultHandler.handleResult(transaction); }, (errorMsg) -> { openOffer.setProcessing(false); openOffer.setNumProcessingAttempts(openOffer.getNumProcessingAttempts() + 1); openOffer.getOffer().setErrorMessage(errorMsg); + if (!openOffer.isCanceled()) { + errorMsg = "Error processing offer, offerId=" + openOffer.getId() + ", attempt=" + openOffer.getNumProcessingAttempts() + ": " + errorMsg; + openOffer.getOffer().setErrorMessage(errorMsg); + + // cancel offer if invalid + if (openOffer.getOffer().getState() == Offer.State.INVALID) { + log.warn("Canceling offer because it's invalid: {}", openOffer.getId()); + doCancelOffer(openOffer); + } + } errorMessageHandler.handleErrorMessage(errorMsg); }); } - private void doProcessPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + private void doProcessOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { new Thread(() -> { try { - // done processing if wallet not initialized - if (xmrWalletService.getWallet() == null) { + // done processing if canceled or wallet not initialized + if (openOffer.isCanceled() || xmrWalletService.getWallet() == null) { resultHandler.handleResult(null); return; } @@ -982,6 +993,33 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } + // validate non-pending state + if (!openOffer.isPending()) { + boolean isValid = true; + Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); + if (openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null) { + isValid = false; + } else if (arbitrator == null) { + log.warn("Offer {} signed by unavailable arbitrator, reposting", openOffer.getId()); + isValid = false; + } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { + log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId()); + isValid = false; + } + if ((openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) && (openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty())) { + log.warn("Offer {} is missing reserve tx hash but has reserved key images, reposting", openOffer.getId()); + isValid = false; + } + if (isValid) { + resultHandler.handleResult(null); + return; + } else { + openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); + openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); + if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING); + } + } + // cancel offer if scheduled txs unavailable if (openOffer.getScheduledTxHashes() != null) { boolean scheduledTxsAvailable = true; @@ -999,6 +1037,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } + // sign and post offer if already funded + if (openOffer.getReserveTxHash() != null) { + signAndPostOffer(openOffer, false, resultHandler, errorMessageHandler); + return; + } + // get amount needed to reserve offer BigInteger amountNeeded = openOffer.getOffer().getAmountNeeded(); @@ -1020,13 +1064,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } else { splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler); } - } else { // sign and post offer if enough funds - boolean hasFundsReserved = openOffer.getReserveTxHash() != null; boolean hasSufficientBalance = xmrWalletService.getAvailableBalance().compareTo(amountNeeded) >= 0; - if (hasFundsReserved || hasSufficientBalance) { + if (hasSufficientBalance) { signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); return; } else if (openOffer.getScheduledTxHashes() == null) { @@ -1036,7 +1078,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } } catch (Exception e) { - if (!openOffer.isCanceled()) log.error("Error processing pending offer: {}\n", e.getMessage(), e); + if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e); errorMessageHandler.handleErrorMessage(e.getMessage()); } }).start(); @@ -1335,7 +1377,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe transaction -> { // set offer state - openOffer.setState(OpenOffer.State.AVAILABLE); openOffer.setScheduledTxHashes(null); openOffer.setScheduledAmount(null); requestPersistence(); @@ -1949,10 +1990,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - private void maybeRepublishOffer(OpenOffer openOffer) { - maybeRepublishOffer(openOffer, null); - } - private void maybeRepublishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) { ThreadUtils.execute(() -> { @@ -1962,76 +1999,48 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } - // determine if offer is valid - boolean isValid = true; - Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); - if (arbitrator == null) { - log.warn("Offer {} signed by unavailable arbitrator, reposting", openOffer.getId()); - isValid = false; - } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { - log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId()); - isValid = false; - } - if ((openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) && (openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty())) { - log.warn("Offer {} is missing reserve tx hash but has reserved key images, reposting", openOffer.getId()); - isValid = false; - } + // reprocess offer then publish + synchronized (processOffersLock) { + CountDownLatch latch = new CountDownLatch(1); + processOffer(getOpenOffers(), openOffer, (transaction) -> { + requestPersistence(); + latch.countDown(); - // if valid, re-add offer to book - if (isValid) { - offerBookService.addOffer(openOffer.getOffer(), - () -> { - if (!stopped) { - - // refresh means we send only the data needed to refresh the TTL (hash, signature and sequence no.) - if (periodicRefreshOffersTimer == null) { - startPeriodicRefreshOffersTimer(); - } - if (completeHandler != null) { - completeHandler.run(); - } - } - }, - errorMessage -> { - if (!stopped) { - log.error("Adding offer to P2P network failed. " + errorMessage); - stopRetryRepublishOffersTimer(); - retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers, - RETRY_REPUBLISH_DELAY_SEC); - if (completeHandler != null) completeHandler.run(); - } - }); - } else { - - // reset offer state to pending - openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); - openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); - openOffer.getOffer().setState(Offer.State.UNKNOWN); - openOffer.setState(OpenOffer.State.PENDING); - - // republish offer - synchronized (processOffersLock) { - CountDownLatch latch = new CountDownLatch(1); - processPendingOffer(getOpenOffers(), openOffer, (transaction) -> { - requestPersistence(); - latch.countDown(); + // skip if prevented from publishing + if (preventedFromPublishing(openOffer)) { if (completeHandler != null) completeHandler.run(); - }, (errorMessage) -> { - if (!openOffer.isCanceled()) { - log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage); - openOffer.getOffer().setErrorMessage(errorMessage); + return; + } + + // publish offer to books + offerBookService.addOffer(openOffer.getOffer(), + () -> { + if (!stopped) { - // cancel offer if invalid - if (openOffer.getOffer().getState() == Offer.State.INVALID) { - log.warn("Canceling offer because it's invalid: {}", openOffer.getId()); - doCancelOffer(openOffer); - } - } - latch.countDown(); - if (completeHandler != null) completeHandler.run(); - }); - HavenoUtils.awaitLatch(latch); - } + // refresh means we send only the data needed to refresh the TTL (hash, signature and sequence no.) + if (periodicRefreshOffersTimer == null) { + startPeriodicRefreshOffersTimer(); + } + if (completeHandler != null) { + completeHandler.run(); + } + } + }, + errorMessage -> { + if (!stopped) { + log.error("Adding offer to P2P network failed. " + errorMessage); + stopRetryRepublishOffersTimer(); + retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers, + RETRY_REPUBLISH_DELAY_SEC); + if (completeHandler != null) completeHandler.run(); + } + }); + }, (errorMessage) -> { + log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage); + latch.countDown(); + if (completeHandler != null) completeHandler.run(); + }); + HavenoUtils.awaitLatch(latch); } }, THREAD_ID); } diff --git a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java index 0b22d9e40e..68d5f9da4f 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java @@ -23,7 +23,7 @@ import haveno.common.handlers.ErrorMessageHandler; import haveno.common.taskrunner.TaskRunner; import haveno.core.locale.Res; import haveno.core.offer.messages.SignOfferResponse; -import haveno.core.offer.placeoffer.tasks.AddToOfferBook; +import haveno.core.offer.placeoffer.tasks.MaybeAddToOfferBook; import haveno.core.offer.placeoffer.tasks.MakerProcessSignOfferResponse; import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds; import haveno.core.offer.placeoffer.tasks.MakerSendSignOfferRequest; @@ -135,7 +135,7 @@ public class PlaceOfferProtocol { ); taskRunner.addTasks( MakerProcessSignOfferResponse.class, - AddToOfferBook.class + MaybeAddToOfferBook.class ); taskRunner.run(); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java similarity index 58% rename from core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java rename to core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java index c5c3cf4f46..8e3e3c23bc 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java @@ -20,13 +20,14 @@ package haveno.core.offer.placeoffer.tasks; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; +import haveno.core.offer.OpenOffer; import haveno.core.offer.placeoffer.PlaceOfferModel; import static com.google.common.base.Preconditions.checkNotNull; -public class AddToOfferBook extends Task { +public class MaybeAddToOfferBook extends Task { - public AddToOfferBook(TaskRunner taskHandler, PlaceOfferModel model) { + public MaybeAddToOfferBook(TaskRunner taskHandler, PlaceOfferModel model) { super(taskHandler, model); } @@ -35,17 +36,22 @@ public class AddToOfferBook extends Task { try { runInterceptHook(); checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOpenOffer().getOffer().getId()); - model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), - () -> { - model.setOfferAddedToOfferBook(true); - complete(); - }, - errorMessage -> { - model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" + - "Please check your network connection and try again."); - - failed(errorMessage); - }); + if (model.getOpenOffer().isPending() || model.getOpenOffer().isAvailable()) { + model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), + () -> { + model.getOpenOffer().setState(OpenOffer.State.AVAILABLE); + model.setOfferAddedToOfferBook(true); + complete(); + }, + errorMessage -> { + model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" + + "Please check your network connection and try again."); + failed(errorMessage); + }); + } else { + complete(); + return; + } } catch (Throwable t) { model.getOpenOffer().getOffer().setErrorMessage("An error occurred.\n" + "Error message:\n" diff --git a/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java b/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java index 00ab2680b5..f1427c16a2 100644 --- a/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java +++ b/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java @@ -22,7 +22,7 @@ import haveno.common.taskrunner.Task; import haveno.common.util.Tuple2; import haveno.core.offer.availability.tasks.ProcessOfferAvailabilityResponse; import haveno.core.offer.availability.tasks.SendOfferAvailabilityRequest; -import haveno.core.offer.placeoffer.tasks.AddToOfferBook; +import haveno.core.offer.placeoffer.tasks.MaybeAddToOfferBook; import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds; import haveno.core.offer.placeoffer.tasks.ValidateOffer; import haveno.core.trade.protocol.tasks.ApplyFilter; @@ -72,7 +72,7 @@ public class DebugView extends InitializableView { FXCollections.observableArrayList(Arrays.asList( ValidateOffer.class, MakerReserveOfferFunds.class, - AddToOfferBook.class) + MaybeAddToOfferBook.class) )); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java index b9cd38efb5..151a72c0d7 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -20,6 +20,8 @@ package haveno.desktop.main.portfolio.editoffer; import com.google.inject.Inject; import com.google.inject.name.Named; + +import haveno.common.UserThread; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.account.witness.AccountAgeWitnessService; @@ -226,8 +228,10 @@ class EditOfferDataModel extends MutableOfferDataModel { openOfferManager.editOpenOfferPublish(editedOffer, triggerPrice, initialState, () -> { openOffer = null; - resultHandler.handleResult(); - }, errorMessageHandler); + UserThread.execute(() -> resultHandler.handleResult()); + }, (errorMsg) -> { + UserThread.execute(() -> errorMessageHandler.handleErrorMessage(errorMsg)); + }); } public void onCancelEditOffer(ErrorMessageHandler errorMessageHandler) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java index 3752ab9dcb..bc804b5576 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java @@ -205,7 +205,8 @@ public class EditOfferView extends MutableOfferView { cancelButton.setDisable(true); busyAnimation.play(); spinnerInfoLabel.setText(Res.get("editOffer.publishOffer")); - //edit offer + + // edit offer model.onPublishOffer(() -> { String key = "editOfferSuccess"; if (DontShowAgainLookup.showAgain(key)) { From 584cc3b6d46e2869002007362e983e2628103cfd Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 2 Apr 2025 17:09:37 -0400 Subject: [PATCH 212/371] replace issues hyperlink from bisq to haveno --- core/src/main/resources/i18n/displayStrings.properties | 10 +++++----- .../main/resources/i18n/displayStrings_cs.properties | 10 +++++----- .../main/resources/i18n/displayStrings_de.properties | 10 +++++----- .../main/resources/i18n/displayStrings_es.properties | 8 ++++---- .../main/resources/i18n/displayStrings_fa.properties | 10 +++++----- .../main/resources/i18n/displayStrings_fr.properties | 10 +++++----- .../main/resources/i18n/displayStrings_it.properties | 10 +++++----- .../main/resources/i18n/displayStrings_ja.properties | 10 +++++----- .../resources/i18n/displayStrings_pt-br.properties | 10 +++++----- .../main/resources/i18n/displayStrings_pt.properties | 10 +++++----- .../main/resources/i18n/displayStrings_ru.properties | 10 +++++----- .../main/resources/i18n/displayStrings_th.properties | 10 +++++----- .../main/resources/i18n/displayStrings_tr.properties | 10 +++++----- .../main/resources/i18n/displayStrings_vi.properties | 10 +++++----- .../resources/i18n/displayStrings_zh-hans.properties | 10 +++++----- .../resources/i18n/displayStrings_zh-hant.properties | 10 +++++----- 16 files changed, 79 insertions(+), 79 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index c434edb6cf..90993c8b27 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -964,7 +964,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee trans portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\n\ Without this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. \ You can make a request to be reimbursed the trade fee here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues]\n\n\ + [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ Feel free to move this trade to failed trades. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, \ but funds have been locked in the deposit transaction.\n\n\ @@ -974,7 +974,7 @@ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=T (with seller receiving full trade amount back as well). \ This way, there is no security risk, and only trade fees are lost. \n\n\ You can request a reimbursement for lost trade fees here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing \ but funds have been locked in the deposit transaction.\n\n\ If the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open \ @@ -983,18 +983,18 @@ portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx= their security deposits (with seller receiving full trade amount back as well). \ Otherwise the trade amount should go to the buyer. \n\n\ You can request a reimbursement for lost trade fees here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\n\ Error: {0}\n\n\ It might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation \ ticket to get advice from Haveno mediators. \n\n\ If the error was critical and the trade cannot be completed, you might have lost your trade fee. \ Request a reimbursement for lost trade fees here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\n\ The trade cannot be completed and you might \ have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\n\ Do you want to move the trade to failed trades?\n\n\ diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index a009d33060..85b6d8ca87 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -963,7 +963,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=Chybí poplatek příjemce portfolio.pending.failedTrade.missingDepositTx=Vkladová transakce (transakce 2-of-2 multisig) chybí.\n\n\ Bez této tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky, ale byl zaplacen váš obchodní poplatek. \ Zde můžete požádat o vrácení obchodního poplatku: \ - [HYPERLINK:https://github.com/bisq-network/support/issues]\n\n\ + [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ Klidně můžete přesunout tento obchod do neúspěšných obchodů. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Zpožděná výplatní transakce chybí, ale prostředky byly uzamčeny v vkladové transakci.\n\n\ @@ -973,7 +973,7 @@ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Z (přičemž prodejce také obdrží plnou částku obchodu). \ Tímto způsobem nehrozí žádné bezpečnostní riziko a jsou ztraceny pouze obchodní poplatky.\n\n\ O vrácení ztracených obchodních poplatků můžete požádat zde: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Zpožděná výplatní transakce chybí, \ ale prostředky byly v depozitní transakci uzamčeny.\n\n\ Pokud kupujícímu chybí také odložená výplatní transakce, bude poučen, aby platbu NEPOSLAL a místo toho otevřel \ @@ -982,18 +982,18 @@ portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx= svých bezpečnostních vkladů (přičemž prodejce také obdrží plnou částku obchodu). \ Jinak by částka obchodu měla jít kupujícímu.\n\n\ O vrácení ztracených obchodních poplatků můžete požádat zde: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Během provádění obchodního protokolu došlo k chybě.\n\n Chyba: {0}\n\n\ Je možné, že tato chyba není kritická a obchod lze dokončit normálně. Pokud si nejste jisti, otevřete si mediační úkol \ a získejte radu od mediátorů Haveno.\n\n\ Pokud byla chyba kritická a obchod nelze dokončit, možná jste ztratili obchodní poplatek. \ O vrácení ztracených obchodních poplatků požádejte zde: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=Obchodní kontrakt není stanoven.\n\n\ Obchod nelze dokončit a možná jste ztratili poplatek \ za obchodování. Pokud ano, můžete požádat o vrácení ztracených obchodních poplatků zde: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Obchodní protokol narazil na některé problémy.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Obchodní protokol narazil na vážný problém.\n\n{0}\n\n\ Chcete obchod přesunout do neúspěšných obchodů?\n\n\ diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 5cf9e07618..47c06e75a4 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -819,11 +819,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Sie haben bereits akzept portfolio.pending.failedTrade.taker.missingTakerFeeTx=Die Transaktion der Abnehmer-Gebühr fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt und keine Handelsgebühr wurde bezahlt. Sie können diesen Handel zu den fehlgeschlagenen Händeln verschieben. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Die Transaktion der Abnehmer-Gebühr fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt. Ihr Angebot ist für andere Händler weiterhin verfügbar. Sie haben die Ersteller-Gebühr also nicht verloren. Sie können diesen Handel zu den fehlgeschlagenen Händeln verschieben. -portfolio.pending.failedTrade.missingDepositTx=Die Einzahlungstransaktion (die 2-of-2 Multisig-Transaktion) fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt aber die Handels-Gebühr wurde bezahlt. Sie können eine Anfrage für eine Rückerstattung der Handels-Gebühr hier einreichen: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nSie können diesen Handel gerne zu den fehlgeschlagenen Händeln verschieben. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nBitte schicken Sie KEINE Geld-(Traditional-) oder Crypto-Zahlungen an den XMR Verkäufer, weil ohne die verzögerte Auszahlungstransaktion später kein Schlichtungsverfahren eröffnet werden kann. Stattdessen öffnen Sie ein Vermittlungs-Ticket mit Cmd/Strg+o. Der Vermittler sollte vorschlagen, dass beide Handelspartner ihre vollständige Sicherheitskaution zurückerstattet bekommen (und der Verkäufer auch seinen Handels-Betrag). Durch diese Vorgehensweise entsteht kein Sicherheitsrisiko und es geht ausschließlich die Handelsgebühr verloren.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nWenn dem Käufer die verzögerte Auszahlungstransaktion auch fehlt, wird er dazu aufgefordert die Bezahlung NICHT zu schicken und stattdessen ein Vermittlungs-Ticket zu eröffnen. Sie sollten auch ein Vermittlungs-Ticket mit Cmd/Strg+o öffnen.\n\nWenn der Käufer die Zahlung noch nicht geschickt hat, sollte der Vermittler vorschlagen, dass beide Handelspartner ihre Sicherheitskaution vollständig zurückerhalten (und der Verkäufer auch den Handels-Betrag). Anderenfalls sollte der Handels-Betrag an den Käufer gehen.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=Während der Ausführung des Handel-Protokolls ist ein Fehler aufgetreten.\n\nFehler: {0}\n\nEs kann sein, dass dieser Fehler nicht gravierend ist und der Handel ganz normal abgeschlossen werden kann. Wenn Sie sich unsicher sind, öffnen Sie ein Vermittlungs-Ticket um den Rat eines Haveno Vermittlers zu erhalten.\n\nWenn der Fehler gravierend war, kann der Handel nicht abgeschlossen werden und Sie haben vielleicht die Handelsgebühr verloren. Sie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=Der Handelsvertrag ist nicht festgelegt.\n\nDer Handel kann nicht abgeschlossen werden und Sie haben möglicherweise die Handelsgebühr verloren. Sollte das der Fall sein, können Sie eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier beantragen: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=Die Einzahlungstransaktion (die 2-of-2 Multisig-Transaktion) fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt aber die Handels-Gebühr wurde bezahlt. Sie können eine Anfrage für eine Rückerstattung der Handels-Gebühr hier einreichen: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nSie können diesen Handel gerne zu den fehlgeschlagenen Händeln verschieben. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nBitte schicken Sie KEINE Geld-(Traditional-) oder Crypto-Zahlungen an den XMR Verkäufer, weil ohne die verzögerte Auszahlungstransaktion später kein Schlichtungsverfahren eröffnet werden kann. Stattdessen öffnen Sie ein Vermittlungs-Ticket mit Cmd/Strg+o. Der Vermittler sollte vorschlagen, dass beide Handelspartner ihre vollständige Sicherheitskaution zurückerstattet bekommen (und der Verkäufer auch seinen Handels-Betrag). Durch diese Vorgehensweise entsteht kein Sicherheitsrisiko und es geht ausschließlich die Handelsgebühr verloren.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nWenn dem Käufer die verzögerte Auszahlungstransaktion auch fehlt, wird er dazu aufgefordert die Bezahlung NICHT zu schicken und stattdessen ein Vermittlungs-Ticket zu eröffnen. Sie sollten auch ein Vermittlungs-Ticket mit Cmd/Strg+o öffnen.\n\nWenn der Käufer die Zahlung noch nicht geschickt hat, sollte der Vermittler vorschlagen, dass beide Handelspartner ihre Sicherheitskaution vollständig zurückerhalten (und der Verkäufer auch den Handels-Betrag). Anderenfalls sollte der Handels-Betrag an den Käufer gehen.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=Während der Ausführung des Handel-Protokolls ist ein Fehler aufgetreten.\n\nFehler: {0}\n\nEs kann sein, dass dieser Fehler nicht gravierend ist und der Handel ganz normal abgeschlossen werden kann. Wenn Sie sich unsicher sind, öffnen Sie ein Vermittlungs-Ticket um den Rat eines Haveno Vermittlers zu erhalten.\n\nWenn der Fehler gravierend war, kann der Handel nicht abgeschlossen werden und Sie haben vielleicht die Handelsgebühr verloren. Sie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=Der Handelsvertrag ist nicht festgelegt.\n\nDer Handel kann nicht abgeschlossen werden und Sie haben möglicherweise die Handelsgebühr verloren. Sollte das der Fall sein, können Sie eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier beantragen: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Das Handels-Protokoll hat ein paar Probleme gefunden.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Das Handels-Protokoll hat ein schwerwiegendes Problem gefunden.\n\n{0}\n\nWollen Sie den Handel zu den fehlgeschlagenen Händeln verschieben?\n\nSie können keine Vermittlungs- oder Schlichtungsverfahren auf der Seite für fehlgeschlagene Händel eröffnen, aber Sie können einen fehlgeschlagene Handel wieder auf die Seite der offenen Händeln zurück verschieben. portfolio.pending.failedTrade.txChainValid.moveToFailed=Das Handels-Protokoll hat ein paar Probleme gefunden.\n\n{0}\n\nDie Transaktionen des Handels wurden veröffentlicht und die Gelder sind gesperrt. Verschieben Sie den Handel nur dann zu den fehlgeschlagenen Händeln, wenn Sie sich wirklich sicher sind. Dies könnte Optionen zur Behebung des Problems verhindern.\n\nWollen Sie den Handel zu den fehlgeschlagenen Händeln verschieben?\n\nSie können keine Vermittlungs- oder Schlichtungsverfahren auf der Seite für fehlgeschlagene Händel eröffnen, aber Sie können einen fehlgeschlagene Handel wieder auf die Seite der offenen Händeln zurück verschieben. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 8a6f4fb02a..49539c0457 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -819,11 +819,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Ya ha aceptado portfolio.pending.failedTrade.taker.missingTakerFeeTx=Falta la transacción de tasa de tomador\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos y no se ha pagado ninguna tasa de intercambio. Puede mover esta operación a intercambios fallidos. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Falta la transacción de tasa de tomador de su par.\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos. Su oferta aún está disponible para otros comerciantes, por lo que no ha perdido la tasa de tomador. Puede mover este intercambio a intercambios fallidos. -portfolio.pending.failedTrade.missingDepositTx=Falta la transacción de depósito (la transacción multifirma 2 de 2).\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos, pero se ha pagado su tarifa comercial. Puede hacer una solicitud para que se le reembolse la tarifa comercial aquí: [HYPERLINK:https://github.com/bisq-network/support/issues].\n\nSiéntase libre de mover esta operación a operaciones fallidas. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción de pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nNO envíe el pago traditional o crypto al vendedor de XMR, porque sin el tx de pago demorado, no se puede abrir el arbitraje. En su lugar, abra un ticket de mediación con Cmd / Ctrl + o. El mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De esta manera, no hay riesgo en la seguridad y solo se pierden las tarifas comerciales.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/bisq-network/support/issues]. -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción del pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nSi al comprador también le falta la transacción de pago demorado, se le indicará que NO envíe el pago y abra un ticket de mediación. También debe abrir un ticket de mediación con Cmd / Ctrl + o.\n\nSi el comprador aún no ha enviado el pago, el mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De lo contrario, el monto comercial debe ir al comprador.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/bisq-network/support/issues]. +portfolio.pending.failedTrade.missingDepositTx=Falta la transacción de depósito (la transacción multifirma 2 de 2).\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos, pero se ha pagado su tarifa comercial. Puede hacer una solicitud para que se le reembolse la tarifa comercial aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues].\n\nSiéntase libre de mover esta operación a operaciones fallidas. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción de pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nNO envíe el pago traditional o crypto al vendedor de XMR, porque sin el tx de pago demorado, no se puede abrir el arbitraje. En su lugar, abra un ticket de mediación con Cmd / Ctrl + o. El mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De esta manera, no hay riesgo en la seguridad y solo se pierden las tarifas comerciales.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción del pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nSi al comprador también le falta la transacción de pago demorado, se le indicará que NO envíe el pago y abra un ticket de mediación. También debe abrir un ticket de mediación con Cmd / Ctrl + o.\n\nSi el comprador aún no ha enviado el pago, el mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De lo contrario, el monto comercial debe ir al comprador.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.errorMsgSet=Hubo un error durante la ejecución del protocolo de intercambio.\n\nError: {0}\n\nPuede ser que este error no sea crítico y que el intercambio se pueda completar normalmente. Si no está seguro, abra un ticket de mediación para obtener consejos de los mediadores de Haveno.\n\nSi el error fue crítico y la operación no se puede completar, es posible que haya perdido su tarifa de operación. Solicite un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:ttps://github.com/bisq-network/support/issues]. -portfolio.pending.failedTrade.missingContract=El contrato del intercambio no está establecido.\n\nLa operación no se puede completar y es posible que haya perdido su tarifa de operación. Si es así, puede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/bisq-network/support/issues]. +portfolio.pending.failedTrade.missingContract=El contrato del intercambio no está establecido.\n\nLa operación no se puede completar y es posible que haya perdido su tarifa de operación. Si es así, puede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.info.popup=El protocolo de intercambio encontró algunos problemas.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=El protocolo de intercambio encontró un problema grave.\n\n{0}\n\n¿Quiere mover la operación a intercambios fallidos?\n\nNo puede abrir mediación o arbitraje desde la vista de operaciones fallidas, pero puede mover un intercambio fallido a la pantalla de intercambios abiertos en cualquier momento. portfolio.pending.failedTrade.txChainValid.moveToFailed=El protocolo de intercambio encontró algunos problemas.\n\n{0}\n\nLas transacciones del intercambio se han publicado y los fondos están bloqueados. Mueva la operación a operaciones fallidas solo si está realmente seguro. Podría impedir opciones para resolver el problema.\n\n¿Quiere mover la operación a operaciones fallidas?\n\nNo puede abrir mediación o el arbitraje desde la vista de intercambios fallidos, pero puede mover un intercambio fallido a la pantalla de intercambios abiertos en cualquier momento. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index c8a52bbc51..6a2ae3ed32 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -818,11 +818,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index e6d353b1c7..97513efd0a 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -820,11 +820,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Vous avez déjà accept portfolio.pending.failedTrade.taker.missingTakerFeeTx=Le frais de transaction du preneur est manquant.\n\nSans ce tx, le trade ne peut être complété. Aucun fonds ont été verrouillés et aucun frais de trade a été payé. Vous pouvez déplacer ce trade vers les trade échoués. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Le frais de transaction du pair preneur est manquant.\n\nSans ce tx, le trade ne peut être complété. Aucun fonds ont été verrouillés. Votre offre est toujours valable pour les autres traders, vous n'avez donc pas perdu le frais de maker. Vous pouvez déplacer ce trade vers les trades échoués. -portfolio.pending.failedTrade.missingDepositTx=Cette transaction de marge (transaction multi-signature de 2 à 2) est manquante.\n\nSans ce tx, la transaction ne peut pas être complétée. Aucun fonds n'est bloqué, mais vos frais de transaction sont toujours payés. Vous pouvez lancer une demande de compensation des frais de transaction ici: [HYPERLINK:https://github.com/bisq-network/support/issues] \nN'hésitez pas à déplacer la transaction vers la transaction échouée. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nVeuillez NE PAS envoyer de Fiat ou d'crypto au vendeur de XMR, car avec le tx de paiement différé, le jugemenbt ne peut être ouvert. À la place, ouvrez un ticket de médiation avec Cmd/Ctrl+O. Le médiateur devrait suggérer que les deux pair reçoivent tous les deux le montant total de leurs dépôts de sécurité (le vendeur aussi doit reçevoir le montant total du trade). De cette manière, il n'y a pas de risque de non sécurité, et seuls les frais du trade sont perdus.\n\nVous pouvez demander le remboursement des frais de trade perdus ici;\n[LIEN:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nSi l'acheteur n'a pas non plus la transaction de paiement différée, il sera informé du fait de ne PAS envoyer le paiement et d'ouvrir un ticket de médiation à la place. Vous devriez aussi ouvrir un ticket de médiation avec Cmd/Ctrl+o.\n\nSi l'acheteur n'a pas encore envoyé le paiement, le médiateur devrait suggérer que les deux pairs reçoivent le montant total de leurs dépôts de sécurité (le vendeur doit aussi reçevoir le montant total du trade). Sinon, le montant du trade revient à l'acheteur.\n\nVous pouvez effectuer une demande de remboursement pour les frais de trade perdus ici: [LIEN:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=Il y'a eu une erreur durant l'exécution du protocole de trade.\n\nErreur: {0}\n\nIl est possible que cette erreur ne soit pas critique, et que le trade puisse être complété normalement. Si vous n'en êtes pas sûr, ouvrez un ticket de médiation pour avoir des conseils de la part des médiateurs de Haveno.\n\nSi cette erreur est critique et que le trade ne peut être complété, il est possible que vous ayez perdu le frais du trade. Effectuez une demande de remboursement ici: [LIEN:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=Le contrat de trade n'est pas complété.\n\nCe trade ne peut être complété et il est possible que vous ayiez perdu votre frais de trade. Dans ce cas, vous pouvez demander un remboursement des frais de trade perdus ici: [LIEN:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=Cette transaction de marge (transaction multi-signature de 2 à 2) est manquante.\n\nSans ce tx, la transaction ne peut pas être complétée. Aucun fonds n'est bloqué, mais vos frais de transaction sont toujours payés. Vous pouvez lancer une demande de compensation des frais de transaction ici: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] \nN'hésitez pas à déplacer la transaction vers la transaction échouée. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nVeuillez NE PAS envoyer de Fiat ou d'crypto au vendeur de XMR, car avec le tx de paiement différé, le jugemenbt ne peut être ouvert. À la place, ouvrez un ticket de médiation avec Cmd/Ctrl+O. Le médiateur devrait suggérer que les deux pair reçoivent tous les deux le montant total de leurs dépôts de sécurité (le vendeur aussi doit reçevoir le montant total du trade). De cette manière, il n'y a pas de risque de non sécurité, et seuls les frais du trade sont perdus.\n\nVous pouvez demander le remboursement des frais de trade perdus ici;\n[LIEN:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nSi l'acheteur n'a pas non plus la transaction de paiement différée, il sera informé du fait de ne PAS envoyer le paiement et d'ouvrir un ticket de médiation à la place. Vous devriez aussi ouvrir un ticket de médiation avec Cmd/Ctrl+o.\n\nSi l'acheteur n'a pas encore envoyé le paiement, le médiateur devrait suggérer que les deux pairs reçoivent le montant total de leurs dépôts de sécurité (le vendeur doit aussi reçevoir le montant total du trade). Sinon, le montant du trade revient à l'acheteur.\n\nVous pouvez effectuer une demande de remboursement pour les frais de trade perdus ici: [LIEN:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=Il y'a eu une erreur durant l'exécution du protocole de trade.\n\nErreur: {0}\n\nIl est possible que cette erreur ne soit pas critique, et que le trade puisse être complété normalement. Si vous n'en êtes pas sûr, ouvrez un ticket de médiation pour avoir des conseils de la part des médiateurs de Haveno.\n\nSi cette erreur est critique et que le trade ne peut être complété, il est possible que vous ayez perdu le frais du trade. Effectuez une demande de remboursement ici: [LIEN:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=Le contrat de trade n'est pas complété.\n\nCe trade ne peut être complété et il est possible que vous ayiez perdu votre frais de trade. Dans ce cas, vous pouvez demander un remboursement des frais de trade perdus ici: [LIEN:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Le protocole de trade a rencontré quelques problèmes/\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Le protocole de trade a rencontré un problème critique.\n\n{0}\n\nVoulez-vous déplacer ce trade vers les trades échoués?\n\nVous ne pouvez pas ouvrir de médiations ou de jugements depuis la liste des trades échoués, mais vous pouvez redéplacer un trade échoué vers l'écran des trades ouverts quand vous le souhaitez. portfolio.pending.failedTrade.txChainValid.moveToFailed=Il y a des problèmes avec cet accord de transaction. \n\n{0}\n\nLa transaction de devis a été validée et les fonds ont été bloqués. Déplacer la transaction vers une transaction échouée uniquement si elle est certaine. Cela peut empêcher les options disponibles pour résoudre le problème. \n\nÊtes-vous sûr de vouloir déplacer cette transaction vers la transaction échouée? \n\nVous ne pouvez pas ouvrir une médiation ou un arbitrage dans une transaction échouée, mais vous pouvez déplacer une transaction échouée vers la transaction incomplète à tout moment. diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 9c037a7da7..a1d3868897 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -818,11 +818,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Hai già accettato portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 566ebf68ce..521200928d 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -819,11 +819,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=すでに受け入れて portfolio.pending.failedTrade.taker.missingTakerFeeTx=欠測テイカー手数料のトランザクション。\n\nこのtxがなければ、トレードを完了できません。資金はロックされず、トレード手数料は支払いませんでした。「失敗トレード」へ送ることができます。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=ピアのテイカー手数料のトランザクションは欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでした。あなたのオファーがまだ他の取引者には有効ですので、メイカー手数料は失っていません。このトレードを「失敗トレード」へ送ることができます。 -portfolio.pending.failedTrade.missingDepositTx=入金トランザクション(2-of-2マルチシグトランザクション)は欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでしたが、トレード手数料は支払いました。トレード手数料の返済要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nこのトレードを「失敗トレード」へ送れます。 -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\nこの法定通貨・アルトコイン支払いをXMR売り手に送信しないで下さい。遅延支払いtxがなければ、係争仲裁は開始されることができません。代りに、「Cmd/Ctrl+o」で調停チケットをオープンして下さい。調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。このような方法でセキュリティーのリスクがなし、トレード手数料のみが失われます。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\n買い手の遅延支払いトランザクションが同じく欠測される場合、相手は支払いを送信せず調停チケットをオープンするように指示されます。同様に「Cmd/Ctrl+o」で調停チケットをオープンするのは賢明でしょう。\n\n買い手はまだ支払いを送信しなかった場合、調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。さもなければ、トレード金額は買い手に支払われるでしょう。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=トレードプロトコルの実行にはエラーが生じました。\n\nエラー: {0}\n\nクリティカル・エラーではない可能性はあり、トレードは普通に完了できるかもしれない。迷う場合は調停チケットをオープンして、Haveno調停者からアドバイスを受けることができます。\n\nクリティカル・エラーでトレードが完了できなかった場合はトレード手数料は失われた可能性があります。失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=トレード契約書は設定されません。\n\nトレードは完了できません。トレード手数料は失われた可能性もあります。その場合は失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=入金トランザクション(2-of-2マルチシグトランザクション)は欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでしたが、トレード手数料は支払いました。トレード手数料の返済要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nこのトレードを「失敗トレード」へ送れます。 +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\nこの法定通貨・アルトコイン支払いをXMR売り手に送信しないで下さい。遅延支払いtxがなければ、係争仲裁は開始されることができません。代りに、「Cmd/Ctrl+o」で調停チケットをオープンして下さい。調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。このような方法でセキュリティーのリスクがなし、トレード手数料のみが失われます。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\n買い手の遅延支払いトランザクションが同じく欠測される場合、相手は支払いを送信せず調停チケットをオープンするように指示されます。同様に「Cmd/Ctrl+o」で調停チケットをオープンするのは賢明でしょう。\n\n買い手はまだ支払いを送信しなかった場合、調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。さもなければ、トレード金額は買い手に支払われるでしょう。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=トレードプロトコルの実行にはエラーが生じました。\n\nエラー: {0}\n\nクリティカル・エラーではない可能性はあり、トレードは普通に完了できるかもしれない。迷う場合は調停チケットをオープンして、Haveno調停者からアドバイスを受けることができます。\n\nクリティカル・エラーでトレードが完了できなかった場合はトレード手数料は失われた可能性があります。失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=トレード契約書は設定されません。\n\nトレードは完了できません。トレード手数料は失われた可能性もあります。その場合は失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=トレードプロトコルは問題に遭遇しました。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=トレードプロトコルは深刻な問題に遭遇しました。\n\n{0}\n\nトレードを「失敗トレード」へ送りますか?\n\n「失敗トレード」画面から調停・仲裁を開始できませんけど、失敗トレードがいつでも「オープントレード」へ戻されることができます。 portfolio.pending.failedTrade.txChainValid.moveToFailed=トレードプロトコルは問題に遭遇しました。\n\n{0}\n\nトレードのトランザクションは公開され、資金はロックされました。絶対に確信している場合のみにトレードを「失敗トレード」へ送りましょう。問題を解決できる選択肢に邪魔する可能性はあります。\n\nトレードを「失敗トレード」へ送りますか?\n\n「失敗トレード」画面から調停・仲裁を開始できませんけど、失敗トレードがいつでも「オープントレード」へ戻されることができます。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 5a46618da3..f9ce2ea82a 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -821,11 +821,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index e5e9f02508..373e07fdeb 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -818,11 +818,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index eb76a4f78b..054e625da2 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -818,11 +818,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 3ba44f304b..e2377b330e 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -818,11 +818,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index bdc840da6e..9f393b9697 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -960,7 +960,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=Karşı tarafın alıcı portfolio.pending.failedTrade.missingDepositTx=Para yatırma işlemi (2-of-2 multisig işlemi) eksik.\n\n\ Bu işlem olmadan, ticaret tamamlanamaz. Hiçbir fon kilitlenmedi ancak ticaret ücretiniz ödendi. \ Ticaret ücretinin geri ödenmesi için burada talepte bulunabilirsiniz: \ - [HYPERLINK:https://github.com/bisq-network/support/issues]\n\n\ + [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ Bu ticareti başarısız ticaretler arasına taşımakta özgürsünüz. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Gecikmiş ödeme işlemi eksik, \ ancak fonlar depozito işleminde kilitlendi.\n\n\ @@ -970,7 +970,7 @@ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=G (satıcı da tam ticaret miktarını geri alır). \ Bu şekilde, güvenlik riski yoktur ve yalnızca ticaret ücretleri kaybedilir. \n\n\ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunabilirsiniz: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Gecikmiş ödeme işlemi eksik \ ancak fonlar depozito işleminde kilitlendi.\n\n\ Eğer alıcı da gecikmiş ödeme işlemini eksikse, onlara ödemeyi göndermemeleri ve \ @@ -979,18 +979,18 @@ portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx= tamamını geri almasını önermelidir (satıcı da tam ticaret miktarını geri alır). \ Aksi takdirde ticaret miktarı alıcıya gitmelidir. \n\n\ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunabilirsiniz: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Ticaret protokolü yürütülürken bir hata oluştu.\n\n\ Hata: {0}\n\n\ Bu hata kritik olmayabilir ve ticaret normal şekilde tamamlanabilir. Emin değilseniz, \ Haveno arabulucularından tavsiye almak için bir arabuluculuk bileti açın. \n\n\ Eğer hata kritikse ve ticaret tamamlanamazsa, ticaret ücretinizi kaybetmiş olabilirsiniz. \ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunun: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=Ticaret sözleşmesi ayarlanmadı.\n\n\ Ticaret tamamlanamaz ve ticaret ücretinizi kaybetmiş olabilirsiniz. \ Eğer öyleyse, kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunun: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Ticaret protokolü bazı sorunlarla karşılaştı.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Ticaret protokolü ciddi bir sorunla karşılaştı.\n\n{0}\n\n\ Ticareti başarısız ticaretler arasına taşımak ister misiniz?\n\n\ diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index e6bfad14ac..8390ad6834 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -818,11 +818,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 96b1de98de..9586f74c99 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -819,11 +819,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=您已经接受了。 portfolio.pending.failedTrade.taker.missingTakerFeeTx=吃单交易费未找到。\n\n如果没有 tx,交易不能完成。没有资金被锁定以及没有支付交易费用。你可以将交易移至失败的交易。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=挂单费交易未找到。\n\n如果没有 tx,交易不能完成。没有资金被锁定以及没有支付交易费用。你可以将交易移至失败的交易。 -portfolio.pending.failedTrade.missingDepositTx=这个保证金交易(2 对 2 多重签名交易)缺失\n\n没有该 tx,交易不能完成。没有资金被锁定但是您的交易手续费仍然已支出。您可以发起一个请求去赔偿改交易手续费在这里:https://github.com/bisq-network/support/issues\n\n请随意的将该交易移至失败交易 -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易缺失,但是资金仍然被锁定在保证金交易中。\n\n请不要给比特币卖家发送法币或数字货币,因为没有延迟交易 tx,不能开启仲裁。使用 Cmd/Ctrl+o开启调解协助。调解员应该建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。这样的话不会有任何的安全问题只会损失交易手续费。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易确实但是资金仍然被锁定在保证金交易中。\n\n如果卖家仍然缺失延迟支付交易,他会接到请勿付款的指示并开启一个调节帮助。你也应该使用 Cmd/Ctrl+O 去打开一个调节协助\n\n如果买家还没有发送付款,调解员应该会建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。否则交易额应该判给买方。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.errorMsgSet=在处理交易协议是发生了一个错误\n\n错误:{0}\n\n这应该不是致命错误,您可以正常的完成交易。如果你仍担忧,打开一个调解协助并从 Haveno 调解员处得到建议。\n\n如果这个错误是致命的那么这个交易就无法完成,你可能会损失交易费。可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.missingContract=没有设置交易合同。\n\n这个交易无法完成,你可能会损失交易手续费。可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues +portfolio.pending.failedTrade.missingDepositTx=这个保证金交易(2 对 2 多重签名交易)缺失\n\n没有该 tx,交易不能完成。没有资金被锁定但是您的交易手续费仍然已支出。您可以发起一个请求去赔偿改交易手续费在这里:https://github.com/haveno-dex/haveno/issues\n\n请随意的将该交易移至失败交易 +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易缺失,但是资金仍然被锁定在保证金交易中。\n\n请不要给比特币卖家发送法币或数字货币,因为没有延迟交易 tx,不能开启仲裁。使用 Cmd/Ctrl+o开启调解协助。调解员应该建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。这样的话不会有任何的安全问题只会损失交易手续费。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易确实但是资金仍然被锁定在保证金交易中。\n\n如果卖家仍然缺失延迟支付交易,他会接到请勿付款的指示并开启一个调节帮助。你也应该使用 Cmd/Ctrl+O 去打开一个调节协助\n\n如果买家还没有发送付款,调解员应该会建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。否则交易额应该判给买方。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.errorMsgSet=在处理交易协议是发生了一个错误\n\n错误:{0}\n\n这应该不是致命错误,您可以正常的完成交易。如果你仍担忧,打开一个调解协助并从 Haveno 调解员处得到建议。\n\n如果这个错误是致命的那么这个交易就无法完成,你可能会损失交易费。可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.missingContract=没有设置交易合同。\n\n这个交易无法完成,你可能会损失交易手续费。可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.info.popup=交易协议出现了问题。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=交易协议出现了严重问题。\n\n{0}\n\n您确定想要将该交易移至失败的交易吗?\n\n您不能在失败的交易中打开一个调解或仲裁,但是你随时可以将失败的交易重新移至未完成交易。 portfolio.pending.failedTrade.txChainValid.moveToFailed=这个交易协议存在一些问题。\n\n{0}\n\n这个报价交易已经被发布以及资金已被锁定。只有在确定情况下将该交易移至失败交易。这可能会阻止解决问题的可用选项。\n\n您确定想要将该交易移至失败的交易吗?\n\n您不能在失败的交易中打开一个调解或仲裁,但是你随时可以将失败的交易重新移至未完成交易。 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 39823b2329..ff998576d0 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -819,11 +819,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=您已經接受了。 portfolio.pending.failedTrade.taker.missingTakerFeeTx=吃單交易費未找到。\n\n如果沒有 tx,交易不能完成。沒有資金被鎖定以及沒有支付交易費用。你可以將交易移至失敗的交易。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=掛單費交易未找到。\n\n如果沒有 tx,交易不能完成。沒有資金被鎖定以及沒有支付交易費用。你可以將交易移至失敗的交易。 -portfolio.pending.failedTrade.missingDepositTx=這個保證金交易(2 對 2 多重簽名交易)缺失\n\n沒有該 tx,交易不能完成。沒有資金被鎖定但是您的交易手續費仍然已支出。您可以發起一個請求去賠償改交易手續費在這裏:https://github.com/bisq-network/support/issues\n\n請隨意的將該交易移至失敗交易 -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易缺失,但是資金仍然被鎖定在保證金交易中。\n\n請不要給比特幣賣家發送法幣或數字貨幣,因為沒有延遲交易 tx,不能開啟仲裁。使用 Cmd/Ctrl+o開啟調解協助。調解員應該建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。這樣的話不會有任何的安全問題只會損失交易手續費。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易確實但是資金仍然被鎖定在保證金交易中。\n\n如果賣家仍然缺失延遲支付交易,他會接到請勿付款的指示並開啟一個調節幫助。你也應該使用 Cmd/Ctrl+O 去打開一個調節協助\n\n如果買家還沒有發送付款,調解員應該會建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。否則交易額應該判給買方。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.errorMsgSet=在處理交易協議是發生了一個錯誤\n\n錯誤:{0}\n\n這應該不是致命錯誤,您可以正常的完成交易。如果你仍擔憂,打開一個調解協助並從 Haveno 調解員處得到建議。\n\n如果這個錯誤是致命的那麼這個交易就無法完成,你可能會損失交易費。可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.missingContract=沒有設置交易合同。\n\n這個交易無法完成,你可能會損失交易手續費。可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues +portfolio.pending.failedTrade.missingDepositTx=這個保證金交易(2 對 2 多重簽名交易)缺失\n\n沒有該 tx,交易不能完成。沒有資金被鎖定但是您的交易手續費仍然已支出。您可以發起一個請求去賠償改交易手續費在這裏:https://github.com/haveno-dex/haveno/issues\n\n請隨意的將該交易移至失敗交易 +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易缺失,但是資金仍然被鎖定在保證金交易中。\n\n請不要給比特幣賣家發送法幣或數字貨幣,因為沒有延遲交易 tx,不能開啟仲裁。使用 Cmd/Ctrl+o開啟調解協助。調解員應該建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。這樣的話不會有任何的安全問題只會損失交易手續費。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易確實但是資金仍然被鎖定在保證金交易中。\n\n如果賣家仍然缺失延遲支付交易,他會接到請勿付款的指示並開啟一個調節幫助。你也應該使用 Cmd/Ctrl+O 去打開一個調節協助\n\n如果買家還沒有發送付款,調解員應該會建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。否則交易額應該判給買方。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.errorMsgSet=在處理交易協議是發生了一個錯誤\n\n錯誤:{0}\n\n這應該不是致命錯誤,您可以正常的完成交易。如果你仍擔憂,打開一個調解協助並從 Haveno 調解員處得到建議。\n\n如果這個錯誤是致命的那麼這個交易就無法完成,你可能會損失交易費。可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.missingContract=沒有設置交易合同。\n\n這個交易無法完成,你可能會損失交易手續費。可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.info.popup=交易協議出現了問題。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=交易協議出現了嚴重問題。\n\n{0}\n\n您確定想要將該交易移至失敗的交易嗎?\n\n您不能在失敗的交易中打開一個調解或仲裁,但是你隨時可以將失敗的交易重新移至未完成交易。 portfolio.pending.failedTrade.txChainValid.moveToFailed=這個交易協議存在一些問題。\n\n{0}\n\n這個報價交易已經被髮布以及資金已被鎖定。只有在確定情況下將該交易移至失敗交易。這可能會阻止解決問題的可用選項。\n\n您確定想要將該交易移至失敗的交易嗎?\n\n您不能在失敗的交易中打開一個調解或仲裁,但是你隨時可以將失敗的交易重新移至未完成交易。 From 8981740b8c97cecfee70adff16878f03e1c62e17 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 3 Apr 2025 10:20:18 -0400 Subject: [PATCH 213/371] update to monero-project v0.18.4.0 --- Makefile | 3 +++ build.gradle | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index ac3d9b9a70..ad51f76809 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,7 @@ monerod1-local: --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ + --rpc-max-connections-per-private-ip 100 \ monerod2-local: ./.localnet/monerod \ @@ -93,6 +94,7 @@ monerod2-local: --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ + --rpc-max-connections-per-private-ip 100 \ monerod3-local: ./.localnet/monerod \ @@ -112,6 +114,7 @@ monerod3-local: --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ + --rpc-max-connections-per-private-ip 100 \ #--proxy 127.0.0.1:49775 \ diff --git a/build.gradle b/build.gradle index cb1cc172b2..20ca924031 100644 --- a/build.gradle +++ b/build.gradle @@ -457,14 +457,14 @@ configure(project(':core')) { doLast { // get monero binaries download url Map moneroBinaries = [ - 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-linux-x86_64.tar.gz', - 'linux-x86_64-sha256' : '92003b6d9104e8fe3c4dff292b782ed9b82b157aaff95200fda35e5c3dcb733a', - 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-linux-aarch64.tar.gz', - 'linux-aarch64-sha256' : '18b069c6c474ce18efea261c875a4d54022520e888712b2a56524d9c92f133b1', - 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-mac.tar.gz', - 'mac-sha256' : 'd308352191cd5a9e5e3932ad15869e033e22e209de459f4fd6460b111377dae2', - 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-windows.zip', - 'windows-sha256' : '9c9e1994d4738e2a89ca28bef343bcad460ea6c06e0dd40de8278ab3033bd6c7' + 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-x86_64.tar.gz', + 'linux-x86_64-sha256' : '44470a3cf2dd9be7f3371a8cc89a34cf9a7e88c442739d87ef9a0ec3ccb65208', + 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-aarch64.tar.gz', + 'linux-aarch64-sha256' : 'c9505524689b0d7a020b8d2fd449c3cb9f8fd546747f9bdcf36cac795179f71c', + 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-mac.tar.gz', + 'mac-sha256' : 'dea6eddefa09630cfff7504609bd5d7981316336c64e5458e242440694187df8', + 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-windows.zip', + 'windows-sha256' : '284820e28c4770d7065fad7863e66fe0058053ca2372b78345d83c222edc572d' ] String osKey From 39c75cd71b2123509db8b12fe1aa5ceed05f9e8c Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 3 Apr 2025 18:26:14 -0400 Subject: [PATCH 214/371] update withdraw confirmation wording with translations --- core/src/main/resources/i18n/displayStrings.properties | 2 +- core/src/main/resources/i18n/displayStrings_cs.properties | 2 +- core/src/main/resources/i18n/displayStrings_de.properties | 1 + core/src/main/resources/i18n/displayStrings_es.properties | 1 + core/src/main/resources/i18n/displayStrings_fa.properties | 1 + core/src/main/resources/i18n/displayStrings_fr.properties | 1 + core/src/main/resources/i18n/displayStrings_it.properties | 1 + core/src/main/resources/i18n/displayStrings_ja.properties | 1 + core/src/main/resources/i18n/displayStrings_pt-br.properties | 1 + core/src/main/resources/i18n/displayStrings_pt.properties | 1 + core/src/main/resources/i18n/displayStrings_ru.properties | 1 + core/src/main/resources/i18n/displayStrings_th.properties | 1 + core/src/main/resources/i18n/displayStrings_tr.properties | 2 +- core/src/main/resources/i18n/displayStrings_vi.properties | 1 + .../main/resources/i18n/displayStrings_zh-hans.properties | 1 + .../main/resources/i18n/displayStrings_zh-hant.properties | 1 + .../haveno/desktop/main/funds/withdrawal/WithdrawalView.java | 5 ++--- 17 files changed, 18 insertions(+), 6 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 90993c8b27..3a6b666102 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -134,7 +134,7 @@ shared.noDateAvailable=No date available shared.noDetailsAvailable=No details available shared.notUsedYet=Not used yet shared.date=Date -shared.sendFundsDetailsWithFee=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2}\n\nThe recipient will receive: {3}\n\nAre you sure you want to withdraw this amount? +shared.sendFundsDetailsWithFee=Sending: {0}\n\nTo receiving address: {1}\n\nAdditional miner fee: {2}\n\nAre you sure you want to send this amount? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copy to clipboard diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 85b6d8ca87..bc1841259c 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -134,7 +134,7 @@ shared.noDateAvailable=Žádné datum není k dispozici shared.noDetailsAvailable=Detaily nejsou k dispozici shared.notUsedYet=Ještě nepoužito shared.date=Datum -shared.sendFundsDetailsWithFee=Odesílání: {0}nNa přijímací adresu: {1}.nPožadován těžební poplatek: {2}\n\nPříjemce dostane: {3}\n\nJste si jisti, že chcete vyplatit tuto částku? +shared.sendFundsDetailsWithFee=Odesílání: {0}\n\nNa přijímací adresu: {1}\n\nDalší poplatek pro těžaře: {2}\n\nJste si jisti, že chcete vyplatit tuto částku? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno zjistil, že tato transakce by vytvořila drobné mince, které jsou pod limitem drobných mincí (a není to povoleno pravidly pro konsenzus Monero). Místo toho budou tyto drobné mince ({0} satoshi {1}) přidány k poplatku za těžbu.\n\n\n shared.copyToClipboard=Kopírovat do schránky diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 47c06e75a4..ce1d2c2a2a 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Kein Datum verfügbar shared.noDetailsAvailable=Keine Details vorhanden shared.notUsedYet=Noch ungenutzt shared.date=Datum +shared.sendFundsDetailsWithFee=Senden: {0}\n\nAn die Empfangsadresse: {1}\n\nZusätzliche Miner-Gebühr: {2}\n\nSind Sie sicher, dass Sie diesen Betrag senden möchten? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Diese Transaktion würde ein Wechselgeld erzeugen das unterhalb des Dust-Grenzwerts liegt (und daher von den Monero-Konsensregeln nicht erlaubt wäre). Stattdessen wird dieser Dust ({0} Satoshi{1}) der Mining-Gebühr hinzugefügt.\n\n\n shared.copyToClipboard=In Zwischenablage kopieren diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 49539c0457..a8fe7a8fa0 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Sin fecha disponible shared.noDetailsAvailable=Sin detalles disponibles shared.notUsedYet=Sin usar aún shared.date=Fecha +shared.sendFundsDetailsWithFee=Enviando: {0}\n\nA la dirección receptora: {1}\n\nTarifa adicional para el minero: {2}\n\n¿Estás seguro de que deseas enviar esta cantidad? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detectó que esta transacción crearía una salida que está por debajo del umbral mínimo considerada polvo (y no está permitida por las reglas de consenso en Monero). En cambio, esta transacción polvo ({0} satoshi {1}) se agregará a la tarifa de minería.\n\n\n shared.copyToClipboard=Copiar al portapapeles diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 6a2ae3ed32..53bd2363ce 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=تاریخ موجود نیست shared.noDetailsAvailable=جزئیاتی در دسترس نیست shared.notUsedYet=هنوز مورد استفاده قرار نگرفته shared.date=تاریخ +shared.sendFundsDetailsWithFee=ارسال: {0}\n\nبه آدرس گیرنده: {1}\n\nهزینه اضافی ماینر: {2}\n\nآیا مطمئن هستید که می‌خواهید این مبلغ را ارسال کنید؟ # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=کپی در کلیپ‌بورد diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 97513efd0a..900e829baa 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Pas de date disponible shared.noDetailsAvailable=Pas de détails disponibles shared.notUsedYet=Pas encore utilisé shared.date=Date +shared.sendFundsDetailsWithFee=Envoyer : {0}\n\nÀ l'adresse de réception : {1}\n\nFrais supplémentaires pour le mineur : {2}\n\nÊtes-vous sûr de vouloir envoyer ce montant ? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno détecte que la transaction produira une sortie inférieure au seuil de fraction minimum (non autorisé par les règles de consensus Monero). Au lieu de cela, ces fractions ({0} satoshi {1}) seront ajoutées aux frais de traitement minier.\n\n\n shared.copyToClipboard=Copier dans le presse-papiers diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index a1d3868897..cbc56c5f5d 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Nessuna data disponibile shared.noDetailsAvailable=Dettagli non disponibili shared.notUsedYet=Non ancora usato shared.date=Data +shared.sendFundsDetailsWithFee=Invio: {0}\n\nAll'indirizzo di ricezione: {1}\n\nCommissione mineraria aggiuntiva: {2}\n\nSei sicuro di voler inviare questa somma? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copia negli appunti diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 521200928d..c1d2281373 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=日付がありません shared.noDetailsAvailable=詳細不明 shared.notUsedYet=未使用 shared.date=日付 +shared.sendFundsDetailsWithFee=送信中: {0}\n\n受取アドレス: {1}\n\n追加のマイナー手数料: {2}\n\nこの金額を送信してもよろしいですか? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Havenoがこのトランザクションはダストの最小閾値以下のおつりアウトプットを生じることを検出しました(それにしたがって、ビットコインのコンセンサス・ルールによって許されない)。代わりに、その ({0} satoshi{1}) のダストはマイニング手数料に追加されます。\n\n\n shared.copyToClipboard=クリップボードにコピー diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index f9ce2ea82a..d7463aacb4 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Sem data disponível shared.noDetailsAvailable=Sem detalhes disponíveis shared.notUsedYet=Ainda não usado shared.date=Data +shared.sendFundsDetailsWithFee=Enviando: {0}\n\nPara o endereço de recebimento: {1}\n\nTaxa adicional do minerador: {2}\n\nTem certeza de que deseja enviar esse valor? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copiar para área de transferência diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 373e07fdeb..b6456daf42 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Sem dada disponível shared.noDetailsAvailable=Sem detalhes disponíveis shared.notUsedYet=Ainda não usado shared.date=Data +shared.sendFundsDetailsWithFee=Enviando: {0}\n\nPara o endereço de recebimento: {1}\n\nTaxa adicional do minerador: {2}\n\nTem certeza de que deseja enviar este valor? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copiar para área de transferência diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 054e625da2..3c66af61fe 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Дата не указана shared.noDetailsAvailable=Подробности не указаны shared.notUsedYet=Ещё не использовано shared.date=Дата +shared.sendFundsDetailsWithFee=Отправка: {0}\n\nНа получающий адрес: {1}\n\nДополнительная комиссия майнера: {2}\n\nВы уверены, что хотите отправить эту сумму? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Скопировать в буфер diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index e2377b330e..8b675222fb 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=ไม่มีวันที่ให้แสดง shared.noDetailsAvailable=ไม่มีรายละเอียด shared.notUsedYet=ยังไม่ได้ใช้งาน shared.date=วันที่ +shared.sendFundsDetailsWithFee=กำลังส่ง: {0}\n\nไปยังที่อยู่ผู้รับ: {1}\n\nค่าธรรมเนียมเหมืองเพิ่มเติม: {2}\n\nคุณแน่ใจหรือไม่ว่าต้องการส่งจำนวนนี้? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=คัดลอกไปที่คลิปบอร์ด diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 9f393b9697..eafef04dfc 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -134,7 +134,7 @@ shared.noDateAvailable=Geçerli tarih yok shared.noDetailsAvailable=Geçerli detay yok shared.notUsedYet=Henüz kullanılmadı shared.date=Tarih -shared.sendFundsDetailsWithFee=Gönderiliyor: {0}\nAlıcı adresine: {1}.\nGerekli madencilik ücreti: {2}\n\nAlıcı alacak: {3}\n\nBu miktarı çekmek istediğinizden emin misiniz? +shared.sendFundsDetailsWithFee=Gönderilen: {0}\n\nAlıcı adresi: {1}\n\nEk madenci ücreti: {2}\n\nBu tutarı göndermek istediğinizden emin misiniz? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno, bu işlemin minimum toz eşiğinin altında bir değişim çıktısı oluşturacağını (ve bu nedenle Monero konsensüs kuralları tarafından izin verilmediğini) tespit etti. Bunun yerine, bu toz ({0} satoshi{1}) madencilik ücretine eklenecektir.\n\n\n shared.copyToClipboard=Panoya kopyala diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 8390ad6834..aeab1530a7 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Ngày tháng không hiển thị shared.noDetailsAvailable=Không có thông tin shared.notUsedYet=Chưa được sử dụng shared.date=Ngày +shared.sendFundsDetailsWithFee=Đang gửi: {0}\n\nĐến địa chỉ nhận: {1}\n\nPhí thợ đào bổ sung: {2}\n\nBạn có chắc chắn muốn gửi số tiền này không? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Sao chép đến clipboard diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 9586f74c99..a783f387e6 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=没有可用数据 shared.noDetailsAvailable=没有可用详细 shared.notUsedYet=尚未使用 shared.date=日期 +shared.sendFundsDetailsWithFee=TOD发送:{0}\n\n接收地址:{1}\n\n额外矿工费:{2}\n\n您确定要发送此金额吗?O # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno 检测到,该交易将产生一个低于最低零头阈值的输出(不被比特币共识规则所允许)。相反,这些零头({0}satoshi{1})将被添加到挖矿手续费中。 shared.copyToClipboard=复制到剪贴板 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index ff998576d0..e3344d1fea 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=沒有可用數據 shared.noDetailsAvailable=沒有可用詳細 shared.notUsedYet=尚未使用 shared.date=日期 +shared.sendFundsDetailsWithFee=發送中:{0}\n\n至接收地址:{1}\n\n額外礦工費:{2}\n\n您確定要發送此金額嗎? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno 檢測到,該交易將產生一個低於最低零頭閾值的輸出(不被比特幣共識規則所允許)。相反,這些零頭({0}satoshi{1})將被添加到挖礦手續費中。 shared.copyToClipboard=複製到剪貼板 diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java index 53b3d70bbf..730b47adf6 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java @@ -302,10 +302,9 @@ public class WithdrawalView extends ActivatableView { BigInteger receiverAmount = tx.getOutgoingTransfer().getDestinations().get(0).getAmount(); BigInteger fee = tx.getFee(); String messageText = Res.get("shared.sendFundsDetailsWithFee", - HavenoUtils.formatXmr(receiverAmount.add(fee), true), + HavenoUtils.formatXmr(receiverAmount, true), withdrawToAddress, - HavenoUtils.formatXmr(fee, true), - HavenoUtils.formatXmr(receiverAmount, true)); + HavenoUtils.formatXmr(fee, true)); // popup confirmation message Popup popup = new Popup(); From 9bd4f70d0254ffc3d72bec24ea5288f9fb9b11c7 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 3 Apr 2025 18:35:08 -0400 Subject: [PATCH 215/371] do not process payment sent & received msgs until deposits confirmed --- .../core/trade/protocol/TradeProtocol.java | 24 ++++++++++++------- .../tasks/ProcessPaymentReceivedMessage.java | 9 +++++++ .../tasks/ProcessPaymentSentMessage.java | 9 +++++++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index b09584ae00..74a50f44f4 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -510,6 +510,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected void handle(PaymentSentMessage message, NodeAddress peer, boolean reprocessOnError) { log.info(LOG_HIGHLIGHT + "handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); + // ignore if not seller or arbitrator + if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { + log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); + return; + } + // validate signature try { HavenoUtils.verifyPaymentSentMessage(trade, message); @@ -522,11 +528,8 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D trade.getBuyer().setPaymentSentMessage(message); trade.requestPersistence(); + // process message on trade thread if (!trade.isInitialized() || trade.isShutDownStarted()) return; - if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { - log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); - return; - } ThreadUtils.execute(() -> { // We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case // that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received @@ -549,7 +552,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D return; } latchTrade(); - expect(anyPhase(Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED) + expect(anyPhase() .with(message) .from(peer)) .setup(tasks( @@ -589,6 +592,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private void handle(PaymentReceivedMessage message, NodeAddress peer, boolean reprocessOnError) { log.info(LOG_HIGHLIGHT + "handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); + // ignore if not buyer or arbitrator + if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { + log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator"); + return; + } + // validate signature try { HavenoUtils.verifyPaymentReceivedMessage(trade, message); @@ -601,12 +610,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D trade.getSeller().setPaymentReceivedMessage(message); trade.requestPersistence(); + // process message on trade thread if (!trade.isInitialized() || trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { - if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { - log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator"); - return; - } synchronized (trade.getLock()) { if (!trade.isInitialized() || trade.isShutDownStarted()) return; latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java index d46b0e0668..5d55bbaea7 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java @@ -72,6 +72,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask { // update to the latest peer address of our peer if message is correct trade.getSeller().setNodeAddress(processModel.getTempTradePeerNodeAddress()); if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses + trade.requestPersistence(); // ack and complete if already processed if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal() && trade.isPayoutPublished()) { @@ -80,6 +81,14 @@ public class ProcessPaymentReceivedMessage extends TradeTask { return; } + // cannot process until wallet sees deposits unlocked + if (!trade.isDepositsUnlocked()) { + trade.syncAndPollWallet(); + if (!trade.isDepositsUnlocked()) { + throw new RuntimeException("Cannot process PaymentReceivedMessage until wallet sees that deposits are unlocked for " + trade.getClass().getSimpleName() + " " + trade.getId()); + } + } + // set state trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getBuyer().setAccountAgeWitness(message.getBuyerAccountAgeWitness()); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java index de7d949ade..1f99d64806 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java @@ -46,6 +46,15 @@ public class ProcessPaymentSentMessage extends TradeTask { // update latest peer address trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress()); + trade.requestPersistence(); + + // cannot process until wallet sees deposits confirmed + if (!trade.isDepositsConfirmed()) { + trade.syncAndPollWallet(); + if (!trade.isDepositsConfirmed()) { + throw new RuntimeException("Cannot process PaymentSentMessage until wallet sees that deposits are confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId()); + } + } // update state from message trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); From 9668dd2369a0ad44861d9cb885147b77608cce42 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 4 Apr 2025 13:55:25 -0400 Subject: [PATCH 216/371] populate trigger price and extra info on duplicate or edit offer --- .../haveno/desktop/main/offer/MutableOfferView.java | 1 + .../desktop/main/offer/MutableOfferViewModel.java | 8 ++++++-- .../duplicateoffer/DuplicateOfferDataModel.java | 11 +++++++---- .../duplicateoffer/DuplicateOfferViewModel.java | 11 +++++++++++ .../main/portfolio/editoffer/EditOfferDataModel.java | 5 ++++- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 1ed15ad845..abdd2d0ec7 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -262,6 +262,7 @@ public abstract class MutableOfferView> exten buyerAsTakerWithoutDepositSlider.setSelected(model.dataModel.getBuyerAsTakerWithoutDeposit().get()); + triggerPriceInputTextField.setText(model.triggerPrice.get()); extraInfoTextArea.setText(model.dataModel.extraInfo.get()); } } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index 6d087b27ea..d49b4f2d15 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -501,7 +501,10 @@ public abstract class MutableOfferViewModel ext }; extraInfoStringListener = (ov, oldValue, newValue) -> { - onExtraInfoTextAreaChanged(); + if (newValue != null) { + extraInfo.set(newValue); + onExtraInfoTextAreaChanged(); + } }; isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); @@ -582,6 +585,7 @@ public abstract class MutableOfferViewModel ext dataModel.getVolume().removeListener(volumeListener); dataModel.getSecurityDepositPct().removeListener(securityDepositAsDoubleListener); dataModel.getBuyerAsTakerWithoutDeposit().removeListener(buyerAsTakerWithoutDepositListener); + dataModel.getExtraInfo().removeListener(extraInfoStringListener); //dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().removeListener(isWalletFundedListener); @@ -843,7 +847,7 @@ public abstract class MutableOfferViewModel ext extraInfoValidationResult.set(getExtraInfoValidationResult()); updateButtonDisableState(); if (extraInfoValidationResult.get().isValid) { - dataModel.setExtraInfo(extraInfo.get()); + setExtraInfoToModel(); } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java index 21ab8a7788..5828b94348 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java @@ -26,6 +26,7 @@ import haveno.core.locale.TradeCurrency; import haveno.core.offer.CreateOfferService; import haveno.core.offer.Offer; import haveno.core.offer.OfferUtil; +import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.PriceFeedService; @@ -89,13 +90,15 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); - setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit()); - - setSecurityDepositPct(getSecurityAsPercent(offer)); - if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } + setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit()); + setSecurityDepositPct(getSecurityAsPercent(offer)); + setExtraInfo(offer.getOfferExtraInfo()); + + OpenOffer openOffer = openOfferManager.getOpenOffer(offer.getId()).orElse(null); + if (openOffer != null) setTriggerPrice(openOffer.getTriggerPrice()); } private double getSecurityAsPercent(Offer offer) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferViewModel.java index 08c5e18f9e..e6e2cf883f 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferViewModel.java @@ -29,6 +29,7 @@ import haveno.core.payment.validation.XmrValidator; import haveno.core.provider.price.PriceFeedService; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; +import haveno.core.util.PriceUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; @@ -76,6 +77,16 @@ class DuplicateOfferViewModel extends MutableOfferViewModel 0) { + triggerPrice.set(PriceUtil.formatMarketPrice(triggerPriceAsLong, dataModel.getCurrencyCode())); + } else { + triggerPrice.set(""); + } + onTriggerPriceTextFieldChanged(); + triggerFocusOutOnAmountFields(); onFocusOutPriceAsPercentageTextField(true, false); } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java index 151a72c0d7..1e7f358572 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -137,6 +137,9 @@ class EditOfferDataModel extends MutableOfferDataModel { securityDepositPct.set(securityDepositPercent); allowAmountUpdate = false; + + triggerPrice = openOffer.getTriggerPrice(); + extraInfo.set(offer.getOfferExtraInfo()); } @Override @@ -164,10 +167,10 @@ class EditOfferDataModel extends MutableOfferDataModel { setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); - setTriggerPrice(openOffer.getTriggerPrice()); if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } + setTriggerPrice(openOffer.getTriggerPrice()); setExtraInfo(offer.getOfferExtraInfo()); } From 7e3a47de4a590222659ad647493d0809ce6995ad Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 5 Apr 2025 17:29:31 -0400 Subject: [PATCH 217/371] prompt to start local node or fallback on startup --- Makefile | 11 +++ .../haveno/core/api/XmrConnectionService.java | 51 ++++++++++-- .../haveno/core/app/HavenoHeadlessApp.java | 2 +- .../java/haveno/core/app/HavenoSetup.java | 13 +-- .../resources/i18n/displayStrings.properties | 9 +- .../i18n/displayStrings_cs.properties | 2 +- .../haveno/desktop/main/MainViewModel.java | 82 +++++++++++++------ 7 files changed, 129 insertions(+), 41 deletions(-) diff --git a/Makefile b/Makefile index ad51f76809..8f32c3ed41 100644 --- a/Makefile +++ b/Makefile @@ -423,6 +423,17 @@ haveno-desktop-stagenet: --apiPort=3204 \ --useNativeXmrWallet=false \ +haveno-daemon-stagenet: + ./haveno-daemon$(APP_EXT) \ + --baseCurrencyNetwork=XMR_STAGENET \ + --useLocalhostForP2P=false \ + --useDevPrivilegeKeys=false \ + --nodePort=9999 \ + --appName=Haveno \ + --apiPassword=apitest \ + --apiPort=3204 \ + --useNativeXmrWallet=false \ + # Mainnet network monerod: diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index d686d64925..346298f58e 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -24,7 +24,6 @@ import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; -import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.xmr.model.EncryptedConnectionList; @@ -73,6 +72,11 @@ public final class XmrConnectionService { private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor + public enum XmrConnectionError { + LOCAL, + CUSTOM + } + private final Object lock = new Object(); private final Object pollLock = new Object(); private final Object listenerLock = new Object(); @@ -90,7 +94,7 @@ public final class XmrConnectionService { private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter - private final SimpleStringProperty connectionServiceFallbackHandler = new SimpleStringProperty(); + private final ObjectProperty connectionServiceError = new SimpleObjectProperty<>(); @Getter private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final LongProperty numUpdates = new SimpleLongProperty(0); @@ -119,7 +123,7 @@ public final class XmrConnectionService { private int numRequestsLastMinute; private long lastSwitchTimestamp; private Set excludedConnections = new HashSet<>(); - private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 60 * 1; // offer to fallback up to once every minute + private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s private boolean fallbackApplied; @Inject @@ -260,7 +264,14 @@ public final class XmrConnectionService { private MoneroRpcConnection getBestConnection(Collection ignoredConnections) { accountService.checkAccountOpen(); - if (!fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) return null; // user needs to explicitly allow fallback after syncing local node + + // user needs to authorize fallback on startup after using locally synced node + if (lastInfo == null && !fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) { + log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback"); + return null; + } + + // get best connection Set ignoredConnectionsSet = new HashSet<>(ignoredConnections); addLocalNodeIfIgnored(ignoredConnectionsSet); MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections @@ -543,6 +554,11 @@ public final class XmrConnectionService { // update connection if (isConnected) { setConnection(connection.getUri()); + + // reset error connecting to local node + if (connectionServiceError.get() == XmrConnectionError.LOCAL && isConnectionLocalHost()) { + connectionServiceError.set(null); + } } else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) { MoneroRpcConnection bestConnection = getBestConnection(); if (bestConnection != null) setConnection(bestConnection); // switch to best connection @@ -632,6 +648,27 @@ public final class XmrConnectionService { return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored(); } + public void startLocalNode() { + + // cannot start local node as seed node + if (HavenoUtils.isSeedNode()) { + throw new RuntimeException("Cannot start local node on seed node"); + } + + // start local node if offline and used as last connection + if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) { + try { + log.info("Starting local node"); + xmrLocalNode.start(); + } catch (Exception e) { + log.error("Unable to start local monero node, error={}\n", e.getMessage(), e); + throw new RuntimeException(e); + } + } else { + throw new RuntimeException("Local node is not offline and used as last connection"); + } + } + private void onConnectionChanged(MoneroRpcConnection currentConnection) { if (isShutDownStarted || !accountService.isAccountOpen()) return; if (currentConnection == null) { @@ -717,14 +754,14 @@ public final class XmrConnectionService { // invoke fallback handling on startup error boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode(); if (lastInfo == null && canFallback) { - if (connectionServiceFallbackHandler.get() == null || connectionServiceFallbackHandler.equals("") && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { + if (connectionServiceError.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { lastFallbackInvocation = System.currentTimeMillis(); if (lastUsedLocalSyncingNode()) { log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage()); - connectionServiceFallbackHandler.set(Res.get("connectionFallback.localNode")); + connectionServiceError.set(XmrConnectionError.LOCAL); } else { log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); - connectionServiceFallbackHandler.set(Res.get("connectionFallback.customNode")); + connectionServiceError.set(XmrConnectionError.CUSTOM); } } return; diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java index fc6eb2d75c..84bdcc746a 100644 --- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java @@ -75,7 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp { log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); acceptedHandler.run(); }); - havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.info("onDisplayMoneroConnectionFallbackHandler: show={}", show)); + havenoSetup.setDisplayMoneroConnectionErrorHandler(show -> log.warn("onDisplayMoneroConnectionErrorHandler: show={}", show)); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index 4ac7b23512..19503fafd8 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -55,6 +55,7 @@ import haveno.core.alert.PrivateNotificationManager; import haveno.core.alert.PrivateNotificationPayload; import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; +import haveno.core.api.XmrConnectionService.XmrConnectionError; import haveno.core.api.XmrLocalNode; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; @@ -158,7 +159,7 @@ public class HavenoSetup { rejectedTxErrorMessageHandler; @Setter @Nullable - private Consumer displayMoneroConnectionFallbackHandler; + private Consumer displayMoneroConnectionErrorHandler; @Setter @Nullable private Consumer displayTorNetworkSettingsHandler; @@ -430,9 +431,9 @@ public class HavenoSetup { getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); // listen for fallback handling - getConnectionServiceFallbackHandler().addListener((observable, oldValue, newValue) -> { - if (displayMoneroConnectionFallbackHandler == null) return; - displayMoneroConnectionFallbackHandler.accept(newValue); + getConnectionServiceError().addListener((observable, oldValue, newValue) -> { + if (displayMoneroConnectionErrorHandler == null) return; + displayMoneroConnectionErrorHandler.accept(newValue); }); log.info("Init P2P network"); @@ -734,8 +735,8 @@ public class HavenoSetup { return xmrConnectionService.getConnectionServiceErrorMsg(); } - public StringProperty getConnectionServiceFallbackHandler() { - return xmrConnectionService.getConnectionServiceFallbackHandler(); + public ObjectProperty getConnectionServiceError() { + return xmrConnectionService.getConnectionServiceError(); } public StringProperty getTopErrorMsg() { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 3a6b666102..3a321d56e0 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2057,9 +2057,12 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=Enter password to unlock -connectionFallback.headline=Connection error -connectionFallback.customNode=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? -connectionFallback.localNode=Error connecting to your last used local node.\n\nDo you want to use the next best available Monero node? +xmrConnectionError.headline=Monero connection error +xmrConnectionError.customNode=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? +xmrConnectionError.localNode=We previously synced using a local Monero node, but it appears to be unreachable.\n\nPlease check that it's running and synced. +xmrConnectionError.localNode.start=Start local node +xmrConnectionError.localNode.start.error=Error starting local node +xmrConnectionError.localNode.fallback=Use next best node torNetworkSettingWindow.header=Tor networks settings torNetworkSettingWindow.noBridges=Don't use bridges diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index bc1841259c..f711402900 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -2056,7 +2056,7 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.title=Suma obchodních poplatků v closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} z celkového objemu obchodů) walletPasswordWindow.headline=Pro odemknutí zadejte heslo -connectionFallback.headline=Chyba připojení +connectionFallback.headline=Chyba připojení k Moneru connectionFallback.customNode=Chyba při připojování k vlastním uzlům Monero.\n\nChcete vyzkoušet další nejlepší dostupný uzel Monero? torNetworkSettingWindow.header=Nastavení sítě Tor diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index 8c36eaf179..4ee10d7846 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -53,6 +53,7 @@ import haveno.core.user.Preferences.UseTorForXmr; import haveno.core.user.User; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; +import haveno.desktop.app.HavenoApp; import haveno.desktop.common.model.ViewModel; import haveno.desktop.components.TxIdTextField; import haveno.desktop.main.account.AccountView; @@ -140,7 +141,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener @SuppressWarnings("FieldCanBeLocal") private MonadicBinding tradesAndUIReady; private final Queue> popupQueue = new PriorityQueue<>(Comparator.comparing(Overlay::getDisplayOrderPriority)); - private Popup moneroConnectionFallbackPopup; + private Popup moneroConnectionErrorPopup; /////////////////////////////////////////////////////////////////////////////////////////// @@ -335,24 +336,59 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener tacWindow.onAction(acceptedHandler::run).show(); }, 1)); - havenoSetup.setDisplayMoneroConnectionFallbackHandler(fallbackMsg -> { - if (fallbackMsg != null && !fallbackMsg.isEmpty()) { - moneroConnectionFallbackPopup = new Popup() - .headLine(Res.get("connectionFallback.headline")) - .warning(fallbackMsg) - .closeButtonText(Res.get("shared.no")) - .actionButtonText(Res.get("shared.yes")) - .onAction(() -> { - havenoSetup.getConnectionServiceFallbackHandler().set(""); - new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); - }) - .onClose(() -> { - log.warn("User has declined to fallback to the next best available Monero node."); - havenoSetup.getConnectionServiceFallbackHandler().set(""); - }); - moneroConnectionFallbackPopup.show(); - } else if (moneroConnectionFallbackPopup != null && moneroConnectionFallbackPopup.isDisplayed()) { - moneroConnectionFallbackPopup.hide(); + havenoSetup.setDisplayMoneroConnectionErrorHandler(connectionError -> { + if (connectionError == null) { + if (moneroConnectionErrorPopup != null) moneroConnectionErrorPopup.hide(); + } else { + switch (connectionError) { + case LOCAL: + moneroConnectionErrorPopup = new Popup() + .headLine(Res.get("xmrConnectionError.headline")) + .warning(Res.get("xmrConnectionError.localNode")) + .actionButtonText(Res.get("xmrConnectionError.localNode.start")) + .onAction(() -> { + log.warn("User has chosen to start local node."); + havenoSetup.getConnectionServiceError().set(null); + new Thread(() -> { + try { + HavenoUtils.xmrConnectionService.startLocalNode(); + } catch (Exception e) { + log.error("Error starting local node: {}", e.getMessage(), e); + new Popup() + .headLine(Res.get("xmrConnectionError.localNode.start.error")) + .warning(e.getMessage()) + .closeButtonText(Res.get("shared.close")) + .onClose(() -> havenoSetup.getConnectionServiceError().set(null)) + .show(); + } + }).start(); + }) + .secondaryActionButtonText(Res.get("xmrConnectionError.localNode.fallback")) + .onSecondaryAction(() -> { + log.warn("User has chosen to fallback to the next best available Monero node."); + havenoSetup.getConnectionServiceError().set(null); + new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + }) + .closeButtonText(Res.get("shared.shutDown")) + .onClose(HavenoApp.getShutDownHandler()); + break; + case CUSTOM: + moneroConnectionErrorPopup = new Popup() + .headLine(Res.get("xmrConnectionError.headline")) + .warning(Res.get("xmrConnectionError.customNode")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + havenoSetup.getConnectionServiceError().set(null); + new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + }) + .closeButtonText(Res.get("shared.no")) + .onClose(() -> { + log.warn("User has declined to fallback to the next best available Monero node."); + havenoSetup.getConnectionServiceError().set(null); + }); + break; + } + moneroConnectionErrorPopup.show(); } }); @@ -360,10 +396,10 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener if (show) { torNetworkSettingsWindow.show(); - // bring connection fallback popup to front if displayed - if (moneroConnectionFallbackPopup != null && moneroConnectionFallbackPopup.isDisplayed()) { - moneroConnectionFallbackPopup.hide(); - moneroConnectionFallbackPopup.show(); + // bring connection error popup to front if displayed + if (moneroConnectionErrorPopup != null && moneroConnectionErrorPopup.isDisplayed()) { + moneroConnectionErrorPopup.hide(); + moneroConnectionErrorPopup.show(); } } else if (torNetworkSettingsWindow.isDisplayed()) { torNetworkSettingsWindow.hide(); From 40e18890d6e28f0f1f3a92fa6337117841f7ac91 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 5 Apr 2025 17:29:55 -0400 Subject: [PATCH 218/371] support cloning up to 10 offers with shared reserved funds (#1668) --- .../main/java/haveno/core/api/CoreApi.java | 32 +- .../haveno/core/api/CoreOffersService.java | 113 +++-- .../haveno/core/api/XmrConnectionService.java | 26 ++ .../haveno/core/offer/CreateOfferService.java | 96 +++- .../haveno/core/offer/OfferBookService.java | 253 ++++++++--- .../java/haveno/core/offer/OfferPayload.java | 4 + .../java/haveno/core/offer/OpenOffer.java | 16 +- .../haveno/core/offer/OpenOfferManager.java | 410 +++++++++++------- .../tasks/MakerReserveOfferFunds.java | 49 ++- .../placeoffer/tasks/MaybeAddToOfferBook.java | 10 + .../dispute/mediation/MediationManager.java | 2 +- .../support/dispute/refund/RefundManager.java | 4 +- .../main/java/haveno/core/trade/Trade.java | 8 +- .../java/haveno/core/trade/TradeManager.java | 4 +- .../tasks/TakerReserveTradeFunds.java | 2 +- .../core/xmr/model/XmrAddressEntry.java | 4 +- .../core/xmr/model/XmrAddressEntryList.java | 23 +- .../haveno/core/xmr/wallet/Restrictions.java | 1 + .../core/xmr/wallet/XmrKeyImagePoller.java | 145 +++---- .../core/xmr/wallet/XmrWalletService.java | 47 +- .../resources/i18n/displayStrings.properties | 40 +- .../i18n/displayStrings_cs.properties | 1 - .../i18n/displayStrings_tr.properties | 1 - .../haveno/daemon/grpc/GrpcOffersService.java | 1 + .../java/haveno/desktop/app/HavenoApp.java | 2 +- .../src/main/java/haveno/desktop/haveno.css | 4 + .../main/funds/deposit/DepositView.java | 2 +- .../funds/withdrawal/WithdrawalListItem.java | 2 +- .../main/offer/MutableOfferDataModel.java | 50 ++- .../desktop/main/offer/MutableOfferView.java | 6 +- .../main/offer/MutableOfferViewModel.java | 4 +- .../offer/createoffer/CreateOfferView.java | 3 +- .../main/offer/offerbook/OfferBook.java | 89 ++-- .../main/offer/offerbook/OfferBookView.java | 5 + .../offer/offerbook/OfferBookViewModel.java | 5 + .../desktop/main/portfolio/PortfolioView.java | 78 +++- .../cloneoffer/CloneOfferDataModel.java | 195 +++++++++ .../portfolio/cloneoffer/CloneOfferView.fxml | 24 + .../portfolio/cloneoffer/CloneOfferView.java | 261 +++++++++++ .../cloneoffer/CloneOfferViewModel.java | 120 +++++ .../closedtrades/ClosedTradesView.java | 2 +- .../DuplicateOfferDataModel.java | 11 - .../duplicateoffer/DuplicateOfferView.java | 1 + .../editoffer/EditOfferDataModel.java | 23 +- .../portfolio/editoffer/EditOfferView.java | 41 +- .../editoffer/EditOfferViewModel.java | 4 +- .../openoffer/OpenOfferListItem.java | 4 + .../portfolio/openoffer/OpenOffersView.fxml | 7 +- .../portfolio/openoffer/OpenOffersView.java | 366 +++++++++++++--- .../openoffer/OpenOffersViewModel.java | 4 + .../java/haveno/desktop/util/FormBuilder.java | 4 +- .../createoffer/CreateOfferDataModelTest.java | 4 +- .../createoffer/CreateOfferViewModelTest.java | 2 +- proto/src/main/proto/grpc.proto | 1 + proto/src/main/proto/pb.proto | 1 + 55 files changed, 2006 insertions(+), 611 deletions(-) create mode 100644 desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java create mode 100644 desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml create mode 100644 desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.java create mode 100644 desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index 99fb3bc74f..e8e83978eb 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -413,21 +413,22 @@ public class CoreApi { } public void postOffer(String currencyCode, - String directionAsString, - String priceAsString, - boolean useMarketBasedPrice, - double marketPriceMargin, - long amountAsLong, - long minAmountAsLong, - double securityDepositPct, - String triggerPriceAsString, - boolean reserveExactAmount, - String paymentAccountId, - boolean isPrivateOffer, - boolean buyerAsTakerWithoutDeposit, - String extraInfo, - Consumer resultHandler, - ErrorMessageHandler errorMessageHandler) { + String directionAsString, + String priceAsString, + boolean useMarketBasedPrice, + double marketPriceMargin, + long amountAsLong, + long minAmountAsLong, + double securityDepositPct, + String triggerPriceAsString, + boolean reserveExactAmount, + String paymentAccountId, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit, + String extraInfo, + String sourceOfferId, + Consumer resultHandler, + ErrorMessageHandler errorMessageHandler) { coreOffersService.postOffer(currencyCode, directionAsString, priceAsString, @@ -442,6 +443,7 @@ public class CoreApi { isPrivateOffer, buyerAsTakerWithoutDeposit, extraInfo, + sourceOfferId, resultHandler, errorMessageHandler); } diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index f9c450b825..3ee7e047f1 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -43,6 +43,7 @@ import static haveno.common.util.MathUtils.exactMultiply; import static haveno.common.util.MathUtils.roundDoubleToLong; import static haveno.common.util.MathUtils.scaleUpByPowerOf10; import haveno.core.locale.CurrencyUtil; +import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; @@ -66,9 +67,7 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.Comparator; import static java.util.Comparator.comparing; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -124,7 +123,6 @@ public class CoreOffersService { return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER; }) .collect(Collectors.toList()); - offers.removeAll(getOffersWithDuplicateKeyImages(offers)); return offers; } @@ -143,12 +141,9 @@ public class CoreOffersService { } List getMyOffers() { - List offers = openOfferManager.getOpenOffers().stream() + return openOfferManager.getOpenOffers().stream() .filter(o -> o.getOffer().isMyOffer(keyRing)) .collect(Collectors.toList()); - Set offersWithDuplicateKeyImages = getOffersWithDuplicateKeyImages(offers.stream().map(OpenOffer::getOffer).collect(Collectors.toList())); // TODO: this is hacky way of filtering offers with duplicate key images - Set offerIdsWithDuplicateKeyImages = offersWithDuplicateKeyImages.stream().map(Offer::getId).collect(Collectors.toSet()); - return offers.stream().filter(o -> !offerIdsWithDuplicateKeyImages.contains(o.getId())).collect(Collectors.toList()); }; List getMyOffers(String direction, String currencyCode) { @@ -179,15 +174,31 @@ public class CoreOffersService { boolean isPrivateOffer, boolean buyerAsTakerWithoutDeposit, String extraInfo, + String sourceOfferId, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); - if (paymentAccount == null) - throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId)); + if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId)); + // clone offer if sourceOfferId given + if (!sourceOfferId.isEmpty()) { + cloneOffer(sourceOfferId, + currencyCode, + priceAsString, + useMarketBasedPrice, + marketPriceMargin, + triggerPriceAsString, + paymentAccountId, + extraInfo, + resultHandler, + errorMessageHandler); + return; + } + + // create new offer String upperCaseCurrencyCode = currencyCode.toUpperCase(); String offerId = createOfferService.getRandomOfferId(); OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase()); @@ -210,17 +221,70 @@ public class CoreOffersService { verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); - // We don't support atm funding from external wallet to keep it simple. - boolean useSavingsWallet = true; - //noinspection ConstantConditions placeOffer(offer, triggerPriceAsString, - useSavingsWallet, + true, reserveExactAmount, + null, transaction -> resultHandler.accept(offer), errorMessageHandler); } + private void cloneOffer(String sourceOfferId, + String currencyCode, + String priceAsString, + boolean useMarketBasedPrice, + double marketPriceMargin, + String triggerPriceAsString, + String paymentAccountId, + String extraInfo, + Consumer resultHandler, + ErrorMessageHandler errorMessageHandler) { + + // get source offer + OpenOffer sourceOpenOffer = getMyOffer(sourceOfferId); + Offer sourceOffer = sourceOpenOffer.getOffer(); + + // get trade currency (default source currency) + if (currencyCode.isEmpty()) currencyCode = sourceOffer.getOfferPayload().getBaseCurrencyCode(); + if (currencyCode.equalsIgnoreCase(Res.getBaseCurrencyCode())) currencyCode = sourceOffer.getOfferPayload().getCounterCurrencyCode(); + String upperCaseCurrencyCode = currencyCode.toUpperCase(); + + // get price (default source price) + Price price = useMarketBasedPrice ? null : priceAsString.isEmpty() ? sourceOffer.isUseMarketBasedPrice() ? null : sourceOffer.getPrice() : Price.parse(upperCaseCurrencyCode, priceAsString); + if (price == null) useMarketBasedPrice = true; + + // get payment account + if (paymentAccountId.isEmpty()) paymentAccountId = sourceOffer.getOfferPayload().getMakerPaymentAccountId(); + PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); + if (paymentAccount == null) throw new IllegalArgumentException(format("payment acRcount with id %s not found", paymentAccountId)); + + // get extra info + if (extraInfo.isEmpty()) extraInfo = sourceOffer.getOfferPayload().getExtraInfo(); + + // create cloned offer + Offer offer = createOfferService.createClonedOffer(sourceOffer, + upperCaseCurrencyCode, + price, + useMarketBasedPrice, + exactMultiply(marketPriceMargin, 0.01), + paymentAccount, + extraInfo); + + // verify cloned offer + verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); + + // place offer + placeOffer(offer, + triggerPriceAsString, + true, + false, // ignored when cloning + sourceOfferId, + transaction -> resultHandler.accept(offer), + errorMessageHandler); + } + + // TODO: this implementation is missing; implement. Offer editOffer(String offerId, String currencyCode, OfferDirection direction, @@ -256,27 +320,6 @@ public class CoreOffersService { // -------------------------- PRIVATE HELPERS ----------------------------- - private Set getOffersWithDuplicateKeyImages(List offers) { - Set duplicateFundedOffers = new HashSet(); - Set seenKeyImages = new HashSet(); - for (Offer offer : offers) { - if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue; - for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) { - if (!seenKeyImages.add(keyImage)) { - for (Offer offer2 : offers) { - if (offer == offer2) continue; - if (offer2.getOfferPayload().getReserveTxKeyImages() == null) continue; - if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { - log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId()); - duplicateFundedOffers.add(offer2); - } - } - } - } - } - return duplicateFundedOffers; - } - private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) { if (!isPaymentAccountValidForOffer(offer, paymentAccount)) { String error = format("cannot create %s offer with payment account %s", @@ -290,6 +333,7 @@ public class CoreOffersService { String triggerPriceAsString, boolean useSavingsWallet, boolean reserveExactAmount, + String sourceOfferId, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode()); @@ -298,6 +342,7 @@ public class CoreOffersService { triggerPriceAsLong, reserveExactAmount, true, + sourceOfferId, resultHandler::accept, errorMessageHandler); } diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 346298f58e..a1e1219172 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -32,6 +32,7 @@ import haveno.core.xmr.nodes.XmrNodes.XmrNode; import haveno.core.xmr.nodes.XmrNodesSetupPreferences; import haveno.core.xmr.setup.DownloadListener; import haveno.core.xmr.setup.WalletsSetup; +import haveno.core.xmr.wallet.XmrKeyImagePoller; import haveno.network.Socks5ProxyProvider; import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PServiceListener; @@ -71,6 +72,8 @@ public final class XmrConnectionService { private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor + private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds + private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes public enum XmrConnectionError { LOCAL, @@ -115,6 +118,7 @@ public final class XmrConnectionService { @Getter private boolean isShutDownStarted; private List listeners = new ArrayList<>(); + private XmrKeyImagePoller keyImagePoller; // connection switching private static final int EXCLUDE_CONNECTION_SECONDS = 180; @@ -403,6 +407,17 @@ public final class XmrConnectionService { return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced } + public XmrKeyImagePoller getKeyImagePoller() { + synchronized (lock) { + if (keyImagePoller == null) keyImagePoller = new XmrKeyImagePoller(); + return keyImagePoller; + } + } + + private long getKeyImageRefreshPeriodMs() { + return isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE; + } + // ----------------------------- APP METHODS ------------------------------ public ReadOnlyIntegerProperty numConnectionsProperty() { @@ -488,6 +503,13 @@ public final class XmrConnectionService { private void initialize() { + // initialize key image poller + getKeyImagePoller(); + new Thread(() -> { + HavenoUtils.waitFor(20000); + keyImagePoller.poll(); // TODO: keep or remove first poll?s + }).start(); + // initialize connections initializeConnections(); @@ -693,6 +715,10 @@ public final class XmrConnectionService { numUpdates.set(numUpdates.get() + 1); }); } + + // update key image poller + keyImagePoller.setDaemon(getDaemon()); + keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); // update polling doPollDaemon(); diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index bca446827c..fab646433b 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -92,6 +92,7 @@ public class CreateOfferService { Version.VERSION.replace(".", ""); } + // TODO: add trigger price? public Offer createAndGetOffer(String offerId, OfferDirection direction, String currencyCode, @@ -105,7 +106,7 @@ public class CreateOfferService { boolean isPrivateOffer, boolean buyerAsTakerWithoutDeposit, String extraInfo) { - log.info("create and get offer with offerId={}, " + + log.info("Create and get offer with offerId={}, " + "currencyCode={}, " + "direction={}, " + "fixedPrice={}, " + @@ -238,6 +239,99 @@ public class CreateOfferService { return offer; } + // TODO: add trigger price? + public Offer createClonedOffer(Offer sourceOffer, + String currencyCode, + Price fixedPrice, + boolean useMarketBasedPrice, + double marketPriceMargin, + PaymentAccount paymentAccount, + String extraInfo) { + log.info("Cloning offer with sourceId={}, " + + "currencyCode={}, " + + "fixedPrice={}, " + + "useMarketBasedPrice={}, " + + "marketPriceMargin={}, " + + "extraInfo={}", + sourceOffer.getId(), + currencyCode, + fixedPrice == null ? null : fixedPrice.getValue(), + useMarketBasedPrice, + marketPriceMargin, + extraInfo); + + OfferPayload sourceOfferPayload = sourceOffer.getOfferPayload(); + String newOfferId = OfferUtil.getRandomOfferId(); + Offer editedOffer = createAndGetOffer(newOfferId, + sourceOfferPayload.getDirection(), + currencyCode, + BigInteger.valueOf(sourceOfferPayload.getAmount()), + BigInteger.valueOf(sourceOfferPayload.getMinAmount()), + fixedPrice, + useMarketBasedPrice, + marketPriceMargin, + sourceOfferPayload.getSellerSecurityDepositPct(), + paymentAccount, + sourceOfferPayload.isPrivateOffer(), + sourceOfferPayload.isBuyerAsTakerWithoutDeposit(), + extraInfo); + + // generate one-time challenge for private offer + String challenge = null; + String challengeHash = null; + if (sourceOfferPayload.isPrivateOffer()) { + challenge = HavenoUtils.generateChallenge(); + challengeHash = HavenoUtils.getChallengeHash(challenge); + } + + OfferPayload editedOfferPayload = editedOffer.getOfferPayload(); + long date = new Date().getTime(); + OfferPayload clonedOfferPayload = new OfferPayload(newOfferId, + date, + sourceOfferPayload.getOwnerNodeAddress(), + sourceOfferPayload.getPubKeyRing(), + sourceOfferPayload.getDirection(), + editedOfferPayload.getPrice(), + editedOfferPayload.getMarketPriceMarginPct(), + editedOfferPayload.isUseMarketBasedPrice(), + sourceOfferPayload.getAmount(), + sourceOfferPayload.getMinAmount(), + sourceOfferPayload.getMakerFeePct(), + sourceOfferPayload.getTakerFeePct(), + sourceOfferPayload.getPenaltyFeePct(), + sourceOfferPayload.getBuyerSecurityDepositPct(), + sourceOfferPayload.getSellerSecurityDepositPct(), + editedOfferPayload.getBaseCurrencyCode(), + editedOfferPayload.getCounterCurrencyCode(), + editedOfferPayload.getPaymentMethodId(), + editedOfferPayload.getMakerPaymentAccountId(), + editedOfferPayload.getCountryCode(), + editedOfferPayload.getAcceptedCountryCodes(), + editedOfferPayload.getBankId(), + editedOfferPayload.getAcceptedBankIds(), + editedOfferPayload.getVersionNr(), + sourceOfferPayload.getBlockHeightAtOfferCreation(), + editedOfferPayload.getMaxTradeLimit(), + editedOfferPayload.getMaxTradePeriod(), + sourceOfferPayload.isUseAutoClose(), + sourceOfferPayload.isUseReOpenAfterAutoClose(), + sourceOfferPayload.getLowerClosePrice(), + sourceOfferPayload.getUpperClosePrice(), + sourceOfferPayload.isPrivateOffer(), + challengeHash, + editedOfferPayload.getExtraDataMap(), + sourceOfferPayload.getProtocolVersion(), + null, + null, + sourceOfferPayload.getReserveTxKeyImages(), + editedOfferPayload.getExtraInfo()); + Offer clonedOffer = new Offer(clonedOfferPayload); + clonedOffer.setPriceFeedService(priceFeedService); + clonedOffer.setChallenge(challenge); + clonedOffer.setState(Offer.State.AVAILABLE); + return clonedOffer; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/haveno/core/offer/OfferBookService.java b/core/src/main/java/haveno/core/offer/OfferBookService.java index 7698aeb1ca..16faa81e57 100644 --- a/core/src/main/java/haveno/core/offer/OfferBookService.java +++ b/core/src/main/java/haveno/core/offer/OfferBookService.java @@ -36,6 +36,9 @@ package haveno.core.offer; import com.google.inject.Inject; import com.google.inject.name.Named; + +import haveno.common.ThreadUtils; +import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.JsonFileManager; @@ -45,45 +48,51 @@ import haveno.core.api.XmrConnectionService; import haveno.core.filter.FilterManager; import haveno.core.locale.Res; import haveno.core.provider.price.PriceFeedService; -import haveno.core.trade.HavenoUtils; import haveno.core.util.JsonUtil; +import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrKeyImageListener; -import haveno.core.xmr.wallet.XmrKeyImagePoller; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.HashMapChangedListener; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; +import haveno.network.utils.Utils; +import lombok.extern.slf4j.Slf4j; + import java.io.File; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import monero.daemon.model.MoneroKeyImageSpentStatus; /** - * Handles storage and retrieval of offers. - * Uses an invalidation flag to only request the full offer map in case there was a change (anyone has added or removed an offer). + * Handles validation and announcement of offers added or removed. */ +@Slf4j public class OfferBookService { + private final static long INVALID_OFFERS_TIMEOUT = 5 * 60 * 1000; // 5 minutes + private final P2PService p2PService; private final PriceFeedService priceFeedService; private final List offerBookChangedListeners = new LinkedList<>(); private final FilterManager filterManager; private final JsonFileManager jsonFileManager; private final XmrConnectionService xmrConnectionService; - - // poll key images of offers - private XmrKeyImagePoller keyImagePoller; - private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds - private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes + private final List validOffers = new ArrayList(); + private final List invalidOffers = new ArrayList(); + private final Map invalidOfferTimers = new HashMap<>(); public interface OfferBookChangedListener { void onAdded(Offer offer); - void onRemoved(Offer offer); } @@ -104,51 +113,45 @@ public class OfferBookService { this.xmrConnectionService = xmrConnectionService; jsonFileManager = new JsonFileManager(storageDir); - // listen for connection changes to monerod - xmrConnectionService.addConnectionListener((connection) -> { - maybeInitializeKeyImagePoller(); - keyImagePoller.setDaemon(xmrConnectionService.getDaemon()); - keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); - }); - // listen for offers p2PService.addHashSetChangedListener(new HashMapChangedListener() { @Override public void onAdded(Collection protectedStorageEntries) { - UserThread.execute(() -> { + ThreadUtils.execute(() -> { protectedStorageEntries.forEach(protectedStorageEntry -> { if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); - maybeInitializeKeyImagePoller(); - keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages()); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); - setReservedFundsSpent(offer); - synchronized (offerBookChangedListeners) { - offerBookChangedListeners.forEach(listener -> listener.onAdded(offer)); + synchronized (validOffers) { + try { + validateOfferPayload(offerPayload); + replaceValidOffer(offer); + announceOfferAdded(offer); + } catch (IllegalArgumentException e) { + // ignore illegal offers + } catch (RuntimeException e) { + replaceInvalidOffer(offer); // offer can become valid later + } } } }); - }); + }, OfferBookService.class.getSimpleName()); } @Override public void onRemoved(Collection protectedStorageEntries) { - UserThread.execute(() -> { + ThreadUtils.execute(() -> { protectedStorageEntries.forEach(protectedStorageEntry -> { if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); - maybeInitializeKeyImagePoller(); - keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages()); + removeValidOffer(offerPayload.getId()); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); - setReservedFundsSpent(offer); - synchronized (offerBookChangedListeners) { - offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer)); - } + announceOfferRemoved(offer); } }); - }); + }, OfferBookService.class.getSimpleName()); } }); @@ -171,6 +174,16 @@ public class OfferBookService { } }); } + + // listen for changes to key images + xmrConnectionService.getKeyImagePoller().addListener(new XmrKeyImageListener() { + @Override + public void onSpentStatusChanged(Map spentStatuses) { + for (String keyImage : spentStatuses.keySet()) { + updateAffectedOffers(keyImage); + } + } + }); } @@ -178,6 +191,10 @@ public class OfferBookService { // API /////////////////////////////////////////////////////////////////////////////////////////// + public boolean hasOffer(String offerId) { + return hasValidOffer(offerId); + } + public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (filterManager.requireUpdateToNewVersionForTrading()) { errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading")); @@ -233,16 +250,9 @@ public class OfferBookService { } public List getOffers() { - return p2PService.getDataMap().values().stream() - .filter(data -> data.getProtectedStoragePayload() instanceof OfferPayload) - .map(data -> { - OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload(); - Offer offer = new Offer(offerPayload); - offer.setPriceFeedService(priceFeedService); - setReservedFundsSpent(offer); - return offer; - }) - .collect(Collectors.toList()); + synchronized (validOffers) { + return new ArrayList<>(validOffers); + } } public List getOffersByCurrency(String direction, String currencyCode) { @@ -266,7 +276,7 @@ public class OfferBookService { } public void shutDown() { - if (keyImagePoller != null) keyImagePoller.clearKeyImages(); + xmrConnectionService.getKeyImagePoller().removeKeyImages(OfferBookService.class.getName()); } @@ -274,37 +284,145 @@ public class OfferBookService { // Private /////////////////////////////////////////////////////////////////////////////////////////// - private synchronized void maybeInitializeKeyImagePoller() { - if (keyImagePoller != null) return; - keyImagePoller = new XmrKeyImagePoller(xmrConnectionService.getDaemon(), getKeyImageRefreshPeriodMs()); + private void announceOfferAdded(Offer offer) { + xmrConnectionService.getKeyImagePoller().addKeyImages(offer.getOfferPayload().getReserveTxKeyImages(), OfferBookService.class.getSimpleName()); + updateReservedFundsSpentStatus(offer); + synchronized (offerBookChangedListeners) { + offerBookChangedListeners.forEach(listener -> listener.onAdded(offer)); + } + } - // handle when key images spent - keyImagePoller.addListener(new XmrKeyImageListener() { - @Override - public void onSpentStatusChanged(Map spentStatuses) { - UserThread.execute(() -> { - for (String keyImage : spentStatuses.keySet()) { - updateAffectedOffers(keyImage); - } - }); + private void announceOfferRemoved(Offer offer) { + updateReservedFundsSpentStatus(offer); + removeKeyImages(offer); + synchronized (offerBookChangedListeners) { + offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer)); + } + + // check if invalid offers are now valid + synchronized (invalidOffers) { + for (Offer invalidOffer : new ArrayList(invalidOffers)) { + try { + validateOfferPayload(invalidOffer.getOfferPayload()); + removeInvalidOffer(invalidOffer.getId()); + replaceValidOffer(invalidOffer); + announceOfferAdded(invalidOffer); + } catch (Exception e) { + // ignore + } } - }); - - // first poll after 20s - // TODO: remove? - new Thread(() -> { - HavenoUtils.waitFor(20000); - keyImagePoller.poll(); - }).start(); + } } - private long getKeyImageRefreshPeriodMs() { - return xmrConnectionService.isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE; + private boolean hasValidOffer(String offerId) { + for (Offer offer : getOffers()) { + if (offer.getId().equals(offerId)) { + return true; + } + } + return false; + } + + private void replaceValidOffer(Offer offer) { + synchronized (validOffers) { + removeValidOffer(offer.getId()); + validOffers.add(offer); + } } + private void replaceInvalidOffer(Offer offer) { + synchronized (invalidOffers) { + removeInvalidOffer(offer.getId()); + invalidOffers.add(offer); + + // remove invalid offer after timeout + synchronized (invalidOfferTimers) { + Timer timer = invalidOfferTimers.get(offer.getId()); + if (timer != null) timer.stop(); + timer = UserThread.runAfter(() -> { + removeInvalidOffer(offer.getId()); + }, INVALID_OFFERS_TIMEOUT); + invalidOfferTimers.put(offer.getId(), timer); + } + } + } + + private void removeValidOffer(String offerId) { + synchronized (validOffers) { + validOffers.removeIf(offer -> offer.getId().equals(offerId)); + } + } + + private void removeInvalidOffer(String offerId) { + synchronized (invalidOffers) { + invalidOffers.removeIf(offer -> offer.getId().equals(offerId)); + + // remove timeout + synchronized (invalidOfferTimers) { + Timer timer = invalidOfferTimers.get(offerId); + if (timer != null) timer.stop(); + invalidOfferTimers.remove(offerId); + } + } + } + + private void validateOfferPayload(OfferPayload offerPayload) { + + // validate offer is not banned + if (filterManager.isOfferIdBanned(offerPayload.getId())) { + throw new IllegalArgumentException("Offer is banned with offerId=" + offerPayload.getId()); + } + + // validate v3 node address compliance + boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() || Utils.isV3Address(offerPayload.getOwnerNodeAddress().getHostName()); + if (!isV3NodeAddressCompliant) { + throw new IllegalArgumentException("Offer with non-V3 node address is not allowed with offerId=" + offerPayload.getId()); + } + + // validate against existing offers + synchronized (validOffers) { + int numOffersWithSharedKeyImages = 0; + for (Offer offer : validOffers) { + + // validate that no offer has overlapping but different key images + if (!offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) && + !Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) { + throw new RuntimeException("Offer with overlapping key images already exists with offerId=" + offer.getId()); + } + + // validate that no offer has same key images, payment method, and currency + if (!offer.getId().equals(offerPayload.getId()) && + offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) && + offer.getOfferPayload().getPaymentMethodId().equals(offerPayload.getPaymentMethodId()) && + offer.getOfferPayload().getBaseCurrencyCode().equals(offerPayload.getBaseCurrencyCode()) && + offer.getOfferPayload().getCounterCurrencyCode().equals(offerPayload.getCounterCurrencyCode())) { + throw new RuntimeException("Offer with same key images, payment method, and currency already exists with offerId=" + offer.getId()); + } + + // count offers with same key images + if (!offer.getId().equals(offerPayload.getId()) && !Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) numOffersWithSharedKeyImages = Math.max(2, numOffersWithSharedKeyImages + 1); + } + + // validate max offers with same key images + if (numOffersWithSharedKeyImages > Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) throw new RuntimeException("More than " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " offers exist with same same key images as new offerId=" + offerPayload.getId()); + } + } + + private void removeKeyImages(Offer offer) { + Set unsharedKeyImages = new HashSet<>(offer.getOfferPayload().getReserveTxKeyImages()); + synchronized (validOffers) { + for (Offer validOffer : validOffers) { + if (validOffer.getId().equals(offer.getId())) continue; + unsharedKeyImages.removeAll(validOffer.getOfferPayload().getReserveTxKeyImages()); + } + } + xmrConnectionService.getKeyImagePoller().removeKeyImages(unsharedKeyImages, OfferBookService.class.getSimpleName()); + } + private void updateAffectedOffers(String keyImage) { for (Offer offer : getOffers()) { if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { + updateReservedFundsSpentStatus(offer); synchronized (offerBookChangedListeners) { offerBookChangedListeners.forEach(listener -> { listener.onRemoved(offer); @@ -315,10 +433,9 @@ public class OfferBookService { } } - private void setReservedFundsSpent(Offer offer) { - if (keyImagePoller == null) return; + private void updateReservedFundsSpentStatus(Offer offer) { for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) { - if (Boolean.TRUE.equals(keyImagePoller.isSpent(keyImage))) { + if (Boolean.TRUE.equals(xmrConnectionService.getKeyImagePoller().isSpent(keyImage))) { offer.setReservedFundsSpent(true); } } diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index 8da91b4b15..1db2cca940 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -347,6 +347,10 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted); } + public boolean isBuyerAsTakerWithoutDeposit() { + return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0; + } + /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index fc4365ecba..f493b1b584 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -48,6 +48,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; +import java.util.UUID; @EqualsAndHashCode public final class OpenOffer implements Tradable { @@ -113,6 +114,9 @@ public final class OpenOffer implements Tradable { @Getter @Setter private boolean deactivatedByTrigger; + @Getter + @Setter + private String groupId; public OpenOffer(Offer offer) { this(offer, 0, false); @@ -127,6 +131,7 @@ public final class OpenOffer implements Tradable { this.triggerPrice = triggerPrice; this.reserveExactAmount = reserveExactAmount; this.challenge = offer.getChallenge(); + this.groupId = UUID.randomUUID().toString(); state = State.PENDING; } @@ -146,6 +151,7 @@ public final class OpenOffer implements Tradable { this.reserveTxKey = openOffer.reserveTxKey; this.challenge = openOffer.challenge; this.deactivatedByTrigger = openOffer.deactivatedByTrigger; + this.groupId = openOffer.groupId; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -164,7 +170,8 @@ public final class OpenOffer implements Tradable { @Nullable String reserveTxHex, @Nullable String reserveTxKey, @Nullable String challenge, - boolean deactivatedByTrigger) { + boolean deactivatedByTrigger, + @Nullable String groupId) { this.offer = offer; this.state = state; this.triggerPrice = triggerPrice; @@ -177,6 +184,8 @@ public final class OpenOffer implements Tradable { this.reserveTxKey = reserveTxKey; this.challenge = challenge; this.deactivatedByTrigger = deactivatedByTrigger; + if (groupId == null) groupId = UUID.randomUUID().toString(); // initialize groupId if not set (added in v1.0.19) + this.groupId = groupId; // reset reserved state to available if (this.state == State.RESERVED) setState(State.AVAILABLE); @@ -199,6 +208,7 @@ public final class OpenOffer implements Tradable { Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge)); + Optional.ofNullable(groupId).ifPresent(e -> builder.setGroupId(groupId)); return protobuf.Tradable.newBuilder().setOpenOffer(builder).build(); } @@ -216,7 +226,8 @@ public final class OpenOffer implements Tradable { ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), ProtoUtil.stringOrNullFromProto(proto.getChallenge()), - proto.getDeactivatedByTrigger()); + proto.getDeactivatedByTrigger(), + ProtoUtil.stringOrNullFromProto(proto.getGroupId())); return openOffer; } @@ -282,6 +293,7 @@ public final class OpenOffer implements Tradable { ",\n reserveExactAmount=" + reserveExactAmount + ",\n scheduledAmount=" + scheduledAmount + ",\n splitOutputTxFee=" + splitOutputTxFee + + ",\n groupId=" + groupId + "\n}"; } } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index e68f90e484..c6a5847f01 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -55,7 +55,7 @@ import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; import haveno.core.exceptions.TradePriceOutOfToleranceException; import haveno.core.filter.FilterManager; -import haveno.core.offer.OfferBookService.OfferBookChangedListener; +import haveno.core.locale.Res; import haveno.core.offer.messages.OfferAvailabilityRequest; import haveno.core.offer.messages.OfferAvailabilityResponse; import haveno.core.offer.messages.SignOfferRequest; @@ -97,7 +97,6 @@ import haveno.network.p2p.peers.PeerManager; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -136,6 +135,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(30); private static final long REFRESH_INTERVAL_MS = OfferPayload.TTL / 2; private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process offer only on republish cycle after this many attempts + private static final long SHUTDOWN_TIMEOUT_MS = 60000; + private static final String OPEN_OFFER_GROUP_KEY_IMAGE_ID = OpenOffer.class.getSimpleName(); + private static final String SIGNED_OFFER_KEY_IMAGE_GROUP_ID = SignedOffer.class.getSimpleName(); private final CoreContext coreContext; private final KeyRing keyRing; @@ -169,12 +171,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe @Getter private final AccountAgeWitnessService accountAgeWitnessService; - // poll key images of signed offers - private XmrKeyImagePoller signedOfferKeyImagePoller; - private static final long SHUTDOWN_TIMEOUT_MS = 60000; - private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds - private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes - private Object processOffersLock = new Object(); // lock for processing offers @@ -227,27 +223,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe this.persistenceManager.initialize(openOffers, "OpenOffers", PersistenceManager.Source.PRIVATE); this.signedOfferPersistenceManager.initialize(signedOffers, "SignedOffers", PersistenceManager.Source.PRIVATE); // arbitrator stores reserve tx for signed offers - - // listen for connection changes to monerod - xmrConnectionService.addConnectionListener((connection) -> maybeInitializeKeyImagePoller()); - - // close open offer if reserved funds spent - offerBookService.addOfferBookChangedListener(new OfferBookChangedListener() { - @Override - public void onAdded(Offer offer) { - - // cancel offer if reserved funds spent - Optional openOfferOptional = getOpenOffer(offer.getId()); - if (openOfferOptional.isPresent() && openOfferOptional.get().getState() != OpenOffer.State.RESERVED && offer.isReservedFundsSpent()) { - log.warn("Canceling open offer because reserved funds have been spent, offerId={}, state={}", offer.getId(), openOfferOptional.get().getState()); - cancelOpenOffer(openOfferOptional.get(), null, null); - } - } - @Override - public void onRemoved(Offer offer) { - // nothing to do - } - }); } @Override @@ -268,34 +243,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe completeHandler); } - private synchronized void maybeInitializeKeyImagePoller() { - if (signedOfferKeyImagePoller != null) return; - signedOfferKeyImagePoller = new XmrKeyImagePoller(xmrConnectionService.getDaemon(), getKeyImageRefreshPeriodMs()); - - // handle when key images confirmed spent - signedOfferKeyImagePoller.addListener(new XmrKeyImageListener() { - @Override - public void onSpentStatusChanged(Map spentStatuses) { - for (Entry entry : spentStatuses.entrySet()) { - if (entry.getValue() == MoneroKeyImageSpentStatus.CONFIRMED) { - removeSignedOffers(entry.getKey()); - } - } - } - }); - - // first poll in 5s - // TODO: remove? - new Thread(() -> { - HavenoUtils.waitFor(5000); - signedOfferKeyImagePoller.poll(); - }).start(); - } - - private long getKeyImageRefreshPeriodMs() { - return xmrConnectionService.isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE; - } - public void onAllServicesInitialized() { p2PService.addDecryptedDirectMessageListener(this); @@ -330,7 +277,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe stopped = true; p2PService.getPeerManager().removeListener(this); p2PService.removeDecryptedDirectMessageListener(this); - if (signedOfferKeyImagePoller != null) signedOfferKeyImagePoller.clearKeyImages(); + xmrConnectionService.getKeyImagePoller().removeKeyImages(OPEN_OFFER_GROUP_KEY_IMAGE_ID); + xmrConnectionService.getKeyImagePoller().removeKeyImages(SIGNED_OFFER_KEY_IMAGE_GROUP_ID); stopPeriodicRefreshOffersTimer(); stopPeriodicRepublishOffersTimer(); @@ -385,11 +333,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe removeOpenOffers(getObservableList(), completeHandler); } - public void removeOpenOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) { - removeOpenOffers(List.of(openOffer), completeHandler); - } - - public void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) { + private void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) { int size = openOffers.size(); // Copy list as we remove in the loop List openOffersList = new ArrayList<>(openOffers); @@ -442,6 +386,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe maybeUpdatePersistedOffers(); + // listen for spent key images to close open and signed offers + xmrConnectionService.getKeyImagePoller().addListener(new XmrKeyImageListener() { + @Override + public void onSpentStatusChanged(Map spentStatuses) { + for (Entry entry : spentStatuses.entrySet()) { + if (XmrKeyImagePoller.isSpent(entry.getValue())) { + cancelOpenOffersOnSpent(entry.getKey()); + removeSignedOffers(entry.getKey()); + } + } + } + }); + // run off user thread so app is not blocked from starting ThreadUtils.submitToPool(() -> { @@ -492,12 +449,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } }); - // initialize key image poller for signed offers - maybeInitializeKeyImagePoller(); + // poll spent status of open offer key images + for (OpenOffer openOffer : getOpenOffers()) { + xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); + } - // poll spent status of key images + // poll spent status of signed offer key images for (SignedOffer signedOffer : signedOffers.getList()) { - signedOfferKeyImagePoller.addKeyImages(signedOffer.getReserveTxKeyImages()); + xmrConnectionService.getKeyImagePoller().addKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); } }, THREAD_ID); }); @@ -544,17 +503,59 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe long triggerPrice, boolean reserveExactAmount, boolean resetAddressEntriesOnError, + String sourceOfferId, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + // check source offer and clone limit + OpenOffer sourceOffer = null; + if (sourceOfferId != null) { + + // get source offer + Optional sourceOfferOptional = getOpenOffer(sourceOfferId); + if (!sourceOfferOptional.isPresent()) { + errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId); + return; + } + sourceOffer = sourceOfferOptional.get(); + + // check clone limit + int numClones = getOpenOfferGroup(sourceOffer.getGroupId()).size(); + if (numClones >= Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) { + errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " cloned offers with shared funds reached."); + return; + } + } + // create open offer - OpenOffer openOffer = new OpenOffer(offer, triggerPrice, reserveExactAmount); + OpenOffer openOffer = new OpenOffer(offer, triggerPrice, sourceOffer == null ? reserveExactAmount : sourceOffer.isReserveExactAmount()); + + // set state from source offer + if (sourceOffer != null) { + openOffer.setReserveTxHash(sourceOffer.getReserveTxHash()); + openOffer.setReserveTxHex(sourceOffer.getReserveTxHex()); + openOffer.setReserveTxKey(sourceOffer.getReserveTxKey()); + openOffer.setGroupId(sourceOffer.getGroupId()); + openOffer.getOffer().getOfferPayload().setReserveTxKeyImages(sourceOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); + xmrWalletService.cloneAddressEntries(sourceOffer.getOffer().getId(), openOffer.getOffer().getId()); + if (hasConflictingClone(openOffer)) openOffer.setState(OpenOffer.State.DEACTIVATED); + } + + // add the open offer + synchronized (processOffersLock) { + addOpenOffer(openOffer); + } + + // done if source offer is pending + if (sourceOffer != null && sourceOffer.isPending()) { + resultHandler.handleResult(null); + return; + } // schedule or post offer ThreadUtils.execute(() -> { synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); - addOpenOffer(openOffer); processOffer(getOpenOffers(), openOffer, (transaction) -> { requestPersistence(); latch.countDown(); @@ -591,18 +592,30 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (openOffer.isPending()) { resultHandler.handleResult(); // ignore if pending } else if (offersToBeEdited.containsKey(openOffer.getId())) { - errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited."); + errorMessageHandler.handleErrorMessage(Res.get("offerbook.cannotActivateEditedOffer.warning")); + } else if (hasConflictingClone(openOffer)) { + errorMessageHandler.handleErrorMessage(Res.get("offerbook.hasConflictingClone.warning")); } else { - Offer offer = openOffer.getOffer(); - offerBookService.activateOffer(offer, - () -> { - openOffer.setState(OpenOffer.State.AVAILABLE); - applyTriggerState(openOffer); - requestPersistence(); - log.debug("activateOpenOffer, offerId={}", offer.getId()); - resultHandler.handleResult(); - }, - errorMessageHandler); + try { + + // validate arbitrator signature + validateSignedState(openOffer); + + // activate offer on offer book + Offer offer = openOffer.getOffer(); + offerBookService.activateOffer(offer, + () -> { + openOffer.setState(OpenOffer.State.AVAILABLE); + applyTriggerState(openOffer); + requestPersistence(); + log.debug("activateOpenOffer, offerId={}", offer.getId()); + resultHandler.handleResult(); + }, + errorMessageHandler); + } catch (Exception e) { + errorMessageHandler.handleErrorMessage(e.getMessage()); + return; + } } } @@ -655,7 +668,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }); } } else { - if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage("You can't remove an offer that is currently edited."); + if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage("You can't cancel an offer that is currently edited."); } } @@ -699,29 +712,44 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe OpenOffer editedOpenOffer = new OpenOffer(editedOffer, triggerPrice, openOffer); if (originalState == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger()) { - editedOpenOffer.setState(OpenOffer.State.AVAILABLE); + if (hasConflictingClone(editedOpenOffer)) { + editedOpenOffer.setState(OpenOffer.State.DEACTIVATED); + } else { + editedOpenOffer.setState(OpenOffer.State.AVAILABLE); + } applyTriggerState(editedOpenOffer); } else { - editedOpenOffer.setState(originalState); + if (originalState == OpenOffer.State.AVAILABLE && hasConflictingClone(editedOpenOffer)) { + editedOpenOffer.setState(OpenOffer.State.DEACTIVATED); + } else { + editedOpenOffer.setState(originalState); + } } addOpenOffer(editedOpenOffer); - // reset arbitrator signature if invalid + // check for valid arbitrator signature after editing Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(editedOpenOffer.getOffer().getOfferPayload().getArbitratorSigner()); if (arbitrator == null || !HavenoUtils.isArbitratorSignatureValid(editedOpenOffer.getOffer().getOfferPayload(), arbitrator)) { + + // reset arbitrator signature editedOpenOffer.getOffer().getOfferPayload().setArbitratorSignature(null); editedOpenOffer.getOffer().getOfferPayload().setArbitratorSigner(null); - } - // process offer which might sign and publish - processOffer(getOpenOffers(), editedOpenOffer, (transaction) -> { + // process offer to sign and publish + processOffer(getOpenOffers(), editedOpenOffer, (transaction) -> { + offersToBeEdited.remove(openOffer.getId()); + requestPersistence(); + resultHandler.handleResult(); + }, (errorMsg) -> { + errorMessageHandler.handleErrorMessage(errorMsg); + }); + } else { + maybeRepublishOffer(editedOpenOffer, null); offersToBeEdited.remove(openOffer.getId()); requestPersistence(); resultHandler.handleResult(); - }, (errorMsg) -> { - errorMessageHandler.handleErrorMessage(errorMsg); - }); + } } else { errorMessageHandler.handleErrorMessage("There is no offer with this id existing to be published."); } @@ -753,26 +781,33 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe Offer offer = openOffer.getOffer(); offer.setState(Offer.State.REMOVED); openOffer.setState(OpenOffer.State.CANCELED); + boolean hasClonedOffer = hasClonedOffer(offer.getId()); // record before removing open offer removeOpenOffer(openOffer); - closedTradableManager.add(openOffer); // TODO: don't add these to closed tradables? + if (!hasClonedOffer) closedTradableManager.add(openOffer); // do not add clones to closed trades TODO: don't add canceled offers to closed tradables? if (resetAddressEntries) xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId()); requestPersistence(); - xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages()); + if (!hasClonedOffer) xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages()); } - // close open offer after key images spent - public void closeOpenOffer(Offer offer) { + // close open offer group after key images spent + public void closeSpentOffer(Offer offer) { getOpenOffer(offer.getId()).ifPresent(openOffer -> { - removeOpenOffer(openOffer); - openOffer.setState(OpenOffer.State.CLOSED); - xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId()); - offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), - () -> log.info("Successfully removed offer {}", offer.getId()), - log::error); - requestPersistence(); + for (OpenOffer groupOffer: getOpenOfferGroup(openOffer.getGroupId())) { + doCloseOpenOffer(groupOffer); + } }); } + private void doCloseOpenOffer(OpenOffer openOffer) { + removeOpenOffer(openOffer); + openOffer.setState(OpenOffer.State.CLOSED); + xmrWalletService.resetAddressEntriesForOpenOffer(openOffer.getId()); + offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), + () -> log.info("Successfully removed offer {}", openOffer.getId()), + log::error); + requestPersistence(); + } + public void reserveOpenOffer(OpenOffer openOffer) { openOffer.setState(OpenOffer.State.RESERVED); requestPersistence(); @@ -783,6 +818,37 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe requestPersistence(); } + public boolean hasConflictingClone(OpenOffer openOffer) { + for (OpenOffer clonedOffer : getOpenOfferGroup(openOffer.getGroupId())) { + if (clonedOffer.getId().equals(openOffer.getId())) continue; + if (clonedOffer.isDeactivated()) continue; // deactivated offers do not conflict + + // pending offers later in the order do not conflict + List openOffers = getOpenOffers(); + if (clonedOffer.isPending() && openOffers.indexOf(clonedOffer) > openOffers.indexOf(openOffer)) { + continue; + } + + // conflicts if same payment method and currency + if (samePaymentMethodAndCurrency(clonedOffer.getOffer(), openOffer.getOffer())) { + return true; + } + } + return false; + } + + public boolean hasConflictingClone(Offer offer, OpenOffer sourceOffer) { + return getOpenOfferGroup(sourceOffer.getGroupId()).stream() + .filter(openOffer -> !openOffer.isDeactivated()) // we only check with activated offers + .anyMatch(openOffer -> samePaymentMethodAndCurrency(openOffer.getOffer(), offer)); + } + + private boolean samePaymentMethodAndCurrency(Offer offer1, Offer offer2) { + return offer1.getPaymentMethodId().equalsIgnoreCase(offer2.getPaymentMethodId()) && + offer1.getCounterCurrencyCode().equalsIgnoreCase(offer2.getCounterCurrencyCode()) && + offer1.getBaseCurrencyCode().equalsIgnoreCase(offer2.getBaseCurrencyCode()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @@ -791,7 +857,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return offer.isMyOffer(keyRing); } - public boolean hasOpenOffers() { + public boolean hasAvailableOpenOffers() { synchronized (openOffers) { for (OpenOffer openOffer : getOpenOffers()) { if (openOffer.getState() == OpenOffer.State.AVAILABLE) { @@ -808,13 +874,38 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } + public List getOpenOfferGroup(String groupId) { + if (groupId == null) throw new IllegalArgumentException("groupId cannot be null"); + synchronized (openOffers) { + return getOpenOffers().stream() + .filter(openOffer -> groupId.equals(openOffer.getGroupId())) + .collect(Collectors.toList()); + } + } + + public boolean hasClonedOffer(String offerId) { + OpenOffer openOffer = getOpenOffer(offerId).orElse(null); + if (openOffer == null) return false; + return getOpenOfferGroup(openOffer.getGroupId()).size() > 1; + } + + public boolean hasClonedOffers() { + synchronized (openOffers) { + for (OpenOffer openOffer : getOpenOffers()) { + if (getOpenOfferGroup(openOffer.getGroupId()).size() > 1) { + return true; + } + } + return false; + } + } + public List getSignedOffers() { synchronized (signedOffers) { return new ArrayList<>(signedOffers.getObservableList()); } } - public ObservableList getObservableSignedOffersList() { synchronized (signedOffers) { return signedOffers.getObservableList(); @@ -846,6 +937,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe synchronized (openOffers) { openOffers.add(openOffer); } + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) { + xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); + } } private void removeOpenOffer(OpenOffer openOffer) { @@ -857,6 +951,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe PlaceOfferProtocol protocol = placeOfferProtocols.remove(openOffer.getId()); if (protocol != null) protocol.cancelOffer(); } + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) { + xmrConnectionService.getKeyImagePoller().removeKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); + } + } + + private void cancelOpenOffersOnSpent(String keyImage) { + for (OpenOffer openOffer : getOpenOffers()) { + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { + log.warn("Canceling open offer because reserved funds have been spent, offerId={}, state={}", openOffer.getId(), openOffer.getState()); + cancelOpenOffer(openOffer, null, null); + } + } } private void addSignedOffer(SignedOffer signedOffer) { @@ -870,7 +976,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // add new signed offer signedOffers.add(signedOffer); - signedOfferKeyImagePoller.addKeyImages(signedOffer.getReserveTxKeyImages()); + xmrConnectionService.getKeyImagePoller().addKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); } } @@ -878,7 +984,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe log.info("Removing SignedOffer for offer {}", signedOffer.getOfferId()); synchronized (signedOffers) { signedOffers.remove(signedOffer); - signedOfferKeyImagePoller.removeKeyImages(signedOffer.getReserveTxKeyImages()); + xmrConnectionService.getKeyImagePoller().removeKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); } } @@ -900,7 +1006,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe List errorMessages = new ArrayList(); synchronized (processOffersLock) { List openOffers = getOpenOffers(); - removeOffersWithDuplicateKeyImages(openOffers); for (OpenOffer offer : openOffers) { if (skipOffersWithTooManyAttempts && offer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts CountDownLatch latch = new CountDownLatch(1); @@ -922,28 +1027,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }, THREAD_ID); } - private void removeOffersWithDuplicateKeyImages(List openOffers) { - - // collect offers with duplicate key images - Set keyImages = new HashSet<>(); - Set offersToRemove = new HashSet<>(); - for (OpenOffer openOffer : openOffers) { - if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) continue; - if (Collections.disjoint(keyImages, openOffer.getOffer().getOfferPayload().getReserveTxKeyImages())) { - keyImages.addAll(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); - } else { - offersToRemove.add(openOffer); - } - } - - // remove offers with duplicate key images - for (OpenOffer offerToRemove : offersToRemove) { - log.warn("Removing open offer which has duplicate key images with other open offers: {}", offerToRemove.getId()); - doCancelOffer(offerToRemove); - openOffers.remove(offerToRemove); - } - } - private void processOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { // skip if already processing @@ -993,33 +1076,40 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } - // validate non-pending state - if (!openOffer.isPending()) { - boolean isValid = true; - Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); - if (openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null) { - isValid = false; - } else if (arbitrator == null) { - log.warn("Offer {} signed by unavailable arbitrator, reposting", openOffer.getId()); - isValid = false; - } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { - log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId()); - isValid = false; + // handle pending offer + if (openOffer.isPending()) { + + // only process the first offer of a pending clone group + if (openOffer.getGroupId() != null) { + List openOfferClones = getOpenOfferGroup(openOffer.getGroupId()); + if (openOfferClones.size() > 1 && !openOfferClones.get(0).getId().equals(openOffer.getId()) && openOfferClones.get(0).isPending()) { + resultHandler.handleResult(null); + return; + } } - if ((openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) && (openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty())) { - log.warn("Offer {} is missing reserve tx hash but has reserved key images, reposting", openOffer.getId()); - isValid = false; - } - if (isValid) { - resultHandler.handleResult(null); + } else { + + // validate non-pending state + try { + validateSignedState(openOffer); + resultHandler.handleResult(null); // done processing if non-pending state is valid return; - } else { + } catch (Exception e) { + log.warn(e.getMessage()); + + // reset arbitrator signature openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING); } } + // sign and post offer if already funded + if (openOffer.getReserveTxHash() != null) { + signAndPostOffer(openOffer, false, resultHandler, errorMessageHandler); + return; + } + // cancel offer if scheduled txs unavailable if (openOffer.getScheduledTxHashes() != null) { boolean scheduledTxsAvailable = true; @@ -1037,12 +1127,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - // sign and post offer if already funded - if (openOffer.getReserveTxHash() != null) { - signAndPostOffer(openOffer, false, resultHandler, errorMessageHandler); - return; - } - // get amount needed to reserve offer BigInteger amountNeeded = openOffer.getOffer().getAmountNeeded(); @@ -1084,6 +1168,21 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }).start(); } + private void validateSignedState(OpenOffer openOffer) { + Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); + if (openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null) { + throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signer"); + } else if (openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null) { + throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signature"); + } else if (arbitrator == null) { + throw new IllegalArgumentException("Offer " + openOffer.getId() + " signed by unavailable arbitrator"); + } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { + throw new IllegalArgumentException("Offer " + openOffer.getId() + " has invalid arbitrator signature"); + } else if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty() || openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty()) { + throw new IllegalArgumentException("Offer " + openOffer.getId() + " is missing reserve tx hash or key images"); + } + } + private MoneroTxWallet getSplitOutputFundingTx(List openOffers, OpenOffer openOffer) { XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); return getSplitOutputFundingTx(openOffers, openOffer, openOffer.getOffer().getAmountNeeded(), addressEntry.getSubaddressIndex()); @@ -2047,7 +2146,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private boolean preventedFromPublishing(OpenOffer openOffer) { if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return true; - return openOffer.isDeactivated() || openOffer.isCanceled() || openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null; + return openOffer.isDeactivated() || + openOffer.isCanceled() || + openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null || + hasConflictingClone(openOffer); } private void startPeriodicRepublishOffersTimer() { diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index e873d1e561..60eaa64cdc 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -19,7 +19,9 @@ package haveno.core.offer.placeoffer.tasks; import java.math.BigInteger; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; @@ -78,6 +80,12 @@ public class MakerReserveOfferFunds extends Task { XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex(); + // copy address entries to clones + for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) { + if (offerClone.getId().equals(offer.getId())) continue; // skip self + model.getXmrWalletService().cloneAddressEntries(openOffer.getId(), offerClone.getId()); + } + // attempt creating reserve tx MoneroTxWallet reserveTx = null; try { @@ -120,23 +128,42 @@ public class MakerReserveOfferFunds extends Task { List reservedKeyImages = new ArrayList(); for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); - // update offer state - openOffer.setReserveTxHash(reserveTx.getHash()); - openOffer.setReserveTxHex(reserveTx.getFullHex()); - openOffer.setReserveTxKey(reserveTx.getKey()); - offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); + // update offer state including clones + if (openOffer.getGroupId() == null) { + openOffer.setReserveTxHash(reserveTx.getHash()); + openOffer.setReserveTxHex(reserveTx.getFullHex()); + openOffer.setReserveTxKey(reserveTx.getKey()); + offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); + } else { + for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) { + offerClone.setReserveTxHash(reserveTx.getHash()); + offerClone.setReserveTxHex(reserveTx.getFullHex()); + offerClone.setReserveTxKey(reserveTx.getKey()); + offerClone.getOffer().getOfferPayload().setReserveTxKeyImages(reservedKeyImages); + } + } - // reset offer funding address entry if unused + // reset offer funding address entries if unused if (fundingEntry != null) { + + // get reserve tx inputs List inputs = model.getXmrWalletService().getOutputs(reservedKeyImages); - boolean usesFundingEntry = false; + + // collect subaddress indices of inputs + Set inputSubaddressIndices = new HashSet<>(); for (MoneroOutputWallet input : inputs) { - if (input.getAccountIndex() == 0 && input.getSubaddressIndex() == fundingEntry.getSubaddressIndex()) { - usesFundingEntry = true; - break; + if (input.getAccountIndex() == 0) inputSubaddressIndices.add(input.getSubaddressIndex()); + } + + // swap funding address entries to available if unused + for (OpenOffer clone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) { + XmrAddressEntry cloneFundingEntry = model.getXmrWalletService().getAddressEntry(clone.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); + if (cloneFundingEntry != null && !inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) { + if (inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) { + model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); + } } } - if (!usesFundingEntry) model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); } } complete(); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java index 8e3e3c23bc..2f8a10108b 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java @@ -36,6 +36,16 @@ public class MaybeAddToOfferBook extends Task { try { runInterceptHook(); checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOpenOffer().getOffer().getId()); + + // deactivate if conflicting offer exists + if (model.getOpenOfferManager().hasConflictingClone(model.getOpenOffer())) { + model.getOpenOffer().setState(OpenOffer.State.DEACTIVATED); + model.setOfferAddedToOfferBook(false); + complete(); + return; + } + + // add to offer book and activate if pending or available if (model.getOpenOffer().isPending() || model.getOpenOffer().isAvailable()) { model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), () -> { diff --git a/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java index 56686faa61..3ae1d99501 100644 --- a/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java @@ -197,7 +197,7 @@ public final class MediationManager extends DisputeManager } } else { Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); - openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer())); } sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); diff --git a/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java b/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java index 034eac6d5a..8748def337 100644 --- a/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java @@ -197,7 +197,7 @@ public final class RefundManager extends DisputeManager { } } else { Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); - openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer())); } sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); @@ -206,7 +206,7 @@ public final class RefundManager extends DisputeManager { tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED); } else { Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); - openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer())); } requestPersistence(); diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 114edcabe5..41027600a2 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -710,7 +710,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { maybePublishTradeStatistics(); // reset address entries - processModel.getXmrWalletService().resetAddressEntriesForTrade(getId()); + processModel.getXmrWalletService().swapPayoutAddressEntryToAvailable(getId()); } // handle when payout unlocks @@ -1755,7 +1755,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // close open offer if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOffer(getId()).isPresent()) { log.info("Closing open offer because {} {} was restored after protocol error", getClass().getSimpleName(), getShortId()); - processModel.getOpenOfferManager().closeOpenOffer(checkNotNull(getOffer())); + processModel.getOpenOfferManager().closeSpentOffer(checkNotNull(getOffer())); } // re-freeze outputs @@ -2371,7 +2371,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean hasBuyerAsTakerWithoutDeposit() { - return getBuyer() == getTaker() && BigInteger.ZERO.equals(getBuyerSecurityDepositBeforeMiningFee()); + return getOffer().getOfferPayload().isBuyerAsTakerWithoutDeposit(); } @Override @@ -2945,7 +2945,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // close open offer or reset address entries if (this instanceof MakerTrade) { - processModel.getOpenOfferManager().closeOpenOffer(getOffer()); + processModel.getOpenOfferManager().closeSpentOffer(getOffer()); HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_PUBLISHED, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); // TODO (woodser): use language translation } else { getXmrWalletService().resetAddressEntriesForOpenOffer(getId()); diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index c98978ae4c..975378ce11 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -977,7 +977,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi removeTrade(trade, true); // TODO The address entry should have been removed already. Check and if its the case remove that. - xmrWalletService.resetAddressEntriesForTrade(trade.getId()); + xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); requestPersistence(); } @@ -1011,7 +1011,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (tradeOptional.isPresent()) { Trade trade = tradeOptional.get(); trade.setDisputeState(disputeState); - xmrWalletService.resetAddressEntriesForTrade(trade.getId()); + xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); requestPersistence(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java index aa0fc9dfe9..9a461b2d83 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -89,7 +89,7 @@ public class TakerReserveTradeFunds extends TradeTask { } catch (Exception e) { // reset state with wallet lock - model.getXmrWalletService().resetAddressEntriesForTrade(trade.getId()); + model.getXmrWalletService().swapPayoutAddressEntryToAvailable(trade.getId()); if (reserveTx != null) { model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); trade.getSelf().setReserveTxKeyImages(null); diff --git a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java index 025d6cda69..d863c92e74 100644 --- a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java +++ b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java @@ -122,12 +122,12 @@ public final class XmrAddressEntry implements PersistablePayload { return context == Context.OFFER_FUNDING; } - public boolean isTrade() { + public boolean isTradePayout() { return context == Context.TRADE_PAYOUT; } public boolean isTradable() { - return isOpenOffer() || isTrade(); + return isOpenOffer() || isTradePayout(); } public Coin getCoinLockedInMultiSig() { diff --git a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java index 73f7379dfb..984f38bdfc 100644 --- a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java +++ b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java @@ -110,10 +110,25 @@ public final class XmrAddressEntryList implements PersistableEnvelope, Persisted } public void swapToAvailable(XmrAddressEntry addressEntry) { - boolean setChangedByRemove = entrySet.remove(addressEntry); - boolean setChangedByAdd = entrySet.add(new XmrAddressEntry(addressEntry.getSubaddressIndex(), addressEntry.getAddressString(), - XmrAddressEntry.Context.AVAILABLE)); - if (setChangedByRemove || setChangedByAdd) { + log.info("swapToAvailable addressEntry to swap={}", addressEntry); + if (entrySet.remove(addressEntry)) { + requestPersistence(); + } + // If we have an address entry which shared the address with another one (shared funding use case) + // then we do not swap to available as we need to protect the address of the remaining entry. + boolean entryWithSameContextStillExists = entrySet.stream().anyMatch(entry -> { + if (addressEntry.getAddressString() != null) { + return addressEntry.getAddressString().equals(entry.getAddressString()) && + addressEntry.getContext() == entry.getContext(); + } + return false; + }); + if (entryWithSameContextStillExists) { + return; + } + // no other uses of the address context remain, so make it available + if (entrySet.add(new XmrAddressEntry(addressEntry.getSubaddressIndex(), addressEntry.getAddressString(), + XmrAddressEntry.Context.AVAILABLE))) { requestPersistence(); } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java index aefb92c41a..5cd181a4aa 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java +++ b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java @@ -31,6 +31,7 @@ public class Restrictions { public static BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.1); public static BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1); public static int MAX_EXTRA_INFO_LENGTH = 1500; + public static int MAX_OFFERS_WITH_SHARED_FUNDS = 10; // At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the // mediated payout. For Refund agent cases we do not have that restriction. diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java index 731c1311e0..1cde84152c 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java @@ -36,15 +36,13 @@ import haveno.core.trade.HavenoUtils; /** * Poll for changes to the spent status of key images. - * - * TODO: move to monero-java? */ @Slf4j public class XmrKeyImagePoller { private MoneroDaemon daemon; private long refreshPeriodMs; - private List keyImages = new ArrayList(); + private Map> keyImageGroups = new HashMap>(); private Set listeners = new HashSet(); private TaskLooper looper; private Map lastStatuses = new HashMap(); @@ -53,9 +51,6 @@ public class XmrKeyImagePoller { /** * Construct the listener. - * - * @param refreshPeriodMs - refresh period in milliseconds - * @param keyImages - key images to listen to */ public XmrKeyImagePoller() { looper = new TaskLooper(() -> poll()); @@ -64,14 +59,13 @@ public class XmrKeyImagePoller { /** * Construct the listener. * + * @param daemon - the Monero daemon to poll * @param refreshPeriodMs - refresh period in milliseconds - * @param keyImages - key images to listen to */ - public XmrKeyImagePoller(MoneroDaemon daemon, long refreshPeriodMs, String... keyImages) { + public XmrKeyImagePoller(MoneroDaemon daemon, long refreshPeriodMs) { looper = new TaskLooper(() -> poll()); setDaemon(daemon); setRefreshPeriodMs(refreshPeriodMs); - setKeyImages(keyImages); } /** @@ -131,36 +125,13 @@ public class XmrKeyImagePoller { return refreshPeriodMs; } - /** - * Get a copy of the key images being listened to. - * - * @return the key images to listen to - */ - public Collection getKeyImages() { - synchronized (keyImages) { - return new ArrayList(keyImages); - } - } - - /** - * Set the key images to listen to. - * - * @return the key images to listen to - */ - public void setKeyImages(String... keyImages) { - synchronized (this.keyImages) { - this.keyImages.clear(); - addKeyImages(keyImages); - } - } - /** * Add a key image to listen to. * * @param keyImage - the key image to listen to */ - public void addKeyImage(String keyImage) { - addKeyImages(keyImage); + public void addKeyImage(String keyImage, String groupId) { + addKeyImages(Arrays.asList(keyImage), groupId); } /** @@ -168,50 +139,26 @@ public class XmrKeyImagePoller { * * @param keyImages - key images to listen to */ - public void addKeyImages(String... keyImages) { - addKeyImages(Arrays.asList(keyImages)); - } - - /** - * Add key images to listen to. - * - * @param keyImages - key images to listen to - */ - public void addKeyImages(Collection keyImages) { - synchronized (this.keyImages) { - for (String keyImage : keyImages) if (!this.keyImages.contains(keyImage)) this.keyImages.add(keyImage); + public void addKeyImages(Collection keyImages, String groupId) { + synchronized (this.keyImageGroups) { + if (!keyImageGroups.containsKey(groupId)) keyImageGroups.put(groupId, new HashSet()); + Set keyImagesGroup = keyImageGroups.get(groupId); + keyImagesGroup.addAll(keyImages); refreshPolling(); } } - /** - * Remove a key image to listen to. - * - * @param keyImage - the key image to unlisten to - */ - public void removeKeyImage(String keyImage) { - removeKeyImages(keyImage); - } - /** * Remove key images to listen to. * * @param keyImages - key images to unlisten to */ - public void removeKeyImages(String... keyImages) { - removeKeyImages(Arrays.asList(keyImages)); - } - - /** - * Remove key images to listen to. - * - * @param keyImages - key images to unlisten to - */ - public void removeKeyImages(Collection keyImages) { - synchronized (this.keyImages) { - Set containedKeyImages = new HashSet(keyImages); - containedKeyImages.retainAll(this.keyImages); - this.keyImages.removeAll(containedKeyImages); + public void removeKeyImages(Collection keyImages, String groupId) { + synchronized (keyImageGroups) { + Set keyImagesGroup = keyImageGroups.get(groupId); + if (keyImagesGroup == null) return; + keyImagesGroup.removeAll(keyImages); + if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId); synchronized (lastStatuses) { for (String lastKeyImage : new HashSet<>(lastStatuses.keySet())) lastStatuses.remove(lastKeyImage); } @@ -219,11 +166,34 @@ public class XmrKeyImagePoller { } } + public void removeKeyImages(String groupId) { + synchronized (keyImageGroups) { + Set keyImagesGroup = keyImageGroups.get(groupId); + if (keyImagesGroup == null) return; + keyImageGroups.remove(groupId); + Set keyImages = getKeyImages(); + synchronized (lastStatuses) { + for (String keyImage : keyImagesGroup) { + if (lastStatuses.containsKey(keyImage) && !keyImages.contains(keyImage)) { + lastStatuses.remove(keyImage); + } + } + } + refreshPolling(); + } + } + /** * Clear the key images which stops polling. */ public void clearKeyImages() { - setKeyImages(); + synchronized (keyImageGroups) { + keyImageGroups.clear(); + synchronized (lastStatuses) { + lastStatuses.clear(); + } + refreshPolling(); + } } /** @@ -235,10 +205,20 @@ public class XmrKeyImagePoller { public Boolean isSpent(String keyImage) { synchronized (lastStatuses) { if (!lastStatuses.containsKey(keyImage)) return null; - return lastStatuses.get(keyImage) != MoneroKeyImageSpentStatus.NOT_SPENT; + return XmrKeyImagePoller.isSpent(lastStatuses.get(keyImage)); } } + /** + * Indicates if the given key image spent status is spent. + * + * @param status the key image spent status to check + * @return true if the key image is spent, false if unspent + */ + public static boolean isSpent(MoneroKeyImageSpentStatus status) { + return status != MoneroKeyImageSpentStatus.NOT_SPENT; + } + /** * Get the last known spent status for the given key image. * @@ -257,16 +237,11 @@ public class XmrKeyImagePoller { return; } - // get copy of key images to fetch - List keyImages = new ArrayList(getKeyImages()); - // fetch spent statuses List spentStatuses = null; + List keyImages = new ArrayList(getKeyImages()); try { - if (keyImages.isEmpty()) spentStatuses = new ArrayList(); - else { - spentStatuses = daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter - } + spentStatuses = keyImages.isEmpty() ? new ArrayList() : daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter } catch (Exception e) { // limit error logging @@ -297,8 +272,8 @@ public class XmrKeyImagePoller { } private void refreshPolling() { - synchronized (keyImages) { - setIsPolling(keyImages.size() > 0 && listeners.size() > 0); + synchronized (keyImageGroups) { + setIsPolling(!getKeyImages().isEmpty() && listeners.size() > 0); } } @@ -313,4 +288,14 @@ public class XmrKeyImagePoller { looper.stop(); } } + + private Set getKeyImages() { + Set allKeyImages = new HashSet(); + synchronized (keyImageGroups) { + for (Set keyImagesGroup : keyImageGroups.values()) { + allKeyImages.addAll(keyImagesGroup); + } + } + return allKeyImages; + } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 7bfc37f8e2..f015280b61 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1008,12 +1008,21 @@ public class XmrWalletService extends XmrWalletBase { public synchronized void swapAddressEntryToAvailable(String offerId, XmrAddressEntry.Context context) { Optional addressEntryOptional = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny(); addressEntryOptional.ifPresent(e -> { - log.info("swap addressEntry with address {} and offerId {} from context {} to available", e.getAddressString(), e.getOfferId(), context); xmrAddressEntryList.swapToAvailable(e); saveAddressEntryList(); }); } + public synchronized void cloneAddressEntries(String offerId, String cloneOfferId) { + List entries = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).collect(Collectors.toList()); + for (XmrAddressEntry entry : entries) { + XmrAddressEntry clonedEntry = new XmrAddressEntry(entry.getSubaddressIndex(), entry.getAddressString(), entry.getContext(), cloneOfferId, null); + Optional existingEntry = getAddressEntry(clonedEntry.getOfferId(), clonedEntry.getContext()); + if (existingEntry.isPresent()) continue; + xmrAddressEntryList.addAddressEntry(clonedEntry); + } + } + public synchronized void resetAddressEntriesForOpenOffer(String offerId) { log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); @@ -1031,7 +1040,7 @@ public class XmrWalletService extends XmrWalletBase { if (trade == null || trade.isPayoutUnlocked()) swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); } - public synchronized void resetAddressEntriesForTrade(String offerId) { + public synchronized void swapPayoutAddressEntryToAvailable(String offerId) { swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); } @@ -1191,26 +1200,34 @@ public class XmrWalletService extends XmrWalletBase { // TODO (woodser): update balance and other listening public void addBalanceListener(XmrBalanceListener listener) { - if (!balanceListeners.contains(listener)) balanceListeners.add(listener); + if (listener == null) throw new IllegalArgumentException("Cannot add null balance listener"); + synchronized (balanceListeners) { + if (!balanceListeners.contains(listener)) balanceListeners.add(listener); + } } public void removeBalanceListener(XmrBalanceListener listener) { - balanceListeners.remove(listener); + if (listener == null) throw new IllegalArgumentException("Cannot add null balance listener"); + synchronized (balanceListeners) { + balanceListeners.remove(listener); + } } public void updateBalanceListeners() { BigInteger availableBalance = getAvailableBalance(); - for (XmrBalanceListener balanceListener : balanceListeners) { - BigInteger balance; - if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex()); - else balance = availableBalance; - ThreadUtils.submitToPool(() -> { - try { - balanceListener.onBalanceChanged(balance); - } catch (Exception e) { - log.warn("Failed to notify balance listener of change: {}\n", e.getMessage(), e); - } - }); + synchronized (balanceListeners) { + for (XmrBalanceListener balanceListener : balanceListeners) { + BigInteger balance; + if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex()); + else balance = availableBalance; + ThreadUtils.submitToPool(() -> { + try { + balanceListener.onBalanceChanged(balance); + } catch (Exception e) { + log.warn("Failed to notify balance listener of change: {}\n", e.getMessage(), e); + } + }); + } } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 3a321d56e0..457323e9f2 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -103,7 +103,6 @@ shared.XMRMinMax=XMR (min - max) shared.removeOffer=Remove offer shared.dontRemoveOffer=Don't remove offer shared.editOffer=Edit offer -shared.duplicateOffer=Duplicate offer shared.openLargeQRWindow=Open large QR code window shared.chooseTradingAccount=Choose trading account shared.faq=Visit FAQ page @@ -385,6 +384,21 @@ offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=Buy XMR with: offerbook.sellXmrFor=Sell XMR for: +offerbook.cloneOffer=Clone offer with shared funds +offerbook.clonedOffer.tooltip=This is a cloned offer with shared funds.\n\Group ID: {0} +offerbook.nonClonedOffer.tooltip=Regular offer without shared funds.\n\Maker reserve transaction ID: {0} +offerbook.hasConflictingClone.warning=This cloned offer with shared funds cannot be activated because it uses \ + the same payment method and currency as another active offer.\n\n\ + You need to edit the offer and change the \ + payment method or currency or deactivate the offer which has the same payment method and currency. +offerbook.cannotActivateEditedOffer.warning=You can't activate an offer that is currently edited. +offerbook.clonedOffer.headline=Cloning an offer +offerbook.clonedOffer.info=Cloning an offer creates a copy without reserving additional funds.\n\n\ + This helps reduce locked capital, making it easier to list the same offer across multiple markets or payment methods.\n\n\ + If one of the cloned offers is taken, the others will close automatically, since they all share the same reserved funds.\n\n\ + Cloned offers must use the same trade amount and security deposit, but they must differ in payment method or currency.\n\n\ + For more information about cloning offers see: [HYPERLINK:https://docs.haveno.exchange/haveno-ui/Cloning_an_offer/] + offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ {0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet @@ -443,8 +457,9 @@ offerbook.warning.requireUpdateToNewVersion=Your version of Haveno is not compat offerbook.warning.offerWasAlreadyUsedInTrade=You cannot take this offer because you already took it earlier. \ It could be that your previous take-offer attempt resulted in a failed trade. -offerbook.warning.arbitratorNotValidated=This offer cannot be taken because the arbitrator is invalid -offerbook.warning.signatureNotValidated=This offer cannot be taken because the arbitrator's signature is invalid +offerbook.warning.arbitratorNotValidated=This offer cannot be taken because the arbitrator is invalid. +offerbook.warning.signatureNotValidated=This offer cannot be taken because the arbitrator's signature is invalid. +offerbook.warning.reserveFundsSpent=This offer cannot be taken because the reserved funds were already spent. offerbook.info.sellAtMarketPrice=You will sell at market price (updated every minute). offerbook.info.buyAtMarketPrice=You will buy at market price (updated every minute). @@ -600,6 +615,7 @@ takeOffer.tac=With taking this offer I agree to the trade conditions as defined #################################################################### openOffer.header.triggerPrice=Trigger price +openOffer.header.groupId=Group ID openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\n\ Please edit the offer to define a new trigger price @@ -610,6 +626,21 @@ editOffer.publishOffer=Publishing your offer. editOffer.failed=Editing of offer failed:\n{0} editOffer.success=Your offer has been successfully edited. editOffer.invalidDeposit=The buyer's security deposit is not within the constraints defined by Haveno and can no longer be edited. +editOffer.openTabWarning=You have already the \"Edit Offer\" tab open. +editOffer.hasConflictingClone=You have edited an offer which uses shared funding with another offer and your edit \ + made the payment method and currency now the same as another active cloned offer. Your edited offer will be \ + deactivated because it is not permitted to publish 2 offers sharing the funds with the same payment method \ + and currency.\n\n\ + You can edit the offer again at \"Portfolio/My open offers\" to fulfill the requirements to activate it. + +cloneOffer.clone=Clone offer +cloneOffer.publishOffer=Publishing cloned offer. +cloneOffer.success=Your offer has been successfully cloned. +cloneOffer.hasConflictingClone=You have not changed the payment method or the currency. You still can clone the offer, but it will \ + be deactivated and not published.\n\n\ + You can edit the offer later again at \"Portfolio/My open offers\" to fulfill the requirements to activate it.\n\n\ + Do you still want to clone the offer? +cloneOffer.openTabWarning=You have already the \"Clone Offer\" tab open. #################################################################### # Portfolio @@ -620,7 +651,8 @@ portfolio.tab.pendingTrades=Open trades portfolio.tab.history=History portfolio.tab.failed=Failed portfolio.tab.editOpenOffer=Edit offer -portfolio.tab.duplicateOffer=Duplicate offer +portfolio.tab.duplicateOffer=Create offer +portfolio.tab.cloneOpenOffer=Clone offer portfolio.context.offerLikeThis=Create new offer like this... portfolio.context.notYourOffer=You can only duplicate offers where you were the maker. diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index f711402900..a3b1974d8b 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -103,7 +103,6 @@ shared.XMRMinMax=XMR (min - max) shared.removeOffer=Odstranit nabídku shared.dontRemoveOffer=Neodstraňovat nabídku shared.editOffer=Upravit nabídku -shared.duplicateOffer=Duplikovat nabídku shared.openLargeQRWindow=Otevřít velké okno s QR kódem shared.chooseTradingAccount=Vyberte obchodní účet shared.faq=Navštívit stránku FAQ diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index eafef04dfc..9ffb43d66a 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -103,7 +103,6 @@ shared.XMRMinMax=XMR (min - max) shared.removeOffer=Teklifi kaldır shared.dontRemoveOffer=Teklifi kaldırma shared.editOffer=Teklifi düzenle -shared.duplicateOffer=Teklifi çoğalt shared.openLargeQRWindow=Büyük QR kodu penceresini aç shared.chooseTradingAccount=İşlem hesabını seç shared.faq=SSS sayfasını ziyaret et diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index cbc6b6b2e3..1c6b8b8de9 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -157,6 +157,7 @@ class GrpcOffersService extends OffersImplBase { req.getIsPrivateOffer(), req.getBuyerAsTakerWithoutDeposit(), req.getExtraInfo(), + req.getSourceOfferId(), offer -> { // This result handling consumer's accept operation will return // the new offer to the gRPC client after async placement is done. diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java index 3e41aa2f8c..16370c2cdb 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java @@ -366,7 +366,7 @@ public class HavenoApp extends Application implements UncaughtExceptionHandler { } // check for open offers - if (injector.getInstance(OpenOfferManager.class).hasOpenOffers()) { + if (injector.getInstance(OpenOfferManager.class).hasAvailableOpenOffers()) { String key = "showOpenOfferWarnPopupAtShutDown"; if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) { new Popup().warning(Res.get("popup.info.shutDownWithOpenOffers")) diff --git a/desktop/src/main/java/haveno/desktop/haveno.css b/desktop/src/main/java/haveno/desktop/haveno.css index 2f50f8d0f3..e3cfac8c0e 100644 --- a/desktop/src/main/java/haveno/desktop/haveno.css +++ b/desktop/src/main/java/haveno/desktop/haveno.css @@ -822,6 +822,10 @@ tree-table-view:focused { -fx-text-fill: -bs-rd-error-red; } +.icon { + -fx-fill: -bs-text-color; +} + .opaque-icon { -fx-fill: -bs-color-gray-bbb; -fx-opacity: 1; diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java index e7c4c89ab9..884df454e7 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java @@ -346,7 +346,7 @@ public class DepositView extends ActivatableView { List addressEntries = xmrWalletService.getAddressEntries(); List items = new ArrayList<>(); for (XmrAddressEntry addressEntry : addressEntries) { - if (addressEntry.isTrade()) continue; // skip reserved for trade + if (addressEntry.isTradePayout()) continue; // do not show trade payout addresses items.add(new DepositListItem(addressEntry, xmrWalletService, formatter)); } diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalListItem.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalListItem.java index ed9a0f0a7d..add84853c3 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalListItem.java @@ -77,7 +77,7 @@ class WithdrawalListItem { public final String getLabel() { if (addressEntry.isOpenOffer()) return Res.getWithCol("shared.offerId") + " " + addressEntry.getShortOfferId(); - else if (addressEntry.isTrade()) + else if (addressEntry.isTradePayout()) return Res.getWithCol("shared.tradeId") + " " + addressEntry.getShortOfferId(); else if (addressEntry.getContext() == XmrAddressEntry.Context.ARBITRATOR) return Res.get("funds.withdrawal.arbitrationFee"); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index 68c5edc733..47bfee0006 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -82,7 +82,7 @@ import lombok.Getter; import org.jetbrains.annotations.NotNull; public abstract class MutableOfferDataModel extends OfferDataModel { - private final CreateOfferService createOfferService; + protected final CreateOfferService createOfferService; protected final OpenOfferManager openOfferManager; private final XmrWalletService xmrWalletService; private final Preferences preferences; @@ -115,7 +115,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { protected PaymentAccount paymentAccount; boolean isTabSelected; - protected double marketPriceMargin = 0; + protected double marketPriceMarginPct = 0; @Getter private boolean marketPriceAvailable; protected boolean allowAmountUpdate = true; @@ -189,12 +189,12 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } private void addListeners() { - xmrWalletService.addBalanceListener(xmrBalanceListener); + if (xmrBalanceListener != null) xmrWalletService.addBalanceListener(xmrBalanceListener); user.getPaymentAccountsAsObservable().addListener(paymentAccountsChangeListener); } private void removeListeners() { - xmrWalletService.removeBalanceListener(xmrBalanceListener); + if (xmrBalanceListener != null) xmrWalletService.removeBalanceListener(xmrBalanceListener); user.getPaymentAccountsAsObservable().removeListener(paymentAccountsChangeListener); } @@ -204,14 +204,16 @@ public abstract class MutableOfferDataModel extends OfferDataModel { /////////////////////////////////////////////////////////////////////////////////////////// // called before activate() - public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { - addressEntry = xmrWalletService.getOrCreateAddressEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING); - xmrBalanceListener = new XmrBalanceListener(getAddressEntry().getSubaddressIndex()) { - @Override - public void onBalanceChanged(BigInteger balance) { - updateBalances(); - } - }; + public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency, boolean initAddressEntry) { + if (initAddressEntry) { + addressEntry = xmrWalletService.getOrCreateAddressEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING); + xmrBalanceListener = new XmrBalanceListener(getAddressEntry().getSubaddressIndex()) { + @Override + public void onBalanceChanged(BigInteger balance) { + updateBalances(); + } + }; + } this.direction = direction; this.tradeCurrency = tradeCurrency; @@ -278,6 +280,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } protected void updateBalances() { + if (addressEntry == null) return; super.updateBalances(); // update remaining balance @@ -302,7 +305,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { minAmount.get(), useMarketBasedPrice.get() ? null : price.get(), useMarketBasedPrice.get(), - useMarketBasedPrice.get() ? marketPriceMargin : 0, + useMarketBasedPrice.get() ? marketPriceMarginPct : 0, securityDepositPct.get(), paymentAccount, buyerAsTakerWithoutDeposit.get(), // private offer if buyer as taker without deposit @@ -316,6 +319,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { triggerPrice, reserveExactAmount, false, // desktop ui resets address entries on cancel + null, resultHandler, errorMessageHandler); } @@ -387,7 +391,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { volume.set(null); minVolume.set(null); price.set(null); - marketPriceMargin = 0; + marketPriceMarginPct = 0; } this.tradeCurrency = tradeCurrency; @@ -416,10 +420,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel { updateBalances(); } - protected void setMarketPriceMarginPct(double marketPriceMargin) { - this.marketPriceMargin = marketPriceMargin; - } - /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @@ -469,7 +469,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } public double getMarketPriceMarginPct() { - return marketPriceMargin; + return marketPriceMarginPct; } long getMaxTradeLimit() { @@ -609,6 +609,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { this.triggerPrice = triggerPrice; } + public void setMarketPriceMarginPct(double marketPriceMarginPct) { + this.marketPriceMarginPct = marketPriceMarginPct; + } + public void setReserveExactAmount(boolean reserveExactAmount) { this.reserveExactAmount = reserveExactAmount; } @@ -684,6 +688,14 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return Restrictions.getMinSecurityDeposit().max(value); } + protected double getSecurityAsPercent(Offer offer) { + BigInteger offerSellerSecurityDeposit = getBoundedSecurityDeposit(offer.getMaxSellerSecurityDeposit()); + double offerSellerSecurityDepositAsPercent = CoinUtil.getAsPercentPerXmr(offerSellerSecurityDeposit, + offer.getAmount()); + return Math.min(offerSellerSecurityDepositAsPercent, + Restrictions.getMaxSecurityDepositAsPercent()); + } + ReadOnlyObjectProperty totalToPayAsProperty() { return totalToPay; } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index abdd2d0ec7..31c02bdc0d 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -297,11 +297,13 @@ public abstract class MutableOfferView> exten model.getDataModel().onTabSelected(isSelected); } - public void initWithData(OfferDirection direction, TradeCurrency tradeCurrency, + public void initWithData(OfferDirection direction, + TradeCurrency tradeCurrency, + boolean initAddressEntry, OfferView.OfferActionHandler offerActionHandler) { this.offerActionHandler = offerActionHandler; - boolean result = model.initWithData(direction, tradeCurrency); + boolean result = model.initWithData(direction, tradeCurrency, initAddressEntry); if (!result) { new Popup().headLine(Res.get("popup.warning.noTradingAccountSetup.headline")) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index d49b4f2d15..fa6cf41729 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -601,8 +601,8 @@ public abstract class MutableOfferViewModel ext // API /////////////////////////////////////////////////////////////////////////////////////////// - boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { - boolean result = dataModel.initWithData(direction, tradeCurrency); + boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency, boolean initAddressEntry) { + boolean result = dataModel.initWithData(direction, tradeCurrency, initAddressEntry); if (dataModel.getAddressEntry() != null) { addressAsString = dataModel.getAddressEntry().getAddressString(); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferView.java index ba9f3d312f..f88bed1808 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferView.java @@ -47,11 +47,10 @@ public class CreateOfferView extends MutableOfferView { super(model, navigation, preferences, offerDetailsWindow, btcFormatter); } - @Override public void initWithData(OfferDirection direction, TradeCurrency tradeCurrency, OfferView.OfferActionHandler offerActionHandler) { - super.initWithData(direction, tradeCurrency, offerActionHandler); + super.initWithData(direction, tradeCurrency, true, offerActionHandler); } @Override diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBook.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBook.java index 2367f51c8d..2e92a9d42d 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBook.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBook.java @@ -19,13 +19,12 @@ package haveno.desktop.main.offer.offerbook; import com.google.inject.Inject; import com.google.inject.Singleton; -import haveno.core.filter.FilterManager; + +import haveno.common.UserThread; import haveno.core.offer.Offer; import haveno.core.offer.OfferBookService; import static haveno.core.offer.OfferDirection.BUY; -import haveno.core.offer.OfferRestrictions; import haveno.network.p2p.storage.P2PDataStorage; -import haveno.network.utils.Utils; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -49,7 +48,6 @@ public class OfferBook { private final ObservableList offerBookListItems = FXCollections.observableArrayList(); private final Map buyOfferCountMap = new HashMap<>(); private final Map sellOfferCountMap = new HashMap<>(); - private final FilterManager filterManager; /////////////////////////////////////////////////////////////////////////////////////////// @@ -57,64 +55,47 @@ public class OfferBook { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - OfferBook(OfferBookService offerBookService, FilterManager filterManager) { + OfferBook(OfferBookService offerBookService) { this.offerBookService = offerBookService; - this.filterManager = filterManager; offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { @Override public void onAdded(Offer offer) { - printOfferBookListItems("Before onAdded"); - // We get onAdded called every time a new ProtectedStorageEntry is received. - // Mostly it is the same OfferPayload but the ProtectedStorageEntry is different. - // We filter here to only add new offers if the same offer (using equals) was not already added and it - // is not banned. + UserThread.execute(() -> { + printOfferBookListItems("Before onAdded"); - if (filterManager.isOfferIdBanned(offer.getId())) { - log.debug("Ignored banned offer. ID={}", offer.getId()); - return; - } - - if (OfferRestrictions.requiresNodeAddressUpdate() && !Utils.isV3Address(offer.getMakerNodeAddress().getHostName())) { - log.debug("Ignored offer with Tor v2 node address. ID={}", offer.getId()); - return; - } - - // Use offer.equals(offer) to see if the OfferBook list contains an exact - // match -- offer.equals(offer) includes comparisons of payload, state - // and errorMessage. - synchronized (offerBookListItems) { - boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer)); - if (!hasSameOffer) { - OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer); - removeDuplicateItem(newOfferBookListItem); - offerBookListItems.add(newOfferBookListItem); // Add replacement. - if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. - log.debug("onAdded: Added new offer {}\n" - + "\twith newItem.payloadHash: {}", - offer.getId(), - newOfferBookListItem.hashOfPayload.getHex()); + // Use offer.equals(offer) to see if the OfferBook list contains an exact + // match -- offer.equals(offer) includes comparisons of payload, state + // and errorMessage. + synchronized (offerBookListItems) { + boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer)); + if (!hasSameOffer) { + OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer); + removeDuplicateItem(newOfferBookListItem); + offerBookListItems.add(newOfferBookListItem); // Add replacement. + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + log.debug("onAdded: Added new offer {}\n" + + "\twith newItem.payloadHash: {}", + offer.getId(), + newOfferBookListItem.hashOfPayload.getHex()); + } + } else { + log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId()); } - } else { - log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId()); + printOfferBookListItems("After onAdded"); } - printOfferBookListItems("After onAdded"); - } + }); } @Override public void onRemoved(Offer offer) { - synchronized (offerBookListItems) { - printOfferBookListItems("Before onRemoved"); - removeOffer(offer); - printOfferBookListItems("After onRemoved"); - } - } - }); - - filterManager.filterProperty().addListener((observable, oldValue, newValue) -> { - if (newValue != null) { - // any notifications + UserThread.execute(() -> { + synchronized (offerBookListItems) { + printOfferBookListItems("Before onRemoved"); + removeOffer(offer); + printOfferBookListItems("After onRemoved"); + } + }); } }); } @@ -212,7 +193,6 @@ public class OfferBook { // Investigate why.... offerBookListItems.clear(); offerBookListItems.addAll(offerBookService.getOffers().stream() - .filter(this::isOfferAllowed) .map(OfferBookListItem::new) .collect(Collectors.toList())); @@ -248,13 +228,6 @@ public class OfferBook { return sellOfferCountMap; } - private boolean isOfferAllowed(Offer offer) { - boolean isBanned = filterManager.isOfferIdBanned(offer.getId()); - boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() - || Utils.isV3Address(offer.getMakerNodeAddress().getHostName()); - return !isBanned && isV3NodeAddressCompliant; - } - private void fillOfferCountMaps() { buyOfferCountMap.clear(); sellOfferCountMap.clear(); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index af47d9bec5..0248b9c522 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -695,8 +695,13 @@ abstract public class OfferBookView fillCurrencies(); + // refresh filter on changes + offerBook.getOfferBookListItems().addListener((ListChangeListener) c -> { + filterOffers(); + }); + filterItemsListener = c -> { final Optional highestAmountOffer = filteredItems.stream() .max(Comparator.comparingLong(o -> o.getOffer().getAmount().longValueExact())); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/PortfolioView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/PortfolioView.java index 3b17109a29..c3a1e76738 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/PortfolioView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/PortfolioView.java @@ -30,6 +30,8 @@ import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.View; import haveno.desktop.main.MainView; +import haveno.desktop.main.overlays.popups.Popup; +import haveno.desktop.main.portfolio.cloneoffer.CloneOfferView; import haveno.desktop.main.portfolio.closedtrades.ClosedTradesView; import haveno.desktop.main.portfolio.duplicateoffer.DuplicateOfferView; import haveno.desktop.main.portfolio.editoffer.EditOfferView; @@ -49,7 +51,7 @@ public class PortfolioView extends ActivatableView { @FXML Tab openOffersTab, pendingTradesTab, closedTradesTab; - private Tab editOpenOfferTab, duplicateOfferTab; + private Tab editOpenOfferTab, duplicateOfferTab, cloneOpenOfferTab; private final Tab failedTradesTab = new Tab(Res.get("portfolio.tab.failed").toUpperCase()); private Tab currentTab; private Navigation.Listener navigationListener; @@ -61,7 +63,8 @@ public class PortfolioView extends ActivatableView { private final FailedTradesManager failedTradesManager; private EditOfferView editOfferView; private DuplicateOfferView duplicateOfferView; - private boolean editOpenOfferViewOpen; + private CloneOfferView cloneOfferView; + private boolean editOpenOfferViewOpen, cloneOpenOfferViewOpen; private OpenOffer openOffer; private OpenOffersView openOffersView; @@ -99,12 +102,16 @@ public class PortfolioView extends ActivatableView { navigation.navigateTo(MainView.class, PortfolioView.class, EditOfferView.class); else if (newValue == duplicateOfferTab) { navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class); + } else if (newValue == cloneOpenOfferTab) { + navigation.navigateTo(MainView.class, PortfolioView.class, CloneOfferView.class); } if (oldValue != null && oldValue == editOpenOfferTab) editOfferView.onTabSelected(false); if (oldValue != null && oldValue == duplicateOfferTab) duplicateOfferView.onTabSelected(false); + if (oldValue != null && oldValue == cloneOpenOfferTab) + cloneOfferView.onTabSelected(false); }; @@ -115,6 +122,8 @@ public class PortfolioView extends ActivatableView { onEditOpenOfferRemoved(); if (removedTabs.size() == 1 && removedTabs.get(0).equals(duplicateOfferTab)) onDuplicateOfferRemoved(); + if (removedTabs.size() == 1 && removedTabs.get(0).equals(cloneOpenOfferTab)) + onCloneOpenOfferRemoved(); }; } @@ -137,6 +146,16 @@ public class PortfolioView extends ActivatableView { navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class); } + private void onCloneOpenOfferRemoved() { + cloneOpenOfferViewOpen = false; + if (cloneOfferView != null) { + cloneOfferView.onClose(); + cloneOfferView = null; + } + + navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class); + } + @Override protected void activate() { failedTradesManager.getObservableList().addListener((ListChangeListener) c -> { @@ -166,6 +185,9 @@ public class PortfolioView extends ActivatableView { } else if (root.getSelectionModel().getSelectedItem() == duplicateOfferTab) { navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class); if (duplicateOfferView != null) duplicateOfferView.onTabSelected(true); + } else if (root.getSelectionModel().getSelectedItem() == cloneOpenOfferTab) { + navigation.navigateTo(MainView.class, PortfolioView.class, CloneOfferView.class); + if (cloneOfferView != null) cloneOfferView.onTabSelected(true); } } @@ -178,10 +200,9 @@ public class PortfolioView extends ActivatableView { } private void loadView(Class viewClass, @Nullable Object data) { - // we want to get activate/deactivate called, so we remove the old view on tab change - // TODO Don't understand the check for currentTab != editOpenOfferTab - if (currentTab != null && currentTab != editOpenOfferTab) - currentTab.setContent(null); + + // nullify current tab to trigger activate/deactivate + if (currentTab != null) currentTab.setContent(null); View view = viewLoader.load(viewClass); @@ -235,6 +256,28 @@ public class PortfolioView extends ActivatableView { view = viewLoader.load(OpenOffersView.class); selectOpenOffersView((OpenOffersView) view); } + } else if (view instanceof CloneOfferView) { + if (data instanceof OpenOffer) { + openOffer = (OpenOffer) data; + } + if (openOffer != null) { + if (cloneOfferView == null) { + cloneOfferView = (CloneOfferView) view; + cloneOfferView.applyOpenOffer(openOffer); + cloneOpenOfferTab = new Tab(Res.get("portfolio.tab.cloneOpenOffer").toUpperCase()); + cloneOfferView.setCloseHandler(() -> { + root.getTabs().remove(cloneOpenOfferTab); + }); + root.getTabs().add(cloneOpenOfferTab); + } + if (currentTab != cloneOpenOfferTab) + cloneOfferView.onTabSelected(true); + + currentTab = cloneOpenOfferTab; + } else { + view = viewLoader.load(OpenOffersView.class); + selectOpenOffersView((OpenOffersView) view); + } } currentTab.setContent(view.getRoot()); @@ -245,20 +288,35 @@ public class PortfolioView extends ActivatableView { openOffersView = view; currentTab = openOffersTab; - OpenOfferActionHandler openOfferActionHandler = openOffer -> { + EditOpenOfferHandler editOpenOfferHandler = openOffer -> { if (!editOpenOfferViewOpen) { editOpenOfferViewOpen = true; PortfolioView.this.openOffer = openOffer; navigation.navigateTo(MainView.class, PortfolioView.this.getClass(), EditOfferView.class); } else { - log.error("You have already a \"Edit Offer\" tab open."); + new Popup().warning(Res.get("editOffer.openTabWarning")).show(); } }; - openOffersView.setOpenOfferActionHandler(openOfferActionHandler); + openOffersView.setEditOpenOfferHandler(editOpenOfferHandler); + + CloneOpenOfferHandler cloneOpenOfferHandler = openOffer -> { + if (!cloneOpenOfferViewOpen) { + cloneOpenOfferViewOpen = true; + PortfolioView.this.openOffer = openOffer; + navigation.navigateTo(MainView.class, PortfolioView.this.getClass(), CloneOfferView.class); + } else { + new Popup().warning(Res.get("cloneOffer.openTabWarning")).show(); + } + }; + openOffersView.setCloneOpenOfferHandler(cloneOpenOfferHandler); } - public interface OpenOfferActionHandler { + public interface EditOpenOfferHandler { void onEditOpenOffer(OpenOffer openOffer); } + + public interface CloneOpenOfferHandler { + void onCloneOpenOffer(OpenOffer openOffer); + } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java new file mode 100644 index 0000000000..24d792005c --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java @@ -0,0 +1,195 @@ +/* + * 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 haveno.desktop.main.portfolio.cloneoffer; + + +import haveno.desktop.Navigation; +import haveno.desktop.main.offer.MutableOfferDataModel; +import haveno.core.account.witness.AccountAgeWitnessService; +import haveno.core.locale.CurrencyUtil; +import haveno.core.locale.TradeCurrency; +import haveno.core.offer.CreateOfferService; +import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; +import haveno.core.offer.OfferUtil; +import haveno.core.offer.OpenOffer; +import haveno.core.offer.OpenOfferManager; +import haveno.core.payment.PaymentAccount; +import haveno.core.proto.persistable.CorePersistenceProtoResolver; +import haveno.core.provider.price.PriceFeedService; +import haveno.core.trade.statistics.TradeStatisticsManager; +import haveno.core.user.Preferences; +import haveno.core.user.User; +import haveno.core.util.FormattingUtils; +import haveno.core.util.coin.CoinFormatter; +import haveno.core.xmr.wallet.XmrWalletService; +import haveno.network.p2p.P2PService; + +import haveno.common.handlers.ErrorMessageHandler; +import haveno.common.handlers.ResultHandler; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +class CloneOfferDataModel extends MutableOfferDataModel { + + private final CorePersistenceProtoResolver corePersistenceProtoResolver; + private OpenOffer sourceOpenOffer; + + @Inject + CloneOfferDataModel(CreateOfferService createOfferService, + OpenOfferManager openOfferManager, + OfferUtil offerUtil, + XmrWalletService xmrWalletService, + Preferences preferences, + User user, + P2PService p2PService, + PriceFeedService priceFeedService, + AccountAgeWitnessService accountAgeWitnessService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter, + CorePersistenceProtoResolver corePersistenceProtoResolver, + TradeStatisticsManager tradeStatisticsManager, + Navigation navigation) { + + super(createOfferService, + openOfferManager, + offerUtil, + xmrWalletService, + preferences, + user, + p2PService, + priceFeedService, + accountAgeWitnessService, + xmrFormatter, + tradeStatisticsManager, + navigation); + this.corePersistenceProtoResolver = corePersistenceProtoResolver; + } + + public void reset() { + direction = null; + tradeCurrency = null; + tradeCurrencyCode.set(null); + useMarketBasedPrice.set(false); + amount.set(null); + minAmount.set(null); + price.set(null); + volume.set(null); + minVolume.set(null); + securityDepositPct.set(0); + paymentAccounts.clear(); + paymentAccount = null; + marketPriceMarginPct = 0; + sourceOpenOffer = null; + } + + public void applyOpenOffer(OpenOffer openOffer) { + this.sourceOpenOffer = openOffer; + + Offer offer = openOffer.getOffer(); + direction = offer.getDirection(); + CurrencyUtil.getTradeCurrency(offer.getCurrencyCode()) + .ifPresent(c -> this.tradeCurrency = c); + tradeCurrencyCode.set(offer.getCurrencyCode()); + + PaymentAccount tmpPaymentAccount = user.getPaymentAccount(openOffer.getOffer().getMakerPaymentAccountId()); + Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()); + if (optionalTradeCurrency.isPresent() && tmpPaymentAccount != null) { + TradeCurrency selectedTradeCurrency = optionalTradeCurrency.get(); + this.paymentAccount = PaymentAccount.fromProto(tmpPaymentAccount.toProtoMessage(), corePersistenceProtoResolver); + if (paymentAccount.getSingleTradeCurrency() != null) + paymentAccount.setSingleTradeCurrency(selectedTradeCurrency); + else + paymentAccount.setSelectedTradeCurrency(selectedTradeCurrency); + } + + allowAmountUpdate = false; + } + + public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { + try { + return super.initWithData(direction, tradeCurrency, false); + } catch (NullPointerException e) { + if (e.getMessage().contains("tradeCurrency")) { + throw new IllegalArgumentException("Offers of removed assets cannot be edited. You can only cancel it.", e); + } + return false; + } + } + + @Override + protected Set getUserPaymentAccounts() { + return Objects.requireNonNull(user.getPaymentAccounts()).stream() + .filter(account -> !account.getPaymentMethod().isBsqSwap()) + .collect(Collectors.toSet()); + } + + @Override + protected PaymentAccount getPreselectedPaymentAccount() { + return paymentAccount; + } + + public void populateData() { + Offer offer = sourceOpenOffer.getOffer(); + // Min amount need to be set before amount as if minAmount is null it would be set by amount + setMinAmount(offer.getMinAmount()); + setAmount(offer.getAmount()); + setPrice(offer.getPrice()); + setVolume(offer.getVolume()); + setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); + setTriggerPrice(sourceOpenOffer.getTriggerPrice()); + if (offer.isUseMarketBasedPrice()) { + setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); + } + setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit()); + setSecurityDepositPct(getSecurityAsPercent(offer)); + setExtraInfo(offer.getOfferExtraInfo()); + } + + public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + Offer clonedOffer = createClonedOffer(); + openOfferManager.placeOffer(clonedOffer, + false, + triggerPrice, + false, + true, + sourceOpenOffer.getId(), + transaction -> resultHandler.handleResult(), + errorMessageHandler); + } + + private Offer createClonedOffer() { + return createOfferService.createClonedOffer(sourceOpenOffer.getOffer(), + tradeCurrencyCode.get(), + useMarketBasedPrice.get() ? null : price.get(), + useMarketBasedPrice.get(), + useMarketBasedPrice.get() ? marketPriceMarginPct : 0, + paymentAccount, + extraInfo.get()); + } + + public boolean hasConflictingClone() { + Offer clonedOffer = createClonedOffer(); + return openOfferManager.hasConflictingClone(clonedOffer, sourceOpenOffer); + } +} \ No newline at end of file diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml new file mode 100644 index 0000000000..80c57192c0 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.java new file mode 100644 index 0000000000..e48bdf80a7 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.java @@ -0,0 +1,261 @@ +/* + * 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 haveno.desktop.main.portfolio.cloneoffer; + +import haveno.desktop.Navigation; +import haveno.desktop.common.view.FxmlView; +import haveno.desktop.components.AutoTooltipButton; +import haveno.desktop.components.BusyAnimation; +import haveno.desktop.main.offer.MutableOfferView; +import haveno.desktop.main.overlays.popups.Popup; +import haveno.desktop.main.overlays.windows.OfferDetailsWindow; + +import haveno.core.locale.CurrencyUtil; +import haveno.core.locale.Res; +import haveno.core.offer.OpenOffer; +import haveno.core.payment.PaymentAccount; +import haveno.core.user.DontShowAgainLookup; +import haveno.core.user.Preferences; +import haveno.core.util.FormattingUtils; +import haveno.core.util.coin.CoinFormatter; +import haveno.common.UserThread; +import haveno.common.util.Tuple4; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.image.ImageView; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; + +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; + +import javafx.collections.ObservableList; + +import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; + +@FxmlView +public class CloneOfferView extends MutableOfferView { + + private BusyAnimation busyAnimation; + private Button cloneButton; + private Button cancelButton; + private Label spinnerInfoLabel; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private CloneOfferView(CloneOfferViewModel model, + Navigation navigation, + Preferences preferences, + OfferDetailsWindow offerDetailsWindow, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { + super(model, navigation, preferences, offerDetailsWindow, btcFormatter); + } + + @Override + protected void initialize() { + super.initialize(); + + addCloneGroup(); + renameAmountGroup(); + } + + private void renameAmountGroup() { + amountTitledGroupBg.setText(Res.get("editOffer.setPrice")); + } + + @Override + protected void doSetFocus() { + // Don't focus in any field before data was set + } + + @Override + protected void doActivate() { + super.doActivate(); + + + addBindings(); + + hideOptionsGroup(); + hideNextButtons(); + + // Lock amount field as it would require bigger changes to support increased amount values. + amountTextField.setDisable(true); + amountBtcLabel.setDisable(true); + minAmountTextField.setDisable(true); + minAmountBtcLabel.setDisable(true); + volumeTextField.setDisable(true); + volumeCurrencyLabel.setDisable(true); + + // Workaround to fix margin on top of amount group + gridPane.setPadding(new Insets(-20, 25, -1, 25)); + + updatePriceToggle(); + updateElementsWithDirection(); + + model.isNextButtonDisabled.setValue(false); + cancelButton.setDisable(false); + + model.onInvalidateMarketPriceMargin(); + model.onInvalidatePrice(); + + // To force re-validation of payment account validation + onPaymentAccountsComboBoxSelected(); + } + + @Override + protected void deactivate() { + super.deactivate(); + + removeBindings(); + } + + @Override + public void onClose() { + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void applyOpenOffer(OpenOffer openOffer) { + model.applyOpenOffer(openOffer); + + initWithData(openOffer.getOffer().getDirection(), + CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get(), + false, + null); + + if (!model.isSecurityDepositValid()) { + new Popup().warning(Res.get("editOffer.invalidDeposit")) + .onClose(this::close) + .show(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Bindings, Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addBindings() { + cloneButton.disableProperty().bind(model.isNextButtonDisabled); + } + + private void removeBindings() { + cloneButton.disableProperty().unbind(); + } + + @Override + protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { + return paymentAccounts; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Build UI elements + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addCloneGroup() { + Tuple4 tuple4 = addButtonBusyAnimationLabelAfterGroup(gridPane, 6, Res.get("cloneOffer.clone")); + + HBox hBox = tuple4.fourth; + hBox.setAlignment(Pos.CENTER_LEFT); + GridPane.setHalignment(hBox, HPos.LEFT); + + cloneButton = tuple4.first; + cloneButton.setMinHeight(40); + cloneButton.setPadding(new Insets(0, 20, 0, 20)); + cloneButton.setGraphicTextGap(10); + + busyAnimation = tuple4.second; + spinnerInfoLabel = tuple4.third; + + cancelButton = new AutoTooltipButton(Res.get("shared.cancel")); + cancelButton.setDefaultButton(false); + cancelButton.setOnAction(event -> close()); + hBox.getChildren().add(cancelButton); + + cloneButton.setOnAction(e -> { + cloneButton.requestFocus(); // fix issue #5460 (when enter key used, focus is wrong) + onClone(); + }); + } + + private void onClone() { + if (model.dataModel.hasConflictingClone()) { + new Popup().warning(Res.get("cloneOffer.hasConflictingClone")) + .actionButtonText(Res.get("shared.yes")) + .onAction(this::doClone) + .closeButtonText(Res.get("shared.no")) + .show(); + } else { + doClone(); + } + } + + private void doClone() { + if (model.isPriceInRange()) { + model.isNextButtonDisabled.setValue(true); + cancelButton.setDisable(true); + busyAnimation.play(); + spinnerInfoLabel.setText(Res.get("cloneOffer.publishOffer")); + model.onCloneOffer(() -> { + UserThread.execute(() -> { + String key = "cloneOfferSuccess"; + if (DontShowAgainLookup.showAgain(key)) { + new Popup() + .feedback(Res.get("cloneOffer.success")) + .dontShowAgainId(key) + .show(); + } + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + close(); + }); + }, + errorMessage -> { + UserThread.execute(() -> { + log.error(errorMessage); + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + model.isNextButtonDisabled.setValue(false); + cancelButton.setDisable(false); + new Popup().warning(errorMessage).show(); + }); + }); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateElementsWithDirection() { + ImageView iconView = new ImageView(); + iconView.setId(model.isShownAsSellOffer() ? "image-sell-white" : "image-buy-white"); + cloneButton.setGraphic(iconView); + cloneButton.setId(model.isShownAsSellOffer() ? "sell-button-big" : "buy-button-big"); + } +} \ No newline at end of file diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java new file mode 100644 index 0000000000..f36e18da79 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java @@ -0,0 +1,120 @@ +/* + * 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 haveno.desktop.main.portfolio.cloneoffer; + +import haveno.desktop.Navigation; +import haveno.desktop.main.offer.MutableOfferViewModel; +import haveno.desktop.main.offer.OfferViewUtil; + +import haveno.core.account.witness.AccountAgeWitnessService; +import haveno.core.offer.OfferUtil; +import haveno.core.offer.OpenOffer; +import haveno.core.payment.validation.FiatVolumeValidator; +import haveno.core.payment.validation.SecurityDepositValidator; +import haveno.core.payment.validation.XmrValidator; +import haveno.core.provider.price.PriceFeedService; +import haveno.core.user.Preferences; +import haveno.core.util.FormattingUtils; +import haveno.core.util.PriceUtil; +import haveno.core.util.coin.CoinFormatter; +import haveno.core.util.validation.AmountValidator4Decimals; +import haveno.core.util.validation.AmountValidator8Decimals; +import haveno.common.handlers.ErrorMessageHandler; +import haveno.common.handlers.ResultHandler; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +class CloneOfferViewModel extends MutableOfferViewModel { + + @Inject + public CloneOfferViewModel(CloneOfferDataModel dataModel, + FiatVolumeValidator fiatVolumeValidator, + AmountValidator4Decimals priceValidator4Decimals, + AmountValidator8Decimals priceValidator8Decimals, + XmrValidator xmrValidator, + SecurityDepositValidator securityDepositValidator, + PriceFeedService priceFeedService, + AccountAgeWitnessService accountAgeWitnessService, + Navigation navigation, + Preferences preferences, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + OfferUtil offerUtil) { + super(dataModel, + fiatVolumeValidator, + priceValidator4Decimals, + priceValidator8Decimals, + xmrValidator, + securityDepositValidator, + priceFeedService, + accountAgeWitnessService, + navigation, + preferences, + btcFormatter, + offerUtil); + syncMinAmountWithAmount = false; + } + + @Override + public void activate() { + super.activate(); + + dataModel.populateData(); + + long triggerPriceAsLong = dataModel.getTriggerPrice(); + dataModel.setTriggerPrice(triggerPriceAsLong); + if (triggerPriceAsLong > 0) { + triggerPrice.set(PriceUtil.formatMarketPrice(triggerPriceAsLong, dataModel.getCurrencyCode())); + } else { + triggerPrice.set(""); + } + onTriggerPriceTextFieldChanged(); + } + + public void applyOpenOffer(OpenOffer openOffer) { + dataModel.reset(); + dataModel.applyOpenOffer(openOffer); + } + + public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + dataModel.onCloneOffer(resultHandler, errorMessageHandler); + } + + public void onInvalidateMarketPriceMargin() { + marketPriceMargin.set(FormattingUtils.formatToPercent(dataModel.getMarketPriceMarginPct())); + } + + public void onInvalidatePrice() { + price.set(FormattingUtils.formatPrice(null)); + price.set(FormattingUtils.formatPrice(dataModel.getPrice().get())); + } + + public boolean isSecurityDepositValid() { + return securityDepositValidator.validate(securityDeposit.get()).isValid; + } + + @Override + public void triggerFocusOutOnAmountFields() { + // do not update BTC Amount or minAmount here + // issue 2798: "after a few edits of offer the BTC amount has increased" + } + + public boolean isShownAsSellOffer() { + return OfferViewUtil.isShownAsSellOffer(getTradeCurrency(), dataModel.getDirection()); + } +} \ No newline at end of file diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java index 3257d21059..ab92f845db 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java @@ -444,7 +444,7 @@ public class ClosedTradesView extends ActivatableViewAndModel onDuplicateOffer(item.getTradable().getOffer())); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java index 5828b94348..4db019c021 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java @@ -35,13 +35,10 @@ import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; -import haveno.core.util.coin.CoinUtil; -import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; import haveno.desktop.main.offer.MutableOfferDataModel; import haveno.network.p2p.P2PService; -import java.math.BigInteger; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -101,14 +98,6 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { if (openOffer != null) setTriggerPrice(openOffer.getTriggerPrice()); } - private double getSecurityAsPercent(Offer offer) { - BigInteger offerSellerSecurityDeposit = getBoundedSecurityDeposit(offer.getMaxSellerSecurityDeposit()); - double offerSellerSecurityDepositAsPercent = CoinUtil.getAsPercentPerXmr(offerSellerSecurityDeposit, - offer.getAmount()); - return Math.min(offerSellerSecurityDepositAsPercent, - Restrictions.getMaxSecurityDepositAsPercent()); - } - @Override protected Set getUserPaymentAccounts() { return Objects.requireNonNull(user.getPaymentAccounts()).stream() diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java index 33285e7c32..7cb24ca3eb 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java @@ -70,6 +70,7 @@ public class DuplicateOfferView extends MutableOfferView { + resultHandler.handleResult(); // process result before nullifying state openOffer = null; - UserThread.execute(() -> resultHandler.handleResult()); + editedOffer = null; }, (errorMsg) -> { - UserThread.execute(() -> errorMessageHandler.handleErrorMessage(errorMsg)); + errorMessageHandler.handleErrorMessage(errorMsg); }); } @@ -243,6 +243,15 @@ class EditOfferDataModel extends MutableOfferDataModel { }, errorMessageHandler); } + public boolean hasConflictingClone() { + Optional editedOpenOffer = openOfferManager.getOpenOffer(openOffer.getId()); + if (!editedOpenOffer.isPresent()) { + log.warn("Edited open offer is no longer present"); + return false; + } + return openOfferManager.hasConflictingClone(editedOpenOffer.get()); + } + @Override protected Set getUserPaymentAccounts() { throw new RuntimeException("Edit offer not supported with XMR"); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java index bc804b5576..8b1d9775e6 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java @@ -19,6 +19,8 @@ package haveno.desktop.main.portfolio.editoffer; import com.google.inject.Inject; import com.google.inject.name.Named; + +import haveno.common.UserThread; import haveno.common.util.Tuple4; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; @@ -140,6 +142,7 @@ public class EditOfferView extends MutableOfferView { initWithData(openOffer.getOffer().getDirection(), CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get(), + false, null); model.onStartEditOffer(errorMessage -> { @@ -208,23 +211,31 @@ public class EditOfferView extends MutableOfferView { // edit offer model.onPublishOffer(() -> { - String key = "editOfferSuccess"; - if (DontShowAgainLookup.showAgain(key)) { - new Popup() - .feedback(Res.get("editOffer.success")) - .dontShowAgainId(key) - .show(); + if (model.dataModel.hasConflictingClone()) { + new Popup().warning(Res.get("editOffer.hasConflictingClone")).show(); + } else { + String key = "editOfferSuccess"; + if (DontShowAgainLookup.showAgain(key)) { + new Popup() + .feedback(Res.get("editOffer.success")) + .dontShowAgainId(key) + .show(); + } } - spinnerInfoLabel.setText(""); - busyAnimation.stop(); - close(); + UserThread.execute(() -> { + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + close(); + }); }, (message) -> { - log.error(message); - spinnerInfoLabel.setText(""); - busyAnimation.stop(); - model.isNextButtonDisabled.setValue(false); - cancelButton.setDisable(false); - new Popup().warning(Res.get("editOffer.failed", message)).show(); + UserThread.execute(() -> { + log.error(message); + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + model.isNextButtonDisabled.setValue(false); + cancelButton.setDisable(false); + new Popup().warning(Res.get("editOffer.failed", message)).show(); + }); }); } }); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java index 34b78be683..53febf4dc5 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java @@ -44,7 +44,7 @@ class EditOfferViewModel extends MutableOfferViewModel { FiatVolumeValidator fiatVolumeValidator, AmountValidator4Decimals priceValidator4Decimals, AmountValidator8Decimals priceValidator8Decimals, - XmrValidator btcValidator, + XmrValidator xmrValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, @@ -56,7 +56,7 @@ class EditOfferViewModel extends MutableOfferViewModel { fiatVolumeValidator, priceValidator4Decimals, priceValidator8Decimals, - btcValidator, + xmrValidator, securityDepositValidator, priceFeedService, accountAgeWitnessService, diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOfferListItem.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOfferListItem.java index 67869a9ff9..cb437ace39 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOfferListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOfferListItem.java @@ -39,4 +39,8 @@ class OpenOfferListItem { public Offer getOffer() { return openOffer.getOffer(); } + + public String getGroupId() { + return openOffer.getGroupId(); + } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml index 30da4d0c19..035ec5fbdc 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml @@ -42,7 +42,8 @@ - + + @@ -50,11 +51,13 @@ - + + + diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java index 7c41eef890..3ad6a8cb06 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -22,8 +22,8 @@ import com.googlecode.jcsv.writer.CSVEntryConverter; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.core.locale.Res; import haveno.core.offer.Offer; -import haveno.core.offer.OfferPayload; import haveno.core.offer.OpenOffer; +import haveno.core.offer.OpenOfferManager; import haveno.core.user.DontShowAgainLookup; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableViewAndModel; @@ -40,8 +40,9 @@ import haveno.desktop.main.funds.withdrawal.WithdrawalView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.portfolio.PortfolioView; -import haveno.desktop.main.portfolio.duplicateoffer.DuplicateOfferView; +import haveno.desktop.main.portfolio.presentation.PortfolioUtil; import static haveno.desktop.util.FormBuilder.getRegularIconButton; +import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import java.util.Comparator; import java.util.HashMap; @@ -51,13 +52,11 @@ import java.util.stream.Collectors; import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; -import javafx.collections.ObservableList; +import javafx.collections.ListChangeListener; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.Parent; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; @@ -73,6 +72,7 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; +import javafx.scene.text.Text; import javafx.stage.Stage; import javafx.util.Callback; import org.jetbrains.annotations.NotNull; @@ -80,12 +80,39 @@ import org.jetbrains.annotations.NotNull; @FxmlView public class OpenOffersView extends ActivatableViewAndModel { + private enum ColumnNames { + OFFER_ID(Res.get("shared.offerId")), + GROUP_ID(Res.get("openOffer.header.groupId")), + DATE(Res.get("shared.dateTime")), + MARKET(Res.get("shared.market")), + PRICE(Res.get("shared.price")), + DEVIATION(Res.get("shared.deviation")), + TRIGGER_PRICE(Res.get("openOffer.header.triggerPrice")), + AMOUNT(Res.get("shared.XMRMinMax")), + VOLUME(Res.get("shared.amountMinMax")), + PAYMENT_METHOD(Res.get("shared.paymentMethod")), + DIRECTION(Res.get("shared.offerType")), + STATUS(Res.get("shared.state")); + + private final String text; + + ColumnNames(String text) { + this.text = text; + } + + @Override + public String toString() { + return text; + } + } + @FXML TableView tableView; @FXML TableColumn priceColumn, deviationColumn, amountColumn, volumeColumn, - marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, - removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn; + marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, groupIdColumn, + removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn, duplicateItemColumn, + cloneItemColumn; @FXML HBox searchBox; @FXML @@ -108,37 +135,48 @@ public class OpenOffersView extends ActivatableViewAndModel sortedList; private FilteredList filteredList; private ChangeListener filterTextFieldListener; - private PortfolioView.OpenOfferActionHandler openOfferActionHandler; + private final OpenOfferManager openOfferManager; + private PortfolioView.EditOpenOfferHandler editOpenOfferHandler; + private PortfolioView.CloneOpenOfferHandler cloneOpenOfferHandler; private ChangeListener widthListener; + private ListChangeListener sortedListChangedListener; private Map> offerStateChangeListeners = new HashMap>(); @Inject - public OpenOffersView(OpenOffersViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow) { + public OpenOffersView(OpenOffersViewModel model, + OpenOfferManager openOfferManager, + Navigation navigation, + OfferDetailsWindow offerDetailsWindow) { super(model); this.navigation = navigation; this.offerDetailsWindow = offerDetailsWindow; + this.openOfferManager = openOfferManager; } @Override public void initialize() { widthListener = (observable, oldValue, newValue) -> onWidthChange((double) newValue); - paymentMethodColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.paymentMethod"))); - priceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.price"))); - deviationColumn.setGraphic(new AutoTooltipTableColumn<>(Res.get("shared.deviation"), + groupIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.GROUP_ID.toString())); + paymentMethodColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PAYMENT_METHOD.toString())); + priceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PRICE.toString())); + deviationColumn.setGraphic(new AutoTooltipTableColumn<>(ColumnNames.DEVIATION.toString(), Res.get("portfolio.closedTrades.deviation.help")).getGraphic()); - amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.XMRMinMax"))); - volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountMinMax"))); - marketColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.market"))); - directionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerType"))); - dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); - offerIdColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerId"))); - triggerPriceColumn.setGraphic(new AutoTooltipLabel(Res.get("openOffer.header.triggerPrice"))); - deactivateItemColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.enabled"))); + triggerPriceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRIGGER_PRICE.toString())); + amountColumn.setGraphic(new AutoTooltipLabel(ColumnNames.AMOUNT.toString())); + volumeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.VOLUME.toString())); + marketColumn.setGraphic(new AutoTooltipLabel(ColumnNames.MARKET.toString())); + directionColumn.setGraphic(new AutoTooltipLabel(ColumnNames.DIRECTION.toString())); + dateColumn.setGraphic(new AutoTooltipLabel(ColumnNames.DATE.toString())); + offerIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.OFFER_ID.toString())); + deactivateItemColumn.setGraphic(new AutoTooltipLabel(ColumnNames.STATUS.toString())); editItemColumn.setGraphic(new AutoTooltipLabel("")); + duplicateItemColumn.setText(""); + cloneItemColumn.setText(""); removeItemColumn.setGraphic(new AutoTooltipLabel("")); setOfferIdColumnCellFactory(); + setGroupIdCellFactory(); setDirectionColumnCellFactory(); setMarketColumnCellFactory(); setPriceColumnCellFactory(); @@ -151,12 +189,15 @@ public class OpenOffersView extends ActivatableViewAndModel o.getOffer().getId())); + groupIdColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getReserveTxHash())); directionColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDirection())); marketColumn.setComparator(Comparator.comparing(model::getMarketLabel)); amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getAmount())); @@ -168,23 +209,21 @@ public class OpenOffersView extends ActivatableViewAndModel o.getOffer().getDate())); paymentMethodColumn.setComparator(Comparator.comparing(o -> Res.get(o.getOffer().getPaymentMethod().getId()))); - dateColumn.setSortType(TableColumn.SortType.DESCENDING); + dateColumn.setSortType(TableColumn.SortType.ASCENDING); tableView.getSortOrder().add(dateColumn); tableView.setRowFactory( tableView -> { final TableRow row = new TableRow<>(); final ContextMenu rowMenu = new ContextMenu(); - MenuItem editItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); - editItem.setOnAction((event) -> { - try { - OfferPayload offerPayload = row.getItem().getOffer().getOfferPayload(); - navigation.navigateToWithData(offerPayload, MainView.class, PortfolioView.class, DuplicateOfferView.class); - } catch (NullPointerException e) { - log.warn("Unable to get offerPayload - {}", e.toString()); - } - }); - rowMenu.getItems().add(editItem); + + MenuItem duplicateOfferMenuItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); + duplicateOfferMenuItem.setOnAction((event) -> onDuplicateOffer(row.getItem())); + rowMenu.getItems().add(duplicateOfferMenuItem); + + MenuItem cloneOfferMenuItem = new MenuItem(Res.get("offerbook.cloneOffer")); + cloneOfferMenuItem.setOnAction((event) -> onCloneOffer(row.getItem())); + rowMenu.getItems().add(cloneOfferMenuItem); row.contextMenuProperty().bind( Bindings.when(Bindings.isNotNull(row.itemProperty())) .then(rowMenu) @@ -207,6 +246,15 @@ public class OpenOffersView extends ActivatableViewAndModel { + c.next(); + if (c.wasAdded() || c.wasRemoved()) { + updateNumberOfOffers(); + updateGroupIdColumnVisibility(); + updateTriggerColumnVisibility(); + } + }; } @Override @@ -214,8 +262,11 @@ public class OpenOffersView extends ActivatableViewAndModel(model.getList()); sortedList = new SortedList<>(filteredList); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + sortedList.addListener(sortedListChangedListener); tableView.setItems(sortedList); + updateGroupIdColumnVisibility(); + updateTriggerColumnVisibility(); updateSelectToggleButtonState(); selectToggleButton.setOnAction(event -> { @@ -231,37 +282,27 @@ public class OpenOffersView extends ActivatableViewAndModel { - ObservableList> tableColumns = tableView.getColumns(); - int reportColumns = tableColumns.size() - 3; // CSV report excludes the last columns (icons) CSVEntryConverter headerConverter = item -> { - String[] columns = new String[reportColumns]; - for (int i = 0; i < columns.length; i++) { - Node graphic = tableColumns.get(i).getGraphic(); - if (graphic instanceof AutoTooltipLabel) { - columns[i] = ((AutoTooltipLabel) graphic).getText(); - } else if (graphic instanceof HBox) { - // Deviation has a Hbox with AutoTooltipLabel as first child in header - columns[i] = ((AutoTooltipLabel) ((Parent) graphic).getChildrenUnmodifiable().get(0)).getText(); - } else { - // Not expected - columns[i] = "N/A"; - } + String[] columns = new String[ColumnNames.values().length]; + for (ColumnNames m : ColumnNames.values()) { + columns[m.ordinal()] = m.toString(); } return columns; }; CSVEntryConverter contentConverter = item -> { - String[] columns = new String[reportColumns]; - columns[0] = model.getOfferId(item); - columns[1] = model.getDate(item); - columns[2] = model.getMarketLabel(item); - columns[3] = model.getPrice(item); - columns[4] = model.getPriceDeviation(item); - columns[5] = model.getTriggerPrice(item); - columns[6] = model.getAmount(item); - columns[7] = model.getVolume(item); - columns[8] = model.getPaymentMethod(item); - columns[9] = model.getDirectionLabel(item); - columns[10] = String.valueOf(!item.getOpenOffer().isDeactivated()); + String[] columns = new String[ColumnNames.values().length]; + columns[ColumnNames.OFFER_ID.ordinal()] = model.getOfferId(item); + columns[ColumnNames.GROUP_ID.ordinal()] = openOfferManager.hasClonedOffer(item.getOffer().getId()) ? getShortenedGroupId(item.getGroupId()) : ""; + columns[ColumnNames.DATE.ordinal()] = model.getDate(item); + columns[ColumnNames.MARKET.ordinal()] = model.getMarketLabel(item); + columns[ColumnNames.PRICE.ordinal()] = model.getPrice(item); + columns[ColumnNames.DEVIATION.ordinal()] = model.getPriceDeviation(item); + columns[ColumnNames.TRIGGER_PRICE.ordinal()] = model.getTriggerPrice(item); + columns[ColumnNames.AMOUNT.ordinal()] = model.getAmount(item); + columns[ColumnNames.VOLUME.ordinal()] = model.getVolume(item); + columns[ColumnNames.PAYMENT_METHOD.ordinal()] = model.getPaymentMethod(item); + columns[ColumnNames.DIRECTION.ordinal()] = model.getDirectionLabel(item); + columns[ColumnNames.STATUS.ordinal()] = String.valueOf(!item.getOpenOffer().isDeactivated()); return columns; }; @@ -280,9 +321,24 @@ public class OpenOffersView extends ActivatableViewAndModel item.getOpenOffer().getTriggerPrice()) + .sum() > 0); + } + @Override protected void deactivate() { sortedList.comparatorProperty().unbind(); + sortedList.removeListener(sortedListChangedListener); exportButton.setOnAction(null); filterTextField.textProperty().removeListener(filterTextFieldListener); @@ -352,7 +408,7 @@ public class OpenOffersView extends ActivatableViewAndModel 1200); + triggerPriceColumn.setVisible(width > 1300); } private void onDeactivateOpenOffer(OpenOffer openOffer) { @@ -361,7 +417,7 @@ public class OpenOffersView extends ActivatableViewAndModel log.debug("Deactivate offer was successful"), (message) -> { log.error(message); - new Popup().warning(Res.get("offerbook.deactivateOffer.failed", message)).show(); + new Popup().warning(message).show(); }); updateSelectToggleButtonState(); } @@ -397,12 +453,18 @@ public class OpenOffersView extends ActivatableViewAndModel { log.debug("Remove offer was successful"); tableView.refresh(); + // We do not show the popup if it's a cloned offer with shared maker reserve tx + if (hasClonedOffer) { + return; + } + String key = "WithdrawFundsAfterRemoveOfferInfo"; if (DontShowAgainLookup.showAgain(key)) { new Popup().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("navigation.funds.availableForWithdrawal"))) @@ -420,10 +482,46 @@ public class OpenOffersView extends ActivatableViewAndModel doCloneOffer(item)) + .show(); + } else { + doCloneOffer(item); + } + } + } + + private void doCloneOffer(OpenOfferListItem item) { + OpenOffer openOffer = item.getOpenOffer(); + if (openOffer == null || openOffer.getOffer() == null || openOffer.getOffer().getOfferPayload() == null) { + return; + } + cloneOpenOfferHandler.onCloneOpenOffer(openOffer); + } + private void setOfferIdColumnCellFactory() { offerIdColumn.setCellValueFactory((openOfferListItem) -> new ReadOnlyObjectWrapper<>(openOfferListItem.getValue())); offerIdColumn.getStyleClass().addAll("number-column", "first-column"); @@ -434,21 +532,28 @@ public class OpenOffersView extends ActivatableViewAndModel call(TableColumn column) { return new TableCell<>() { - private HyperlinkWithIcon field; + private HyperlinkWithIcon hyperlinkWithIcon; @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); - if (item != null && !empty) { - field = new HyperlinkWithIcon(model.getOfferId(item)); - field.setOnAction(event -> offerDetailsWindow.show(item.getOffer())); - field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); - setGraphic(field); + hyperlinkWithIcon = new HyperlinkWithIcon(item.getOffer().getShortId()); + if (model.isDeactivated(item)) { + // getStyleClass().add("offer-disabled"); does not work with hyperlinkWithIcon;-( + hyperlinkWithIcon.setStyle("-fx-text-fill: -bs-color-gray-3;"); + hyperlinkWithIcon.getIcon().setOpacity(0.2); + } + hyperlinkWithIcon.setOnAction(event -> { + offerDetailsWindow.show(item.getOffer()); + }); + + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); + setGraphic(hyperlinkWithIcon); } else { setGraphic(null); - if (field != null) - field.setOnAction(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); } } }; @@ -456,6 +561,55 @@ public class OpenOffersView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + groupIdColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + + return new TableCell<>() { + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + getStyleClass().removeAll("offer-disabled"); + if (item != null) { + Label label; + Text icon; + if (openOfferManager.hasClonedOffer(item.getOpenOffer().getId())) { + label = new Label(getShortenedGroupId(item.getOpenOffer().getGroupId())); + icon = FormBuilder.getRegularIconForLabel(MaterialDesignIcon.LINK, label, "icon"); + icon.setVisible(true); + setTooltip(new Tooltip(Res.get("offerbook.clonedOffer.tooltip", item.getOpenOffer().getReserveTxHash()))); + } else { + label = new Label(""); + icon = FormBuilder.getRegularIconForLabel(MaterialDesignIcon.LINK_OFF, label, "icon"); + icon.setVisible(false); + setTooltip(new Tooltip(Res.get("offerbook.nonClonedOffer.tooltip", item.getOpenOffer().getReserveTxHash()))); + } + + if (model.isDeactivated(item)) { + getStyleClass().add("offer-disabled"); + icon.setOpacity(0.2); + } + setGraphic(label); + } else { + setGraphic(null); + } + } + }; + } + }); + } + + private String getShortenedGroupId(String groupId) { + if (groupId.length() > 5) { + return groupId.substring(0, 5); + } + return groupId; + } + private void setDateColumnCellFactory() { dateColumn.setCellValueFactory((openOfferListItem) -> new ReadOnlyObjectWrapper<>(openOfferListItem.getValue())); dateColumn.setCellFactory( @@ -779,6 +933,74 @@ public class OpenOffersView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + duplicateItemColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + if (button == null) { + button = getRegularIconButton(MaterialDesignIcon.CONTENT_COPY); + button.setTooltip(new Tooltip(Res.get("portfolio.context.offerLikeThis"))); + setGraphic(button); + } + button.setOnAction(event -> onDuplicateOffer(item)); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + } + + private void setCloneColumnCellFactory() { + cloneItemColumn.getStyleClass().add("avatar-column"); + cloneItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + cloneItemColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + if (button == null) { + button = getRegularIconButton(MaterialDesignIcon.BOX_SHADOW); + button.setTooltip(new Tooltip(Res.get("offerbook.cloneOffer"))); + setGraphic(button); + } + button.setOnAction(event -> onCloneOffer(item)); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + } + private void setTriggerIconColumnCellFactory() { triggerIconColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); triggerIconColumn.setCellFactory( @@ -854,8 +1076,12 @@ public class OpenOffersView extends ActivatableViewAndModel return item.getOffer().getShortId(); } + String getGroupId(OpenOfferListItem item) { + return item.getGroupId(); + } + String getAmount(OpenOfferListItem item) { return (item != null) ? DisplayUtils.formatAmount(item.getOffer(), btcFormatter) : ""; } diff --git a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java index 7df49216a2..ae3e7ed266 100644 --- a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java @@ -2340,8 +2340,8 @@ public class FormBuilder { return getRegularIconForLabel(icon, label, null); } - public static Text getRegularIconForLabel(GlyphIcons icon, Label label, String style) { - return getIconForLabel(icon, "1.231em", label, style); + public static Text getRegularIconForLabel(GlyphIcons icon, Label label, String styleClass) { + return getIconForLabel(icon, "1.231em", label, styleClass); } public static Text getIcon(GlyphIcons icon) { diff --git a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java index 33d43b7bc5..e9d39f47bf 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java @@ -88,7 +88,7 @@ public class CreateOfferDataModelTest { when(user.getPaymentAccounts()).thenReturn(paymentAccounts); when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); - model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD")); + model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD"), true); assertEquals("USD", model.getTradeCurrencyCode().get()); } @@ -109,7 +109,7 @@ public class CreateOfferDataModelTest { when(user.findFirstPaymentAccountWithCurrency(new TraditionalCurrency("USD"))).thenReturn(zelleAccount); when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); - model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD")); + model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD"), true); assertEquals("USD", model.getTradeCurrencyCode().get()); } } diff --git a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java index a8c6ede578..19756fc523 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java @@ -117,7 +117,7 @@ public class CreateOfferViewModelTest { coinFormatter, tradeStats, null); - dataModel.initWithData(OfferDirection.BUY, new CryptoCurrency("XMR", "monero")); + dataModel.initWithData(OfferDirection.BUY, new CryptoCurrency("XMR", "monero"), true); dataModel.activate(); model = new CreateOfferViewModel(dataModel, diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 75ad4a0658..b9615b5bcb 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -528,6 +528,7 @@ message PostOfferRequest { bool is_private_offer = 12; bool buyer_as_taker_without_deposit = 13; string extra_info = 14; + string source_offer_id = 15; } message PostOfferReply { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index f919f4f1e7..5cdde1f0ce 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1422,6 +1422,7 @@ message OpenOffer { string reserve_tx_key = 11; string challenge = 12; bool deactivated_by_trigger = 13; + string group_id = 14; } message Tradable { From f87dc3a4d1bed989f9ff84682a9d0488b038feec Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 5 Apr 2025 17:59:34 -0400 Subject: [PATCH 219/371] update information popup for cloned offers --- core/src/main/resources/i18n/displayStrings.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 457323e9f2..690c237af4 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -396,7 +396,7 @@ offerbook.clonedOffer.headline=Cloning an offer offerbook.clonedOffer.info=Cloning an offer creates a copy without reserving additional funds.\n\n\ This helps reduce locked capital, making it easier to list the same offer across multiple markets or payment methods.\n\n\ If one of the cloned offers is taken, the others will close automatically, since they all share the same reserved funds.\n\n\ - Cloned offers must use the same trade amount and security deposit, but they must differ in payment method or currency.\n\n\ + Cloned offers must share the same trade amount and security deposit, but use a different payment method or currency.\n\n\ For more information about cloning offers see: [HYPERLINK:https://docs.haveno.exchange/haveno-ui/Cloning_an_offer/] offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ From 055b7d13763c909e58d5753bdb96bcbb4e3e0514 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 5 Apr 2025 18:44:27 -0400 Subject: [PATCH 220/371] fix link to clone offer documentation --- core/src/main/resources/i18n/displayStrings.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 690c237af4..888f850e86 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -397,7 +397,7 @@ offerbook.clonedOffer.info=Cloning an offer creates a copy without reserving add This helps reduce locked capital, making it easier to list the same offer across multiple markets or payment methods.\n\n\ If one of the cloned offers is taken, the others will close automatically, since they all share the same reserved funds.\n\n\ Cloned offers must share the same trade amount and security deposit, but use a different payment method or currency.\n\n\ - For more information about cloning offers see: [HYPERLINK:https://docs.haveno.exchange/haveno-ui/Cloning_an_offer/] + For more information about cloning offers see: [HYPERLINK:https://docs.haveno.exchange/haveno-ui/cloning_an_offer/] offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ {0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. From 3c6914ac7e0735bc8d3ca019d4c3359cf445373f Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 6 Apr 2025 15:26:09 -0400 Subject: [PATCH 221/371] filter boxes search currencies, payment details, and extra info --- .../offer/offerbook/OfferBookViewModel.java | 21 ++++++++++++++++++- .../main/overlays/windows/ContractWindow.java | 3 ++- .../portfolio/openoffer/OpenOffersView.java | 21 +++++++++++-------- .../pendingtrades/PendingTradesListItem.java | 9 ++++++++ .../main/support/dispute/DisputeView.java | 2 +- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index 9bd1da0277..821b081454 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -602,10 +602,29 @@ abstract class OfferBookViewModel extends ActivatableViewModel { nextPredicate = nextPredicate.or(offerBookListItem -> offerBookListItem.getOffer().getId().toLowerCase().contains(filterText.toLowerCase())); - // filter payment method + // filter full payment method nextPredicate = nextPredicate.or(offerBookListItem -> Res.get(offerBookListItem.getOffer().getPaymentMethod().getId()).toLowerCase().contains(filterText.toLowerCase())); + // filter short payment method + nextPredicate = nextPredicate.or(offerBookListItem -> { + return getPaymentMethod(offerBookListItem).toLowerCase().contains(filterText.toLowerCase()); + }); + + // filter currencies + nextPredicate = nextPredicate.or(offerBookListItem -> { + return offerBookListItem.getOffer().getCurrencyCode().toLowerCase().contains(filterText.toLowerCase()) || + offerBookListItem.getOffer().getBaseCurrencyCode().toLowerCase().contains(filterText.toLowerCase()) || + CurrencyUtil.getNameAndCode(offerBookListItem.getOffer().getCurrencyCode()).toLowerCase().contains(filterText.toLowerCase()) || + CurrencyUtil.getNameAndCode(offerBookListItem.getOffer().getBaseCurrencyCode()).toLowerCase().contains(filterText.toLowerCase()); + }); + + // filter extra info + nextPredicate = nextPredicate.or(offerBookListItem -> { + return offerBookListItem.getOffer().getCombinedExtraInfo() != null && + offerBookListItem.getOffer().getCombinedExtraInfo().toLowerCase().contains(filterText.toLowerCase()); + }); + filteredItems.setPredicate(predicate.and(nextPredicate)); } else { filteredItems.setPredicate(predicate); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java index 4cefcbde26..6a238e56ee 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java @@ -248,7 +248,8 @@ public class ContractWindow extends Overlay { } addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.makerDepositTransactionId"), contract.getMakerDepositTxHash()); - addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerDepositTransactionId"), contract.getTakerDepositTxHash()); + if (contract.getTakerDepositTxHash() != null) + addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerDepositTransactionId"), contract.getTakerDepositTxHash()); if (dispute.getDelayedPayoutTxId() != null) addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxId"), dispute.getDelayedPayoutTxId()); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java index 3ad6a8cb06..87e044e703 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -376,31 +376,34 @@ public class OpenOffersView extends ActivatableViewAndModel implements tooltip.setShowDuration(Duration.seconds(10)); filterTextField.setTooltip(tooltip); filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText()); - HBox.setHgrow(filterTextField, Priority.NEVER); + HBox.setHgrow(filterTextField, Priority.ALWAYS); alertIconLabel = new Label(); Text icon = getIconForLabel(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, "2em", alertIconLabel); From 9027ce663441683e70e101b22d683396d8fcff83 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 6 Apr 2025 17:43:23 -0400 Subject: [PATCH 222/371] fix concurrency issues by synchronizing on base persistable list --- .../haveno/core/offer/OpenOfferManager.java | 121 +++++++++--------- .../haveno/core/offer/SignedOfferList.java | 10 +- .../core/payment/PaymentAccountList.java | 10 +- .../support/dispute/DisputeListService.java | 50 ++++---- .../core/support/dispute/DisputeManager.java | 30 +++-- .../arbitration/ArbitrationManager.java | 24 ++-- .../mediation/MediationDisputeList.java | 6 +- .../dispute/refund/RefundDisputeList.java | 7 +- .../core/trade/CleanupMailboxMessages.java | 30 +++-- .../core/trade/ClosedTradableManager.java | 56 ++++---- .../java/haveno/core/trade/TradeManager.java | 72 ++++++----- .../trade/failed/FailedTradesManager.java | 28 ++-- .../signedoffer/SignedOffersDataModel.java | 4 +- .../java/haveno/desktop/util/GUIUtil.java | 20 +-- .../p2p/mailbox/MailboxMessageList.java | 14 +- 15 files changed, 262 insertions(+), 220 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index c6a5847f01..14f51439b1 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -35,6 +35,8 @@ package haveno.core.offer; import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; import com.google.inject.Inject; import haveno.common.ThreadUtils; import haveno.common.Timer; @@ -261,7 +263,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } private void cleanUpAddressEntries() { - Set openOffersIdSet = openOffers.getList().stream().map(OpenOffer::getId).collect(Collectors.toSet()); + Set openOffersIdSet; + synchronized (openOffers.getList()) { + openOffersIdSet = openOffers.getList().stream().map(OpenOffer::getId).collect(Collectors.toSet()); + } xmrWalletService.getAddressEntriesForOpenOffer().stream() .filter(e -> !openOffersIdSet.contains(e.getOfferId())) .forEach(e -> { @@ -292,7 +297,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe ThreadUtils.execute(() -> { // remove offers from offer book - synchronized (openOffers) { + synchronized (openOffers.getList()) { openOffers.forEach(openOffer -> { if (openOffer.getState() == OpenOffer.State.AVAILABLE) { offerBookService.removeOfferAtShutDown(openOffer.getOffer().getOfferPayload()); @@ -334,15 +339,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } private void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) { - int size = openOffers.size(); - // Copy list as we remove in the loop - List openOffersList = new ArrayList<>(openOffers); - openOffersList.forEach(openOffer -> cancelOpenOffer(openOffer, () -> { - }, errorMessage -> { - log.warn("Error removing open offer: " + errorMessage); - })); - if (completeHandler != null) - UserThread.runAfter(completeHandler, size * 200 + 500, TimeUnit.MILLISECONDS); + synchronized (openOffers) { + int size = openOffers.size(); + // Copy list as we remove in the loop + List openOffersList = new ArrayList<>(openOffers); + openOffersList.forEach(openOffer -> cancelOpenOffer(openOffer, () -> { + }, errorMessage -> { + log.warn("Error removing open offer: " + errorMessage); + })); + if (completeHandler != null) + UserThread.runAfter(completeHandler, size * 200 + 500, TimeUnit.MILLISECONDS); + } } @@ -450,13 +457,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }); // poll spent status of open offer key images - for (OpenOffer openOffer : getOpenOffers()) { - xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); + synchronized (openOffers.getList()) { + for (OpenOffer openOffer : openOffers.getList()) { + xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); + } } // poll spent status of signed offer key images - for (SignedOffer signedOffer : signedOffers.getList()) { - xmrConnectionService.getKeyImagePoller().addKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); + synchronized (signedOffers.getList()) { + for (SignedOffer signedOffer : signedOffers.getList()) { + xmrConnectionService.getKeyImagePoller().addKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); + } } }, THREAD_ID); }); @@ -858,29 +869,25 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } public boolean hasAvailableOpenOffers() { - synchronized (openOffers) { - for (OpenOffer openOffer : getOpenOffers()) { - if (openOffer.getState() == OpenOffer.State.AVAILABLE) { - return true; - } + for (OpenOffer openOffer : getOpenOffers()) { + if (openOffer.getState() == OpenOffer.State.AVAILABLE) { + return true; } - return false; } + return false; } public List getOpenOffers() { - synchronized (openOffers) { - return new ArrayList<>(getObservableList()); + synchronized (openOffers.getList()) { + return ImmutableList.copyOf(getObservableList()); } } public List getOpenOfferGroup(String groupId) { if (groupId == null) throw new IllegalArgumentException("groupId cannot be null"); - synchronized (openOffers) { - return getOpenOffers().stream() - .filter(openOffer -> groupId.equals(openOffer.getGroupId())) - .collect(Collectors.toList()); - } + return getOpenOffers().stream() + .filter(openOffer -> groupId.equals(openOffer.getGroupId())) + .collect(Collectors.toList()); } public boolean hasClonedOffer(String offerId) { @@ -890,24 +897,22 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } public boolean hasClonedOffers() { - synchronized (openOffers) { - for (OpenOffer openOffer : getOpenOffers()) { - if (getOpenOfferGroup(openOffer.getGroupId()).size() > 1) { - return true; - } + for (OpenOffer openOffer : getOpenOffers()) { + if (getOpenOfferGroup(openOffer.getGroupId()).size() > 1) { + return true; } - return false; } + return false; } public List getSignedOffers() { - synchronized (signedOffers) { - return new ArrayList<>(signedOffers.getObservableList()); + synchronized (signedOffers.getList()) { + return ImmutableList.copyOf(signedOffers.getObservableList()); } } public ObservableList getObservableSignedOffersList() { - synchronized (signedOffers) { + synchronized (signedOffers.getList()) { return signedOffers.getObservableList(); } } @@ -917,9 +922,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } public Optional getOpenOffer(String offerId) { - synchronized (openOffers) { - return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst(); - } + return getOpenOffers().stream().filter(e -> e.getId().equals(offerId)).findFirst(); } public boolean hasOpenOffer(String offerId) { @@ -927,14 +930,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } public Optional getSignedOfferById(String offerId) { - synchronized (signedOffers) { - return signedOffers.stream().filter(e -> e.getOfferId().equals(offerId)).findFirst(); - } + return getSignedOffers().stream().filter(e -> e.getOfferId().equals(offerId)).findFirst(); } private void addOpenOffer(OpenOffer openOffer) { log.info("Adding open offer {}", openOffer.getId()); - synchronized (openOffers) { + synchronized (openOffers.getList()) { openOffers.add(openOffer); } if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) { @@ -944,7 +945,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private void removeOpenOffer(OpenOffer openOffer) { log.info("Removing open offer {}", openOffer.getId()); - synchronized (openOffers) { + synchronized (openOffers.getList()) { openOffers.remove(openOffer); } synchronized (placeOfferProtocols) { @@ -957,17 +958,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } private void cancelOpenOffersOnSpent(String keyImage) { - for (OpenOffer openOffer : getOpenOffers()) { - if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { - log.warn("Canceling open offer because reserved funds have been spent, offerId={}, state={}", openOffer.getId(), openOffer.getState()); - cancelOpenOffer(openOffer, null, null); + synchronized (openOffers.getList()) { + for (OpenOffer openOffer : openOffers.getList()) { + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { + log.warn("Canceling open offer because reserved funds have been spent, offerId={}, state={}", openOffer.getId(), openOffer.getState()); + cancelOpenOffer(openOffer, null, null); + } } } } private void addSignedOffer(SignedOffer signedOffer) { log.info("Adding SignedOffer for offer {}", signedOffer.getOfferId()); - synchronized (signedOffers) { + synchronized (signedOffers.getList()) { // remove signed offers with common key images for (String keyImage : signedOffer.getReserveTxKeyImages()) { @@ -982,16 +985,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private void removeSignedOffer(SignedOffer signedOffer) { log.info("Removing SignedOffer for offer {}", signedOffer.getOfferId()); - synchronized (signedOffers) { + synchronized (signedOffers.getList()) { signedOffers.remove(signedOffer); - xmrConnectionService.getKeyImagePoller().removeKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); } + xmrConnectionService.getKeyImagePoller().removeKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); } private void removeSignedOffers(String keyImage) { - for (SignedOffer signedOffer : new ArrayList(signedOffers.getList())) { - if (signedOffer.getReserveTxKeyImages().contains(keyImage)) { - removeSignedOffer(signedOffer); + synchronized (signedOffers.getList()) { + for (SignedOffer signedOffer : getSignedOffers()) { + if (signedOffer.getReserveTxKeyImages().contains(keyImage)) { + removeSignedOffer(signedOffer); + } } } } @@ -2070,7 +2075,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe OpenOffer openOffer = list.remove(0); boolean contained = false; - synchronized (openOffers) { + synchronized (openOffers.getList()) { contained = openOffers.contains(openOffer); } if (contained) { @@ -2171,7 +2176,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (periodicRefreshOffersTimer == null) periodicRefreshOffersTimer = UserThread.runPeriodically(() -> { if (!stopped) { - synchronized (openOffers) { + synchronized (openOffers.getList()) { int size = openOffers.size(); //we clone our list as openOffers might change during our delayed call final ArrayList openOffersList = new ArrayList<>(openOffers.getList()); @@ -2186,7 +2191,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe UserThread.runAfterRandomDelay(() -> { // we need to check if in the meantime the offer has been removed boolean contained = false; - synchronized (openOffers) { + synchronized (openOffers.getList()) { contained = openOffers.contains(openOffer); } if (contained) maybeRefreshOffer(openOffer, 0, 1); diff --git a/core/src/main/java/haveno/core/offer/SignedOfferList.java b/core/src/main/java/haveno/core/offer/SignedOfferList.java index a77d0402d9..4d75a14dd4 100644 --- a/core/src/main/java/haveno/core/offer/SignedOfferList.java +++ b/core/src/main/java/haveno/core/offer/SignedOfferList.java @@ -47,10 +47,12 @@ public final class SignedOfferList extends PersistableListAsObservable { @Override public Message toProtoMessage() { - return protobuf.PersistableEnvelope.newBuilder() - .setPaymentAccountList(protobuf.PaymentAccountList.newBuilder() - .addAllPaymentAccount(getList().stream().map(PaymentAccount::toProtoMessage).collect(Collectors.toList()))) - .build(); + synchronized (getList()) { + return protobuf.PersistableEnvelope.newBuilder() + .setPaymentAccountList(protobuf.PaymentAccountList.newBuilder() + .addAllPaymentAccount(getList().stream().map(PaymentAccount::toProtoMessage).collect(Collectors.toList()))) + .build(); + } } public static PaymentAccountList fromProto(protobuf.PaymentAccountList proto, CoreProtoResolver coreProtoResolver) { diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeListService.java b/core/src/main/java/haveno/core/support/dispute/DisputeListService.java index 2c90565614..c2a1521a79 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeListService.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeListService.java @@ -74,10 +74,12 @@ public abstract class DisputeListService> impleme @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(getFileName(), persisted -> { - disputeList.setAll(persisted.getList()); - completeHandler.run(); - }, - completeHandler); + synchronized (persisted.getList()) { + disputeList.setAll(persisted.getList()); + } + completeHandler.run(); + }, + completeHandler); } protected String getFileName() { @@ -145,26 +147,30 @@ public abstract class DisputeListService> impleme private void onDisputesChangeListener(List addedList, @Nullable List removedList) { if (removedList != null) { - removedList.forEach(dispute -> { - disputedTradeIds.remove(dispute.getTradeId()); + synchronized (removedList) { + removedList.forEach(dispute -> { + disputedTradeIds.remove(dispute.getTradeId()); + }); + } + } + synchronized (addedList) { + addedList.forEach(dispute -> { + // for each dispute added, keep track of its "BadgeCountProperty" + EasyBind.subscribe(dispute.getBadgeCountProperty(), + isAlerting -> { + // We get the event before the list gets updated, so we execute on next frame + UserThread.execute(() -> { + synchronized (disputeList.getObservableList()) { + int numAlerts = (int) disputeList.getList().stream() + .mapToLong(x -> x.getBadgeCountProperty().getValue()) + .sum(); + numOpenDisputes.set(numAlerts); + } + }); + }); + disputedTradeIds.add(dispute.getTradeId()); }); } - addedList.forEach(dispute -> { - // for each dispute added, keep track of its "BadgeCountProperty" - EasyBind.subscribe(dispute.getBadgeCountProperty(), - isAlerting -> { - // We get the event before the list gets updated, so we execute on next frame - UserThread.execute(() -> { - synchronized (disputeList.getObservableList()) { - int numAlerts = (int) disputeList.getList().stream() - .mapToLong(x -> x.getBadgeCountProperty().getValue()) - .sum(); - numOpenDisputes.set(numAlerts); - } - }); - }); - disputedTradeIds.add(dispute.getTradeId()); - }); } public void requestPersistence() { diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index 047fe85456..ac0ede6e35 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -288,14 +288,16 @@ public abstract class DisputeManager> extends Sup cleanupDisputes(); List disputes = getDisputeList().getList(); - disputes.forEach(dispute -> { - try { - DisputeValidation.validateNodeAddresses(dispute, config); - } catch (DisputeValidation.ValidationException e) { - log.error(e.toString()); - validationExceptions.add(e); - } - }); + synchronized (disputes) { + disputes.forEach(dispute -> { + try { + DisputeValidation.validateNodeAddresses(dispute, config); + } catch (DisputeValidation.ValidationException e) { + log.error(e.toString()); + validationExceptions.add(e); + } + }); + } maybeClearSensitiveData(); } @@ -318,11 +320,13 @@ public abstract class DisputeManager> extends Sup public void maybeClearSensitiveData() { log.info("{} checking closed disputes eligibility for having sensitive data cleared", super.getClass().getSimpleName()); Instant safeDate = closedTradableManager.getSafeDateForSensitiveDataClearing(); - getDisputeList().getList().stream() - .filter(e -> e.isClosed()) - .filter(e -> e.getOpeningDate().toInstant().isBefore(safeDate)) - .forEach(Dispute::maybeClearSensitiveData); - requestPersistence(); + synchronized (getDisputeList().getList()) { + getDisputeList().getList().stream() + .filter(e -> e.isClosed()) + .filter(e -> e.getOpeningDate().toInstant().isBefore(safeDate)) + .forEach(Dispute::maybeClearSensitiveData); + requestPersistence(); + } } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 6ef9cd69ed..5ac7cd389a 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -176,18 +176,20 @@ public final class ArbitrationManager extends DisputeManager toRemoves = new HashSet<>(); List disputes = getDisputeList().getList(); - for (Dispute dispute : disputes) { + synchronized (disputes) { + for (Dispute dispute : disputes) { - // get dispute's trade - final Trade trade = tradeManager.getTrade(dispute.getTradeId()); - if (trade == null) { - log.warn("Dispute trade {} does not exist", dispute.getTradeId()); - return; - } - - // collect dispute if owned by arbitrator - if (dispute.getTraderPubKeyRing().equals(trade.getArbitrator().getPubKeyRing())) { - toRemoves.add(dispute); + // get dispute's trade + final Trade trade = tradeManager.getTrade(dispute.getTradeId()); + if (trade == null) { + log.warn("Dispute trade {} does not exist", dispute.getTradeId()); + return; + } + + // collect dispute if owned by arbitrator + if (dispute.getTraderPubKeyRing().equals(trade.getArbitrator().getPubKeyRing())) { + toRemoves.add(dispute); + } } } for (Dispute toRemove : toRemoves) { diff --git a/core/src/main/java/haveno/core/support/dispute/mediation/MediationDisputeList.java b/core/src/main/java/haveno/core/support/dispute/mediation/MediationDisputeList.java index 6387984b20..121c1c5a3b 100644 --- a/core/src/main/java/haveno/core/support/dispute/mediation/MediationDisputeList.java +++ b/core/src/main/java/haveno/core/support/dispute/mediation/MediationDisputeList.java @@ -55,8 +55,10 @@ public final class MediationDisputeList extends DisputeList { @Override public Message toProtoMessage() { - return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder() - .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); + synchronized (getList()) { + return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder() + .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); + } } public static MediationDisputeList fromProto(protobuf.MediationDisputeList proto, diff --git a/core/src/main/java/haveno/core/support/dispute/refund/RefundDisputeList.java b/core/src/main/java/haveno/core/support/dispute/refund/RefundDisputeList.java index 2ad31a0107..eb041c8fac 100644 --- a/core/src/main/java/haveno/core/support/dispute/refund/RefundDisputeList.java +++ b/core/src/main/java/haveno/core/support/dispute/refund/RefundDisputeList.java @@ -58,9 +58,10 @@ public final class RefundDisputeList extends DisputeList { @Override public Message toProtoMessage() { forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.REFUND), "Support type has to be REFUND")); - - return protobuf.PersistableEnvelope.newBuilder().setRefundDisputeList(protobuf.RefundDisputeList.newBuilder() - .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); + synchronized (getList()) { + return protobuf.PersistableEnvelope.newBuilder().setRefundDisputeList(protobuf.RefundDisputeList.newBuilder() + .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); + } } public static RefundDisputeList fromProto(protobuf.RefundDisputeList proto, diff --git a/core/src/main/java/haveno/core/trade/CleanupMailboxMessages.java b/core/src/main/java/haveno/core/trade/CleanupMailboxMessages.java index ee10d599ee..a201a985c2 100644 --- a/core/src/main/java/haveno/core/trade/CleanupMailboxMessages.java +++ b/core/src/main/java/haveno/core/trade/CleanupMailboxMessages.java @@ -55,21 +55,23 @@ public class CleanupMailboxMessages { } public void handleTrades(List trades) { - // We wrap in a try catch as in failed trades we cannot be sure if expected data is set, so we could get - // a NullPointer and do not want that this escalate to the user. - try { - if (p2PService.isBootstrapped()) { - cleanupMailboxMessages(trades); - } else { - p2PService.addP2PServiceListener(new BootstrapListener() { - @Override - public void onDataReceived() { - cleanupMailboxMessages(trades); - } - }); + synchronized (trades) { + // We wrap in a try catch as in failed trades we cannot be sure if expected data is set, so we could get + // a NullPointer and do not want that this escalate to the user. + try { + if (p2PService.isBootstrapped()) { + cleanupMailboxMessages(trades); + } else { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onDataReceived() { + cleanupMailboxMessages(trades); + } + }); + } + } catch (Throwable t) { + log.error("Cleanup mailbox messages failed. {}", t.toString()); } - } catch (Throwable t) { - log.error("Cleanup mailbox messages failed. {}", t.toString()); } } diff --git a/core/src/main/java/haveno/core/trade/ClosedTradableManager.java b/core/src/main/java/haveno/core/trade/ClosedTradableManager.java index cac8e9e261..02920c0e3d 100644 --- a/core/src/main/java/haveno/core/trade/ClosedTradableManager.java +++ b/core/src/main/java/haveno/core/trade/ClosedTradableManager.java @@ -81,13 +81,15 @@ public class ClosedTradableManager implements PersistedDataHost { @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { - closedTradables.setAll(persisted.getList()); - closedTradables.stream() - .filter(tradable -> tradable.getOffer() != null) - .forEach(tradable -> tradable.getOffer().setPriceFeedService(priceFeedService)); - completeHandler.run(); - }, - completeHandler); + synchronized (persisted.getList()) { + closedTradables.setAll(persisted.getList()); + closedTradables.stream() + .filter(tradable -> tradable.getOffer() != null) + .forEach(tradable -> tradable.getOffer().setPriceFeedService(priceFeedService)); + } + completeHandler.run(); + }, + completeHandler); } public void onAllServicesInitialized() { @@ -96,7 +98,7 @@ public class ClosedTradableManager implements PersistedDataHost { } public void add(Tradable tradable) { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { if (closedTradables.add(tradable)) { maybeClearSensitiveData(); requestPersistence(); @@ -105,7 +107,7 @@ public class ClosedTradableManager implements PersistedDataHost { } public void remove(Tradable tradable) { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { if (closedTradables.remove(tradable)) { requestPersistence(); } @@ -117,17 +119,17 @@ public class ClosedTradableManager implements PersistedDataHost { } public ObservableList getObservableList() { - synchronized (closedTradables) { - return closedTradables.getObservableList(); - } + return closedTradables.getObservableList(); } public List getTradableList() { - return ImmutableList.copyOf(new ArrayList<>(getObservableList())); + synchronized (closedTradables.getList()) { + return ImmutableList.copyOf(new ArrayList<>(getObservableList())); + } } public List getClosedTrades() { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { return ImmutableList.copyOf(getObservableList().stream() .filter(e -> e instanceof Trade) .map(e -> (Trade) e) @@ -136,7 +138,7 @@ public class ClosedTradableManager implements PersistedDataHost { } public List getCanceledOpenOffers() { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { return ImmutableList.copyOf(getObservableList().stream() .filter(e -> (e instanceof OpenOffer) && ((OpenOffer) e).getState().equals(CANCELED)) .map(e -> (OpenOffer) e) @@ -145,19 +147,19 @@ public class ClosedTradableManager implements PersistedDataHost { } public Optional getTradableById(String id) { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { return closedTradables.stream().filter(e -> e.getId().equals(id)).findFirst(); } } public Optional getTradeById(String id) { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { return getClosedTrades().stream().filter(e -> e.getId().equals(id)).findFirst(); } } public void maybeClearSensitiveData() { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { log.info("checking closed trades eligibility for having sensitive data cleared"); closedTradables.stream() .filter(e -> e instanceof Trade) @@ -170,11 +172,11 @@ public class ClosedTradableManager implements PersistedDataHost { public boolean canTradeHaveSensitiveDataCleared(String tradeId) { Instant safeDate = getSafeDateForSensitiveDataClearing(); - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { return closedTradables.stream() - .filter(e -> e.getId().equals(tradeId)) - .filter(e -> e.getDate().toInstant().isBefore(safeDate)) - .count() > 0; + .filter(e -> e.getId().equals(tradeId)) + .filter(e -> e.getDate().toInstant().isBefore(safeDate)) + .count() > 0; } } @@ -205,9 +207,11 @@ public class ClosedTradableManager implements PersistedDataHost { } public BigInteger getTotalTradeFee(List tradableList) { - return BigInteger.valueOf(tradableList.stream() - .mapToLong(tradable -> getTradeFee(tradable).longValueExact()) - .sum()); + synchronized (tradableList) { + return BigInteger.valueOf(tradableList.stream() + .mapToLong(tradable -> getTradeFee(tradable).longValueExact()) + .sum()); + } } private BigInteger getTradeFee(Tradable tradable) { @@ -229,7 +233,7 @@ public class ClosedTradableManager implements PersistedDataHost { } public void removeTrade(Trade trade) { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { if (closedTradables.remove(trade)) { requestPersistence(); } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 975378ce11..1d75c8188e 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -269,13 +269,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { - tradableList.setAll(persisted.getList()); - tradableList.stream() - .filter(trade -> trade.getOffer() != null) - .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); - completeHandler.run(); - }, - completeHandler); + synchronized (persisted.getList()) { + tradableList.setAll(persisted.getList()); + tradableList.stream() + .filter(trade -> trade.getOffer() != null) + .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); + } + completeHandler.run(); + }, + completeHandler); } @@ -992,7 +994,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi log.info("TradeManager.removeTrade() " + trade.getId()); // remove trade - synchronized (tradableList) { + synchronized (tradableList.getList()) { if (!tradableList.remove(trade)) return; } @@ -1036,18 +1038,20 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private void updateTradePeriodState() { if (isShutDownStarted) return; - for (Trade trade : new ArrayList(tradableList.getList())) { - if (!trade.isPayoutPublished()) { - Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); - Date halfTradePeriodDate = trade.getHalfTradePeriodDate(); - if (maxTradePeriodDate != null && halfTradePeriodDate != null) { - Date now = new Date(); - if (now.after(maxTradePeriodDate)) { - trade.setPeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER); - requestPersistence(); - } else if (now.after(halfTradePeriodDate)) { - trade.setPeriodState(Trade.TradePeriodState.SECOND_HALF); - requestPersistence(); + synchronized (tradableList.getList()) { + for (Trade trade : tradableList.getList()) { + if (!trade.isPayoutPublished()) { + Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); + Date halfTradePeriodDate = trade.getHalfTradePeriodDate(); + if (maxTradePeriodDate != null && halfTradePeriodDate != null) { + Date now = new Date(); + if (now.after(maxTradePeriodDate)) { + trade.setPeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER); + requestPersistence(); + } else if (now.after(halfTradePeriodDate)) { + trade.setPeriodState(Trade.TradePeriodState.SECOND_HALF); + requestPersistence(); + } } } } @@ -1093,7 +1097,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public Stream getTradesStreamWithFundsLockedIn() { - synchronized (tradableList) { + synchronized (tradableList.getList()) { return getObservableList().stream().filter(Trade::isFundsLockedIn); } } @@ -1108,7 +1112,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi public Set getSetOfFailedOrClosedTradeIdsFromLockedInFunds() throws TradeTxException { AtomicReference tradeTxException = new AtomicReference<>(); - synchronized (tradableList) { + synchronized (tradableList.getList()) { Set tradesIdSet = getTradesStreamWithFundsLockedIn() .filter(Trade::hasFailed) .map(Trade::getId) @@ -1170,7 +1174,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi initPersistedTrade(trade); UserThread.execute(() -> { - synchronized (tradableList) { + synchronized (tradableList.getList()) { if (!tradableList.contains(trade)) { tradableList.add(trade); } @@ -1241,7 +1245,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public ObservableList getObservableList() { - synchronized (tradableList) { + synchronized (tradableList.getList()) { return tradableList.getObservableList(); } } @@ -1274,33 +1278,33 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public Optional getOpenTrade(String tradeId) { - synchronized (tradableList) { + synchronized (tradableList.getList()) { return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst(); } } public boolean hasOpenTrade(Trade trade) { - synchronized (tradableList) { + synchronized (tradableList.getList()) { return tradableList.contains(trade); } } public boolean hasFailedScheduledTrade(String offerId) { - synchronized (failedTradesManager) { - return failedTradesManager.getTradeById(offerId).isPresent() && failedTradesManager.getTradeById(offerId).get().isProtocolErrorHandlingScheduled(); - } + return failedTradesManager.getTradeById(offerId).isPresent() && failedTradesManager.getTradeById(offerId).get().isProtocolErrorHandlingScheduled(); } public Optional getOpenTradeByUid(String tradeUid) { - synchronized (tradableList) { + synchronized (tradableList.getList()) { return tradableList.stream().filter(e -> e.getUid().equals(tradeUid)).findFirst(); } } public List getAllTrades() { - synchronized (tradableList) { + synchronized (tradableList.getList()) { List trades = new ArrayList(); - trades.addAll(tradableList.getList()); + synchronized (tradableList.getList()) { + trades.addAll(tradableList.getList()); + } trades.addAll(closedTradableManager.getClosedTrades()); trades.addAll(failedTradesManager.getObservableList()); return trades; @@ -1308,7 +1312,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public List getOpenTrades() { - synchronized (tradableList) { + synchronized (tradableList.getList()) { return ImmutableList.copyOf(getObservableList().stream() .filter(e -> e instanceof Trade) .map(e -> e) @@ -1329,7 +1333,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void addTrade(Trade trade) { - synchronized (tradableList) { + synchronized (tradableList.getList()) { if (tradableList.add(trade)) { requestPersistence(); } diff --git a/core/src/main/java/haveno/core/trade/failed/FailedTradesManager.java b/core/src/main/java/haveno/core/trade/failed/FailedTradesManager.java index a99c28242e..a188e6638e 100644 --- a/core/src/main/java/haveno/core/trade/failed/FailedTradesManager.java +++ b/core/src/main/java/haveno/core/trade/failed/FailedTradesManager.java @@ -70,13 +70,15 @@ public class FailedTradesManager implements PersistedDataHost { @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { - failedTrades.setAll(persisted.getList()); - failedTrades.stream() - .filter(trade -> trade.getOffer() != null) - .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); - completeHandler.run(); - }, - completeHandler); + synchronized (persisted.getList()) { + failedTrades.setAll(persisted.getList()); + failedTrades.stream() + .filter(trade -> trade.getOffer() != null) + .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); + } + completeHandler.run(); + }, + completeHandler); } public void onAllServicesInitialized() { @@ -84,7 +86,7 @@ public class FailedTradesManager implements PersistedDataHost { } public void add(Trade trade) { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { if (failedTrades.add(trade)) { requestPersistence(); } @@ -92,7 +94,7 @@ public class FailedTradesManager implements PersistedDataHost { } public void removeTrade(Trade trade) { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { if (failedTrades.remove(trade)) { requestPersistence(); } @@ -104,26 +106,26 @@ public class FailedTradesManager implements PersistedDataHost { } public ObservableList getObservableList() { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { return failedTrades.getObservableList(); } } public Optional getTradeById(String id) { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { return failedTrades.stream().filter(e -> e.getId().equals(id)).findFirst(); } } public Stream getTradesStreamWithFundsLockedIn() { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { return failedTrades.stream() .filter(Trade::isFundsLockedIn); } } public void unFailTrade(Trade trade) { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { if (unFailTradeCallback == null) return; diff --git a/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOffersDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOffersDataModel.java index 311cca3476..7c1cd7f539 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOffersDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOffersDataModel.java @@ -62,7 +62,9 @@ class SignedOffersDataModel extends ActivatableDataModel { private void applyList() { list.clear(); - list.addAll(openOfferManager.getObservableSignedOffersList().stream().map(SignedOfferListItem::new).collect(Collectors.toList())); + synchronized (openOfferManager.getObservableSignedOffersList()) { + list.addAll(openOfferManager.getObservableSignedOffersList().stream().map(SignedOfferListItem::new).collect(Collectors.toList())); + } // we sort by date, the earliest first list.sort((o1, o2) -> new Date(o2.getSignedOffer().getTimeStamp()).compareTo(new Date(o1.getSignedOffer().getTimeStamp()))); diff --git a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java index a5a0b86dfe..e825e29e6e 100644 --- a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java @@ -206,15 +206,17 @@ public class GUIUtil { persistenceManager.readPersisted(fileName, persisted -> { StringBuilder msg = new StringBuilder(); HashSet paymentAccounts = new HashSet<>(); - persisted.getList().forEach(paymentAccount -> { - String id = paymentAccount.getId(); - if (user.getPaymentAccount(id) == null) { - paymentAccounts.add(paymentAccount); - msg.append(Res.get("guiUtil.accountExport.tradingAccount", id)); - } else { - msg.append(Res.get("guiUtil.accountImport.noImport", id)); - } - }); + synchronized (persisted.getList()) { + persisted.getList().forEach(paymentAccount -> { + String id = paymentAccount.getId(); + if (user.getPaymentAccount(id) == null) { + paymentAccounts.add(paymentAccount); + msg.append(Res.get("guiUtil.accountExport.tradingAccount", id)); + } else { + msg.append(Res.get("guiUtil.accountImport.noImport", id)); + } + }); + } user.addImportedPaymentAccounts(paymentAccounts); new Popup().feedback(Res.get("guiUtil.accountImport.imported", path, msg)).show(); }, diff --git a/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java b/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java index 451d3e7e7f..e1e20225c4 100644 --- a/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java +++ b/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java @@ -48,12 +48,14 @@ public class MailboxMessageList extends PersistableList { @Override public Message toProtoMessage() { - return protobuf.PersistableEnvelope.newBuilder() - .setMailboxMessageList(protobuf.MailboxMessageList.newBuilder() - .addAllMailboxItem(getList().stream() - .map(MailboxItem::toProtoMessage) - .collect(Collectors.toList()))) - .build(); + synchronized (getList()) { + return protobuf.PersistableEnvelope.newBuilder() + .setMailboxMessageList(protobuf.MailboxMessageList.newBuilder() + .addAllMailboxItem(getList().stream() + .map(MailboxItem::toProtoMessage) + .collect(Collectors.toList()))) + .build(); + } } public static MailboxMessageList fromProto(protobuf.MailboxMessageList proto, From 08b0b3643602c1f0827aefbaaa2ee72ac71cc543 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 6 Apr 2025 20:09:40 -0400 Subject: [PATCH 223/371] do not open or create wallet after shut down started --- .../java/haveno/core/app/HavenoExecutable.java | 12 ++++++------ .../core/app/misc/ExecutableForAppWithP2p.java | 10 +++++----- .../haveno/core/xmr/wallet/XmrWalletService.java | 6 +++--- .../p2p/network/TorNetworkNodeNetlayer.java | 16 ++++++++-------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/haveno/core/app/HavenoExecutable.java b/core/src/main/java/haveno/core/app/HavenoExecutable.java index 5f2d14622b..92cd43e827 100644 --- a/core/src/main/java/haveno/core/app/HavenoExecutable.java +++ b/core/src/main/java/haveno/core/app/HavenoExecutable.java @@ -100,7 +100,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven protected AppModule module; protected Config config; @Getter - protected boolean isShutdownInProgress; + protected boolean isShutDownStarted; private boolean isReadOnly; private Thread keepRunningThread; private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS); @@ -330,12 +330,12 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) { log.info("Starting graceful shut down of {}", getClass().getSimpleName()); - // ignore if shut down in progress - if (isShutdownInProgress) { - log.info("Ignoring call to gracefulShutDown, already in progress"); + // ignore if shut down started + if (isShutDownStarted) { + log.info("Ignoring call to gracefulShutDown, already started"); return; } - isShutdownInProgress = true; + isShutDownStarted = true; ResultHandler resultHandler; if (shutdownCompletedHandler != null) { @@ -357,9 +357,9 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven // notify trade protocols and wallets to prepare for shut down before shutting down Set tasks = new HashSet(); + tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted()); - tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted()); try { ThreadUtils.awaitTasks(tasks, tasks.size(), 90000l); // run in parallel with timeout } catch (Exception e) { diff --git a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java index 725ccd877c..0437bca6b4 100644 --- a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java @@ -105,21 +105,21 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { public void gracefulShutDown(ResultHandler resultHandler) { log.info("Starting graceful shut down of {}", getClass().getSimpleName()); - // ignore if shut down in progress - if (isShutdownInProgress) { - log.info("Ignoring call to gracefulShutDown, already in progress"); + // ignore if shut down started + if (isShutDownStarted) { + log.info("Ignoring call to gracefulShutDown, already started"); return; } - isShutdownInProgress = true; + isShutDownStarted = true; try { if (injector != null) { // notify trade protocols and wallets to prepare for shut down Set tasks = new HashSet(); + tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted()); - tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted()); try { ThreadUtils.awaitTasks(tasks, tasks.size(), 120000l); // run in parallel with timeout } catch (Exception e) { diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index f015280b61..e62a78ae3b 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1659,6 +1659,7 @@ public class XmrWalletService extends XmrWalletBase { walletRpc.stopSyncing(); // create wallet + if (isShutDownStarted) throw new IllegalStateException("Cannot create wallet '" + config.getPath() + "' because shutdown is started"); MoneroRpcConnection connection = xmrConnectionService.getConnection(); log.info("Creating RPC wallet " + config.getPath() + " connected to monerod=" + connection.getUri()); long time = System.currentTimeMillis(); @@ -1668,9 +1669,8 @@ public class XmrWalletService extends XmrWalletBase { log.info("Done creating RPC wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); return walletRpc; } catch (Exception e) { - log.warn("Could not create wallet '" + config.getPath() + "': " + e.getMessage() + "\n", e); if (walletRpc != null) forceCloseWallet(walletRpc, config.getPath()); - throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'. Please close Haveno, stop all monero-wallet-rpc processes in your task manager, and restart Haveno."); + throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'. Please close Haveno, stop all monero-wallet-rpc processes in your task manager, and restart Haveno.\n\nError message: " + e.getMessage()); } } @@ -1690,6 +1690,7 @@ public class XmrWalletService extends XmrWalletBase { if (!applyProxyUri) connection.setProxyUri(null); // try opening wallet + if (isShutDownStarted) throw new IllegalStateException("Cannot open wallet '" + config.getPath() + "' because shutdown is started"); log.info("Opening RPC wallet '{}' with monerod={}, proxyUri={}", config.getPath(), connection.getUri(), connection.getProxyUri()); config.setServer(connection); try { @@ -1764,7 +1765,6 @@ public class XmrWalletService extends XmrWalletBase { log.info("Done opening RPC wallet " + config.getPath()); return walletRpc; } catch (Exception e) { - log.warn("Could not open wallet '" + config.getPath() + "': " + e.getMessage() + "\n", e); if (walletRpc != null) forceCloseWallet(walletRpc, config.getPath()); throw new IllegalStateException("Could not open wallet '" + config.getPath() + "'. Please close Haveno, stop all monero-wallet-rpc processes in your task manager, and restart Haveno.\n\nError message: " + e.getMessage()); } diff --git a/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeNetlayer.java b/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeNetlayer.java index b4d7e9785c..8c99e1f986 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeNetlayer.java +++ b/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeNetlayer.java @@ -40,8 +40,8 @@ public class TorNetworkNodeNetlayer extends TorNetworkNode { private Tor tor; private final String torControlHost; private Timer shutDownTimeoutTimer; - private boolean shutDownInProgress; - private boolean shutDownComplete; + private boolean isShutDownStarted; + private boolean isShutDownComplete; public TorNetworkNodeNetlayer(int servicePort, NetworkProtoResolver networkProtoResolver, @@ -65,20 +65,20 @@ public class TorNetworkNodeNetlayer extends TorNetworkNode { @Override public void shutDown(@Nullable Runnable shutDownCompleteHandler) { log.info("TorNetworkNodeNetlayer shutdown started"); - if (shutDownComplete) { + if (isShutDownComplete) { log.info("TorNetworkNodeNetlayer shutdown already completed"); if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); return; } - if (shutDownInProgress) { - log.warn("Ignoring request to shut down because shut down is in progress"); + if (isShutDownStarted) { + log.warn("Ignoring request to shut down because shut down already started"); return; } - shutDownInProgress = true; + isShutDownStarted = true; shutDownTimeoutTimer = UserThread.runAfter(() -> { log.error("A timeout occurred at shutDown"); - shutDownComplete = true; + isShutDownComplete = true; if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); executor.shutdownNow(); }, SHUT_DOWN_TIMEOUT); @@ -96,7 +96,7 @@ public class TorNetworkNodeNetlayer extends TorNetworkNode { log.error("Shutdown TorNetworkNodeNetlayer failed with exception", e); } finally { shutDownTimeoutTimer.stop(); - shutDownComplete = true; + isShutDownComplete = true; if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); } }); From 1c92d9665104b04049b773a829983bc6d847d20d Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 7 Apr 2025 15:51:41 -0400 Subject: [PATCH 224/371] fix offer publishing with mutable list --- core/src/main/java/haveno/core/offer/OpenOfferManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 14f51439b1..9917ecb897 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -2064,10 +2064,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe stopPeriodicRefreshOffersTimer(); ThreadUtils.execute(() -> { - processListForRepublishOffers(getOpenOffers()); + processListForRepublishOffers(new ArrayList<>(getOpenOffers())); // list will be modified }, THREAD_ID); } + // modifies the given list private void processListForRepublishOffers(List list) { if (list.isEmpty()) { return; From 501485ec71adb79b1bae8f89626790fbb6cb0f14 Mon Sep 17 00:00:00 2001 From: boldsuck Date: Mon, 7 Apr 2025 21:57:37 +0200 Subject: [PATCH 225/371] Add release to build workflow (#1685) --- .github/workflows/build.yml | 47 ++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8fc6b6a481..6543846e0a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,3 +1,6 @@ +# GitHub Releases requires a tag, e.g: +# git tag -s 1.0.19-1 -m "haveno-v1.0.19-1" +# git push origin 1.0.19-1 name: CI on: @@ -69,10 +72,9 @@ jobs: "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append shell: powershell - - name: Move Release Files on Unix - if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }} + - name: Move Release Files for Linux + if: ${{ matrix.os == 'ubuntu-22.04' }} run: | - if [ "${{ matrix.os }}" == "ubuntu-22.04" ]; then mkdir ${{ github.workspace }}/release-linux-rpm mkdir ${{ github.workspace }}/release-linux-deb mkdir ${{ github.workspace }}/release-linux-flatpak @@ -85,31 +87,36 @@ jobs: cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-rpm cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-appimage cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-flatpak - else + cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 + shell: bash + - name: Move Release Files for macOS + if: ${{ matrix.os == 'macos-13' }} + run: | mkdir ${{ github.workspace }}/release-macos mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos - fi + cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256 shell: bash - name: Move Release Files on Windows if: ${{ matrix.os == 'windows-latest' }} run: | mkdir ${{ github.workspace }}/release-windows Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe - Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows + Copy-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows + Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/haveno-${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 shell: powershell # win - uses: actions/upload-artifact@v4 name: "Windows artifacts" - if: ${{ matrix.os == 'windows-latest'}} + if: ${{ matrix.os == 'windows-latest' }} with: name: haveno-windows path: ${{ github.workspace }}/release-windows # macos - uses: actions/upload-artifact@v4 name: "macOS artifacts" - if: ${{ matrix.os == 'macos-13' }} + if: ${{ matrix.os == 'macos-13' }} with: name: haveno-macos path: ${{ github.workspace }}/release-macos @@ -120,6 +127,7 @@ jobs: with: name: haveno-linux-deb path: ${{ github.workspace }}/release-linux-deb + - uses: actions/upload-artifact@v4 name: "Linux - rpm artifact" if: ${{ matrix.os == 'ubuntu-22.04' }} @@ -140,3 +148,26 @@ jobs: with: name: haveno-linux-flatpak path: ${{ github.workspace }}/release-linux-flatpak + + - name: Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb + ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm + ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage + ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak + ${{ github.workspace }}/haveno-${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 + ${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg + ${{ github.workspace }}/haveno-${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256 + ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe + ${{ github.workspace }}/haveno-${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 + +# https://git-scm.com/docs/git-tag - git-tag Docu +# +# git tag - lists all local tags +# git tag -d 1.0.19-1 - delete local tag +# +# git ls-remote --tags - lists all remote tags +# git push origin --delete refs/tags/1.0.19-1 - delete remote tag From 34b55bc86b52b3a7c330c09dd1d754e9df7b17ca Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 7 Apr 2025 18:40:56 -0400 Subject: [PATCH 226/371] precede version tags with 'v' for release hash files --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6543846e0a..f1751c0d36 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,7 +87,7 @@ jobs: cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-rpm cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-appimage cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-flatpak - cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 + cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 shell: bash - name: Move Release Files for macOS if: ${{ matrix.os == 'macos-13' }} @@ -95,7 +95,7 @@ jobs: mkdir ${{ github.workspace }}/release-macos mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos - cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256 + cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256 shell: bash - name: Move Release Files on Windows if: ${{ matrix.os == 'windows-latest' }} @@ -103,7 +103,7 @@ jobs: mkdir ${{ github.workspace }}/release-windows Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe Copy-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows - Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/haveno-${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 + Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 shell: powershell # win @@ -158,11 +158,11 @@ jobs: ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak - ${{ github.workspace }}/haveno-${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 + ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 ${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg - ${{ github.workspace }}/haveno-${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256 + ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256 ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe - ${{ github.workspace }}/haveno-${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 + ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 # https://git-scm.com/docs/git-tag - git-tag Docu # From d78709e1f9d0a6c8def627d23cca874758b38529 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 8 Apr 2025 09:16:17 -0400 Subject: [PATCH 227/371] start trade period from unlock time instead of first confirmation --- core/src/main/java/haveno/core/trade/Trade.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 41027600a2..760499aff7 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2167,18 +2167,19 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (daemonRpc == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod"); if (getMakerDepositTx() == null || (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?"); - long maxHeight = Math.max(getMakerDepositTx().getHeight(), hasBuyerAsTakerWithoutDeposit() ? 0l : getTakerDepositTx().getHeight()); - long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp(); + // get unlock time of last deposit tx + long unlockHeight = Math.max(getMakerDepositTx().getHeight() + XmrWalletService.NUM_BLOCKS_UNLOCK - 1, hasBuyerAsTakerWithoutDeposit() ? 0l : getTakerDepositTx().getHeight() + XmrWalletService.NUM_BLOCKS_UNLOCK - 1); + long unlockTime = daemonRpc.getBlockByHeight(unlockHeight).getTimestamp() * 1000; // If block date is in future (Date in blocks can be off by +/- 2 hours) we use our current date. // If block date is earlier than our trade date we use our trade date. - if (blockTime > now) + if (unlockTime > now) startTime = now; else - startTime = Math.max(blockTime, tradeTime); + startTime = Math.max(unlockTime, tradeTime); log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}", - new Date(startTime), new Date(tradeTime), new Date(blockTime)); + new Date(startTime), new Date(tradeTime), new Date(unlockTime)); } public boolean hasFailed() { From 7243d7fa38d23965f647fa559001028685bfbed6 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 9 Apr 2025 08:15:14 -0400 Subject: [PATCH 228/371] show user friendly error on non-ascii password --- common/src/main/java/haveno/common/crypto/KeyStorage.java | 5 +++++ .../desktop/main/account/content/password/PasswordView.java | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/haveno/common/crypto/KeyStorage.java b/common/src/main/java/haveno/common/crypto/KeyStorage.java index 05280b8d4a..5edfb89575 100644 --- a/common/src/main/java/haveno/common/crypto/KeyStorage.java +++ b/common/src/main/java/haveno/common/crypto/KeyStorage.java @@ -243,6 +243,11 @@ public class KeyStorage { //noinspection ResultOfMethodCallIgnored storageDir.mkdirs(); + // password must be ascii + if (password != null && !password.matches("\\p{ASCII}*")) { + throw new IllegalArgumentException("Password must be ASCII."); + } + var oldPasswordChars = oldPassword == null ? new char[0] : oldPassword.toCharArray(); var passwordChars = password == null ? new char[0] : password.toCharArray(); try { diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/password/PasswordView.java b/desktop/src/main/java/haveno/desktop/main/account/content/password/PasswordView.java index e59309c75f..7db3d7ddab 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/password/PasswordView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/password/PasswordView.java @@ -39,7 +39,6 @@ import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabel; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addPasswordTextField; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; -import org.apache.commons.lang3.exception.ExceptionUtils; import haveno.desktop.util.Layout; import haveno.desktop.util.validation.PasswordValidator; import javafx.beans.value.ChangeListener; @@ -160,7 +159,7 @@ public class PasswordView extends ActivatableView { } catch (Throwable t) { log.error("Error applying password: {}\n", t.getMessage(), t); new Popup() - .warning(Res.get("password.walletEncryptionFailed") + "\n\n" + ExceptionUtils.getStackTrace(t)) + .warning(Res.get("password.walletEncryptionFailed") + "\n\n" + t.getMessage()) .show(); } } From ad38e3b80c0d6807f78d5e6b51d033a5eb58b56a Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 8 Apr 2025 09:19:40 -0400 Subject: [PATCH 229/371] remove trade lock while shutting down trade thread --- .../main/java/haveno/core/trade/Trade.java | 14 +++--- .../trade/protocol/BuyerAsMakerProtocol.java | 46 +++++++++---------- .../haveno/core/xmr/wallet/XmrWalletBase.java | 6 ++- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 760499aff7..1a0b513e1e 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1607,13 +1607,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // shut down trade threads - synchronized (getLock()) { - isInitialized = false; - isShutDown = true; - List shutDownThreads = new ArrayList<>(); - shutDownThreads.add(() -> ThreadUtils.shutDown(getId())); - ThreadUtils.awaitTasks(shutDownThreads); - } + isInitialized = false; + isShutDown = true; + List shutDownThreads = new ArrayList<>(); + shutDownThreads.add(() -> ThreadUtils.shutDown(getId())); + ThreadUtils.awaitTasks(shutDownThreads); // save and close if (wallet != null) { @@ -2513,7 +2511,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { try { syncWallet(pollWallet); } catch (Exception e) { - if (!isShutDown && walletExists()) { + if (!isShutDownStarted && walletExists()) { log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java index 9dc1b64405..4d29a8fb42 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java @@ -62,28 +62,28 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol ErrorMessageHandler errorMessageHandler) { log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { - synchronized (trade.getLock()) { - latchTrade(); - this.errorMessageHandler = errorMessageHandler; - expect(phase(Trade.Phase.INIT) - .with(message) - .from(peer)) - .setup(tasks( - ApplyFilter.class, - ProcessInitTradeRequest.class, - MakerSendInitTradeRequestToArbitrator.class) - .using(new TradeTaskRunner(trade, - () -> { - startTimeout(); - handleTaskRunnerSuccess(peer, message); - }, - errorMessage -> { - handleTaskRunnerFault(peer, message, errorMessage); - })) - .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) - .executeTasks(true); - awaitTradeLatch(); - } - }, trade.getId()); + synchronized (trade.getLock()) { + latchTrade(); + this.errorMessageHandler = errorMessageHandler; + expect(phase(Trade.Phase.INIT) + .with(message) + .from(peer)) + .setup(tasks( + ApplyFilter.class, + ProcessInitTradeRequest.class, + MakerSendInitTradeRequestToArbitrator.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(); + handleTaskRunnerSuccess(peer, message); + }, + errorMessage -> { + handleTaskRunnerFault(peer, message, errorMessage); + })) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) + .executeTasks(true); + awaitTradeLatch(); + } + }, trade.getId()); } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java index 3f4ba3aa03..6ad5a7180e 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java @@ -110,8 +110,10 @@ public abstract class XmrWalletBase { try { height = wallet.getHeight(); // can get read timeout while syncing } catch (Exception e) { - log.warn("Error getting wallet height while syncing with progress: " + e.getMessage()); - if (wallet != null && !isShutDownStarted) log.warn(ExceptionUtils.getStackTrace(e)); + if (wallet != null && !isShutDownStarted) { + log.warn("Error getting wallet height while syncing with progress: " + e.getMessage()); + log.warn(ExceptionUtils.getStackTrace(e)); + } // stop polling and release latch syncProgressError = e; From 974c6a0d8690c3b6b6bff01e3b76f22c8510201f Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 9 Apr 2025 10:40:55 -0400 Subject: [PATCH 230/371] shut down p2p service last to fix timeout on shut down --- .../haveno/core/app/HavenoExecutable.java | 29 ++++++++++--------- .../app/misc/ExecutableForAppWithP2p.java | 21 +++++++------- .../main/java/haveno/core/trade/Trade.java | 2 +- .../core/trade/protocol/TradeProtocol.java | 5 ++-- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/haveno/core/app/HavenoExecutable.java b/core/src/main/java/haveno/core/app/HavenoExecutable.java index 92cd43e827..8f2998eb0d 100644 --- a/core/src/main/java/haveno/core/app/HavenoExecutable.java +++ b/core/src/main/java/haveno/core/app/HavenoExecutable.java @@ -366,36 +366,37 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven log.error("Failed to notify all services to prepare for shutdown: {}\n", e.getMessage(), e); } - injector.getInstance(TradeManager.class).shutDown(); injector.getInstance(PriceFeedService.class).shutDown(); injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown(); // shut down open offer manager - log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService"); + log.info("Shutting down OpenOfferManager"); injector.getInstance(OpenOfferManager.class).shutDown(() -> { - // shut down offer book service - injector.getInstance(OfferBookService.class).shutDown(); + // listen for shut down of wallets setup + injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { - // shut down p2p service - injector.getInstance(P2PService.class).shutDown(() -> { + // shut down p2p service + log.info("Shutting down P2P service"); + injector.getInstance(P2PService.class).shutDown(() -> { - // shut down monero wallets and connections - log.info("Shutting down wallet and connection services"); - injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { - // done shutting down log.info("Graceful shutdown completed. Exiting now."); module.close(injector); completeShutdown(resultHandler, EXIT_SUCCESS, systemExit); }); - injector.getInstance(BtcWalletService.class).shutDown(); - injector.getInstance(XmrWalletService.class).shutDown(); - injector.getInstance(XmrConnectionService.class).shutDown(); - injector.getInstance(WalletsSetup.class).shutDown(); }); + + // shut down trade and wallet services + log.info("Shutting down trade and wallet services"); + injector.getInstance(OfferBookService.class).shutDown(); + injector.getInstance(TradeManager.class).shutDown(); + injector.getInstance(BtcWalletService.class).shutDown(); + injector.getInstance(XmrWalletService.class).shutDown(); + injector.getInstance(XmrConnectionService.class).shutDown(); + injector.getInstance(WalletsSetup.class).shutDown(); }); } catch (Throwable t) { log.error("App shutdown failed with exception: {}\n", t.getMessage(), t); diff --git a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java index 0437bca6b4..9c8016d506 100644 --- a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java @@ -127,25 +127,21 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { } JsonFileManager.shutDownAllInstances(); - injector.getInstance(TradeManager.class).shutDown(); injector.getInstance(PriceFeedService.class).shutDown(); injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown(); // shut down open offer manager - log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService"); + log.info("Shutting down OpenOfferManager"); injector.getInstance(OpenOfferManager.class).shutDown(() -> { - // shut down offer book service - injector.getInstance(OfferBookService.class).shutDown(); + // listen for shut down of wallets setup + injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { - // shut down p2p service - injector.getInstance(P2PService.class).shutDown(() -> { - - // shut down monero wallets and connections - log.info("Shutting down wallet and connection services"); - injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { + // shut down p2p service + log.info("Shutting down P2P service"); + injector.getInstance(P2PService.class).shutDown(() -> { module.close(injector); PersistenceManager.flushAllDataToDiskAtShutdown(() -> { @@ -155,6 +151,11 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); }); }); + + // shut down trade and wallet services + log.info("Shutting down trade and wallet services"); + injector.getInstance(OfferBookService.class).shutDown(); + injector.getInstance(TradeManager.class).shutDown(); injector.getInstance(BtcWalletService.class).shutDown(); injector.getInstance(XmrWalletService.class).shutDown(); injector.getInstance(XmrConnectionService.class).shutDown(); diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 1a0b513e1e..8930966ba5 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2541,7 +2541,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (pollWallet) doPollWallet(); } catch (Exception e) { - ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); + if (!isShutDownStarted) ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); throw e; } } diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 74a50f44f4..c425f343ef 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -257,12 +257,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void maybeSendDepositsConfirmedMessages() { - if (!trade.isInitialized() || trade.isShutDownStarted()) return; + if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down ThreadUtils.execute(() -> { + if (!trade.isInitialized() || trade.isShutDownStarted()) return; if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return; depositsConfirmedTasksCalled = true; synchronized (trade.getLock()) { - if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down + if (!trade.isInitialized() || trade.isShutDownStarted()) return; latchTrade(); expect(new Condition(trade)) .setup(tasks(getDepositsConfirmedTasks()) From 35eb65d17328ac597c38edc9cd2a06b81e8d3935 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:17:04 -0400 Subject: [PATCH 231/371] clear monero connection error & popup on successful poll --- core/src/main/java/haveno/core/api/XmrConnectionService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index a1e1219172..ba3b58d3e0 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -808,6 +808,7 @@ public final class XmrConnectionService { // connected to daemon isConnected = true; + connectionServiceError.set(null); // determine if blockchain is syncing locally boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0 From fe1fb88ce089799ae4b2606429aae33c111035f6 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 9 Apr 2025 08:38:53 -0400 Subject: [PATCH 232/371] import multisig hex on trade thread when scheduled --- core/src/main/java/haveno/core/trade/Trade.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 8930966ba5..00f4765958 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1121,8 +1121,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!isInitialized || isShutDownStarted) return; synchronized (getLock()) { if (processModel.isImportMultisigHexScheduled()) { + importMultisigHex(); processModel.setImportMultisigHexScheduled(false); - ThreadUtils.submitToPool(() -> importMultisigHex()); } } }, getId()); From 765a32fd9f7818c4b3b32a77141c752128f3914d Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 9 Apr 2025 10:23:51 -0400 Subject: [PATCH 233/371] improve error message when open offer is removed while initializing trade --- .../protocol/tasks/MaybeSendSignContractRequest.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index 1d2170cb53..6f10625e35 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -19,6 +19,7 @@ package haveno.core.trade.protocol.tasks; import haveno.common.app.Version; import haveno.common.taskrunner.TaskRunner; +import haveno.core.offer.OpenOffer; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; @@ -35,6 +36,7 @@ import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; import java.util.Date; +import java.util.Optional; import java.util.UUID; // TODO (woodser): separate classes for deposit tx creation and contract request, or combine into ProcessInitMultisigRequest @@ -87,8 +89,13 @@ public class MaybeSendSignContractRequest extends TradeTask { Integer subaddressIndex = null; boolean reserveExactAmount = false; if (trade instanceof MakerTrade) { - reserveExactAmount = processModel.getOpenOfferManager().getOpenOffer(trade.getId()).get().isReserveExactAmount(); - if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex(); + Optional openOffer = processModel.getOpenOfferManager().getOpenOffer(trade.getId()); + if (openOffer.isPresent()) { + reserveExactAmount = openOffer.get().isReserveExactAmount(); + if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex(); + } else { + throw new RuntimeException("Cannot request contract signature because open offer has been removed for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + } } // thaw reserved outputs From 31fbf9c4e826bdb21069c25584f40e1d8e1227c6 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 9 Apr 2025 10:22:10 -0400 Subject: [PATCH 234/371] ignore fault on mailbox message task after shut down --- .../core/trade/protocol/tasks/SendMailboxMessageTask.java | 1 + p2p/src/main/java/haveno/network/p2p/P2PService.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java index 157d797410..24638c5e70 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java @@ -82,6 +82,7 @@ public abstract class SendMailboxMessageTask extends TradeTask { @Override public void onFault(String errorMessage) { + if (processModel.getP2PService().isShutDownStarted()) return; log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", message.getClass().getSimpleName(), peersNodeAddress, message.getOfferId(), message.getUid(), errorMessage); SendMailboxMessageTask.this.onFault(errorMessage, message); } diff --git a/p2p/src/main/java/haveno/network/p2p/P2PService.java b/p2p/src/main/java/haveno/network/p2p/P2PService.java index 117ed4a494..e7a810332e 100644 --- a/p2p/src/main/java/haveno/network/p2p/P2PService.java +++ b/p2p/src/main/java/haveno/network/p2p/P2PService.java @@ -109,6 +109,8 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis @Getter private static NodeAddress myNodeAddress; + @Getter + private boolean isShutDownStarted = false; /////////////////////////////////////////////////////////////////////////////////////////// @@ -192,6 +194,7 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis private void doShutDown() { log.info("P2PService doShutDown started"); + isShutDownStarted = true; if (p2PDataStorage != null) { p2PDataStorage.shutDown(); From f19ed1932598c643ad0a760911517dfb2e017ea2 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 10 Apr 2025 08:28:13 -0400 Subject: [PATCH 235/371] remove log highlighting with character literal --- .../src/main/java/haveno/core/trade/protocol/TradeProtocol.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index c425f343ef..a1ad25ddaf 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -96,7 +96,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private static final String TIMEOUT_REACHED = "Timeout reached."; public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other wallet functions public static final long REPROCESS_DELAY_MS = 5000; - public static final String LOG_HIGHLIGHT = "\u001B[36m"; // cyan + public static final String LOG_HIGHLIGHT = ""; // TODO: how to highlight some logs with cyan? ("\u001B[36m")? coloring works in the terminal but prints character literals to .log files protected final ProcessModel processModel; protected final Trade trade; From 295c91760c940ab2398404b6139372e1f22485c7 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:42:56 -0400 Subject: [PATCH 236/371] place offer runs off main thread --- .../haveno/core/offer/OpenOfferManager.java | 94 +++++++++---------- .../main/java/haveno/core/trade/Trade.java | 5 +- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 9917ecb897..aec7700c92 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -517,54 +517,54 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe String sourceOfferId, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - - // check source offer and clone limit - OpenOffer sourceOffer = null; - if (sourceOfferId != null) { - - // get source offer - Optional sourceOfferOptional = getOpenOffer(sourceOfferId); - if (!sourceOfferOptional.isPresent()) { - errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId); - return; - } - sourceOffer = sourceOfferOptional.get(); - - // check clone limit - int numClones = getOpenOfferGroup(sourceOffer.getGroupId()).size(); - if (numClones >= Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) { - errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " cloned offers with shared funds reached."); - return; - } - } - - // create open offer - OpenOffer openOffer = new OpenOffer(offer, triggerPrice, sourceOffer == null ? reserveExactAmount : sourceOffer.isReserveExactAmount()); - - // set state from source offer - if (sourceOffer != null) { - openOffer.setReserveTxHash(sourceOffer.getReserveTxHash()); - openOffer.setReserveTxHex(sourceOffer.getReserveTxHex()); - openOffer.setReserveTxKey(sourceOffer.getReserveTxKey()); - openOffer.setGroupId(sourceOffer.getGroupId()); - openOffer.getOffer().getOfferPayload().setReserveTxKeyImages(sourceOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); - xmrWalletService.cloneAddressEntries(sourceOffer.getOffer().getId(), openOffer.getOffer().getId()); - if (hasConflictingClone(openOffer)) openOffer.setState(OpenOffer.State.DEACTIVATED); - } - - // add the open offer - synchronized (processOffersLock) { - addOpenOffer(openOffer); - } - - // done if source offer is pending - if (sourceOffer != null && sourceOffer.isPending()) { - resultHandler.handleResult(null); - return; - } - - // schedule or post offer ThreadUtils.execute(() -> { + + // check source offer and clone limit + OpenOffer sourceOffer = null; + if (sourceOfferId != null) { + + // get source offer + Optional sourceOfferOptional = getOpenOffer(sourceOfferId); + if (!sourceOfferOptional.isPresent()) { + errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId); + return; + } + sourceOffer = sourceOfferOptional.get(); + + // check clone limit + int numClones = getOpenOfferGroup(sourceOffer.getGroupId()).size(); + if (numClones >= Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) { + errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " cloned offers with shared funds reached."); + return; + } + } + + // create open offer + OpenOffer openOffer = new OpenOffer(offer, triggerPrice, sourceOffer == null ? reserveExactAmount : sourceOffer.isReserveExactAmount()); + + // set state from source offer + if (sourceOffer != null) { + openOffer.setReserveTxHash(sourceOffer.getReserveTxHash()); + openOffer.setReserveTxHex(sourceOffer.getReserveTxHex()); + openOffer.setReserveTxKey(sourceOffer.getReserveTxKey()); + openOffer.setGroupId(sourceOffer.getGroupId()); + openOffer.getOffer().getOfferPayload().setReserveTxKeyImages(sourceOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); + xmrWalletService.cloneAddressEntries(sourceOffer.getOffer().getId(), openOffer.getOffer().getId()); + if (hasConflictingClone(openOffer)) openOffer.setState(OpenOffer.State.DEACTIVATED); + } + + // add the open offer + synchronized (processOffersLock) { + addOpenOffer(openOffer); + } + + // done if source offer is pending + if (sourceOffer != null && sourceOffer.isPending()) { + resultHandler.handleResult(null); + return; + } + + // schedule or post offer synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); processOffer(getOpenOffers(), openOffer, (transaction) -> { diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 00f4765958..b1c893abaa 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2728,7 +2728,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } } catch (Exception e) { - if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); + if (HavenoUtils.isUnresponsive(e)) { + if (isShutDownStarted) forceCloseWallet(); + else forceRestartTradeWallet(); + } else { boolean isWalletConnected = isWalletConnectedToDaemon(); if (wallet != null && !isShutDownStarted && isWalletConnected) { From 5bff265ccad1ec74297bf37fc9aaa7243259ed1e Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:46:57 -0400 Subject: [PATCH 237/371] take offer runs on trade thread --- .../java/haveno/core/trade/TradeManager.java | 123 +++++++++--------- 1 file changed, 62 insertions(+), 61 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 1d75c8188e..420dbb9119 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -868,69 +868,70 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi boolean isTakerApiUser, TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { + ThreadUtils.execute(() -> { + checkArgument(!wasOfferAlreadyUsedInTrade(offer.getId())); - checkArgument(!wasOfferAlreadyUsedInTrade(offer.getId())); - - // validate inputs - if (amount.compareTo(offer.getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount"); - if (amount.compareTo(offer.getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount"); - - // ensure trade is not already open - Optional tradeOptional = getOpenTrade(offer.getId()); - if (tradeOptional.isPresent()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + offer.getId() + " is already open"); - - // create trade - Trade trade; - if (offer.isBuyOffer()) { - trade = new SellerAsTakerTrade(offer, - amount, - offer.getPrice().getValue(), - xmrWalletService, - getNewProcessModel(offer), - UUID.randomUUID().toString(), - offer.getMakerNodeAddress(), - P2PService.getMyNodeAddress(), - null, - offer.getChallenge()); - } else { - trade = new BuyerAsTakerTrade(offer, - amount, - offer.getPrice().getValue(), - xmrWalletService, - getNewProcessModel(offer), - UUID.randomUUID().toString(), - offer.getMakerNodeAddress(), - P2PService.getMyNodeAddress(), - null, - offer.getChallenge()); - } - trade.getProcessModel().setUseSavingsWallet(useSavingsWallet); - trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact()); - trade.getMaker().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId()); - trade.getMaker().setPubKeyRing(offer.getPubKeyRing()); - trade.getSelf().setPubKeyRing(keyRing.getPubKeyRing()); - trade.getSelf().setPaymentAccountId(paymentAccountId); - trade.getSelf().setPaymentMethodId(user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId()); - - // initialize trade protocol - TradeProtocol tradeProtocol = createTradeProtocol(trade); - addTrade(trade); - - initTradeAndProtocol(trade, tradeProtocol); - trade.addInitProgressStep(); - - // process with protocol - ((TakerProtocol) tradeProtocol).onTakeOffer(result -> { - tradeResultHandler.handleResult(trade); + // validate inputs + if (amount.compareTo(offer.getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount"); + if (amount.compareTo(offer.getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount"); + + // ensure trade is not already open + Optional tradeOptional = getOpenTrade(offer.getId()); + if (tradeOptional.isPresent()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + offer.getId() + " is already open"); + + // create trade + Trade trade; + if (offer.isBuyOffer()) { + trade = new SellerAsTakerTrade(offer, + amount, + offer.getPrice().getValue(), + xmrWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString(), + offer.getMakerNodeAddress(), + P2PService.getMyNodeAddress(), + null, + offer.getChallenge()); + } else { + trade = new BuyerAsTakerTrade(offer, + amount, + offer.getPrice().getValue(), + xmrWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString(), + offer.getMakerNodeAddress(), + P2PService.getMyNodeAddress(), + null, + offer.getChallenge()); + } + trade.getProcessModel().setUseSavingsWallet(useSavingsWallet); + trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact()); + trade.getMaker().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId()); + trade.getMaker().setPubKeyRing(offer.getPubKeyRing()); + trade.getSelf().setPubKeyRing(keyRing.getPubKeyRing()); + trade.getSelf().setPaymentAccountId(paymentAccountId); + trade.getSelf().setPaymentMethodId(user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId()); + + // initialize trade protocol + TradeProtocol tradeProtocol = createTradeProtocol(trade); + addTrade(trade); + + initTradeAndProtocol(trade, tradeProtocol); + trade.addInitProgressStep(); + + // process with protocol + ((TakerProtocol) tradeProtocol).onTakeOffer(result -> { + tradeResultHandler.handleResult(trade); + requestPersistence(); + }, errorMessage -> { + log.warn("Taker error during trade initialization: " + errorMessage); + trade.onProtocolError(); + xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move this into protocol error handling + errorMessageHandler.handleErrorMessage(errorMessage); + }); + requestPersistence(); - }, errorMessage -> { - log.warn("Taker error during trade initialization: " + errorMessage); - trade.onProtocolError(); - xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move this into protocol error handling - errorMessageHandler.handleErrorMessage(errorMessage); - }); - - requestPersistence(); + }, offer.getId()); } private ProcessModel getNewProcessModel(Offer offer) { From 454fc912989f4112a93488ab533da42c9bbb773f Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:51:05 -0400 Subject: [PATCH 238/371] fix startup when missing multisig wallets --- .../main/java/haveno/core/trade/Trade.java | 11 +++---- .../java/haveno/core/trade/TradeManager.java | 24 +++++++-------- .../core/xmr/wallet/XmrWalletService.java | 30 ++++++++++--------- .../haveno/desktop/main/MainViewModel.java | 1 + .../pendingtrades/steps/TradeStepView.java | 1 + 5 files changed, 36 insertions(+), 31 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index b1c893abaa..5a6f502d0c 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -753,11 +753,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { importMultisigHexIfScheduled(); }); - // trade is initialized - isInitialized = true; - // done if deposit not requested or payout unlocked if (!isDepositRequested() || isPayoutUnlocked()) { + isInitialized = true; isFullyInitialized = true; return; } @@ -769,14 +767,17 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (payoutTx != null && payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) { log.warn("Payout state for {} {} is {} but payout is unlocked, updating state", getClass().getSimpleName(), getId(), getPayoutState()); setPayoutStateUnlocked(); + isInitialized = true; isFullyInitialized = true; return; } else { - log.warn("Missing trade wallet for {} {}, state={}, marked completed={}", getClass().getSimpleName(), getShortId(), getState(), isCompleted()); - return; + throw new RuntimeException("Missing trade wallet for " + getClass().getSimpleName() + " " + getShortId() + ", state=" + getState() + ", marked completed=" + isCompleted()); } } + // trade is initialized + isInitialized = true; + // init syncing if deposit requested if (isDepositRequested()) { tryInitSyncing(); diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 420dbb9119..ea01a7c927 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -470,6 +470,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (!isShutDownStarted) { log.warn("Error initializing {} {}: {}\n", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e); trade.setInitError(e); + trade.prependErrorMessage(e.getMessage()); } } }); @@ -1041,18 +1042,17 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (isShutDownStarted) return; synchronized (tradableList.getList()) { for (Trade trade : tradableList.getList()) { - if (!trade.isPayoutPublished()) { - Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); - Date halfTradePeriodDate = trade.getHalfTradePeriodDate(); - if (maxTradePeriodDate != null && halfTradePeriodDate != null) { - Date now = new Date(); - if (now.after(maxTradePeriodDate)) { - trade.setPeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER); - requestPersistence(); - } else if (now.after(halfTradePeriodDate)) { - trade.setPeriodState(Trade.TradePeriodState.SECOND_HALF); - requestPersistence(); - } + if (!trade.isInitialized() || trade.isPayoutPublished()) continue; + Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); + Date halfTradePeriodDate = trade.getHalfTradePeriodDate(); + if (maxTradePeriodDate != null && halfTradePeriodDate != null) { + Date now = new Date(); + if (now.after(maxTradePeriodDate)) { + trade.setPeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER); + requestPersistence(); + } else if (now.after(halfTradePeriodDate)) { + trade.setPeriodState(Trade.TradePeriodState.SECOND_HALF); + requestPersistence(); } } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index e62a78ae3b..499d20c0a7 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -689,22 +689,24 @@ public class XmrWalletService extends XmrWalletBase { } private MoneroTxWallet createTradeTxFromSubaddress(BigInteger feeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, Integer subaddressIndex) { + synchronized (walletLock) { - // create tx - MoneroTxConfig txConfig = new MoneroTxConfig() - .setAccountIndex(0) - .setSubaddressIndices(subaddressIndex) - .addDestination(sendAddress, sendAmount) - .setSubtractFeeFrom(0) // pay mining fee from send amount - .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); - if (!BigInteger.valueOf(0).equals(feeAmount)) txConfig.addDestination(feeAddress, feeAmount); - MoneroTxWallet tradeTx = createTx(txConfig); + // create tx + MoneroTxConfig txConfig = new MoneroTxConfig() + .setAccountIndex(0) + .setSubaddressIndices(subaddressIndex) + .addDestination(sendAddress, sendAmount) + .setSubtractFeeFrom(0) // pay mining fee from send amount + .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); + if (!BigInteger.valueOf(0).equals(feeAmount)) txConfig.addDestination(feeAddress, feeAmount); + MoneroTxWallet tradeTx = createTx(txConfig); - // freeze inputs - List keyImages = new ArrayList(); - for (MoneroOutput input : tradeTx.getInputs()) keyImages.add(input.getKeyImage().getHex()); - freezeOutputs(keyImages); - return tradeTx; + // freeze inputs + List keyImages = new ArrayList(); + for (MoneroOutput input : tradeTx.getInputs()) keyImages.add(input.getKeyImage().getHex()); + freezeOutputs(keyImages); + return tradeTx; + } } public MoneroTx verifyReserveTx(String offerId, BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendTradeAmount, BigInteger securityDeposit, String returnAddress, String txHash, String txHex, String txKey, List keyImages) { diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index 4ee10d7846..16cef449d6 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -228,6 +228,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener new Popup().warning("Error initializing trade" + " " + trade.getShortId() + "\n\n" + trade.getInitError().getMessage()) .show(); + return; } // check trade period diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index 263e4d0ef0..3af019e6d0 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -405,6 +405,7 @@ public abstract class TradeStepView extends AnchorPane { } private void updateTimeLeft() { + if (!trade.isInitialized()) return; if (timeLeftTextField != null) { // TODO (woodser): extra TradeStepView created but not deactivated on trade.setState(), so deactivate when model's trade is null From 96eab3d42f68bbdd8430c6edb2c504b6db0d2c37 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:51:31 -0400 Subject: [PATCH 239/371] do not fix reserved outputs after shut down started --- core/src/main/java/haveno/core/trade/TradeManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index ea01a7c927..14ac565c26 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -496,6 +496,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } // freeze or thaw outputs + if (isShutDownStarted) return; xmrWalletService.fixReservedOutputs(); // reset any available funded address entries From 31782e5255055da5a8656936e02e5d35067c3bf8 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 11 Apr 2025 18:11:26 -0400 Subject: [PATCH 240/371] do not cancel open offer when funds spent and reserved for trade --- core/src/main/java/haveno/core/offer/OpenOfferManager.java | 4 ++-- .../java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index aec7700c92..64c6c86a15 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -960,8 +960,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private void cancelOpenOffersOnSpent(String keyImage) { synchronized (openOffers.getList()) { for (OpenOffer openOffer : openOffers.getList()) { - if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { - log.warn("Canceling open offer because reserved funds have been spent, offerId={}, state={}", openOffer.getId(), openOffer.getState()); + if (openOffer.getState() != OpenOffer.State.RESERVED && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { + log.warn("Canceling open offer because reserved funds have been spent unexpectedly, offerId={}, state={}", openOffer.getId(), openOffer.getState()); cancelOpenOffer(openOffer, null, null); } } diff --git a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java index 68d5f9da4f..91e413eed4 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java @@ -89,7 +89,6 @@ public class PlaceOfferProtocol { handleError("Offer was canceled: " + model.getOpenOffer().getOffer().getId()); // cancel is treated as error for callers to handle } - // TODO (woodser): switch to fluent public void handleSignOfferResponse(SignOfferResponse response, NodeAddress sender) { log.debug("handleSignOfferResponse() " + model.getOpenOffer().getOffer().getId()); model.setSignOfferResponse(response); From 9a5d2d58627e710038f47dce608687d7fe60f88a Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 12 Apr 2025 06:32:53 -0400 Subject: [PATCH 241/371] reset offer protocol after first result --- .../haveno/core/offer/OpenOfferManager.java | 29 ++++++++++++------- .../offer/placeoffer/PlaceOfferProtocol.java | 26 +++++++++++++---- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 64c6c86a15..37983347af 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -937,9 +937,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe log.info("Adding open offer {}", openOffer.getId()); synchronized (openOffers.getList()) { openOffers.add(openOffer); - } - if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) { - xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) { + xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); + } } } @@ -947,14 +947,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe log.info("Removing open offer {}", openOffer.getId()); synchronized (openOffers.getList()) { openOffers.remove(openOffer); + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) { + xmrConnectionService.getKeyImagePoller().removeKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); + } } - synchronized (placeOfferProtocols) { - PlaceOfferProtocol protocol = placeOfferProtocols.remove(openOffer.getId()); - if (protocol != null) protocol.cancelOffer(); - } - if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) { - xmrConnectionService.getKeyImagePoller().removeKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); - } + + // cancel place offer protocol + ThreadUtils.execute(() -> { + synchronized (processOffersLock) { + synchronized (placeOfferProtocols) { + PlaceOfferProtocol protocol = placeOfferProtocols.remove(openOffer.getId()); + if (protocol != null) protocol.cancelOffer(); + } + } + }, THREAD_ID); } private void cancelOpenOffersOnSpent(String keyImage) { @@ -1455,7 +1461,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private void signAndPostOffer(OpenOffer openOffer, boolean useSavingsWallet, // TODO: remove this? - TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + TransactionResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { log.info("Signing and posting offer " + openOffer.getId()); // create model diff --git a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java index 91e413eed4..f5def0a433 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java @@ -31,6 +31,8 @@ import haveno.core.offer.placeoffer.tasks.ValidateOffer; import haveno.core.trade.handlers.TransactionResultHandler; import haveno.core.trade.protocol.TradeProtocol; import haveno.network.p2p.NodeAddress; + +import org.bitcoinj.core.Transaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,8 +41,8 @@ public class PlaceOfferProtocol { private final PlaceOfferModel model; private Timer timeoutTimer; - private final TransactionResultHandler resultHandler; - private final ErrorMessageHandler errorMessageHandler; + private TransactionResultHandler resultHandler; + private ErrorMessageHandler errorMessageHandler; private TaskRunner taskRunner; @@ -118,7 +120,7 @@ public class PlaceOfferProtocol { () -> { log.debug("sequence at handleSignOfferResponse completed"); stopTimeoutTimer(); - resultHandler.handleResult(model.getTransaction()); // TODO (woodser): XMR transaction instead + handleResult(model.getTransaction()); // TODO: use XMR transaction instead }, (errorMessage) -> { if (model.isOfferAddedToOfferBook()) { @@ -140,21 +142,27 @@ public class PlaceOfferProtocol { taskRunner.run(); } - public void startTimeoutTimer() { + public synchronized void startTimeoutTimer() { + if (resultHandler == null) return; stopTimeoutTimer(); timeoutTimer = UserThread.runAfter(() -> { handleError(Res.get("createOffer.timeoutAtPublishing")); }, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); } - private void stopTimeoutTimer() { + private synchronized void stopTimeoutTimer() { if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } } - private void handleError(String errorMessage) { + private synchronized void handleResult(Transaction transaction) { + resultHandler.handleResult(transaction); + resetHandlers(); + } + + private synchronized void handleError(String errorMessage) { if (timeoutTimer != null) { taskRunner.cancel(); if (!model.getOpenOffer().isCanceled()) { @@ -163,5 +171,11 @@ public class PlaceOfferProtocol { stopTimeoutTimer(); errorMessageHandler.handleErrorMessage(errorMessage); } + resetHandlers(); + } + + private synchronized void resetHandlers() { + resultHandler = null; + errorMessageHandler = null; } } From 71b23e0ed9ba3776a8879aa4207747771895c8a4 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 12 Apr 2025 06:47:22 -0400 Subject: [PATCH 242/371] remove unused code from core trades service --- .../haveno/core/api/CoreTradesService.java | 39 +------------------ 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java index 9854167c71..5e53ff64e4 100644 --- a/core/src/main/java/haveno/core/api/CoreTradesService.java +++ b/core/src/main/java/haveno/core/api/CoreTradesService.java @@ -54,9 +54,6 @@ import haveno.core.trade.protocol.BuyerProtocol; import haveno.core.trade.protocol.SellerProtocol; import haveno.core.user.User; import haveno.core.util.coin.CoinUtil; -import haveno.core.util.validation.BtcAddressValidator; -import haveno.core.xmr.model.AddressEntry; -import static haveno.core.xmr.model.AddressEntry.Context.TRADE_PAYOUT; import haveno.core.xmr.wallet.BtcWalletService; import static java.lang.String.format; import java.math.BigInteger; @@ -67,7 +64,6 @@ import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.bitcoinj.core.Coin; @Singleton @Slf4j @@ -83,7 +79,6 @@ class CoreTradesService { private final TakeOfferModel takeOfferModel; private final TradeManager tradeManager; private final TraderChatManager traderChatManager; - private final TradeUtil tradeUtil; private final OfferUtil offerUtil; private final User user; @@ -105,7 +100,6 @@ class CoreTradesService { this.takeOfferModel = takeOfferModel; this.tradeManager = tradeManager; this.traderChatManager = traderChatManager; - this.tradeUtil = tradeUtil; this.offerUtil = offerUtil; this.user = user; } @@ -205,7 +199,7 @@ class CoreTradesService { String getTradeRole(String tradeId) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); - return tradeUtil.getRole(getTrade(tradeId)); + return TradeUtil.getRole(getTrade(tradeId)); } Trade getTrade(String tradeId) { @@ -265,40 +259,9 @@ class CoreTradesService { return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol; } - private Coin getEstimatedTxFee(String fromAddress, String toAddress, Coin amount) { - // TODO This and identical logic should be refactored into TradeUtil. - try { - return btcWalletService.getFeeEstimationTransaction(fromAddress, - toAddress, - amount, - TRADE_PAYOUT).getFee(); - } catch (Exception ex) { - log.error("", ex); - throw new IllegalStateException(format("could not estimate tx fee: %s", ex.getMessage())); - } - } - // Throws a RuntimeException trade is already closed. private void verifyTradeIsNotClosed(String tradeId) { if (getClosedTrade(tradeId).isPresent()) throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId)); } - - // Throws a RuntimeException if address is not valid. - private void verifyIsValidBTCAddress(String address) { - try { - new BtcAddressValidator().validate(address); - } catch (Throwable t) { - log.error("", t); - throw new IllegalArgumentException(format("'%s' is not a valid btc address", address)); - } - } - - // Throws a RuntimeException if address has a zero balance. - private void verifyFundsNotWithdrawn(AddressEntry fromAddressEntry) { - Coin fromAddressBalance = btcWalletService.getBalanceForAddress(fromAddressEntry.getAddress()); - if (fromAddressBalance.isZero()) - throw new IllegalStateException(format("funds already withdrawn from address '%s'", - fromAddressEntry.getAddressString())); - } } From f5515caad5b595ff4e150eff45a878528de925d0 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 12 Apr 2025 10:20:25 -0400 Subject: [PATCH 243/371] improve responsiveness of extra info input by reinvoking main thread --- .../java/haveno/desktop/main/offer/MutableOfferViewModel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index fa6cf41729..fb971b6296 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -503,7 +503,7 @@ public abstract class MutableOfferViewModel ext extraInfoStringListener = (ov, oldValue, newValue) -> { if (newValue != null) { extraInfo.set(newValue); - onExtraInfoTextAreaChanged(); + UserThread.execute(() -> onExtraInfoTextAreaChanged()); } }; From 57c2408d079b5d2147f812d4030b239036b4912f Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 12 Apr 2025 10:36:49 -0400 Subject: [PATCH 244/371] fix npe sorting open offers by group id --- .../haveno/desktop/main/portfolio/openoffer/OpenOffersView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java index 87e044e703..3282ab078a 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -197,7 +197,7 @@ public class OpenOffersView extends ActivatableViewAndModel o.getOffer().getId())); - groupIdColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getReserveTxHash())); + groupIdColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getReserveTxHash() == null ? "" : o.getOpenOffer().getReserveTxHash())); directionColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDirection())); marketColumn.setComparator(Comparator.comparing(model::getMarketLabel)); amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getAmount())); From f1c09161f47df953f910bf460e68d5a9753257c1 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 13 Apr 2025 12:03:04 -0400 Subject: [PATCH 245/371] add lock symbol when confirming private offers --- .../resources/i18n/displayStrings.properties | 8 ++++--- .../i18n/displayStrings_cs.properties | 8 ++++--- .../i18n/displayStrings_de.properties | 8 ++++--- .../i18n/displayStrings_es.properties | 8 ++++--- .../i18n/displayStrings_fa.properties | 8 ++++--- .../i18n/displayStrings_fr.properties | 8 ++++--- .../i18n/displayStrings_it.properties | 8 ++++--- .../i18n/displayStrings_ja.properties | 8 ++++--- .../i18n/displayStrings_pt-br.properties | 8 ++++--- .../i18n/displayStrings_pt.properties | 8 ++++--- .../i18n/displayStrings_ru.properties | 8 ++++--- .../i18n/displayStrings_th.properties | 8 ++++--- .../i18n/displayStrings_tr.properties | 8 ++++--- .../i18n/displayStrings_vi.properties | 8 ++++--- .../i18n/displayStrings_zh-hans.properties | 8 ++++--- .../i18n/displayStrings_zh-hant.properties | 8 ++++--- .../overlays/windows/OfferDetailsWindow.java | 2 +- .../haveno/desktop/util/DisplayUtils.java | 21 ++++++++++--------- 18 files changed, 92 insertions(+), 59 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 888f850e86..4a978381ed 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -43,8 +43,8 @@ shared.buyMonero=Buy Monero shared.sellMonero=Sell Monero shared.buyCurrency=Buy {0} shared.sellCurrency=Sell {0} -shared.buyCurrencyLocked=Buy {0} 🔒 -shared.sellCurrencyLocked=Sell {0} 🔒 +shared.buyCurrency.locked=Buy {0} 🔒 +shared.sellCurrency.locked=Sell {0} 🔒 shared.buyingXMRWith=buying XMR with {0} shared.sellingXMRFor=selling XMR for {0} shared.buyingCurrency=buying {0} (selling XMR) @@ -2463,12 +2463,14 @@ navigation.support=\"Support\" formatter.formatVolumeLabel={0} amount{1} formatter.makerTaker=Maker as {0} {1} / Taker as {2} {3} -formatter.makerTakerLocked=Maker as {0} {1} / Taker as {2} {3} 🔒 +formatter.makerTaker.locked=Maker as {0} {1} / Taker as {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=You are {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=You are creating an offer to {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=You are creating an offer to {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=You are creating an offer to {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=You are creating an offer to {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} as maker formatter.asTaker={0} {1} as taker diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index a3b1974d8b..16b6a05bba 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -43,8 +43,8 @@ shared.buyMonero=Koupit monero shared.sellMonero=Prodat monero shared.buyCurrency=Koupit {0} shared.sellCurrency=Prodat {0} -shared.buyCurrencyLocked=Koupit {0} 🔒 -shared.sellCurrencyLocked=Prodat {0} 🔒 +shared.buyCurrency.locked=Koupit {0} 🔒 +shared.sellCurrency.locked=Prodat {0} 🔒 shared.buyingXMRWith=nakoupit XMR za {0} shared.sellingXMRFor=prodat XMR za {0} shared.buyingCurrency=nakoupit {0} (prodat XMR) @@ -2425,12 +2425,14 @@ navigation.support=\"Podpora\" formatter.formatVolumeLabel={0} částka{1} formatter.makerTaker=Tvůrce jako {0} {1} / Příjemce jako {2} {3} -formatter.makerTakerLocked=Tvůrce jako {0} {1} / Příjemce jako {2} {3} 🔒 +formatter.makerTaker.locked=Tvůrce jako {0} {1} / Příjemce jako {2} {3} 🔒 formatter.youAreAsMaker=Jste {1} {0} (jako tvůrce) / Příjemce je {3} {2} formatter.youAreAsTaker=Jste {1} {0} (jako příjemce) / Tvůrce je {3} {2} formatter.youAre={0}te {1} ({2}te {3}) formatter.youAreCreatingAnOffer.traditional=Vytváříte nabídku: {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Vytváříte nabídku: {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Vytváříte nabídku: {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Vytváříte nabídku: {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} jako tvůrce formatter.asTaker={0} {1} jako příjemce diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index ce1d2c2a2a..4663384a66 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -40,8 +40,8 @@ shared.buyMonero=Monero kaufen shared.sellMonero=Monero verkaufen shared.buyCurrency={0} kaufen shared.sellCurrency={0} verkaufen -shared.buyCurrencyLocked={0} kaufen 🔒 -shared.sellCurrencyLocked={0} verkaufen 🔒 +shared.buyCurrency.locked={0} kaufen 🔒 +shared.sellCurrency.locked={0} verkaufen 🔒 shared.buyingXMRWith=kaufe XMR mit {0} shared.sellingXMRFor=verkaufe XMR für {0} shared.buyingCurrency=kaufe {0} (verkaufe XMR) @@ -1831,12 +1831,14 @@ navigation.support=\"Support\" formatter.formatVolumeLabel={0} Betrag{1} formatter.makerTaker=Ersteller als {0} {1} / Abnehmer als {2} {3} -formatter.makerTakerLocked=Ersteller als {0} {1} / Abnehmer als {2} {3} 🔒 +formatter.makerTaker.locked=Ersteller als {0} {1} / Abnehmer als {2} {3} 🔒 formatter.youAreAsMaker=Sie sind: {1} {0} (Ersteller) / Abnehmer ist: {3} {2} formatter.youAreAsTaker=Sie sind: {1} {0} (Abnehmer) / Ersteller ist: {3} {2} formatter.youAre=Sie {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Sie erstellen ein Angebot um {1} zu {0} +formatter.youAreCreatingAnOffer.traditional.locked=Sie erstellen ein Angebot um {1} zu {0} 🔒 formatter.youAreCreatingAnOffer.crypto=Sie erstellen ein Angebot {1} zu {0} ({3} zu {2}) +formatter.youAreCreatingAnOffer.crypto.locked=Sie erstellen ein Angebot {1} zu {0} ({3} zu {2}) 🔒 formatter.asMaker={0} {1} als Ersteller formatter.asTaker={0} {1} als Abnehmer diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index a8fe7a8fa0..a8f105d6c0 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -40,8 +40,8 @@ shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} -shared.buyCurrencyLocked=Comprar {0} 🔒 -shared.sellCurrencyLocked=Vender {0} 🔒 +shared.buyCurrency.locked=Comprar {0} 🔒 +shared.sellCurrency.locked=Vender {0} 🔒 shared.buyingXMRWith=Comprando XMR con {0} shared.sellingXMRFor=Vendiendo XMR por {0} shared.buyingCurrency=comprando {0} (Vendiendo XMR) @@ -1832,12 +1832,14 @@ navigation.support=\"Soporte\" formatter.formatVolumeLabel={0} cantidad{1} formatter.makerTaker=Creador como {0} {1} / Tomador como {2} {3} -formatter.makerTakerLocked=Creador como {0} {1} / Tomador como {2} {3} 🔒 +formatter.makerTaker.locked=Creador como {0} {1} / Tomador como {2} {3} 🔒 formatter.youAreAsMaker=Usted es: {1} {0} (creador) / El tomador es: {3} {2} formatter.youAreAsTaker=Usted es: {1} {0} (tomador) / Creador es: {3} {2} formatter.youAre=Usted es {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Está creando una oferta a {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Está creando una oferta a {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Está creando una oferta a {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Está creando una oferta a {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} como creador formatter.asTaker={0} {1} como tomador diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 53bd2363ce..dd0cbee6d3 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -40,8 +40,8 @@ shared.buyMonero=خرید بیتکوین shared.sellMonero=بیتکوین بفروشید shared.buyCurrency=خرید {0} shared.sellCurrency=فروش {0} -shared.buyCurrencyLocked=بخر {0} 🔒 -shared.sellCurrencyLocked=فروش {0} 🔒 +shared.buyCurrency.locked=بخر {0} 🔒 +shared.sellCurrency.locked=فروش {0} 🔒 shared.buyingXMRWith=خرید بیتکوین با {0} shared.sellingXMRFor=فروش بیتکوین با {0} shared.buyingCurrency=خرید {0} ( فروش بیتکوین) @@ -1825,12 +1825,14 @@ navigation.support=\"پشتیبانی\" formatter.formatVolumeLabel={0} مبلغ {1} formatter.makerTaker=سفارش گذار به عنوان {0} {1} / پذیرنده به عنوان {2} {3} -formatter.makerTakerLocked=سفارش گذار به عنوان {0} {1} / پذیرنده به عنوان {2} {3} 🔒 +formatter.makerTaker.locked=سفارش گذار به عنوان {0} {1} / پذیرنده به عنوان {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=شما {0} {1} ({2} {3}) هستید formatter.youAreCreatingAnOffer.traditional=شما در حال ایجاد یک پیشنهاد به {0} {1} هستید +formatter.youAreCreatingAnOffer.traditional.locked=🔒 شما در حال ایجاد یک پیشنهاد به {0} {1} هستید formatter.youAreCreatingAnOffer.crypto=شما در حال ایجاد یک پیشنهاد به {0} {1} ({2} {3}) هستید +formatter.youAreCreatingAnOffer.crypto.locked=🔒 شما در حال ایجاد یک پیشنهاد به {0} {1} ({2} {3}) هستید formatter.asMaker={0} {1} به عنوان سفارش گذار formatter.asTaker={0} {1} به عنوان پذیرنده diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 900e829baa..f4395ca9c4 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -40,8 +40,8 @@ shared.buyMonero=Achat Monero shared.sellMonero=Vendre des Moneros shared.buyCurrency=Achat {0} shared.sellCurrency=Vendre {0} -shared.buyCurrencyLocked=Achat {0} 🔒 -shared.sellCurrencyLocked=Vendre {0} 🔒 +shared.buyCurrency.locked=Achat {0} 🔒 +shared.sellCurrency.locked=Vendre {0} 🔒 shared.buyingXMRWith=achat XMR avec {0} shared.sellingXMRFor=vendre XMR pour {0} shared.buyingCurrency=achat {0} (vente XMR) @@ -1833,12 +1833,14 @@ navigation.support=\"Assistance\" formatter.formatVolumeLabel={0} montant{1} formatter.makerTaker=Maker comme {0} {1} / Taker comme {2} {3} -formatter.makerTakerLocked=Maker comme {0} {1} / Taker comme {2} {3} 🔒 +formatter.makerTaker.locked=Maker comme {0} {1} / Taker comme {2} {3} 🔒 formatter.youAreAsMaker=Vous êtes {1} {0} (maker) / Le preneur est: {3} {2} formatter.youAreAsTaker=Vous êtes: {1} {0} (preneur) / Le maker est: {3} {2} formatter.youAre=Vous êtes {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Vous êtes en train de créer un ordre pour {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Vous êtes en train de créer un ordre pour {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Vous êtes en train de créer un ordre pour {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Vous êtes en train de créer un ordre pour {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} en tant que maker formatter.asTaker={0} {1} en tant que taker diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index cbc56c5f5d..4a5d177c0a 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -40,8 +40,8 @@ shared.buyMonero=Acquista monero shared.sellMonero=Vendi monero shared.buyCurrency=Acquista {0} shared.sellCurrency=Vendi {0} -shared.buyCurrencyLocked=Acquista {0} 🔒 -shared.sellCurrencyLocked=Vendi {0} 🔒 +shared.buyCurrency.locked=Acquista {0} 🔒 +shared.sellCurrency.locked=Vendi {0} 🔒 shared.buyingXMRWith=acquistando XMR con {0} shared.sellingXMRFor=vendendo XMR per {0} shared.buyingCurrency=comprando {0} (vendendo XMR) @@ -1828,12 +1828,14 @@ navigation.support=\"Supporto\" formatter.formatVolumeLabel={0} importo{1} formatter.makerTaker=Maker come {0} {1} / Taker come {2} {3} -formatter.makerTakerLocked=Maker come {0} {1} / Taker come {2} {3} 🔒 +formatter.makerTaker.locked=Maker come {0} {1} / Taker come {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Sei {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Stai creando un'offerta per {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Stai creando un'offerta per {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Stai creando un'offerta per {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Stai creando un'offerta per {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} come maker formatter.asTaker={0} {1} come taker diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index c1d2281373..3514d37343 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -40,8 +40,8 @@ shared.buyMonero=ビットコインを買う shared.sellMonero=ビットコインを売る shared.buyCurrency={0}を買う shared.sellCurrency={0}を売る -shared.buyCurrencyLocked={0}を買う 🔒 -shared.sellCurrencyLocked={0}を売る 🔒 +shared.buyCurrency.locked={0}を買う 🔒 +shared.sellCurrency.locked={0}を売る 🔒 shared.buyingXMRWith=XMRを{0}で買う shared.sellingXMRFor=XMRを{0}で売る shared.buyingCurrency={0}を購入中 (XMRを売却中) @@ -1831,12 +1831,14 @@ navigation.support=「サポート」 formatter.formatVolumeLabel={0} 額{1} formatter.makerTaker=メイカーは{0} {1} / テイカーは{2} {3} -formatter.makerTakerLocked=メイカーは{0} {1} / テイカーは{2} {3} 🔒 +formatter.makerTaker.locked=メイカーは{0} {1} / テイカーは{2} {3} 🔒 formatter.youAreAsMaker=あなたは:{1} {0}(メイカー) / テイカーは:{3} {2} formatter.youAreAsTaker=あなたは:{1} {0}(テイカー) / メイカーは{3} {2} formatter.youAre=あなたは{0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=あなたはオファーを{0} {1}に作成中です +formatter.youAreCreatingAnOffer.traditional.locked=あなたはオファーを{0} {1}に作成中です 🔒 formatter.youAreCreatingAnOffer.crypto=あなたはオファーを{0} {1} ({2} {3})に作成中です +formatter.youAreCreatingAnOffer.crypto.locked=あなたはオファーを{0} {1} ({2} {3})に作成中です 🔒 formatter.asMaker={0} {1}のメイカー formatter.asTaker={0} {1}のテイカー diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index d7463aacb4..03d90cc9fd 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -40,8 +40,8 @@ shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} -shared.buyCurrencyLocked=Comprar {0} 🔒 -shared.sellCurrencyLocked=Vender {0} 🔒 +shared.buyCurrency.locked=Comprar {0} 🔒 +shared.sellCurrency.locked=Vender {0} 🔒 shared.buyingXMRWith=comprando XMR com {0} shared.sellingXMRFor=vendendo XMR por {0} shared.buyingCurrency=comprando {0} (vendendo XMR) @@ -1835,12 +1835,14 @@ navigation.support=\"Suporte\" formatter.formatVolumeLabel={0} quantia{1} formatter.makerTaker=Ofertante: {1} de {0} / Aceitador: {3} de {2} -formatter.makerTakerLocked=Ofertante: {1} de {0} / Aceitador: {3} de {2} 🔒 +formatter.makerTaker.locked=Ofertante: {1} de {0} / Aceitador: {3} de {2} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Você está {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Você está criando uma oferta para {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Você está criando uma oferta para {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Você está criando uma oferta para {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Você está criando uma oferta para {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} como ofertante formatter.asTaker={0} {1} como aceitador diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index b6456daf42..0ec7c93184 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -40,8 +40,8 @@ shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} -shared.buyCurrencyLocked=Comprar {0} 🔒 -shared.sellCurrencyLocked=Vender {0} 🔒 +shared.buyCurrency.locked=Comprar {0} 🔒 +shared.sellCurrency.locked=Vender {0} 🔒 shared.buyingXMRWith=comprando XMR com {0} shared.sellingXMRFor=vendendo XMR por {0} shared.buyingCurrency=comprando {0} (vendendo XMR) @@ -1825,12 +1825,14 @@ navigation.support=\"Apoio\" formatter.formatVolumeLabel={0} quantia{1} formatter.makerTaker=Ofertante como {0} {1} / Aceitador como {2} {3} -formatter.makerTakerLocked=Ofertante como {0} {1} / Aceitador como {2} {3} 🔒 +formatter.makerTaker.locked=Ofertante como {0} {1} / Aceitador como {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Você é {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Você está criando uma oferta para {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Você está criando uma oferta para {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Você está criando uma oferta para {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Você está criando uma oferta para {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} como ofertante formatter.asTaker={0} {1} como aceitador diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 3c66af61fe..46828f97b1 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -40,8 +40,8 @@ shared.buyMonero=Купить биткойн shared.sellMonero=Продать биткойн shared.buyCurrency=Купить {0} shared.sellCurrency=Продать {0} -shared.buyCurrencyLocked=Купить {0} 🔒 -shared.sellCurrencyLocked=Продать {0} 🔒 +shared.buyCurrency.locked=Купить {0} 🔒 +shared.sellCurrency.locked=Продать {0} 🔒 shared.buyingXMRWith=покупка ВТС за {0} shared.sellingXMRFor=продажа ВТС за {0} shared.buyingCurrency=покупка {0} (продажа ВТС) @@ -1826,12 +1826,14 @@ navigation.support=\«Поддержка\» formatter.formatVolumeLabel={0} сумма {1} formatter.makerTaker=Мейкер как {0} {1} / Тейкер как {2} {3} -formatter.makerTakerLocked=Мейкер как {0} {1} / Тейкер как {2} {3} 🔒 +formatter.makerTaker.locked=Мейкер как {0} {1} / Тейкер как {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Вы {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Вы создаете предложение {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Вы создаете предложение {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Вы создаете предложение {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Вы создаете предложение {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} как мейкер formatter.asTaker={0} {1} как тейкер diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 8b675222fb..56fe9e67c9 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -40,8 +40,8 @@ shared.buyMonero=ซื้อ monero (บิตคอยน์) shared.sellMonero=ขาย monero (บิตคอยน์) shared.buyCurrency=ซื้อ {0} shared.sellCurrency=ขาย {0} -shared.buyCurrencyLocked=ซื้อ {0} 🔒 -shared.sellCurrencyLocked=ขาย {0} 🔒 +shared.buyCurrency.locked=ซื้อ {0} 🔒 +shared.sellCurrency.locked=ขาย {0} 🔒 shared.buyingXMRWith=การซื้อ XMR กับ {0} shared.sellingXMRFor=การขาย XMR แก่ {0} shared.buyingCurrency=การซื้อ {0} (การขาย XMR) @@ -1826,12 +1826,14 @@ navigation.support=\"ช่วยเหลือและสนับสนุ formatter.formatVolumeLabel={0} จำนวนยอด{1} formatter.makerTaker=ผู้สร้าง เป็น {0} {1} / ผู้รับเป็น {2} {3} -formatter.makerTakerLocked=ผู้สร้าง เป็น {0} {1} / ผู้รับเป็น {2} {3} 🔒 +formatter.makerTaker.locked=ผู้สร้าง เป็น {0} {1} / ผู้รับเป็น {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=คุณคือ {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=คุณกำลังสร้างข้อเสนอให้ {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=คุณกำลังสร้างข้อเสนอให้ {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=คุณกำลังสร้างข้อเสนอให้กับ {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=คุณกำลังสร้างข้อเสนอให้กับ {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} ในฐานะผู้สร้าง formatter.asTaker={0} {1} ในฐานะคนรับ diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 9ffb43d66a..28034c72a3 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -43,8 +43,8 @@ shared.buyMonero=Monero Satın Al shared.sellMonero=Monero Sat shared.buyCurrency={0} satın al shared.sellCurrency={0} sat -shared.buyCurrencyLocked={0} satın al 🔒 -shared.sellCurrencyLocked={0} sat 🔒 +shared.buyCurrency.locked={0} satın al 🔒 +shared.sellCurrency.locked={0} sat 🔒 shared.buyingXMRWith={0} ile XMR satın alınıyor shared.sellingXMRFor={0} karşılığında XMR satılıyor shared.buyingCurrency={0} satın alınıyor (XMR satılıyor) @@ -2413,12 +2413,14 @@ navigation.support="Destek" formatter.formatVolumeLabel={0} miktar{1} formatter.makerTaker=Yapan olarak {0} {1} / Alan olarak {2} {3} -formatter.makerTakerLocked=Yapıcı olarak {0} {1} / Alan olarak {2} {3} 🔒 +formatter.makerTaker.locked=Yapıcı olarak {0} {1} / Alan olarak {2} {3} 🔒 formatter.youAreAsMaker=Yapan sizsiniz: {1} {0} (maker) / Alan: {3} {2} formatter.youAreAsTaker=Alan sizsiniz: {1} {0} (taker) / Yapan: {3} {2} formatter.youAre=Şu anda sizsiniz {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Şu anda bir teklif oluşturuyorsunuz: {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Şu anda bir teklif oluşturuyorsunuz: {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Şu anda bir teklif oluşturuyorsunuz: {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Şu anda bir teklif oluşturuyorsunuz: {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} olarak formatter.asTaker={0} {1} olarak diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index aeab1530a7..e6dd802dee 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -40,8 +40,8 @@ shared.buyMonero=Mua monero shared.sellMonero=Bán monero shared.buyCurrency=Mua {0} shared.sellCurrency=Bán {0} -shared.buyCurrencyLocked=Mua {0} 🔒 -shared.sellCurrencyLocked=Bán {0} 🔒 +shared.buyCurrency.locked=Mua {0} 🔒 +shared.sellCurrency.locked=Bán {0} 🔒 shared.buyingXMRWith=đang mua XMR với {0} shared.sellingXMRFor=đang bán XMR với {0} shared.buyingCurrency=đang mua {0} (đang bán XMR) @@ -1828,12 +1828,14 @@ navigation.support=\"Hỗ trợ\" formatter.formatVolumeLabel={0} giá trị {1} formatter.makerTaker=Người tạo là {0} {1} / Người nhận là {2} {3} -formatter.makerTakerLocked=Người tạo là {0} {1} / Người nhận là {2} {3} 🔒 +formatter.makerTaker.locked=Người tạo là {0} {1} / Người nhận là {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Bạn là {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Bạn đang tạo một chào giá đến {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Bạn đang tạo một chào giá đến {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Bạn đang tạo một chào giá đến {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Bạn đang tạo một chào giá đến {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} như người tạo formatter.asTaker={0} {1} như người nhận diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index a783f387e6..f1b086eec3 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -40,8 +40,8 @@ shared.buyMonero=买入比特币 shared.sellMonero=卖出比特币 shared.buyCurrency=买入 {0} shared.sellCurrency=卖出 {0} -shared.buyCurrencyLocked=买入 {0} 🔒 -shared.sellCurrencyLocked=卖出 {0} 🔒 +shared.buyCurrency.locked=买入 {0} 🔒 +shared.sellCurrency.locked=卖出 {0} 🔒 shared.buyingXMRWith=用 {0} 买入 XMR shared.sellingXMRFor=卖出 XMR 为 {0} shared.buyingCurrency=买入 {0}(卖出 XMR) @@ -1835,12 +1835,14 @@ navigation.support=“帮助” formatter.formatVolumeLabel={0} 数量 {1} formatter.makerTaker=卖家 {0} {1} / 买家 {2} {3} -formatter.makerTakerLocked=卖家 {0} {1} / 买家 {2} {3} 🔒 +formatter.makerTaker.locked=卖家 {0} {1} / 买家 {2} {3} 🔒 formatter.youAreAsMaker=您是 {1} {0} 卖家 / 买家是 {3} {2} formatter.youAreAsTaker=您是 {1} {0} 买家 / 卖家是 {3} {2} formatter.youAre=您是 {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=您创建新的报价 {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=您创建新的报价 {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=您正创建报价 {0} {1}({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=您正创建报价 {0} {1}({2} {3}) 🔒 formatter.asMaker={0} {1} 是卖家 formatter.asTaker={0} {1} 是买家 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index e3344d1fea..94554bc12f 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -40,8 +40,8 @@ shared.buyMonero=買入比特幣 shared.sellMonero=賣出比特幣 shared.buyCurrency=買入 {0} shared.sellCurrency=賣出 {0} -shared.buyCurrencyLocked=買入 {0} 🔒 -shared.sellCurrencyLocked=賣出 {0} 🔒 +shared.buyCurrency.locked=買入 {0} 🔒 +shared.sellCurrency.locked=賣出 {0} 🔒 shared.buyingXMRWith=用 {0} 買入 XMR shared.sellingXMRFor=賣出 XMR 為 {0} shared.buyingCurrency=買入 {0}(賣出 XMR) @@ -1829,12 +1829,14 @@ navigation.support=“幫助” formatter.formatVolumeLabel={0} 數量 {1} formatter.makerTaker=賣家 {0} {1} / 買家 {2} {3} -formatter.makerTakerLocked=賣家 {0} {1} / 買家 {2} {3} 🔒 +formatter.makerTaker.locked=賣家 {0} {1} / 買家 {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=您是 {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=您創建新的報價 {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=您創建新的報價 {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=您正創建報價 {0} {1}({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=您正創建報價 {0} {1}({2} {3}) 🔒 formatter.asMaker={0} {1} 是賣家 formatter.asTaker={0} {1} 是買家 diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index 2139bf2813..dd645246f7 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -211,7 +211,7 @@ public class OfferDetailsWindow extends Overlay { xmrDirectionInfo = direction == OfferDirection.SELL ? toReceive : toSpend; } else if (placeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, - DisplayUtils.getOfferDirectionForCreateOffer(direction, currencyCode), firstRowDistance); + DisplayUtils.getOfferDirectionForCreateOffer(direction, currencyCode, offer.isPrivateOffer()), firstRowDistance); counterCurrencyDirectionInfo = direction == OfferDirection.SELL ? toReceive : toSpend; xmrDirectionInfo = direction == OfferDirection.BUY ? toReceive : toSpend; } else { diff --git a/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java b/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java index 9c59eeeeb5..8c725188c1 100644 --- a/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java +++ b/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java @@ -34,6 +34,7 @@ import java.util.Optional; @Slf4j public class DisplayUtils { private static final int SCALE = 3; + private static final String LOCKED = ".locked"; public static String formatDateTime(Date date) { return FormattingUtils.formatDateTime(date, true); @@ -118,16 +119,16 @@ public class DisplayUtils { public static String getDirectionWithCode(OfferDirection direction, String currencyCode, boolean isPrivate) { if (CurrencyUtil.isTraditionalCurrency(currencyCode)) - return (direction == OfferDirection.BUY) ? Res.get(isPrivate ? "shared.buyCurrencyLocked" : "shared.buyCurrency", Res.getBaseCurrencyCode()) : Res.get(isPrivate ? "shared.sellCurrencyLocked" : "shared.sellCurrency", Res.getBaseCurrencyCode()); + return (direction == OfferDirection.BUY) ? Res.get("shared.buyCurrency" + (isPrivate ? LOCKED : ""), Res.getBaseCurrencyCode()) : Res.get("shared.sellCurrency" + (isPrivate ? LOCKED : ""), Res.getBaseCurrencyCode()); else - return (direction == OfferDirection.SELL) ? Res.get(isPrivate ? "shared.buyCurrencyLocked" : "shared.buyCurrency", currencyCode) : Res.get(isPrivate ? "shared.sellCurrencyLocked" : "shared.sellCurrency", currencyCode); + return (direction == OfferDirection.SELL) ? Res.get("shared.buyCurrency" + (isPrivate ? LOCKED : ""), currencyCode) : Res.get("shared.sellCurrency" + (isPrivate ? LOCKED : ""), currencyCode); } - public static String getDirectionBothSides(OfferDirection direction, boolean isLocked) { + public static String getDirectionBothSides(OfferDirection direction, boolean isPrivate) { String currencyCode = Res.getBaseCurrencyCode(); return direction == OfferDirection.BUY ? - Res.get(isLocked ? "formatter.makerTakerLocked" : "formatter.makerTaker", currencyCode, Res.get("shared.buyer"), currencyCode, Res.get("shared.seller")) : - Res.get(isLocked ? "formatter.makerTakerLocked" : "formatter.makerTaker", currencyCode, Res.get("shared.seller"), currencyCode, Res.get("shared.buyer")); + Res.get("formatter.makerTaker" + (isPrivate ? LOCKED : ""), currencyCode, Res.get("shared.buyer"), currencyCode, Res.get("shared.seller")) : + Res.get("formatter.makerTaker" + (isPrivate ? LOCKED : ""), currencyCode, Res.get("shared.seller"), currencyCode, Res.get("shared.buyer")); } public static String getDirectionForBuyer(boolean isMyOffer, String currencyCode) { @@ -170,16 +171,16 @@ public class DisplayUtils { } } - public static String getOfferDirectionForCreateOffer(OfferDirection direction, String currencyCode) { + public static String getOfferDirectionForCreateOffer(OfferDirection direction, String currencyCode, boolean isPrivate) { String baseCurrencyCode = Res.getBaseCurrencyCode(); if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { return direction == OfferDirection.BUY ? - Res.get("formatter.youAreCreatingAnOffer.traditional", Res.get("shared.buy"), baseCurrencyCode) : - Res.get("formatter.youAreCreatingAnOffer.traditional", Res.get("shared.sell"), baseCurrencyCode); + Res.get("formatter.youAreCreatingAnOffer.traditional" + (isPrivate ? LOCKED : ""), Res.get("shared.buy"), baseCurrencyCode) : + Res.get("formatter.youAreCreatingAnOffer.traditional" + (isPrivate ? LOCKED : ""), Res.get("shared.sell"), baseCurrencyCode); } else { return direction == OfferDirection.SELL ? - Res.get("formatter.youAreCreatingAnOffer.crypto", Res.get("shared.buy"), currencyCode, Res.get("shared.selling"), baseCurrencyCode) : - Res.get("formatter.youAreCreatingAnOffer.crypto", Res.get("shared.sell"), currencyCode, Res.get("shared.buying"), baseCurrencyCode); + Res.get("formatter.youAreCreatingAnOffer.crypto" + (isPrivate ? LOCKED : ""), Res.get("shared.buy"), currencyCode, Res.get("shared.selling"), baseCurrencyCode) : + Res.get("formatter.youAreCreatingAnOffer.crypto" + (isPrivate ? LOCKED : ""), Res.get("shared.sell"), currencyCode, Res.get("shared.buying"), baseCurrencyCode); } } From a30b41de4ba7237232f5d6a7dc64506c1ca815b7 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 13 Apr 2025 15:17:13 -0400 Subject: [PATCH 246/371] fix deadlock by setting offer error message property on calling thread --- core/src/main/java/haveno/core/offer/Offer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index fac72f827d..8df8511b3a 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -18,7 +18,6 @@ package haveno.core.offer; import haveno.common.ThreadUtils; -import haveno.common.UserThread; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.ErrorMessageHandler; @@ -281,7 +280,7 @@ public class Offer implements NetworkPayload, PersistablePayload { } public void setErrorMessage(String errorMessage) { - UserThread.await(() -> errorMessageProperty.set(errorMessage)); + errorMessageProperty.set(errorMessage); } From 52f0c20c8cecaf74f136377783956b4aae21b58c Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 13 Apr 2025 19:49:43 -0400 Subject: [PATCH 247/371] do not switch xmr node preference with fixed connection --- core/src/main/java/haveno/core/api/XmrConnectionService.java | 2 +- core/src/main/java/haveno/core/user/Preferences.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index ba3b58d3e0..dc38547df6 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -885,7 +885,7 @@ public final class XmrConnectionService { } private boolean isFixedConnection() { - return !"".equals(config.xmrNode) && !fallbackApplied; + return !"".equals(config.xmrNode) && (!HavenoUtils.isLocalHost(config.xmrNode) || !xmrLocalNode.shouldBeIgnored()) && !fallbackApplied; } private boolean isCustomConnections() { diff --git a/core/src/main/java/haveno/core/user/Preferences.java b/core/src/main/java/haveno/core/user/Preferences.java index a3756041bf..02b1d82aaf 100644 --- a/core/src/main/java/haveno/core/user/Preferences.java +++ b/core/src/main/java/haveno/core/user/Preferences.java @@ -35,6 +35,7 @@ import haveno.core.locale.TradeCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountUtil; +import haveno.core.trade.HavenoUtils; import haveno.core.xmr.XmrNodeSettings; import haveno.core.xmr.nodes.XmrNodes; import haveno.core.xmr.nodes.XmrNodes.MoneroNodesOption; @@ -289,7 +290,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid setUseTorForXmr(config.useTorForXmr); // switch to public nodes if no provided nodes available - if (getMoneroNodesOptionOrdinal() == XmrNodes.MoneroNodesOption.PROVIDED.ordinal() && xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(this)).isEmpty()) { + boolean isFixedConnection = !"".equals(config.xmrNode) && (!HavenoUtils.isLocalHost(config.xmrNode) || !config.ignoreLocalXmrNode); + if (!isFixedConnection && getMoneroNodesOptionOrdinal() == XmrNodes.MoneroNodesOption.PROVIDED.ordinal() && xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(this)).isEmpty()) { log.warn("No provided nodes available, switching to public nodes"); setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); } From 53b0f203dee5ee1f74c0729e3efcc27da09727cd Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 14 Apr 2025 08:26:28 -0400 Subject: [PATCH 248/371] init main wallet with connection applied in same thread --- core/src/main/java/haveno/core/offer/OpenOfferManager.java | 1 - core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java | 2 +- .../main/java/haveno/core/xmr/wallet/XmrWalletService.java | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 37983347af..e5bf40b241 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1952,7 +1952,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe /////////////////////////////////////////////////////////////////////////////////////////// private void maybeUpdatePersistedOffers() { - // We need to clone to avoid ConcurrentModificationException List openOffersClone = getOpenOffers(); openOffersClone.forEach(originalOpenOffer -> { Offer originalOffer = originalOpenOffer.getOffer(); diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java index 6ad5a7180e..9646f5e3f4 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java @@ -57,7 +57,7 @@ public abstract class XmrWalletBase { // private private boolean testReconnectOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked - private String testReconnectMonerod1 = "http://node.community.rino.io:18081"; + private String testReconnectMonerod1 = "http://xmr-node.cakewallet.com:18081"; private String testReconnectMonerod2 = "http://nodex.monerujo.io:18081"; public XmrWalletBase() { diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 499d20c0a7..8b319622b9 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1467,8 +1467,8 @@ public class XmrWalletService extends XmrWalletBase { log.info("Monero wallet unlocked balance={}, pending balance={}, total balance={}", unlockedBalance, balance.subtract(unlockedBalance), balance); } - // reapply connection after wallet synced (might reinitialize wallet on new thread) - ThreadUtils.execute(() -> onConnectionChanged(xmrConnectionService.getConnection()), THREAD_ID); + // reapply connection after wallet synced (might reinitialize wallet with proxy) + onConnectionChanged(xmrConnectionService.getConnection()); // reset internal state if main wallet was swapped resetIfWalletChanged(); From 13dc34a8051b7518586081a0849347f114966dc3 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 14 Apr 2025 09:27:58 -0400 Subject: [PATCH 249/371] schedule to import multisig hex after 5 confirmations --- core/src/main/java/haveno/core/trade/Trade.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 5a6f502d0c..9d4112ff3f 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -143,7 +143,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS; - private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 10; + private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 5; protected final Object pollLock = new Object(); protected static final Object importMultisigLock = new Object(); private boolean pollInProgress; From bf055556f16216d941dfb0f3caccafde44337c51 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 14 Apr 2025 20:45:15 -0400 Subject: [PATCH 250/371] fix offers being deleted after minimum version update --- core/src/main/java/haveno/core/offer/OpenOfferManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index e5bf40b241..1dec0aff56 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -2023,7 +2023,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe originalOfferPayload.getAcceptedCountryCodes(), originalOfferPayload.getBankId(), originalOfferPayload.getAcceptedBankIds(), - originalOfferPayload.getVersionNr(), + Version.VERSION, originalOfferPayload.getBlockHeightAtOfferCreation(), originalOfferPayload.getMaxTradeLimit(), originalOfferPayload.getMaxTradePeriod(), From c87b8a5b45c0ff513c07cd0562dbf66fc61eb58b Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 14 Apr 2025 21:14:46 -0400 Subject: [PATCH 251/371] add note to keep arbitrator key in the repo to preserve signed accounts --- docs/deployment-guide.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index 67b5236d33..e53de210ed 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -194,6 +194,9 @@ The arbitrator is now registered and ready to accept requests for dispute resolu 1. Start the arbitrator's desktop application using the application launcher or e.g. `make arbitrator-desktop-mainnet` from the root of the repository. 2. Go to the `Account` tab and click the button to unregister the arbitrator. +> **Note** +> To preserve signed accounts, the arbitrator public key must remain in the repository, even after revoking. + ## Set a network filter on mainnet On mainnet, the p2p network is expected to have a filter object for offers, onions, currencies, payment methods, etc. From 60ceff66955f0be9a9659477f380dce6bfb6a61f Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:57:19 -0400 Subject: [PATCH 252/371] fix redundant key image notifications --- .../haveno/core/xmr/wallet/XmrKeyImagePoller.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java index 1cde84152c..3bebb71b7f 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java @@ -144,7 +144,6 @@ public class XmrKeyImagePoller { if (!keyImageGroups.containsKey(groupId)) keyImageGroups.put(groupId, new HashSet()); Set keyImagesGroup = keyImageGroups.get(groupId); keyImagesGroup.addAll(keyImages); - refreshPolling(); } } @@ -159,8 +158,13 @@ public class XmrKeyImagePoller { if (keyImagesGroup == null) return; keyImagesGroup.removeAll(keyImages); if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId); + Set allKeyImages = getKeyImages(); synchronized (lastStatuses) { - for (String lastKeyImage : new HashSet<>(lastStatuses.keySet())) lastStatuses.remove(lastKeyImage); + for (String keyImage : keyImages) { + if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) { + lastStatuses.remove(keyImage); + } + } } refreshPolling(); } @@ -171,10 +175,10 @@ public class XmrKeyImagePoller { Set keyImagesGroup = keyImageGroups.get(groupId); if (keyImagesGroup == null) return; keyImageGroups.remove(groupId); - Set keyImages = getKeyImages(); + Set allKeyImages = getKeyImages(); synchronized (lastStatuses) { for (String keyImage : keyImagesGroup) { - if (lastStatuses.containsKey(keyImage) && !keyImages.contains(keyImage)) { + if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) { lastStatuses.remove(keyImage); } } @@ -265,6 +269,7 @@ public class XmrKeyImagePoller { // announce changes if (!changedStatuses.isEmpty()) { + log.info("Announcing " + changedStatuses.size() + " key image spent status changes"); for (XmrKeyImageListener listener : new ArrayList(listeners)) { listener.onSpentStatusChanged(changedStatuses); } From 22db354cb2254925ec5799d9578fd5ed2d238671 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 16 Apr 2025 10:06:03 -0400 Subject: [PATCH 253/371] do not re-filter offers for every offer book change --- .../haveno/core/offer/OfferBookService.java | 28 +++++++++---------- .../offer/offerbook/OfferBookViewModel.java | 8 ++++-- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OfferBookService.java b/core/src/main/java/haveno/core/offer/OfferBookService.java index 16faa81e57..50981a8fa6 100644 --- a/core/src/main/java/haveno/core/offer/OfferBookService.java +++ b/core/src/main/java/haveno/core/offer/OfferBookService.java @@ -149,6 +149,20 @@ public class OfferBookService { Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); announceOfferRemoved(offer); + + // check if invalid offers are now valid + synchronized (invalidOffers) { + for (Offer invalidOffer : new ArrayList(invalidOffers)) { + try { + validateOfferPayload(invalidOffer.getOfferPayload()); + removeInvalidOffer(invalidOffer.getId()); + replaceValidOffer(invalidOffer); + announceOfferAdded(invalidOffer); + } catch (Exception e) { + // ignore + } + } + } } }); }, OfferBookService.class.getSimpleName()); @@ -298,20 +312,6 @@ public class OfferBookService { synchronized (offerBookChangedListeners) { offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer)); } - - // check if invalid offers are now valid - synchronized (invalidOffers) { - for (Offer invalidOffer : new ArrayList(invalidOffers)) { - try { - validateOfferPayload(invalidOffer.getOfferPayload()); - removeInvalidOffer(invalidOffer.getId()); - replaceValidOffer(invalidOffer); - announceOfferAdded(invalidOffer); - } catch (Exception e) { - // ignore - } - } - } } private boolean hasValidOffer(String offerId) { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index 821b081454..9a52902f3e 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -174,9 +174,11 @@ abstract class OfferBookViewModel extends ActivatableViewModel { tradeCurrencyListChangeListener = c -> fillCurrencies(); // refresh filter on changes - offerBook.getOfferBookListItems().addListener((ListChangeListener) c -> { - filterOffers(); - }); + // TODO: This is removed because it's expensive to re-filter offers for every change (high cpu for many offers). + // This was used to ensure offer list is fully refreshed, but is unnecessary after refactoring OfferBookService to clone offers? + // offerBook.getOfferBookListItems().addListener((ListChangeListener) c -> { + // filterOffers(); + // }); filterItemsListener = c -> { final Optional highestAmountOffer = filteredItems.stream() From bbfc5d5fedaf9015105696b5237b56ccf60b3c16 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 16 Apr 2025 11:11:31 -0400 Subject: [PATCH 254/371] remove max version verification so arbitrators can be behind --- .../src/main/java/haveno/core/offer/OpenOfferManager.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 1dec0aff56..c857ce533e 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1574,14 +1574,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - // verify the max version number - if (Version.compare(request.getOfferPayload().getVersionNr(), Version.VERSION) > 0) { - errorMessage = "Offer version number is too high: " + request.getOfferPayload().getVersionNr() + " > " + Version.VERSION; - log.warn(errorMessage); - sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); - return; - } - // verify maker and taker fees boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0; if (hasBuyerAsTakerWithoutDeposit) { From 8eccbcce4315c64dfaa0f7aa986fa84d7a9cc699 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:37:04 -0400 Subject: [PATCH 255/371] skip offer signature validation for cloned offer until signed --- .../haveno/core/offer/OpenOfferManager.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index c857ce533e..a928494c1d 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1101,17 +1101,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } else { // validate non-pending state - try { - validateSignedState(openOffer); - resultHandler.handleResult(null); // done processing if non-pending state is valid - return; - } catch (Exception e) { - log.warn(e.getMessage()); + boolean skipValidation = openOffer.isDeactivated() && hasConflictingClone(openOffer) && openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null; // clone with conflicting offer is deactivated and unsigned at first + if (!skipValidation) { + try { + validateSignedState(openOffer); + resultHandler.handleResult(null); // done processing if non-pending state is valid + return; + } catch (Exception e) { + log.warn(e.getMessage()); - // reset arbitrator signature - openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); - openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); - if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING); + // reset arbitrator signature + openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); + openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); + if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING); + } } } From 58590d60df8c9b6e96596efffee6a6c2e5e0c332 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:58:28 -0400 Subject: [PATCH 256/371] add arbitrator2 mainnet config to Makefile --- Makefile | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Makefile b/Makefile index 8f32c3ed41..7bca61db60 100644 --- a/Makefile +++ b/Makefile @@ -485,6 +485,31 @@ arbitrator-desktop-mainnet: --xmrNode=http://127.0.0.1:18081 \ --useNativeXmrWallet=false \ +arbitrator2-daemon-mainnet: + ./haveno-daemon$(APP_EXT) \ + --baseCurrencyNetwork=XMR_MAINNET \ + --useLocalhostForP2P=false \ + --useDevPrivilegeKeys=false \ + --nodePort=9999 \ + --appName=haveno-XMR_MAINNET_arbitrator2 \ + --apiPassword=apitest \ + --apiPort=1205 \ + --passwordRequired=false \ + --xmrNode=http://127.0.0.1:18081 \ + --useNativeXmrWallet=false \ + +arbitrator2-desktop-mainnet: + ./haveno-desktop$(APP_EXT) \ + --baseCurrencyNetwork=XMR_MAINNET \ + --useLocalhostForP2P=false \ + --useDevPrivilegeKeys=false \ + --nodePort=9999 \ + --appName=haveno-XMR_MAINNET_arbitrator2 \ + --apiPassword=apitest \ + --apiPort=1205 \ + --xmrNode=http://127.0.0.1:18081 \ + --useNativeXmrWallet=false \ + haveno-daemon-mainnet: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ From 821ef16d8ff67df7569147d1cefe85da1b0ce583 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 17 Apr 2025 16:15:26 -0400 Subject: [PATCH 257/371] refresh polling when key images added --- .../src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java index 3bebb71b7f..73332dc4fa 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java @@ -144,6 +144,7 @@ public class XmrKeyImagePoller { if (!keyImageGroups.containsKey(groupId)) keyImageGroups.put(groupId, new HashSet()); Set keyImagesGroup = keyImageGroups.get(groupId); keyImagesGroup.addAll(keyImages); + refreshPolling(); } } @@ -269,7 +270,6 @@ public class XmrKeyImagePoller { // announce changes if (!changedStatuses.isEmpty()) { - log.info("Announcing " + changedStatuses.size() + " key image spent status changes"); for (XmrKeyImageListener listener : new ArrayList(listeners)) { listener.onSpentStatusChanged(changedStatuses); } From 8f778be4d9e26d4abc614ce719354ddd5522ec91 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 17 Apr 2025 16:40:25 -0400 Subject: [PATCH 258/371] show scrollbar as needed when creating offer --- .../desktop/main/offer/MutableOfferView.java | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 31c02bdc0d..e28ecff72b 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -380,8 +380,6 @@ public abstract class MutableOfferView> exten } private void onShowPayFundsScreen() { - scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); - nextButton.setVisible(false); nextButton.setManaged(false); nextButton.setOnAction(null); @@ -445,13 +443,7 @@ public abstract class MutableOfferView> exten // temporarily disabled due to high CPU usage (per issue #4649) // waitingForFundsSpinner.play(); - payFundsTitledGroupBg.setVisible(true); - totalToPayTextField.setVisible(true); - addressTextField.setVisible(true); - qrCodeImageView.setVisible(true); - balanceTextField.setVisible(true); - cancelButton2.setVisible(true); - reserveExactAmountSlider.setVisible(true); + showFundingGroup(); } private void updateOfferElementsStyle() { @@ -986,6 +978,7 @@ public abstract class MutableOfferView> exten gridPane.setVgap(5); GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane); scrollPane.setContent(gridPane); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); } private void addPaymentGroup() { @@ -1179,6 +1172,40 @@ public abstract class MutableOfferView> exten cancelButton1.setManaged(false); } + protected void hideFundingGroup() { + payFundsTitledGroupBg.setVisible(false); + payFundsTitledGroupBg.setManaged(false); + totalToPayTextField.setVisible(false); + totalToPayTextField.setManaged(false); + addressTextField.setVisible(false); + addressTextField.setManaged(false); + qrCodeImageView.setVisible(false); + qrCodeImageView.setManaged(false); + balanceTextField.setVisible(false); + balanceTextField.setManaged(false); + cancelButton2.setVisible(false); + cancelButton2.setManaged(false); + reserveExactAmountSlider.setVisible(false); + reserveExactAmountSlider.setManaged(false); + } + + protected void showFundingGroup() { + payFundsTitledGroupBg.setVisible(true); + payFundsTitledGroupBg.setManaged(true); + totalToPayTextField.setVisible(true); + totalToPayTextField.setManaged(true); + addressTextField.setVisible(true); + addressTextField.setManaged(true); + qrCodeImageView.setVisible(true); + qrCodeImageView.setManaged(true); + balanceTextField.setVisible(true); + balanceTextField.setManaged(true); + cancelButton2.setVisible(true); + cancelButton2.setManaged(true); + reserveExactAmountSlider.setVisible(true); + reserveExactAmountSlider.setManaged(true); + } + private VBox getSecurityDepositBox() { Tuple3 tuple = getEditableValueBoxWithInfo( Res.get("createOffer.securityDeposit.prompt")); @@ -1326,6 +1353,8 @@ public abstract class MutableOfferView> exten }); cancelButton2.setDefaultButton(false); cancelButton2.setVisible(false); + + hideFundingGroup(); } private void openWallet() { From 695f2b8dd35bee2f8220c6fce2d39094ce482351 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 17 Apr 2025 19:58:12 -0400 Subject: [PATCH 259/371] fix startup error with localhost, support fallback from provided nodes --- .../haveno/core/api/XmrConnectionService.java | 98 +++++++++++-------- .../java/haveno/core/api/XmrLocalNode.java | 11 ++- .../haveno/core/app/HavenoHeadlessApp.java | 2 +- .../java/haveno/core/app/HavenoSetup.java | 14 +-- .../java/haveno/core/xmr/nodes/XmrNodes.java | 13 ++- .../resources/i18n/displayStrings.properties | 3 +- .../haveno/desktop/main/MainViewModel.java | 40 ++++++-- 7 files changed, 115 insertions(+), 66 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index dc38547df6..53eba276a0 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -75,9 +75,10 @@ public final class XmrConnectionService { private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes - public enum XmrConnectionError { + public enum XmrConnectionFallbackType { LOCAL, - CUSTOM + CUSTOM, + PROVIDED } private final Object lock = new Object(); @@ -97,7 +98,7 @@ public final class XmrConnectionService { private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter - private final ObjectProperty connectionServiceError = new SimpleObjectProperty<>(); + private final ObjectProperty connectionServiceFallbackType = new SimpleObjectProperty<>(); @Getter private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final LongProperty numUpdates = new SimpleLongProperty(0); @@ -129,6 +130,7 @@ public final class XmrConnectionService { private Set excludedConnections = new HashSet<>(); private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s private boolean fallbackApplied; + private boolean usedSyncingLocalNodeBeforeStartup; @Inject public XmrConnectionService(P2PService p2PService, @@ -156,7 +158,13 @@ public final class XmrConnectionService { p2PService.addP2PServiceListener(new P2PServiceListener() { @Override public void onTorNodeReady() { - ThreadUtils.submitToPool(() -> initialize()); + ThreadUtils.submitToPool(() -> { + try { + initialize(); + } catch (Exception e) { + log.warn("Error initializing connection service, error={}\n", e.getMessage(), e); + } + }); } @Override public void onHiddenServicePublished() {} @@ -270,7 +278,7 @@ public final class XmrConnectionService { accountService.checkAccountOpen(); // user needs to authorize fallback on startup after using locally synced node - if (lastInfo == null && !fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) { + if (fallbackRequiredBeforeConnectionSwitch()) { log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback"); return null; } @@ -283,6 +291,10 @@ public final class XmrConnectionService { return bestConnection; } + private boolean fallbackRequiredBeforeConnectionSwitch() { + return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored()); + } + private void addLocalNodeIfIgnored(Collection ignoredConnections) { if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri())); } @@ -458,15 +470,20 @@ public final class XmrConnectionService { public void fallbackToBestConnection() { if (isShutDownStarted) return; - if (xmrNodes.getProvidedXmrNodes().isEmpty()) { + fallbackApplied = true; + if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) { log.warn("Falling back to public nodes"); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); + initializeConnections(); } else { log.warn("Falling back to provided nodes"); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); + initializeConnections(); + if (getConnection() == null) { + log.warn("No provided nodes available, falling back to public nodes"); + fallbackToBestConnection(); + } } - fallbackApplied = true; - initializeConnections(); } // ------------------------------- HELPERS -------------------------------- @@ -578,8 +595,8 @@ public final class XmrConnectionService { setConnection(connection.getUri()); // reset error connecting to local node - if (connectionServiceError.get() == XmrConnectionError.LOCAL && isConnectionLocalHost()) { - connectionServiceError.set(null); + if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) { + connectionServiceFallbackType.set(null); } } else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) { MoneroRpcConnection bestConnection = getBestConnection(); @@ -602,8 +619,10 @@ public final class XmrConnectionService { // add default connections for (XmrNode node : xmrNodes.getAllXmrNodes()) { if (node.hasClearNetAddress()) { - MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority()); - if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); + if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) { + MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority()); + if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); + } } if (node.hasOnionAddress()) { MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); @@ -615,8 +634,10 @@ public final class XmrConnectionService { // add default connections for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { if (node.hasClearNetAddress()) { - MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority()); - addConnection(connection); + if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) { + MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority()); + addConnection(connection); + } } if (node.hasOnionAddress()) { MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); @@ -632,6 +653,11 @@ public final class XmrConnectionService { } } + // set if last node was locally syncing + if (!isInitialized) { + usedSyncingLocalNodeBeforeStartup = connectionList.getCurrentConnectionUri().isPresent() && xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get()) && preferences.getXmrNodeSettings().getSyncBlockchain(); + } + // set connection proxies log.info("TOR proxy URI: " + getProxyUri()); for (MoneroRpcConnection connection : connectionManager.getConnections()) { @@ -666,29 +692,16 @@ public final class XmrConnectionService { onConnectionChanged(connectionManager.getConnection()); } - private boolean lastUsedLocalSyncingNode() { - return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored(); - } - - public void startLocalNode() { + public void startLocalNode() throws Exception { // cannot start local node as seed node if (HavenoUtils.isSeedNode()) { throw new RuntimeException("Cannot start local node on seed node"); } - // start local node if offline and used as last connection - if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) { - try { - log.info("Starting local node"); - xmrLocalNode.start(); - } catch (Exception e) { - log.error("Unable to start local monero node, error={}\n", e.getMessage(), e); - throw new RuntimeException(e); - } - } else { - throw new RuntimeException("Local node is not offline and used as last connection"); - } + // start local node + log.info("Starting local node"); + xmrLocalNode.start(); } private void onConnectionChanged(MoneroRpcConnection currentConnection) { @@ -768,7 +781,7 @@ public final class XmrConnectionService { try { // poll daemon - if (daemon == null) switchToBestConnection(); + if (daemon == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection(); try { if (daemon == null) throw new RuntimeException("No connection to Monero daemon"); lastInfo = daemon.getInfo(); @@ -778,16 +791,19 @@ public final class XmrConnectionService { if (isShutDownStarted) return; // invoke fallback handling on startup error - boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode(); + boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup; if (lastInfo == null && canFallback) { - if (connectionServiceError.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { + if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { lastFallbackInvocation = System.currentTimeMillis(); - if (lastUsedLocalSyncingNode()) { + if (usedSyncingLocalNodeBeforeStartup) { log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage()); - connectionServiceError.set(XmrConnectionError.LOCAL); + connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL); + } else if (isProvidedConnections()) { + log.warn("Failed to fetch daemon info from provided connections on startup: " + e.getMessage()); + connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED); } else { log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); - connectionServiceError.set(XmrConnectionError.CUSTOM); + connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM); } } return; @@ -808,7 +824,7 @@ public final class XmrConnectionService { // connected to daemon isConnected = true; - connectionServiceError.set(null); + connectionServiceFallbackType.set(null); // determine if blockchain is syncing locally boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0 @@ -885,10 +901,14 @@ public final class XmrConnectionService { } private boolean isFixedConnection() { - return !"".equals(config.xmrNode) && (!HavenoUtils.isLocalHost(config.xmrNode) || !xmrLocalNode.shouldBeIgnored()) && !fallbackApplied; + return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied; } private boolean isCustomConnections() { return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; } + + private boolean isProvidedConnections() { + return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED; + } } diff --git a/core/src/main/java/haveno/core/api/XmrLocalNode.java b/core/src/main/java/haveno/core/api/XmrLocalNode.java index 7295202c64..0928340d25 100644 --- a/core/src/main/java/haveno/core/api/XmrLocalNode.java +++ b/core/src/main/java/haveno/core/api/XmrLocalNode.java @@ -109,17 +109,18 @@ public class XmrLocalNode { public boolean shouldBeIgnored() { if (config.ignoreLocalXmrNode) return true; - // determine if local node is configured + // ignore if fixed connection is not local + if (!"".equals(config.xmrNode)) return !HavenoUtils.isLocalHost(config.xmrNode); + + // check if local node is within configuration boolean hasConfiguredLocalNode = false; for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { - if (node.getAddress() != null && equalsUri("http://" + node.getAddress() + ":" + node.getPort())) { + if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) { hasConfiguredLocalNode = true; break; } } - if (!hasConfiguredLocalNode) return true; - - return false; + return !hasConfiguredLocalNode; } public void addListener(XmrLocalNodeListener listener) { diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java index 84bdcc746a..0bdac1abc1 100644 --- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java @@ -75,7 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp { log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); acceptedHandler.run(); }); - havenoSetup.setDisplayMoneroConnectionErrorHandler(show -> log.warn("onDisplayMoneroConnectionErrorHandler: show={}", show)); + havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.warn("onDisplayMoneroConnectionFallbackHandler: show={}", show)); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index 19503fafd8..192e3870b7 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -55,7 +55,7 @@ import haveno.core.alert.PrivateNotificationManager; import haveno.core.alert.PrivateNotificationPayload; import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; -import haveno.core.api.XmrConnectionService.XmrConnectionError; +import haveno.core.api.XmrConnectionService.XmrConnectionFallbackType; import haveno.core.api.XmrLocalNode; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; @@ -159,7 +159,7 @@ public class HavenoSetup { rejectedTxErrorMessageHandler; @Setter @Nullable - private Consumer displayMoneroConnectionErrorHandler; + private Consumer displayMoneroConnectionFallbackHandler; @Setter @Nullable private Consumer displayTorNetworkSettingsHandler; @@ -431,9 +431,9 @@ public class HavenoSetup { getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); // listen for fallback handling - getConnectionServiceError().addListener((observable, oldValue, newValue) -> { - if (displayMoneroConnectionErrorHandler == null) return; - displayMoneroConnectionErrorHandler.accept(newValue); + getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> { + if (displayMoneroConnectionFallbackHandler == null) return; + displayMoneroConnectionFallbackHandler.accept(newValue); }); log.info("Init P2P network"); @@ -735,8 +735,8 @@ public class HavenoSetup { return xmrConnectionService.getConnectionServiceErrorMsg(); } - public ObjectProperty getConnectionServiceError() { - return xmrConnectionService.getConnectionServiceError(); + public ObjectProperty getConnectionServiceFallbackType() { + return xmrConnectionService.getConnectionServiceFallbackType(); } public StringProperty getTopErrorMsg() { diff --git a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java index a8fa1ade26..c38ae9b411 100644 --- a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java +++ b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java @@ -184,10 +184,6 @@ public class XmrNodes { this.operator = operator; } - public boolean hasOnionAddress() { - return onionAddress != null; - } - public String getHostNameOrAddress() { if (hostName != null) return hostName; @@ -195,10 +191,19 @@ public class XmrNodes { return address; } + public boolean hasOnionAddress() { + return onionAddress != null; + } + public boolean hasClearNetAddress() { return hostName != null || address != null; } + public String getClearNetUri() { + if (!hasClearNetAddress()) throw new IllegalStateException("XmrNode does not have clearnet address"); + return "http://" + getHostNameOrAddress() + ":" + port; + } + @Override public String toString() { return "onionAddress='" + onionAddress + '\'' + diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 4a978381ed..7c8ab0d6f6 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2090,7 +2090,8 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amoun walletPasswordWindow.headline=Enter password to unlock xmrConnectionError.headline=Monero connection error -xmrConnectionError.customNode=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? +xmrConnectionError.providedNodes=Error connecting to provided Monero node(s).\n\nDo you want to use the next best available Monero node? +xmrConnectionError.customNodes=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? xmrConnectionError.localNode=We previously synced using a local Monero node, but it appears to be unreachable.\n\nPlease check that it's running and synced. xmrConnectionError.localNode.start=Start local node xmrConnectionError.localNode.start.error=Error starting local node diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index 16cef449d6..03c38e99ab 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -337,7 +337,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener tacWindow.onAction(acceptedHandler::run).show(); }, 1)); - havenoSetup.setDisplayMoneroConnectionErrorHandler(connectionError -> { + havenoSetup.setDisplayMoneroConnectionFallbackHandler(connectionError -> { if (connectionError == null) { if (moneroConnectionErrorPopup != null) moneroConnectionErrorPopup.hide(); } else { @@ -349,7 +349,6 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener .actionButtonText(Res.get("xmrConnectionError.localNode.start")) .onAction(() -> { log.warn("User has chosen to start local node."); - havenoSetup.getConnectionServiceError().set(null); new Thread(() -> { try { HavenoUtils.xmrConnectionService.startLocalNode(); @@ -359,16 +358,20 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener .headLine(Res.get("xmrConnectionError.localNode.start.error")) .warning(e.getMessage()) .closeButtonText(Res.get("shared.close")) - .onClose(() -> havenoSetup.getConnectionServiceError().set(null)) + .onClose(() -> havenoSetup.getConnectionServiceFallbackType().set(null)) .show(); + } finally { + havenoSetup.getConnectionServiceFallbackType().set(null); } }).start(); }) .secondaryActionButtonText(Res.get("xmrConnectionError.localNode.fallback")) .onSecondaryAction(() -> { log.warn("User has chosen to fallback to the next best available Monero node."); - havenoSetup.getConnectionServiceError().set(null); - new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + new Thread(() -> { + HavenoUtils.xmrConnectionService.fallbackToBestConnection(); + havenoSetup.getConnectionServiceFallbackType().set(null); + }).start(); }) .closeButtonText(Res.get("shared.shutDown")) .onClose(HavenoApp.getShutDownHandler()); @@ -376,16 +379,35 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener case CUSTOM: moneroConnectionErrorPopup = new Popup() .headLine(Res.get("xmrConnectionError.headline")) - .warning(Res.get("xmrConnectionError.customNode")) + .warning(Res.get("xmrConnectionError.customNodes")) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { - havenoSetup.getConnectionServiceError().set(null); - new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + new Thread(() -> { + HavenoUtils.xmrConnectionService.fallbackToBestConnection(); + havenoSetup.getConnectionServiceFallbackType().set(null); + }).start(); }) .closeButtonText(Res.get("shared.no")) .onClose(() -> { log.warn("User has declined to fallback to the next best available Monero node."); - havenoSetup.getConnectionServiceError().set(null); + havenoSetup.getConnectionServiceFallbackType().set(null); + }); + break; + case PROVIDED: + moneroConnectionErrorPopup = new Popup() + .headLine(Res.get("xmrConnectionError.headline")) + .warning(Res.get("xmrConnectionError.providedNodes")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + new Thread(() -> { + HavenoUtils.xmrConnectionService.fallbackToBestConnection(); + havenoSetup.getConnectionServiceFallbackType().set(null); + }).start(); + }) + .closeButtonText(Res.get("shared.no")) + .onClose(() -> { + log.warn("User has declined to fallback to the next best available Monero node."); + havenoSetup.getConnectionServiceFallbackType().set(null); }); break; } From 39909e7936d1e2537d201b8824d6750caea8abb8 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 17 Apr 2025 20:55:01 -0400 Subject: [PATCH 260/371] bump version to 1.1.0 --- build.gradle | 2 +- .../src/main/java/haveno/common/app/Version.java | 2 +- .../main/resources/i18n/displayStrings.properties | 1 + .../linux/exchange.haveno.Haveno.metainfo.xml | 2 +- desktop/package/macosx/Info.plist | 4 ++-- .../settings/network/NetworkSettingsView.fxml | 7 +++++-- .../settings/network/NetworkSettingsView.java | 15 +++++++++++---- .../main/java/haveno/seednode/SeedNodeMain.java | 2 +- 8 files changed, 23 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index 20ca924031..aec0e7c89f 100644 --- a/build.gradle +++ b/build.gradle @@ -610,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.19-SNAPSHOT' + version = '1.1.0-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 3dc04889b8..c0dd9352ff 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.0.19"; + public static final String VERSION = "1.1.0"; /** * Holds a list of the tagged resource files for optimizing the getData requests. diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 7c8ab0d6f6..9d500aefe5 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -237,6 +237,7 @@ shared.pending=Pending shared.me=Me shared.maker=Maker shared.taker=Taker +shared.none=None #################################################################### diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index fc5f50c2b5..9227c587a6 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -60,6 +60,6 @@ - + diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index a24f430c12..fd0a765320 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.19 + 1.1.0 CFBundleShortVersionString - 1.0.19 + 1.1.0 CFBundleExecutable Haveno diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml index 1f3e8840d7..bbfb4c6e05 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml @@ -91,7 +91,7 @@
    - + @@ -159,7 +159,10 @@ - + + + diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java index 0773217cd1..a767d032bc 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java @@ -75,7 +75,7 @@ public class NetworkSettingsView extends ActivatableView { @FXML InputTextField xmrNodesInputTextField; @FXML - TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField; + TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField, minVersionForTrading; @FXML Label p2PPeersLabel, moneroConnectionsLabel; @FXML @@ -176,6 +176,7 @@ public class NetworkSettingsView extends ActivatableView { sentDataTextField.setPromptText(Res.get("settings.net.sentDataLabel")); receivedDataTextField.setPromptText(Res.get("settings.net.receivedDataLabel")); chainHeightTextField.setPromptText(Res.get("settings.net.chainHeightLabel")); + minVersionForTrading.setPromptText(Res.get("filterWindow.disableTradeBelowVersion")); roundTripTimeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.roundTripTimeColumn"))); sentBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.sentBytesColumn"))); receivedBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.receivedBytesColumn"))); @@ -275,7 +276,7 @@ public class NetworkSettingsView extends ActivatableView { showShutDownPopup(); } }; - filterPropertyListener = (observable, oldValue, newValue) -> applyPreventPublicXmrNetwork(); + filterPropertyListener = (observable, oldValue, newValue) -> applyFilter(); // disable radio buttons if no nodes available if (xmrNodes.getProvidedXmrNodes().isEmpty()) { @@ -298,7 +299,7 @@ public class NetworkSettingsView extends ActivatableView { moneroPeersToggleGroup.selectedToggleProperty().addListener(moneroPeersToggleGroupListener); if (filterManager.getFilter() != null) - applyPreventPublicXmrNetwork(); + applyFilter(); filterManager.filterProperty().addListener(filterPropertyListener); @@ -492,7 +493,9 @@ public class NetworkSettingsView extends ActivatableView { } - private void applyPreventPublicXmrNetwork() { + private void applyFilter() { + + // prevent public xmr network final boolean preventPublicXmrNetwork = isPreventPublicXmrNetwork(); usePublicNodesRadio.setDisable(isPublicNodesDisabled()); if (preventPublicXmrNetwork && selectedMoneroNodesOption == XmrNodes.MoneroNodesOption.PUBLIC) { @@ -501,6 +504,10 @@ public class NetworkSettingsView extends ActivatableView { selectMoneroPeersToggle(); onMoneroPeersToggleSelected(false); } + + // set min version for trading + String minVersion = filterManager.getDisableTradeBelowVersion(); + minVersionForTrading.textProperty().setValue(minVersion == null ? Res.get("shared.none") : minVersion); } private boolean isPublicNodesDisabled() { diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index 35d4bbbe17..1e9c4c5061 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.0.19"; + private static final String VERSION = "1.1.0"; private SeedNode seedNode; private Timer checkConnectionLossTime; From bfef0f9492a3764f819a4b6d096b5da68e7bf220 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:44:33 -0400 Subject: [PATCH 261/371] fix hanging while posting or canceling offer --- .../main/java/haveno/core/xmr/Balances.java | 17 +++---- .../java/haveno/desktop/main/MainView.java | 2 +- .../main/offer/MutableOfferViewModel.java | 45 +++++++++++-------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/Balances.java b/core/src/main/java/haveno/core/xmr/Balances.java index fe49b941fd..15f6ff8a74 100644 --- a/core/src/main/java/haveno/core/xmr/Balances.java +++ b/core/src/main/java/haveno/core/xmr/Balances.java @@ -37,7 +37,6 @@ package haveno.core.xmr; import com.google.inject.Inject; import haveno.common.ThreadUtils; -import haveno.common.UserThread; import haveno.core.api.model.XmrBalanceInfo; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; @@ -163,18 +162,12 @@ public class Balances { // calculate reserved balance reservedBalance = reservedOfferBalance.add(reservedTradeBalance); + // play sound if funds received + boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0; + if (fundsReceived) HavenoUtils.playCashRegisterSound(); + // notify balance update - UserThread.execute(() -> { - - // check if funds received - boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0; - if (fundsReceived) { - HavenoUtils.playCashRegisterSound(); - } - - // increase counter to notify listeners - updateCounter.set(updateCounter.get() + 1); - }); + updateCounter.set(updateCounter.get() + 1); } } } diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index f294eea7bf..eaec5d1154 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -353,7 +353,7 @@ public class MainView extends InitializableView { settingsButtonWithBadge.getStyleClass().add("new"); navigation.addListener((viewPath, data) -> { - UserThread.await(() -> { + UserThread.await(() -> { // TODO: this uses `await` to fix nagivation link from market view to offer book, but await can cause hanging, so execute should be used if (viewPath.size() != 2 || viewPath.indexOf(MainView.class) != 0) return; Class viewClass = viewPath.tip(); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index fb971b6296..48adb22f9f 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -19,6 +19,8 @@ package haveno.desktop.main.offer; import com.google.inject.Inject; import com.google.inject.name.Named; + +import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.handlers.ErrorMessageHandler; @@ -108,7 +110,7 @@ public abstract class MutableOfferViewModel ext private String amountDescription; private String addressAsString; private final String paymentLabel; - private boolean createOfferRequested; + private boolean createOfferInProgress; public boolean createOfferCanceled; public final StringProperty amount = new SimpleStringProperty(); @@ -638,32 +640,37 @@ public abstract class MutableOfferViewModel ext /////////////////////////////////////////////////////////////////////////////////////////// void onPlaceOffer(Offer offer, Runnable resultHandler) { - errorMessage.set(null); - createOfferRequested = true; - createOfferCanceled = false; - - dataModel.onPlaceOffer(offer, transaction -> { - resultHandler.run(); - if (!createOfferCanceled) placeOfferCompleted.set(true); + ThreadUtils.execute(() -> { errorMessage.set(null); - }, errMessage -> { - createOfferRequested = false; - if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo")); - else errorMessage.set(errMessage); + createOfferInProgress = true; + createOfferCanceled = false; + + dataModel.onPlaceOffer(offer, transaction -> { + createOfferInProgress = false; + resultHandler.run(); + if (!createOfferCanceled) placeOfferCompleted.set(true); + errorMessage.set(null); + }, errMessage -> { + createOfferInProgress = false; + if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo")); + else errorMessage.set(errorMessage.get()); + + UserThread.execute(() -> { + updateButtonDisableState(); + updateSpinnerInfo(); + resultHandler.run(); + }); + }); UserThread.execute(() -> { updateButtonDisableState(); updateSpinnerInfo(); - resultHandler.run(); }); - }); - - updateButtonDisableState(); - updateSpinnerInfo(); + }, getClass().getSimpleName()); } public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - createOfferRequested = false; + log.info("Canceling posting offer {}", offer.getId()); createOfferCanceled = true; OpenOfferManager openOfferManager = HavenoUtils.openOfferManager; Optional openOffer = openOfferManager.getOpenOffer(offer.getId()); @@ -1355,7 +1362,7 @@ public abstract class MutableOfferViewModel ext inputDataValid = inputDataValid && getExtraInfoValidationResult().isValid; isNextButtonDisabled.set(!inputDataValid); - isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || !dataModel.getIsXmrWalletFunded().get()); + isPlaceOfferButtonDisabled.set(createOfferInProgress || !inputDataValid || !dataModel.getIsXmrWalletFunded().get()); } private ValidationResult getExtraInfoValidationResult() { From 13e13d945d21d22630f86c7785b8dd48d773f62e Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:29:33 -0400 Subject: [PATCH 262/371] print pub key hex when missing for signature data --- .../java/haveno/core/account/sign/SignedWitnessService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java b/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java index b4ba7b58a8..f86a0c2bb2 100644 --- a/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java +++ b/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java @@ -335,12 +335,13 @@ public class SignedWitnessService { String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash()); String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8); ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey()); - if (arbitratorManager.isPublicKeyInList(Utilities.encodeToHex(key.getPubKey()))) { + String pubKeyHex = Utilities.encodeToHex(key.getPubKey()); + if (arbitratorManager.isPublicKeyInList(pubKeyHex)) { key.verifyMessage(message, signatureBase64); verifySignatureWithECKeyResultCache.put(hash, true); return true; } else { - log.warn("Provided EC key is not in list of valid arbitrators."); + log.warn("Provided EC key is not in list of valid arbitrators: " + pubKeyHex); verifySignatureWithECKeyResultCache.put(hash, false); return false; } From c7a3a9740ff7386ba2a6dfdefe1c2a02372d79e5 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 19 Apr 2025 16:54:01 -0400 Subject: [PATCH 263/371] fixes when cloned offers are taken at the same time --- .../main/java/haveno/core/trade/Trade.java | 9 ++++- .../core/trade/protocol/TradeProtocol.java | 16 +++++++-- .../ArbitratorProcessDepositRequest.java | 33 +++++++++++-------- .../tasks/ProcessDepositResponse.java | 9 ++--- .../haveno/core/xmr/wallet/XmrWalletBase.java | 1 - .../main/funds/deposit/DepositView.java | 3 +- 6 files changed, 47 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 9d4112ff3f..2dfa5b0f1f 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2648,7 +2648,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } setDepositTxs(txs); - if (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if either deposit tx not seen + if (!isPublished(getMaker().getDepositTx()) || (!hasBuyerAsTakerWithoutDeposit() && !isPublished(getTaker().getDepositTx()))) return; // skip if deposit txs not published successfully setStateDepositsSeen(); // set actual security deposits @@ -2750,6 +2750,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + private static boolean isPublished(MoneroTx tx) { + if (tx == null) return false; + if (Boolean.TRUE.equals(tx.isFailed())) return false; + if (!Boolean.TRUE.equals(tx.inTxPool()) && !Boolean.TRUE.equals(tx.isConfirmed())) return false; + return true; + } + private void syncWalletIfBehind() { synchronized (walletLock) { if (isWalletBehind()) { diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index a1ad25ddaf..d87d5f3d5d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -460,9 +460,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D .using(new TradeTaskRunner(trade, () -> { stopTimeout(); - this.errorMessageHandler = null; // TODO: set this when trade state is >= DEPOSIT_PUBLISHED - handleTaskRunnerSuccess(sender, response); - if (tradeResultHandler != null) tradeResultHandler.handleResult(trade); // trade is initialized + + // tasks may complete successfully but process an error + if (trade.getInitError() == null) { + this.errorMessageHandler = null; // TODO: set this when trade state is >= DEPOSIT_PUBLISHED + handleTaskRunnerSuccess(sender, response); + if (tradeResultHandler != null) tradeResultHandler.handleResult(trade); // trade is initialized + } else { + handleTaskRunnerSuccess(sender, response); + if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(trade.getInitError().getMessage()); + } + + this.tradeResultHandler = null; + this.errorMessageHandler = null; }, errorMessage -> { handleTaskRunnerFault(sender, response, errorMessage); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java index 3a7fc7ace9..c11ed57ee4 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java @@ -95,6 +95,18 @@ public class ArbitratorProcessDepositRequest extends TradeTask { // set peer's signature sender.setContractSignature(signature); + // subscribe to trade state once to send responses with ack or nack + if (!hasBothContractSignatures()) { + trade.stateProperty().addListener((obs, oldState, newState) -> { + if (oldState == newState) return; + if (newState == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED) { + sendDepositResponsesOnce(trade.getProcessModel().error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : trade.getProcessModel().error.getMessage()); + } else if (newState.ordinal() >= Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS.ordinal()) { + sendDepositResponsesOnce(null); + } + }); + } + // collect expected values Offer offer = trade.getOffer(); boolean isFromTaker = sender == trade.getTaker(); @@ -138,7 +150,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { // relay deposit txs when both requests received MoneroDaemon daemon = trade.getXmrWalletService().getDaemon(); - if (processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) { + if (hasBothContractSignatures()) { // check timeout and extend just before relaying if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out before relaying deposit txs for {} {}" + trade.getClass().getSimpleName() + " " + trade.getShortId()); @@ -182,22 +194,15 @@ public class ArbitratorProcessDepositRequest extends TradeTask { throw e; } } else { - - // subscribe to trade state once to send responses with ack or nack - trade.stateProperty().addListener((obs, oldState, newState) -> { - if (oldState == newState) return; - if (newState == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED) { - sendDepositResponsesOnce(trade.getProcessModel().error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : trade.getProcessModel().error.getMessage()); - } else if (newState.ordinal() >= Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS.ordinal()) { - sendDepositResponsesOnce(null); - } - }); - if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId()); if (processModel.getTaker().getDepositTxHex() == null && !trade.hasBuyerAsTakerWithoutDeposit()) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId()); } } + private boolean hasBothContractSignatures() { + return processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null; + } + private boolean isTimedOut() { return !processModel.getTradeManager().hasOpenTrade(trade); } @@ -210,7 +215,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { // log error if (errorMessage != null) { - log.warn("Sending deposit responses with error={}", errorMessage, new Throwable("Stack trace")); + log.warn("Sending deposit responses for tradeId={}, error={}", trade.getId(), errorMessage); } // create deposit response @@ -229,7 +234,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { } private void sendDepositResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, DepositResponse response) { - log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), trade.getProcessModel().error); + log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), response.getErrorMessage()); processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() { @Override public void onArrived() { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java index dcaf1a7e76..454763e15b 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java @@ -38,13 +38,14 @@ public class ProcessDepositResponse extends TradeTask { try { runInterceptHook(); - // throw if error + // handle error DepositResponse message = (DepositResponse) processModel.getTradeMessage(); if (message.getErrorMessage() != null) { - log.warn("Unregistering trade {} {} because deposit response has error message={}", trade.getClass().getSimpleName(), trade.getShortId(), message.getErrorMessage()); + log.warn("Deposit response for {} {} has error message={}", trade.getClass().getSimpleName(), trade.getShortId(), message.getErrorMessage()); trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); - processModel.getTradeManager().unregisterTrade(trade); - throw new RuntimeException(message.getErrorMessage()); + trade.setInitError(new RuntimeException(message.getErrorMessage())); + complete(); + return; } // record security deposits diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java index 9646f5e3f4..594f58b6fb 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java @@ -105,7 +105,6 @@ public abstract class XmrWalletBase { // start polling wallet for progress syncProgressLatch = new CountDownLatch(1); syncProgressLooper = new TaskLooper(() -> { - if (wallet == null) return; long height; try { height = wallet.getHeight(); // can get read timeout while syncing diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java index 884df454e7..e035a04f23 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java @@ -346,7 +346,8 @@ public class DepositView extends ActivatableView { List addressEntries = xmrWalletService.getAddressEntries(); List items = new ArrayList<>(); for (XmrAddressEntry addressEntry : addressEntries) { - if (addressEntry.isTradePayout()) continue; // do not show trade payout addresses + DepositListItem item = new DepositListItem(addressEntry, xmrWalletService, formatter); + if (addressEntry.isTradePayout() && BigInteger.ZERO.equals(item.getBalanceAsBI())) continue; // do not show empty trade payout addresses items.add(new DepositListItem(addressEntry, xmrWalletService, formatter)); } From cf9a37f29569dfb0b40db8ff9ad1f4d4e7deee0e Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 19 Apr 2025 22:28:32 -0400 Subject: [PATCH 264/371] improve error handling when clones taken at the same time --- .../main/java/haveno/core/trade/Trade.java | 47 ++++++++++++------- .../java/haveno/core/trade/TradeManager.java | 18 +++++-- .../core/trade/protocol/TradeProtocol.java | 2 +- .../main/funds/deposit/DepositView.java | 3 +- 4 files changed, 46 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 2dfa5b0f1f..f121470e3a 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -145,6 +145,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS; private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 5; protected final Object pollLock = new Object(); + private final Object removeTradeOnErrorLock = new Object(); protected static final Object importMultisigLock = new Object(); private boolean pollInProgress; private boolean restartInProgress; @@ -1608,11 +1609,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // shut down trade threads - isInitialized = false; isShutDown = true; List shutDownThreads = new ArrayList<>(); shutDownThreads.add(() -> ThreadUtils.shutDown(getId())); ThreadUtils.awaitTasks(shutDownThreads); + stopProtocolTimeout(); + isInitialized = false; // save and close if (wallet != null) { @@ -1765,24 +1767,30 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } private void removeTradeOnError() { - log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState()); + synchronized (removeTradeOnErrorLock) { - // force close and re-open wallet in case stuck - forceCloseWallet(); - if (isDepositRequested()) getWallet(); + // skip if already shut down or removed + if (isShutDown || !processModel.getTradeManager().hasTrade(getId())) return; + log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState()); - // shut down trade thread - try { - ThreadUtils.shutDown(getId(), 1000l); - } catch (Exception e) { - log.warn("Error shutting down trade thread for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); + // force close and re-open wallet in case stuck + forceCloseWallet(); + if (isDepositRequested()) getWallet(); + + // clear and shut down trade + onShutDownStarted(); + clearAndShutDown(); + + // shut down trade thread + try { + ThreadUtils.shutDown(getId(), 5000l); + } catch (Exception e) { + log.warn("Error shutting down trade thread for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); + } + + // unregister trade + processModel.getTradeManager().unregisterTrade(this); } - - // clear and shut down trade - clearAndShutDown(); - - // unregister trade - processModel.getTradeManager().unregisterTrade(this); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -1824,6 +1832,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); } + public void stopProtocolTimeout() { + if (!isInitialized) return; + TradeProtocol protocol = getProtocol(); + if (protocol == null) return; + protocol.stopTimeout(); + } + public void setState(State state) { if (isInitialized) { // We don't want to log at startup the setState calls from all persisted trades diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 14ac565c26..9b092decac 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -563,9 +563,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi Optional openOfferOptional = openOfferManager.getOpenOffer(request.getOfferId()); if (!openOfferOptional.isPresent()) return; OpenOffer openOffer = openOfferOptional.get(); - if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; Offer offer = openOffer.getOffer(); + // check availability + if (openOffer.getState() != OpenOffer.State.AVAILABLE) { + log.warn("Ignoring InitTradeRequest to maker because offer is not available, offerId={}, sender={}", request.getOfferId(), sender); + return; + } + // validate challenge if (openOffer.getChallenge() != null && !HavenoUtils.getChallengeHash(openOffer.getChallenge()).equals(HavenoUtils.getChallengeHash(request.getChallenge()))) { log.warn("Ignoring InitTradeRequest to maker because challenge is incorrect, tradeId={}, sender={}", request.getOfferId(), sender); @@ -980,9 +985,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi closedTradableManager.add(trade); trade.setCompleted(true); removeTrade(trade, true); - - // TODO The address entry should have been removed already. Check and if its the case remove that. - xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); + xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that. requestPersistence(); } @@ -990,6 +993,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi log.warn("Unregistering {} {}", trade.getClass().getSimpleName(), trade.getId()); removeTrade(trade, true); removeFailedTrade(trade); + xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that. requestPersistence(); } @@ -1274,11 +1278,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return offer.getDirection() == OfferDirection.SELL; } - // TODO (woodser): make Optional versus Trade return types consistent + // TODO: make Optional versus Trade return types consistent public Trade getTrade(String tradeId) { return getOpenTrade(tradeId).orElseGet(() -> getClosedTrade(tradeId).orElseGet(() -> getFailedTrade(tradeId).orElseGet(() -> null))); } + public boolean hasTrade(String tradeId) { + return getTrade(tradeId) != null; + } + public Optional getOpenTrade(String tradeId) { synchronized (tradableList.getList()) { return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst(); diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index d87d5f3d5d..65fcee23c6 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -842,7 +842,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } - protected synchronized void stopTimeout() { + public synchronized void stopTimeout() { synchronized (timeoutTimerLock) { if (timeoutTimer != null) { timeoutTimer.stop(); diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java index e035a04f23..884df454e7 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java @@ -346,8 +346,7 @@ public class DepositView extends ActivatableView { List addressEntries = xmrWalletService.getAddressEntries(); List items = new ArrayList<>(); for (XmrAddressEntry addressEntry : addressEntries) { - DepositListItem item = new DepositListItem(addressEntry, xmrWalletService, formatter); - if (addressEntry.isTradePayout() && BigInteger.ZERO.equals(item.getBalanceAsBI())) continue; // do not show empty trade payout addresses + if (addressEntry.isTradePayout()) continue; // do not show trade payout addresses items.add(new DepositListItem(addressEntry, xmrWalletService, formatter)); } From 38615edf866a410173457282700d8a3213528062 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 21 Apr 2025 09:02:17 -0400 Subject: [PATCH 265/371] re-arrange deployment sections and use markup for notes & warnings --- docs/deployment-guide.md | 63 ++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index e53de210ed..686c6cd615 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -5,10 +5,11 @@ This guide describes how to deploy a Haveno network: - Manage services on a VPS - Fork and build Haveno - Start a Monero node -- Build and start price nodes - Add seed nodes - Add arbitrators - Configure trade fees and other configuration +- Build and start price nodes +- Set a network filter - Build Haveno installers for distribution - Send alerts to update the application and other maintenance @@ -69,14 +70,6 @@ Optionally customize and deploy monero-stagenet.service and monero-stagenet.conf You can also start the Monero node in your current terminal session by running `make monerod` for mainnet or `make monerod-stagenet` for stagenet. -## Build and start price nodes - -The price node is separated from Haveno and is run as a standalone service. To deploy a pricenode on both TOR and clearnet, see the instructions on the repository: https://github.com/haveno-dex/haveno-pricenode. - -After the price node is built and deployed, add the price node to `DEFAULT_NODES` in [ProvidersRepository.java](https://github.com/haveno-dex/haveno/blob/3cdd88b56915c7f8afd4f1a39e6c1197c2665d63/core/src/main/java/haveno/core/provider/ProvidersRepository.java#L50). - -Customize and deploy haveno-pricenode.env and haveno-pricenode.service to run as a system service. - ## Add seed nodes ### Seed nodes without Proof of Work (PoW) @@ -139,7 +132,7 @@ Each seed node requires a locally running Monero node. You can use the default p Rebuild all seed nodes any time the list of registered seed nodes changes. -> **Notes** +> [!note] > * Avoid all seed nodes going offline at the same time. If all seed nodes go offline at the same time, the network will be reset, including registered arbitrators, the network filter object, and trade history. In that case, arbitrators need to restart or re-register, and the network filter object needs to be re-applied. This should be done immediately or clients will cancel their offers due to the signing arbitrators being unregistered and no replacements being available to re-sign. > * At least 2 seed nodes should be run because the seed nodes restart once per day. @@ -180,35 +173,21 @@ For each arbitrator: The arbitrator is now registered and ready to accept requests for dispute resolution. -**Notes** -- Arbitrators must use a local Monero node with unrestricted RPC in order to submit and flush transactions from the pool. -- Arbitrators should remain online as much as possible in order to balance trades and avoid clients spending time trying to contact offline arbitrators. A VPS or dedicated machine running 24/7 is highly recommended. -- Remember that for the network to run correctly and people to be able to open and accept trades, at least one arbitrator must be registered on the network. -- IMPORTANT: Do not reuse keypairs on multiple arbitrator instances. +> [!note] +> * Arbitrators must use a local Monero node with unrestricted RPC in order to submit and flush transactions from the pool. +> * Arbitrators should remain online as much as possible in order to balance trades and avoid clients spending time trying to contact offline arbitrators. A VPS or dedicated machine running 24/7 is highly recommended. +> * Remember that for the network to run correctly and people to be able to open and accept trades, at least one arbitrator must be registered on the network. +> * IMPORTANT: Do not reuse keypairs on multiple arbitrator instances. ## Remove an arbitrator -> **Note** -> Ensure the arbitrator's trades are completed before retiring the instance. +> [!warning] +> * Ensure the arbitrator's trades are completed before retiring the instance. +> * To preserve signed accounts, the arbitrator public key must remain in the repository, even after revoking. 1. Start the arbitrator's desktop application using the application launcher or e.g. `make arbitrator-desktop-mainnet` from the root of the repository. 2. Go to the `Account` tab and click the button to unregister the arbitrator. -> **Note** -> To preserve signed accounts, the arbitrator public key must remain in the repository, even after revoking. - -## Set a network filter on mainnet - -On mainnet, the p2p network is expected to have a filter object for offers, onions, currencies, payment methods, etc. - -To set the network's filter object: - -1. Enter `ctrl + f` in the arbitrator or other Haveno instance to open the Filter window. -2. Enter a developer private key from the previous steps and click "Add Filter" to register. - -> **Note** -> If all seed nodes are restarted at the same time, arbitrators and the filter object will become unregistered and will need to be re-registered. - ## Change the default folder name for Haveno application data To avoid user data corruption when using multiple Haveno networks, change the default folder name for Haveno's application data on your network: @@ -246,10 +225,30 @@ Set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `true` for the arbitrator to assig Otherwise set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `false` and set the XMR address in `getGlobalTradeFeeAddress()` to collect all trade fees to a single address (e.g. a multisig wallet shared among network administrators). +## Build and start price nodes + +The price node is separated from Haveno and is run as a standalone service. To deploy a pricenode on both TOR and clearnet, see the instructions on the repository: https://github.com/haveno-dex/haveno-pricenode. + +After the price node is built and deployed, add the price node to `DEFAULT_NODES` in [ProvidersRepository.java](https://github.com/haveno-dex/haveno/blob/3cdd88b56915c7f8afd4f1a39e6c1197c2665d63/core/src/main/java/haveno/core/provider/ProvidersRepository.java#L50). + +Customize and deploy haveno-pricenode.env and haveno-pricenode.service to run as a system service. + ## Update the download URL Change every instance of `https://haveno.exchange/downloads` to your download URL. For example, `https://havenoexample.com/downloads`. +## Set a network filter on mainnet + +On mainnet, the p2p network is expected to have a filter object for offers, onions, currencies, payment methods, etc. + +To set the network's filter object: + +1. Enter `ctrl + f` in the arbitrator or other Haveno instance to open the Filter window. +2. Enter a developer private key from the previous steps and click "Add Filter" to register. + +> [!note] +> If all seed nodes are restarted at the same time, arbitrators and the filter object will become unregistered and will need to be re-registered. + ## Start users for testing Start user1 on Monero's mainnet using `make user1-desktop-mainnet` or Monero's stagenet using `make user1-desktop-stagenet`. From 77429472f4d353d74ef166651549472af1d90d50 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 21 Apr 2025 09:26:49 -0400 Subject: [PATCH 266/371] fix error popup when arbitrator nacks signing offer --- .../java/haveno/desktop/main/offer/MutableOfferViewModel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index 48adb22f9f..821f9e8a5c 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -653,7 +653,7 @@ public abstract class MutableOfferViewModel ext }, errMessage -> { createOfferInProgress = false; if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo")); - else errorMessage.set(errorMessage.get()); + else errorMessage.set(errMessage); UserThread.execute(() -> { updateButtonDisableState(); From a3d3f51f02780f8722340700e9eeee48fb2357d1 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 21 Apr 2025 10:29:06 -0400 Subject: [PATCH 267/371] change popup from warning to error on take offer error --- .../java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index dc11216f53..58f6bf4158 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -649,7 +649,7 @@ public class TakeOfferView extends ActivatableViewAndModel { if (newValue != null) { - new Popup().warning(Res.get("takeOffer.error.message", model.errorMessage.get())) + new Popup().error(Res.get("takeOffer.error.message", model.errorMessage.get())) .onClose(() -> { errorPopupDisplayed.set(true); model.resetErrorMessage(); From 9a14d5552efca8d326648968f2a6596575a35f66 Mon Sep 17 00:00:00 2001 From: XMRZombie Date: Mon, 21 Apr 2025 15:11:29 +0000 Subject: [PATCH 268/371] Update tails script to expect installer instead of archive Update haveno-install.sh Archive extraction bypass, also renaming the filename to package_filename trough mv for keeping install.sh stable --- scripts/install_tails/haveno-install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/install_tails/haveno-install.sh b/scripts/install_tails/haveno-install.sh index e9a8c37bf4..16529f35e0 100755 --- a/scripts/install_tails/haveno-install.sh +++ b/scripts/install_tails/haveno-install.sh @@ -124,7 +124,7 @@ OUTPUT=$(gpg --digest-algo SHA256 --verify "${signature_filename}" "${binary_fil if ! echo "$OUTPUT" | grep -q "Good signature from"; then echo_red "Verification failed: $OUTPUT" exit 1; - else 7z x "${binary_filename}" && mv haveno*.deb "${package_filename}" + else mv -f "${binary_filename}" "${package_filename}" fi echo_blue "Haveno binaries have been successfully verified." @@ -136,7 +136,7 @@ mkdir -p "${install_dir}" # Delete old Haveno binaries #rm -f "${install_dir}/"*.deb* -mv "${binary_filename}" "${package_filename}" "${key_filename}" "${signature_filename}" "${install_dir}" +mv "${package_filename}" "${key_filename}" "${signature_filename}" "${install_dir}" echo_blue "Files moved to persistent directory ${install_dir}" From 923b3ad73bf921b57b2dd6e9029c2924f8160fe3 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:39:47 -0400 Subject: [PATCH 269/371] do not await updating trade state properties on trade thread --- core/src/main/java/haveno/core/trade/Trade.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index f121470e3a..e218bf970f 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1852,7 +1852,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { this.state = state; requestPersistence(); - UserThread.await(() -> { + UserThread.execute(() -> { stateProperty.set(state); phaseProperty.set(state.getPhase()); }); @@ -1884,7 +1884,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { this.payoutState = payoutState; requestPersistence(); - UserThread.await(() -> payoutStateProperty.set(payoutState)); + UserThread.execute(() -> payoutStateProperty.set(payoutState)); } public void setDisputeState(DisputeState disputeState) { From de5250e89a613d19cc3fd1a34c3168fcef3f619e Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:14:22 -0400 Subject: [PATCH 270/371] persist trade with payment confirmation msgs before processing --- .../main/java/haveno/core/trade/Trade.java | 4 + .../java/haveno/core/trade/TradeManager.java | 4 + .../core/trade/protocol/TradeProtocol.java | 210 +++++++++--------- 3 files changed, 114 insertions(+), 104 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index e218bf970f..769d7e95af 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -811,6 +811,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (processModel.getTradeManager() != null) processModel.getTradeManager().requestPersistence(); } + public void persistNow(@Nullable Runnable completeHandler) { + processModel.getTradeManager().persistNow(completeHandler); + } + public TradeProtocol getProtocol() { return processModel.getTradeManager().getTradeProtocol(this); } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 9b092decac..e34cc9b1a7 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -546,6 +546,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi persistenceManager.requestPersistence(); } + public void persistNow(@Nullable Runnable completeHandler) { + persistenceManager.persistNow(completeHandler); + } + private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender) { log.info("TradeManager handling InitTradeRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 65fcee23c6..09c26d8a1d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -537,62 +537,63 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // save message for reprocessing trade.getBuyer().setPaymentSentMessage(message); - trade.requestPersistence(); + trade.persistNow(() -> { - // process message on trade thread - if (!trade.isInitialized() || trade.isShutDownStarted()) return; - ThreadUtils.execute(() -> { - // We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case - // that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received - // a mailbox message with PaymentSentMessage. - // TODO A better fix would be to add a listener for the wallet sync state and process - // the mailbox msg once wallet is ready and trade state set. - synchronized (trade.getLock()) { - if (!trade.isInitialized() || trade.isShutDownStarted()) return; - if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) { - log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); - handleTaskRunnerSuccess(peer, message); - return; + // process message on trade thread + if (!trade.isInitialized() || trade.isShutDownStarted()) return; + ThreadUtils.execute(() -> { + // We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case + // that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received + // a mailbox message with PaymentSentMessage. + // TODO A better fix would be to add a listener for the wallet sync state and process + // the mailbox msg once wallet is ready and trade state set. + synchronized (trade.getLock()) { + if (!trade.isInitialized() || trade.isShutDownStarted()) return; + if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) { + log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); + handleTaskRunnerSuccess(peer, message); + return; + } + if (trade.getPayoutTx() != null) { + log.warn("We received a PaymentSentMessage but we have already created the payout tx " + + "so we ignore the message. This can happen if the ACK message to the peer did not " + + "arrive and the peer repeats sending us the message. We send another ACK msg."); + sendAckMessage(peer, message, true, null); + removeMailboxMessageAfterProcessing(message); + return; + } + latchTrade(); + expect(anyPhase() + .with(message) + .from(peer)) + .setup(tasks( + ApplyFilter.class, + ProcessPaymentSentMessage.class, + VerifyPeersAccountAgeWitness.class) + .using(new TradeTaskRunner(trade, + () -> { + handleTaskRunnerSuccess(peer, message); + }, + (errorMessage) -> { + log.warn("Error processing payment sent message: " + errorMessage); + processModel.getTradeManager().requestPersistence(); + + // schedule to reprocess message unless deleted + if (trade.getBuyer().getPaymentSentMessage() != null) { + UserThread.runAfter(() -> { + reprocessPaymentSentMessageCount++; + maybeReprocessPaymentSentMessage(reprocessOnError); + }, trade.getReprocessDelayInSeconds(reprocessPaymentSentMessageCount)); + } else { + handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack + } + unlatchTrade(); + }))) + .executeTasks(true); + awaitTradeLatch(); } - if (trade.getPayoutTx() != null) { - log.warn("We received a PaymentSentMessage but we have already created the payout tx " + - "so we ignore the message. This can happen if the ACK message to the peer did not " + - "arrive and the peer repeats sending us the message. We send another ACK msg."); - sendAckMessage(peer, message, true, null); - removeMailboxMessageAfterProcessing(message); - return; - } - latchTrade(); - expect(anyPhase() - .with(message) - .from(peer)) - .setup(tasks( - ApplyFilter.class, - ProcessPaymentSentMessage.class, - VerifyPeersAccountAgeWitness.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, message); - }, - (errorMessage) -> { - log.warn("Error processing payment sent message: " + errorMessage); - processModel.getTradeManager().requestPersistence(); - - // schedule to reprocess message unless deleted - if (trade.getBuyer().getPaymentSentMessage() != null) { - UserThread.runAfter(() -> { - reprocessPaymentSentMessageCount++; - maybeReprocessPaymentSentMessage(reprocessOnError); - }, trade.getReprocessDelayInSeconds(reprocessPaymentSentMessageCount)); - } else { - handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack - } - unlatchTrade(); - }))) - .executeTasks(true); - awaitTradeLatch(); - } - }, trade.getId()); + }, trade.getId()); + }); } // received by buyer and arbitrator @@ -619,59 +620,60 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // save message for reprocessing trade.getSeller().setPaymentReceivedMessage(message); - trade.requestPersistence(); + trade.persistNow(() -> { - // process message on trade thread - if (!trade.isInitialized() || trade.isShutDownStarted()) return; - ThreadUtils.execute(() -> { - synchronized (trade.getLock()) { - if (!trade.isInitialized() || trade.isShutDownStarted()) return; - latchTrade(); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); + // process message on trade thread + if (!trade.isInitialized() || trade.isShutDownStarted()) return; + ThreadUtils.execute(() -> { + synchronized (trade.getLock()) { + if (!trade.isInitialized() || trade.isShutDownStarted()) return; + latchTrade(); + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); - // check minimum trade phase - if (trade.isBuyer() && trade.getPhase().ordinal() < Trade.Phase.PAYMENT_SENT.ordinal()) { - log.warn("Received PaymentReceivedMessage before payment sent for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); - return; + // check minimum trade phase + if (trade.isBuyer() && trade.getPhase().ordinal() < Trade.Phase.PAYMENT_SENT.ordinal()) { + log.warn("Received PaymentReceivedMessage before payment sent for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); + return; + } + if (trade.isArbitrator() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) { + log.warn("Received PaymentReceivedMessage before deposits confirmed for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); + return; + } + if (trade.isSeller() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) { + log.warn("Received PaymentReceivedMessage before deposits unlocked for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); + return; + } + + expect(anyPhase() + .with(message) + .from(peer)) + .setup(tasks( + ProcessPaymentReceivedMessage.class) + .using(new TradeTaskRunner(trade, + () -> { + handleTaskRunnerSuccess(peer, message); + }, + errorMessage -> { + log.warn("Error processing payment received message: " + errorMessage); + processModel.getTradeManager().requestPersistence(); + + // schedule to reprocess message unless deleted + if (trade.getSeller().getPaymentReceivedMessage() != null) { + UserThread.runAfter(() -> { + reprocessPaymentReceivedMessageCount++; + maybeReprocessPaymentReceivedMessage(reprocessOnError); + }, trade.getReprocessDelayInSeconds(reprocessPaymentReceivedMessageCount)); + } else { + handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack + } + unlatchTrade(); + }))) + .executeTasks(true); + awaitTradeLatch(); } - if (trade.isArbitrator() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) { - log.warn("Received PaymentReceivedMessage before deposits confirmed for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); - return; - } - if (trade.isSeller() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) { - log.warn("Received PaymentReceivedMessage before deposits unlocked for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); - return; - } - - expect(anyPhase() - .with(message) - .from(peer)) - .setup(tasks( - ProcessPaymentReceivedMessage.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, message); - }, - errorMessage -> { - log.warn("Error processing payment received message: " + errorMessage); - processModel.getTradeManager().requestPersistence(); - - // schedule to reprocess message unless deleted - if (trade.getSeller().getPaymentReceivedMessage() != null) { - UserThread.runAfter(() -> { - reprocessPaymentReceivedMessageCount++; - maybeReprocessPaymentReceivedMessage(reprocessOnError); - }, trade.getReprocessDelayInSeconds(reprocessPaymentReceivedMessageCount)); - } else { - handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack - } - unlatchTrade(); - }))) - .executeTasks(true); - awaitTradeLatch(); - } - }, trade.getId()); + }, trade.getId()); + }); } public void onWithdrawCompleted() { From bd9c28fafa42937e7d6fcf153e9d88a7e26955a6 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:05:39 -0400 Subject: [PATCH 271/371] remove expected warning when wallet is null --- core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 8b319622b9..0e91e5fa1d 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1448,7 +1448,7 @@ public class XmrWalletService extends XmrWalletBase { try { syncWithProgress(true); // repeat sync to latest target height } catch (Exception e) { - log.warn("Error syncing wallet with progress on startup: " + e.getMessage()); + if (wallet != null) log.warn("Error syncing wallet with progress on startup: " + e.getMessage()); forceCloseMainWallet(); requestSwitchToNextBestConnection(sourceConnection); maybeInitMainWallet(true, numSyncAttempts - 1); // re-initialize wallet and sync again From 8611593a3f35d9d522d98ccac08290de9baca1fc Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:23:53 -0400 Subject: [PATCH 272/371] do not force restart main wallet on connection change with same config --- .../haveno/core/xmr/wallet/XmrWalletService.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 0e91e5fa1d..838ebfc5bc 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1367,11 +1367,16 @@ public class XmrWalletService extends XmrWalletBase { }, THREAD_ID); } else { - // force restart main wallet if connection changed while syncing - if (wallet != null) { - log.warn("Force restarting main wallet because connection changed while syncing"); - forceRestartMainWallet(); + // check if ignored + if (wallet == null || isShutDownStarted) return; + if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) { + updatePollPeriod(); + return; } + + // force restart main wallet if connection changed while syncing + log.warn("Force restarting main wallet because connection changed while syncing"); + forceRestartMainWallet(); } }); From 58506b02f595d810711e3f82ac47b4561910f105 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:38:51 -0400 Subject: [PATCH 273/371] recover from payment received nack with updated multisig info --- .../haveno/core/network/MessageState.java | 3 +- .../java/haveno/core/trade/SellerTrade.java | 5 + .../main/java/haveno/core/trade/Trade.java | 8 +- .../java/haveno/core/trade/TradeManager.java | 7 +- .../core/trade/protocol/ProcessModel.java | 2 +- .../haveno/core/trade/protocol/TradePeer.java | 8 +- .../core/trade/protocol/TradeProtocol.java | 93 +++++++++++++++---- .../SellerSendPaymentReceivedMessage.java | 16 ++-- .../steps/buyer/BuyerStep3View.java | 1 + .../steps/seller/SellerStep3View.java | 3 + .../java/haveno/network/p2p/AckMessage.java | 28 ++++++ proto/src/main/proto/pb.proto | 1 + 12 files changed, 138 insertions(+), 37 deletions(-) diff --git a/core/src/main/java/haveno/core/network/MessageState.java b/core/src/main/java/haveno/core/network/MessageState.java index c4ae7f8f40..759b87d0a2 100644 --- a/core/src/main/java/haveno/core/network/MessageState.java +++ b/core/src/main/java/haveno/core/network/MessageState.java @@ -23,5 +23,6 @@ public enum MessageState { ARRIVED, STORED_IN_MAILBOX, ACKNOWLEDGED, - FAILED + FAILED, + NACKED } diff --git a/core/src/main/java/haveno/core/trade/SellerTrade.java b/core/src/main/java/haveno/core/trade/SellerTrade.java index fae3cce7a1..ddccfe59c3 100644 --- a/core/src/main/java/haveno/core/trade/SellerTrade.java +++ b/core/src/main/java/haveno/core/trade/SellerTrade.java @@ -19,6 +19,7 @@ package haveno.core.trade; import haveno.core.offer.Offer; import haveno.core.trade.protocol.ProcessModel; +import haveno.core.trade.protocol.SellerProtocol; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @@ -59,5 +60,9 @@ public abstract class SellerTrade extends Trade { public boolean confirmPermitted() { return true; } + + public boolean isFinished() { + return super.isFinished() && ((SellerProtocol) getProtocol()).needsToResendPaymentReceivedMessages(); + } } diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 769d7e95af..b7387a9bd7 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -787,12 +787,15 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isFinished() { - return isPayoutUnlocked() && isCompleted() && !getProtocol().needsToResendPaymentReceivedMessages(); + return isPayoutUnlocked() && isCompleted(); } public void resetToPaymentSentState() { setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); - for (TradePeer peer : getAllPeers()) peer.setPaymentReceivedMessage(null); + for (TradePeer peer : getAllPeers()) { + peer.setPaymentReceivedMessage(null); + peer.setPaymentReceivedMessageState(MessageState.UNDEFINED); + } setPayoutTxHex(null); } @@ -2105,6 +2108,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private MessageState getPaymentSentMessageState() { if (isPaymentReceived()) return MessageState.ACKNOWLEDGED; if (getSeller().getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; + if (getSeller().getPaymentSentMessageStateProperty().get() == MessageState.NACKED) return MessageState.NACKED; switch (state) { case BUYER_SENT_PAYMENT_SENT_MSG: return MessageState.SENT; diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index e34cc9b1a7..9befbd6e82 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -683,7 +683,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (!sender.equals(request.getTakerNodeAddress())) { if (sender.equals(request.getMakerNodeAddress())) { log.warn("Received InitTradeRequest from maker to arbitrator for trade that is already initializing, tradeId={}, sender={}", request.getOfferId(), sender); - sendAckMessage(sender, trade.getMaker().getPubKeyRing(), request, false, "Trade is already initializing for " + getClass().getSimpleName() + " " + trade.getId()); + sendAckMessage(sender, trade.getMaker().getPubKeyRing(), request, false, "Trade is already initializing for " + getClass().getSimpleName() + " " + trade.getId(), null); } else { log.warn("Ignoring InitTradeRequest from non-taker, tradeId={}, sender={}", request.getOfferId(), sender); } @@ -1212,7 +1212,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // Getters, Utils /////////////////////////////////////////////////////////////////////////////////////////// - public void sendAckMessage(NodeAddress peer, PubKeyRing peersPubKeyRing, TradeMessage message, boolean result, @Nullable String errorMessage) { + public void sendAckMessage(NodeAddress peer, PubKeyRing peersPubKeyRing, TradeMessage message, boolean result, @Nullable String errorMessage, String updatedMultisigHex) { // create ack message String tradeId = message.getOfferId(); @@ -1223,7 +1223,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi sourceUid, tradeId, result, - errorMessage); + errorMessage, + updatedMultisigHex); // send ack message log.info("Send AckMessage for {} to peer {}. tradeId={}, sourceUid={}", diff --git a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java index ae6a27e7f0..4f5ce9e66a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -311,7 +311,7 @@ public class ProcessModel implements Model, PersistablePayload { void setDepositTxSentAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : - MessageState.FAILED; + MessageState.NACKED; setDepositTxMessageState(messageState); } diff --git a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java index 11c035a329..7ec33716a1 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java @@ -208,21 +208,21 @@ public final class TradePeer implements PersistablePayload { void setDepositsConfirmedAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : - MessageState.FAILED; + MessageState.NACKED; setDepositsConfirmedMessageState(messageState); } void setPaymentSentAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : - MessageState.FAILED; + MessageState.NACKED; setPaymentSentMessageState(messageState); } void setPaymentReceivedAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : - MessageState.FAILED; + MessageState.NACKED; setPaymentReceivedMessageState(messageState); } @@ -256,7 +256,7 @@ public final class TradePeer implements PersistablePayload { } public boolean isPaymentReceivedMessageReceived() { - return paymentReceivedMessageStateProperty.get() == MessageState.ACKNOWLEDGED || paymentReceivedMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX; + return paymentReceivedMessageStateProperty.get() == MessageState.ACKNOWLEDGED || paymentReceivedMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX || paymentReceivedMessageStateProperty.get() == MessageState.NACKED; } @Override diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 09c26d8a1d..6272d40749 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -42,6 +42,7 @@ import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.taskrunner.Task; +import haveno.core.network.MessageState; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; @@ -272,7 +273,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D handleTaskRunnerSuccess(null, null, "maybeSendDepositsConfirmedMessages"); }, (errorMessage) -> { - handleTaskRunnerFault(null, null, "maybeSendDepositsConfirmedMessages", errorMessage); + handleTaskRunnerFault(null, null, "maybeSendDepositsConfirmedMessages", errorMessage, null); }))) .executeTasks(true); awaitTradeLatch(); @@ -280,10 +281,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D }, trade.getId()); } - public boolean needsToResendPaymentReceivedMessages() { - return false; // seller protocol overrides - } - public void maybeReprocessPaymentSentMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { @@ -627,6 +624,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D ThreadUtils.execute(() -> { synchronized (trade.getLock()) { if (!trade.isInitialized() || trade.isShutDownStarted()) return; + if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal()) { + log.warn("Received another PaymentReceivedMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); + handleTaskRunnerSuccess(peer, message); + return; + } latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); @@ -665,7 +667,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D maybeReprocessPaymentReceivedMessage(reprocessOnError); }, trade.getReprocessDelayInSeconds(reprocessPaymentReceivedMessageCount)); } else { - handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack + handleTaskRunnerFault(peer, message, null, errorMessage, trade.getSelf().getUpdatedMultisigHex()); // otherwise send nack } unlatchTrade(); }))) @@ -694,7 +696,8 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D handleTaskRunnerFault(null, null, result.name(), - result.getInfo()); + result.getInfo(), + null); } }); } @@ -734,7 +737,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private void onAckMessage(AckMessage ackMessage, NodeAddress sender) { // ignore if trade is completely finished - if (trade.isFinished()) return; + if (trade.isFinished()) return; // get trade peer TradePeer peer = trade.getTradePeer(sender); @@ -755,7 +758,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D peer.setNodeAddress(sender); } - // set trade state on deposit request nack + // handle nack of deposit request if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) { if (!ackMessage.isSuccess()) { trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); @@ -763,13 +766,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } - // handle ack for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time + // handle ack message for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time if (ackMessage.getSourceMsgClassName().equals(DepositsConfirmedMessage.class.getSimpleName())) { peer.setDepositsConfirmedAckMessage(ackMessage); processModel.getTradeManager().requestPersistence(); } - // handle ack for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time + // handle ack message for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) { if (trade.getTradePeer(sender) == trade.getSeller()) { trade.getSeller().setPaymentSentAckMessage(ackMessage); @@ -785,15 +788,55 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } - // handle ack for PaymentReceivedMessage, which automatically re-sends if not ACKed in a certain time + // handle ack message for PaymentReceivedMessage, which automatically re-sends if not ACKed in a certain time if (ackMessage.getSourceMsgClassName().equals(PaymentReceivedMessage.class.getSimpleName())) { + + // ack message from buyer if (trade.getTradePeer(sender) == trade.getBuyer()) { trade.getBuyer().setPaymentReceivedAckMessage(ackMessage); - if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG); - else trade.setState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); + + // handle successful ack + if (ackMessage.isSuccess()) { + trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG); + } + + // handle nack + else { + log.warn("We received a NACK for our PaymentReceivedMessage to the buyer for {} {}", trade.getClass().getSimpleName(), trade.getId()); + + // update multisig hex + if (ackMessage.getUpdatedMultisigHex() != null) { + trade.getBuyer().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); + } + + // reset state if not processed + if (trade.isPaymentReceived() && !trade.isPayoutPublished() && !isPaymentReceivedMessageAckedByEither()) { + log.warn("Resetting state to payment sent for {} {}", trade.getClass().getSimpleName(), trade.getId()); + trade.resetToPaymentSentState(); + } + } processModel.getTradeManager().requestPersistence(); - } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { + } + + // ack message from arbitrator + else if (trade.getTradePeer(sender) == trade.getArbitrator()) { trade.getArbitrator().setPaymentReceivedAckMessage(ackMessage); + + // handle nack + if (!ackMessage.isSuccess()) { + log.warn("We received a NACK for our PaymentReceivedMessage to the arbitrator for {} {}", trade.getClass().getSimpleName(), trade.getId()); + + // update multisig hex + if (ackMessage.getUpdatedMultisigHex() != null) { + trade.getArbitrator().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); + } + + // reset state if not processed + if (trade.isPaymentReceived() && !trade.isPayoutPublished() && !isPaymentReceivedMessageAckedByEither()) { + log.warn("Resetting state to payment sent for {} {}", trade.getClass().getSimpleName(), trade.getId()); + trade.resetToPaymentSentState(); + } + } processModel.getTradeManager().requestPersistence(); } else { log.warn("Received AckMessage from unexpected peer for {}, sender={}, trade={} {}, messageUid={}, success={}, errorMsg={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.isSuccess(), ackMessage.getErrorMessage()); @@ -813,7 +856,17 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D trade.onAckMessage(ackMessage, sender); } + private boolean isPaymentReceivedMessageAckedByEither() { + if (trade.getBuyer().getPaymentReceivedMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return true; + if (trade.getArbitrator().getPaymentReceivedMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return true; + return false; + } + protected void sendAckMessage(NodeAddress peer, TradeMessage message, boolean result, @Nullable String errorMessage) { + sendAckMessage(peer, message, result, errorMessage, null); + } + + protected void sendAckMessage(NodeAddress peer, TradeMessage message, boolean result, @Nullable String errorMessage, String updatedMultisigHex) { // get peer's pub key ring PubKeyRing peersPubKeyRing = getPeersPubKeyRing(peer); @@ -823,7 +876,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } // send ack message - processModel.getTradeManager().sendAckMessage(peer, peersPubKeyRing, message, result, errorMessage); + processModel.getTradeManager().sendAckMessage(peer, peersPubKeyRing, message, result, errorMessage, updatedMultisigHex); } @@ -870,11 +923,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } protected void handleTaskRunnerFault(NodeAddress sender, TradeMessage message, String errorMessage) { - handleTaskRunnerFault(sender, message, message.getClass().getSimpleName(), errorMessage); + handleTaskRunnerFault(sender, message, message.getClass().getSimpleName(), errorMessage, null); } protected void handleTaskRunnerFault(FluentProtocol.Event event, String errorMessage) { - handleTaskRunnerFault(null, null, event.name(), errorMessage); + handleTaskRunnerFault(null, null, event.name(), errorMessage, null); } @@ -936,11 +989,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D unlatchTrade(); } - void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage) { + void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage, String updatedMultisigHex) { log.error("Task runner failed with error {}. Triggered from {}. Monerod={}" , errorMessage, source, trade.getXmrWalletService().getXmrConnectionService().getConnection()); if (message != null) { - sendAckMessage(ackReceiver, message, false, errorMessage); + sendAckMessage(ackReceiver, message, false, errorMessage, updatedMultisigHex); } handleError(errorMessage); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java index 202d4c8c79..9fe31dba60 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java @@ -90,8 +90,8 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag try { runInterceptHook(); - // skip if already received - if (isReceived()) { + // skip if stopped + if (stopSending()) { if (!isCompleted()) complete(); return; } @@ -191,8 +191,8 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag private void tryToSendAgainLater() { - // skip if already received - if (isReceived()) return; + // skip if stopped + if (stopSending()) return; if (resendCounter >= MAX_RESEND_ATTEMPTS) { cleanup(); @@ -226,12 +226,16 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag } private void onMessageStateChange(MessageState newValue) { - if (isReceived()) { + if (isMessageReceived()) { cleanup(); } } - protected boolean isReceived() { + protected boolean isMessageReceived() { return getReceiver().isPaymentReceivedMessageReceived(); } + + protected boolean stopSending() { + return isMessageReceived() || !trade.isPaymentReceived(); // stop if received or trade state reset // TODO: also stop after some number of blocks? + } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java index b28eda4a10..dba3fe4a3d 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java @@ -112,6 +112,7 @@ public class BuyerStep3View extends TradeStepView { iconLabel.getStyleClass().add("trade-msg-state-stored"); break; case FAILED: + case NACKED: textFieldWithIcon.setIcon(AwesomeIcon.EXCLAMATION_SIGN); iconLabel.getStyleClass().add("trade-msg-state-acknowledged"); break; diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index c6fe0cab23..20e12a3f56 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -151,6 +151,9 @@ public class SellerStep3View extends TradeStepView { break; } } + + // update confirm button state + confirmButton.setDisable(!confirmPaymentReceivedPermitted()); }); } diff --git a/p2p/src/main/java/haveno/network/p2p/AckMessage.java b/p2p/src/main/java/haveno/network/p2p/AckMessage.java index 7a7a0ff990..6d6470d568 100644 --- a/p2p/src/main/java/haveno/network/p2p/AckMessage.java +++ b/p2p/src/main/java/haveno/network/p2p/AckMessage.java @@ -53,6 +53,8 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, private final boolean success; @Nullable private final String errorMessage; + @Nullable + private final String updatedMultisigHex; /** * @@ -79,6 +81,27 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, sourceId, success, errorMessage, + null, + Version.getP2PMessageVersion()); + } + + public AckMessage(NodeAddress senderNodeAddress, + AckMessageSourceType sourceType, + String sourceMsgClassName, + String sourceUid, + String sourceId, + boolean success, + String errorMessage, + String updatedMultisigHex) { + this(UUID.randomUUID().toString(), + senderNodeAddress, + sourceType, + sourceMsgClassName, + sourceUid, + sourceId, + success, + errorMessage, + updatedMultisigHex, Version.getP2PMessageVersion()); } @@ -95,6 +118,7 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, String sourceId, boolean success, @Nullable String errorMessage, + String updatedMultisigInfo, String messageVersion) { super(messageVersion); this.uid = uid; @@ -105,6 +129,7 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, this.sourceId = sourceId; this.success = success; this.errorMessage = errorMessage; + this.updatedMultisigHex = updatedMultisigInfo; } public protobuf.AckMessage toProtoMessage() { @@ -126,6 +151,7 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, .setSuccess(success); Optional.ofNullable(sourceUid).ifPresent(builder::setSourceUid); Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage); + Optional.ofNullable(updatedMultisigHex).ifPresent(builder::setUpdatedMultisigHex); return builder; } @@ -139,6 +165,7 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, proto.getSourceId(), proto.getSuccess(), proto.getErrorMessage().isEmpty() ? null : proto.getErrorMessage(), + proto.getUpdatedMultisigHex().isEmpty() ? null : proto.getUpdatedMultisigHex(), messageVersion); } @@ -163,6 +190,7 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, ",\n sourceId='" + sourceId + '\'' + ",\n success=" + success + ",\n errorMessage='" + errorMessage + '\'' + + ",\n updatedMultisigInfo='" + updatedMultisigHex + '\'' + "\n} " + super.toString(); } } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 5cdde1f0ce..022031d08d 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -216,6 +216,7 @@ message AckMessage { string source_id = 6; // id of source (tradeId, disputeId) bool success = 7; // true if source message was processed successfully string error_message = 8; // optional error message if source message processing failed + string updated_multisig_hex = 9; // data to update the multisig state } message PrefixedSealedAndSignedMessage { From adcd5da4313ad5f3f3f44cd4202bd159c47f614c Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 25 Apr 2025 08:32:41 -0400 Subject: [PATCH 274/371] fix shut down of trade and wallet services in seed node --- .../app/misc/ExecutableForAppWithP2p.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java index 9c8016d506..3184d9ba11 100644 --- a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java @@ -151,23 +151,23 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); }); }); - - // shut down trade and wallet services - log.info("Shutting down trade and wallet services"); - injector.getInstance(OfferBookService.class).shutDown(); - injector.getInstance(TradeManager.class).shutDown(); - injector.getInstance(BtcWalletService.class).shutDown(); - injector.getInstance(XmrWalletService.class).shutDown(); - injector.getInstance(XmrConnectionService.class).shutDown(); - injector.getInstance(WalletsSetup.class).shutDown(); }); + + // shut down trade and wallet services + log.info("Shutting down trade and wallet services"); + injector.getInstance(OfferBookService.class).shutDown(); + injector.getInstance(TradeManager.class).shutDown(); + injector.getInstance(BtcWalletService.class).shutDown(); + injector.getInstance(XmrWalletService.class).shutDown(); + injector.getInstance(XmrConnectionService.class).shutDown(); + injector.getInstance(WalletsSetup.class).shutDown(); }); // we wait max 5 sec. UserThread.runAfter(() -> { PersistenceManager.flushAllDataToDiskAtShutdown(() -> { resultHandler.handleResult(); - log.info("Graceful shutdown caused a timeout. Exiting now."); + log.warn("Graceful shutdown caused a timeout. Exiting now."); UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); }); }, 5); From 350aa1083991d6e8f015302009e3c823bd228531 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:49:48 -0400 Subject: [PATCH 275/371] tolerate miner fees within 5x of each other --- .../dispute/arbitration/ArbitrationManager.java | 6 ++---- .../main/java/haveno/core/trade/HavenoUtils.java | 13 +++++++++++++ core/src/main/java/haveno/core/trade/Trade.java | 6 ++---- .../haveno/core/xmr/wallet/XmrWalletService.java | 6 ++---- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 5ac7cd389a..848dbdb6f5 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -487,10 +487,8 @@ public final class ArbitrationManager extends DisputeManager XmrWalletService.MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + arbitratorSignedPayoutTx.getFee()); - log.info("Payout tx fee {} is within tolerance, diff %={}", arbitratorSignedPayoutTx.getFee(), feeDiff); + HavenoUtils.verifyMinerFee(feeEstimateTx.getFee(), arbitratorSignedPayoutTx.getFee()); + log.info("Dispute payout tx fee {} is within tolerance"); } } else { disputeTxSet.setMultisigTxHex(trade.getPayoutTxHex()); diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index d238d78843..74caf5d7f2 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -96,6 +96,7 @@ public class HavenoUtils { public static final double MAKER_FEE_PCT = 0.0015; // 0.15% public static final double TAKER_FEE_PCT = 0.0075; // 0.75% public static final double MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT = MAKER_FEE_PCT + TAKER_FEE_PCT; // customize maker's fee when no deposit or fee from taker + public static final double MINER_FEE_TOLERANCE_FACTOR = 5.0; // miner fees must be within 5x of each other // other configuration public static final long LOG_POLL_ERROR_PERIOD_MS = 1000 * 60 * 4; // log poll errors up to once every 4 minutes @@ -650,4 +651,16 @@ public class HavenoUtils { } }).start(); } + + public static void verifyMinerFee(BigInteger expected, BigInteger actual) { + BigInteger max = expected.max(actual); + BigInteger min = expected.min(actual); + if (min.compareTo(BigInteger.ZERO) <= 0) { + throw new IllegalArgumentException("Miner fees must be greater than zero"); + } + double factor = divide(max, min); + if (factor > MINER_FEE_TOLERANCE_FACTOR) { + throw new IllegalArgumentException("Miner fees are not within " + MINER_FEE_TOLERANCE_FACTOR + "x of each other. Expected=" + expected + ", actual=" + actual + ", factor=" + factor); + } + } } diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index b7387a9bd7..aa2330a78f 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1457,10 +1457,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getId()); saveWallet(); // save wallet before creating fee estimate tx MoneroTxWallet feeEstimateTx = createPayoutTx(); - BigInteger feeEstimate = feeEstimateTx.getFee(); - double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal? - if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee()); - log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff); + HavenoUtils.verifyMinerFee(feeEstimateTx.getFee(), payoutTx.getFee()); + log.info("Payout tx fee {} is within tolerance"); } // set signed payout tx hex diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 838ebfc5bc..fc83118cbd 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -110,7 +110,6 @@ public class XmrWalletService extends XmrWalletBase { public static final String MONERO_BINS_DIR = Config.appDataDir().getAbsolutePath(); public static final String MONERO_WALLET_RPC_NAME = Utilities.isWindows() ? "monero-wallet-rpc.exe" : "monero-wallet-rpc"; public static final String MONERO_WALLET_RPC_PATH = MONERO_BINS_DIR + File.separator + MONERO_WALLET_RPC_NAME; - public static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee public static final MoneroTxPriority PROTOCOL_FEE_PRIORITY = MoneroTxPriority.DEFAULT; public static final int MONERO_LOG_LEVEL = -1; // monero library log level, -1 to disable private static final MoneroNetworkType MONERO_NETWORK_TYPE = getMoneroNetworkType(); @@ -767,9 +766,8 @@ public class XmrWalletService extends XmrWalletBase { // verify miner fee BigInteger minerFeeEstimate = getFeeEstimate(tx.getWeight()); - double minerFeeDiff = tx.getFee().subtract(minerFeeEstimate).abs().doubleValue() / minerFeeEstimate.doubleValue(); - if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee() + ", diff%=" + minerFeeDiff); - log.info("Trade miner fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff); + HavenoUtils.verifyMinerFee(minerFeeEstimate, tx.getFee()); + log.info("Trade miner fee {} is within tolerance"); // verify proof to fee address BigInteger actualTradeFee = BigInteger.ZERO; From 0b8e43b7a822dec42c94cb9d8aeccadac6f4c731 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:54:44 -0400 Subject: [PATCH 276/371] preserve old behavior when nack does not include updated multisig --- .../core/trade/protocol/TradeProtocol.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 6272d40749..844d13e86a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -804,15 +804,15 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D else { log.warn("We received a NACK for our PaymentReceivedMessage to the buyer for {} {}", trade.getClass().getSimpleName(), trade.getId()); - // update multisig hex + // nack includes updated multisig hex since v1.1.1 if (ackMessage.getUpdatedMultisigHex() != null) { trade.getBuyer().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); - } - // reset state if not processed - if (trade.isPaymentReceived() && !trade.isPayoutPublished() && !isPaymentReceivedMessageAckedByEither()) { - log.warn("Resetting state to payment sent for {} {}", trade.getClass().getSimpleName(), trade.getId()); - trade.resetToPaymentSentState(); + // reset state if not processed + if (trade.isPaymentReceived() && !trade.isPayoutPublished() && !isPaymentReceivedMessageAckedByEither()) { + log.warn("Resetting state to payment sent for {} {}", trade.getClass().getSimpleName(), trade.getId()); + trade.resetToPaymentSentState(); + } } } processModel.getTradeManager().requestPersistence(); @@ -826,15 +826,15 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D if (!ackMessage.isSuccess()) { log.warn("We received a NACK for our PaymentReceivedMessage to the arbitrator for {} {}", trade.getClass().getSimpleName(), trade.getId()); - // update multisig hex + // nack includes updated multisig hex since v1.1.1 if (ackMessage.getUpdatedMultisigHex() != null) { trade.getArbitrator().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); - } - // reset state if not processed - if (trade.isPaymentReceived() && !trade.isPayoutPublished() && !isPaymentReceivedMessageAckedByEither()) { - log.warn("Resetting state to payment sent for {} {}", trade.getClass().getSimpleName(), trade.getId()); - trade.resetToPaymentSentState(); + // reset state if not processed + if (trade.isPaymentReceived() && !trade.isPayoutPublished() && !isPaymentReceivedMessageAckedByEither()) { + log.warn("Resetting state to payment sent for {} {}", trade.getClass().getSimpleName(), trade.getId()); + trade.resetToPaymentSentState(); + } } } processModel.getTradeManager().requestPersistence(); From ae5ee15a8526e2cdaff8bd5330f9b4569874dbec Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 26 Apr 2025 15:57:23 -0400 Subject: [PATCH 277/371] instruct to try again on nack --- .../src/main/java/haveno/core/trade/protocol/TradeProtocol.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 844d13e86a..5ff09324fd 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -849,7 +849,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D log.info("Received AckMessage for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); } else { log.warn("Received AckMessage with error state for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); - handleError(ackMessage.getErrorMessage()); + handleError("Your peer had a problem processing your message. Please ensure you and your peer are running the latest version and try again.\n\nError details:\n" + ackMessage.getErrorMessage()); } // notify trade listeners From 81eaeb6df0924bbd330956ec5dc11e312a541ef6 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 26 Apr 2025 16:02:21 -0400 Subject: [PATCH 278/371] bump version to v1.1.1 --- build.gradle | 2 +- common/src/main/java/haveno/common/app/Version.java | 2 +- desktop/package/linux/exchange.haveno.Haveno.metainfo.xml | 2 +- desktop/package/macosx/Info.plist | 4 ++-- seednode/src/main/java/haveno/seednode/SeedNodeMain.java | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index aec0e7c89f..0c8a4412f2 100644 --- a/build.gradle +++ b/build.gradle @@ -610,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.1.0-SNAPSHOT' + version = '1.1.1-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index c0dd9352ff..d7be891aea 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.1.0"; + public static final String VERSION = "1.1.1"; /** * Holds a list of the tagged resource files for optimizing the getData requests. diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index 9227c587a6..57aeb02f09 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -60,6 +60,6 @@ - + diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index fd0a765320..c6bf09f2de 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.1.0 + 1.1.1 CFBundleShortVersionString - 1.1.0 + 1.1.1 CFBundleExecutable Haveno diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index 1e9c4c5061..b846dff4c9 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.1.0"; + private static final String VERSION = "1.1.1"; private SeedNode seedNode; private Timer checkConnectionLossTime; From c214919aa590372c8e3f4ebb2f43df08414afb96 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 28 Apr 2025 06:40:17 -0400 Subject: [PATCH 279/371] fix concurrent modification in portfolio by locking sequence number map --- .../network/p2p/storage/P2PDataStorage.java | 16 ++++-- .../persistence/SequenceNumberMap.java | 56 +++++++++++++------ 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java b/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java index 40be21ef32..0517097190 100644 --- a/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java +++ b/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java @@ -187,7 +187,9 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { - sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); + synchronized (persisted.getMap()) { + sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); + } completeHandler.run(); }, completeHandler); @@ -198,7 +200,9 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers public void readPersistedSync() { SequenceNumberMap persisted = persistenceManager.getPersisted(); if (persisted != null) { - sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); + synchronized (persisted.getMap()) { + sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); + } } } @@ -641,9 +645,11 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers } removeFromMapAndDataStore(toRemoveList); - if (sequenceNumberMap.size() > this.maxSequenceNumberMapSizeBeforePurge) { - sequenceNumberMap.setMap(getPurgedSequenceNumberMap(sequenceNumberMap.getMap())); - requestPersistence(); + synchronized (sequenceNumberMap.getMap()) { + if (sequenceNumberMap.size() > this.maxSequenceNumberMapSizeBeforePurge) { + sequenceNumberMap.setMap(getPurgedSequenceNumberMap(sequenceNumberMap.getMap())); + requestPersistence(); + } } } } diff --git a/p2p/src/main/java/haveno/network/p2p/storage/persistence/SequenceNumberMap.java b/p2p/src/main/java/haveno/network/p2p/storage/persistence/SequenceNumberMap.java index f774c4c3c4..43cf2c1e1e 100644 --- a/p2p/src/main/java/haveno/network/p2p/storage/persistence/SequenceNumberMap.java +++ b/p2p/src/main/java/haveno/network/p2p/storage/persistence/SequenceNumberMap.java @@ -19,8 +19,6 @@ package haveno.network.p2p.storage.persistence; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.network.p2p.storage.P2PDataStorage; -import lombok.Getter; -import lombok.Setter; import java.util.HashMap; import java.util.Map; @@ -33,8 +31,6 @@ import java.util.stream.Collectors; * Hence this Persistable class. */ public class SequenceNumberMap implements PersistableEnvelope { - @Getter - @Setter private Map map = new ConcurrentHashMap<>(); public SequenceNumberMap() { @@ -46,20 +42,24 @@ public class SequenceNumberMap implements PersistableEnvelope { /////////////////////////////////////////////////////////////////////////////////////////// private SequenceNumberMap(Map map) { - this.map.putAll(map); + synchronized (this.map) { + this.map.putAll(map); + } } @Override public protobuf.PersistableEnvelope toProtoMessage() { - return protobuf.PersistableEnvelope.newBuilder() - .setSequenceNumberMap(protobuf.SequenceNumberMap.newBuilder() - .addAllSequenceNumberEntries(map.entrySet().stream() - .map(entry -> protobuf.SequenceNumberEntry.newBuilder() - .setBytes(entry.getKey().toProtoMessage()) - .setMapValue(entry.getValue().toProtoMessage()) - .build()) - .collect(Collectors.toList()))) - .build(); + synchronized (map) { + return protobuf.PersistableEnvelope.newBuilder() + .setSequenceNumberMap(protobuf.SequenceNumberMap.newBuilder() + .addAllSequenceNumberEntries(map.entrySet().stream() + .map(entry -> protobuf.SequenceNumberEntry.newBuilder() + .setBytes(entry.getKey().toProtoMessage()) + .setMapValue(entry.getValue().toProtoMessage()) + .build()) + .collect(Collectors.toList()))) + .build(); + } } public static SequenceNumberMap fromProto(protobuf.SequenceNumberMap proto) { @@ -74,20 +74,40 @@ public class SequenceNumberMap implements PersistableEnvelope { // API /////////////////////////////////////////////////////////////////////////////////////////// + public Map getMap() { + synchronized (map) { + return map; + } + } + + public void setMap(Map map) { + synchronized (this.map) { + this.map = map; + } + } + // Delegates public int size() { - return map.size(); + synchronized (map) { + return map.size(); + } } public boolean containsKey(P2PDataStorage.ByteArray key) { - return map.containsKey(key); + synchronized (map) { + return map.containsKey(key); + } } public P2PDataStorage.MapValue get(P2PDataStorage.ByteArray key) { - return map.get(key); + synchronized (map) { + return map.get(key); + } } public void put(P2PDataStorage.ByteArray key, P2PDataStorage.MapValue value) { - map.put(key, value); + synchronized (map) { + map.put(key, value); + } } } From c2fbd4b16ff2c694f504401d33d3e2105dbd5d5e Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:05:26 -0400 Subject: [PATCH 280/371] update donation address --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e90b06ce3..5b2e8015eb 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ To bring Haveno to life, we need resources. If you have the possibility, please

    Donate Monero
    - 42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F + 47fo8N5m2VVW4uojadGQVJ34LFR9yXwDrZDRugjvVSjcTWV2WFSoc1XfNpHmxwmVtfNY9wMBch6259G6BXXFmhU49YG1zfB

    If you are using a wallet that supports OpenAlias (like the 'official' CLI and GUI wallets), you can simply put `fund@haveno.exchange` as the "receiver" address. From e3946f3abaeab79a7db9bfc0024f647dd5cdf71c Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 1 May 2025 08:29:45 -0400 Subject: [PATCH 281/371] move settings tab to last --- .../src/main/java/haveno/desktop/main/MainView.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index eaec5d1154..7856a2021f 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -172,8 +172,8 @@ public class MainView extends InitializableView { ToggleButton fundsButton = new NavButton(FundsView.class, Res.get("mainView.menu.funds").toUpperCase()); ToggleButton supportButton = new NavButton(SupportView.class, Res.get("mainView.menu.support")); - ToggleButton settingsButton = new NavButton(SettingsView.class, Res.get("mainView.menu.settings")); ToggleButton accountButton = new NavButton(AccountView.class, Res.get("mainView.menu.account")); + ToggleButton settingsButton = new NavButton(SettingsView.class, Res.get("mainView.menu.settings")); JFXBadge portfolioButtonWithBadge = new JFXBadge(portfolioButton); JFXBadge supportButtonWithBadge = new JFXBadge(supportButton); @@ -199,10 +199,10 @@ public class MainView extends InitializableView { fundsButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT6, keyEvent)) { supportButton.fire(); - } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT7, keyEvent)) { - settingsButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT8, keyEvent)) { accountButton.fire(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT7, keyEvent)) { + settingsButton.fire(); } }); } @@ -305,8 +305,8 @@ public class MainView extends InitializableView { primaryNav.getStyleClass().add("nav-primary"); HBox.setHgrow(primaryNav, Priority.SOMETIMES); - HBox secondaryNav = new HBox(supportButtonWithBadge, getNavigationSpacer(), settingsButtonWithBadge, - getNavigationSpacer(), accountButton, getNavigationSpacer()); + HBox secondaryNav = new HBox(supportButtonWithBadge, getNavigationSpacer(), accountButton, + getNavigationSpacer(), settingsButtonWithBadge, getNavigationSpacer()); secondaryNav.getStyleClass().add("nav-secondary"); HBox.setHgrow(secondaryNav, Priority.SOMETIMES); From c4758d1e4b5488eb04f6a046204a697eb94beb9e Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 6 May 2025 07:49:23 -0400 Subject: [PATCH 282/371] use default 'password' to authenticate with wallet rpc server --- core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index fc83118cbd..f2d16943d1 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1785,7 +1785,7 @@ public class XmrWalletService extends XmrWalletBase { List cmd = new ArrayList<>(Arrays.asList( // modifiable list MONERO_WALLET_RPC_PATH, "--rpc-login", - MONERO_WALLET_RPC_USERNAME + ":" + getWalletPassword(), + MONERO_WALLET_RPC_USERNAME + ":" + MONERO_WALLET_RPC_DEFAULT_PASSWORD, "--wallet-dir", walletDir.toString())); // omit --mainnet flag since it does not exist From fe3283f3b0a7299bc3c05aafacbb494d899a3333 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 8 May 2025 07:08:51 -0400 Subject: [PATCH 283/371] update donation qr and readme --- README.md | 8 +++----- media/donate_monero.png | Bin 28501 -> 34213 bytes 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5b2e8015eb..c8078a97aa 100644 --- a/README.md +++ b/README.md @@ -73,13 +73,11 @@ If you are not able to contribute code and want to contribute development resour To incentivize development and reward contributors, we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the [issues labeled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md). -## Support and sponsorships +## Support -To bring Haveno to life, we need resources. If you have the possibility, please consider [becoming a sponsor](https://haveno.exchange/sponsors/) or donating to the project: +To bring Haveno to life, we need resources. If you have the possibility, please consider donating to the project: -

    +

    Donate Monero
    47fo8N5m2VVW4uojadGQVJ34LFR9yXwDrZDRugjvVSjcTWV2WFSoc1XfNpHmxwmVtfNY9wMBch6259G6BXXFmhU49YG1zfB

    - -If you are using a wallet that supports OpenAlias (like the 'official' CLI and GUI wallets), you can simply put `fund@haveno.exchange` as the "receiver" address. diff --git a/media/donate_monero.png b/media/donate_monero.png index 35b3e21d8fccf03ebb74c756a16e53b710aa50b0..83fe64fed62a54a9742320337a8bf57a4c51d1eb 100644 GIT binary patch literal 34213 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
      84&piW`kULl;qLP> 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Ãv@jbRQJ0 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 Date: Mon, 12 May 2025 21:27:39 +0000 Subject: [PATCH 284/371] Update Qubes/Whonix install for individual files (#1739) --- scripts/install_whonix_qubes/INSTALL.md | 29 +++++++------------ scripts/install_whonix_qubes/README.md | 6 ++-- .../1-TemplateVM/1.0-haveno-templatevm.sh | 16 +++++----- .../scripts/3-AppVM/3.0-haveno-appvm.sh | 4 +-- 4 files changed, 23 insertions(+), 32 deletions(-) diff --git a/scripts/install_whonix_qubes/INSTALL.md b/scripts/install_whonix_qubes/INSTALL.md index c56b35cacd..adee6d2e60 100644 --- a/scripts/install_whonix_qubes/INSTALL.md +++ b/scripts/install_whonix_qubes/INSTALL.md @@ -147,13 +147,13 @@ $ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno ##### In `haveno-template` TemplateVM: ```shell -% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" ```

      Example:

      ```shell -% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno-linux-deb.zip" "ABAF11C65A2970B130ABE3C479BE3E4300411886" +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb" "ABAF11C65A2970B130ABE3C479BE3E4300411886" ``` #### *TemplateVM Using Precompiled Package From `git` Repository (CLI)* @@ -195,10 +195,9 @@ $ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno ```shell # export https_proxy=http://127.0.0.1:8082 -# curl -sSLo /tmp/hashes.txt https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt -# curl -sSLo /tmp/hashes.txt.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt.sig -# curl -sSLo /tmp/haveno.zip https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip -# curl -sSLo /tmp/haveno.zip.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip.sig +# curl -sSLo /tmp/haveno.deb https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb +# curl -sSLo /tmp/haveno.deb.sig https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb.sig +# curl -sSLo /tmp/haveno-jar.SHA-256 https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-SNAPSHOT-all.jar.SHA-256 ```

      Note:

      @@ -207,28 +206,22 @@ $ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno

      For Whonix On Anything Other Than Qubes OS:

      ```shell -# curl -sSLo /tmp/hashes.txt https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt -# curl -sSLo /tmp/hashes.txt.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt.sig -# curl -sSLo /tmp/haveno.zip https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip -# curl -sSLo /tmp/haveno.zip.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip.sig +# curl -sSLo /tmp/haveno.deb https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb +# curl -sSLo /tmp/haveno.deb.sig https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb.sig +# curl -sSLo /tmp/haveno-jar.SHA-256 https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-SNAPSHOT-all.jar.SHA-256 ```

      Note:

      Above are dummy URLS which MUST be replaced with actual working URLs

      -###### Verify Release Files +###### Verify & Install Package File ```shell -# if gpg --digest-algo SHA256 --verify /tmp/hashes.txt.sig >/dev/null 2>&1; then printf $'SHASUM file has a VALID signature!\n'; else printf $'SHASUMS failed signature check\n' && sleep 5 && exit 1; fi -``` - -###### Verify Hash, Unpack & Install Package -```shell -# if [[ $(cat /tmp/hashes.txt) =~ $(sha512sum /tmp/haveno*.zip | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && mkdir -p /usr/share/desktop-directories && cd /tmp && unzip /tmp/haveno*.zip && apt install -y /tmp/haveno*.deb; else printf $'WARNING: Bad Hash!\n' && exit; fi +# if gpg --digest-algo SHA256 --verify /tmp/haveno.deb.sig >/dev/null 2>&1; then printf $'PACKAGE file has a VALID signature!\n' && mkdir -p /usr/share/desktop-directories && apt install -y /tmp/haveno*.deb; else printf $'PACKAGE failed signature check\n' && sleep 5 && exit 1; fi ``` ###### Verify Jar ```shell -# if [[ $(cat /tmp/desktop*.SHA-256) =~ $(sha256sum /opt/haveno/lib/app/desktop*.jar | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && printf 'Happy trading!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi +# if [[ $(cat /tmp/haveno-jar.SHA-256) =~ $(sha256sum /opt/haveno/lib/app/desktop*.jar | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && printf 'Happy trading!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi ``` #### *TemplateVM Building From Source via `git` Repository (Scripted)* diff --git a/scripts/install_whonix_qubes/README.md b/scripts/install_whonix_qubes/README.md index 72670e41ca..21b6beb468 100644 --- a/scripts/install_whonix_qubes/README.md +++ b/scripts/install_whonix_qubes/README.md @@ -25,17 +25,17 @@ $ bash 0.0-dom0.sh && bash 0.1-dom0.sh && bash 0.2-dom0.sh ## **Build TemplateVM** -### *Via Binary Archive* +### *Via Package* #### **In `haveno-template` `TemplateVM`:** ```shell -% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" ```

      Example:

      ```shell -% sudo bash 1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno-linux-deb.zip" "ABAF11C65A2970B130ABE3C479BE3E4300411886" +% sudo bash 1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb" "ABAF11C65A2970B130ABE3C479BE3E4300411886" ``` ### *Via Source* diff --git a/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh b/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh index f1ab43ae1b..2dff819eaa 100644 --- a/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh +++ b/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh @@ -3,8 +3,8 @@ function remote { - if [[ -z $PRECOMPILED_URL || -z $FINGERPRINT ]]; then - printf "\nNo arguments provided!\n\nThis script requires two arguments to be provided:\nBinary URL & PGP Fingerprint\n\nPlease review documentation and try again.\n\nExiting now ...\n" + if [[ -z $PACKAGE_URL || -z $FINGERPRINT ]]; then + printf "\nNo arguments provided!\n\nThis script requires two arguments to be provided:\nPackage URL & PGP Fingerprint\n\nPlease review documentation and try again.\n\nExiting now ...\n" exit 1 fi ## Update & Upgrade @@ -32,12 +32,11 @@ function remote { ## Define URL & PGP Fingerprint etc. vars: - user_url=$PRECOMPILED_URL + user_url=$PACKAGE_URL base_url=$(printf ${user_url} | awk -F'/' -v OFS='/' '{$NF=""}1') expected_fingerprint=$FINGERPRINT - binary_filename=$(awk -F'/' '{ print $NF }' <<< "$user_url") - package_filename="haveno.deb" - signature_filename="${binary_filename}.sig" + package_filename=$(awk -F'/' '{ print $NF }' <<< "$user_url") + signature_filename="${package_filename}.sig" key_filename="$(printf "$expected_fingerprint" | tr -d ' ' | sed -E 's/.*(................)/\1/' )".asc wget_flags="--tries=10 --timeout=10 --waitretry=5 --retry-connrefused --show-progress" @@ -46,7 +45,6 @@ function remote { printf "\nUser URL=$user_url\n" printf "\nBase URL=$base_url\n" printf "\nFingerprint=$expected_fingerprint\n" - printf "\nBinary Name=$binary_filename\n" printf "\nPackage Name=$package_filename\n" printf "\nSig Filename=$signature_filename\n" printf "\nKey Filename=$key_filename\n" @@ -94,7 +92,7 @@ function remote { ## Verify the downloaded binary with the signature: echo_blue "Verifying the signature of the downloaded file ..." if gpg --digest-algo SHA256 --verify "${signature_filename}" >/dev/null 2>&1; then - 7z x "${binary_filename}" && mv haveno*.deb "${package_filename}"; + mkdir -p /usr/share/desktop-directories; else echo_red "Verification failed!" && sleep 5 exit 1; fi @@ -172,7 +170,7 @@ if ! [[ $# -eq 2 || $# -eq 3 ]] ; then fi if [[ $# -eq 2 ]] ; then - PRECOMPILED_URL=$1 + PACKAGE_URL=$1 FINGERPRINT=$2 remote fi diff --git a/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh b/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh index 11582a8314..52b4950ef6 100644 --- a/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh +++ b/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh @@ -1,5 +1,5 @@ #!/bin/zsh -## ./haveno-on-qubes/scripts/3.0-haveno-appvm_taker.sh +## ./haveno-on-qubes/scripts/3.0-haveno-appvm_taker.sh ## Function to print messages in blue: echo_blue() { @@ -42,7 +42,7 @@ whonix_firewall ### Create Desktop Launcher: echo_blue "Creating desktop launcher ..." mkdir -p /home/$(ls /home)/\.local/share/applications -sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --torControlUseSafeCookieAuth --torControlCookieFile=/var/run/tor/control.authcookie --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop chown -R $(ls /home):$(ls /home) /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop From 4c30e4625bfb7fc7c5a8939d3a57b05e81fa2ba9 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 25 May 2025 08:57:32 -0400 Subject: [PATCH 285/371] improve error message when offer's arbitrator is not registered --- .../haveno/core/offer/OfferFilterService.java | 38 ++++++++++--------- .../haveno/core/offer/OpenOfferManager.java | 2 +- .../resources/i18n/displayStrings.properties | 2 +- .../i18n/displayStrings_cs.properties | 2 +- .../i18n/displayStrings_tr.properties | 2 +- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OfferFilterService.java b/core/src/main/java/haveno/core/offer/OfferFilterService.java index e64a1ee6eb..298fb51bcd 100644 --- a/core/src/main/java/haveno/core/offer/OfferFilterService.java +++ b/core/src/main/java/haveno/core/offer/OfferFilterService.java @@ -127,6 +127,9 @@ public class OfferFilterService { if (isMyInsufficientTradeLimit(offer)) { return Result.IS_MY_INSUFFICIENT_TRADE_LIMIT; } + if (!hasValidArbitrator(offer)) { + return Result.ARBITRATOR_NOT_VALIDATED; + } if (!hasValidSignature(offer)) { return Result.SIGNATURE_NOT_VALIDATED; } @@ -215,27 +218,28 @@ public class OfferFilterService { return result; } - private boolean hasValidSignature(Offer offer) { + private boolean hasValidArbitrator(Offer offer) { + Arbitrator arbitrator = getArbitrator(offer); + return arbitrator != null; + } - // get accepted arbitrator by address + private Arbitrator getArbitrator(Offer offer) { + + // get arbitrator by address Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()); + if (arbitrator != null) return arbitrator; - // accepted arbitrator is null if we are the signing arbitrator - if (arbitrator == null && offer.getOfferPayload().getArbitratorSigner() != null) { - Arbitrator thisArbitrator = user.getRegisteredArbitrator(); - if (thisArbitrator != null && thisArbitrator.getNodeAddress().equals(offer.getOfferPayload().getArbitratorSigner())) { - if (thisArbitrator.getNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) arbitrator = thisArbitrator; // TODO: unnecessary to compare arbitrator and p2pservice address? - } else { - - // // otherwise log warning that arbitrator is unregistered - // List arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList()); - // if (!arbitratorAddresses.isEmpty()) { - // log.warn("No arbitrator is registered with offer's signer. offerId={}, arbitrator signer={}, accepted arbitrators={}", offer.getId(), offer.getOfferPayload().getArbitratorSigner(), arbitratorAddresses); - // } - } - } + // check if we are the signing arbitrator + Arbitrator thisArbitrator = user.getRegisteredArbitrator(); + if (thisArbitrator != null && thisArbitrator.getNodeAddress().equals(offer.getOfferPayload().getArbitratorSigner())) return thisArbitrator; - if (arbitrator == null) return false; // invalid arbitrator + // cannot get arbitrator + return null; + } + + private boolean hasValidSignature(Offer offer) { + Arbitrator arbitrator = getArbitrator(offer); + if (arbitrator == null) return false; return HavenoUtils.isArbitratorSignatureValid(offer.getOfferPayload(), arbitrator); } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index a928494c1d..710cb92d54 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1189,7 +1189,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } else if (openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null) { throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signature"); } else if (arbitrator == null) { - throw new IllegalArgumentException("Offer " + openOffer.getId() + " signed by unavailable arbitrator"); + throw new IllegalArgumentException("Offer " + openOffer.getId() + " signed by unregistered arbitrator"); } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { throw new IllegalArgumentException("Offer " + openOffer.getId() + " has invalid arbitrator signature"); } else if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty() || openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty()) { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 9d500aefe5..8133a7b0f0 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -458,7 +458,7 @@ offerbook.warning.requireUpdateToNewVersion=Your version of Haveno is not compat offerbook.warning.offerWasAlreadyUsedInTrade=You cannot take this offer because you already took it earlier. \ It could be that your previous take-offer attempt resulted in a failed trade. -offerbook.warning.arbitratorNotValidated=This offer cannot be taken because the arbitrator is invalid. +offerbook.warning.arbitratorNotValidated=This offer cannot be taken because the arbitrator is not registered. offerbook.warning.signatureNotValidated=This offer cannot be taken because the arbitrator's signature is invalid. offerbook.warning.reserveFundsSpent=This offer cannot be taken because the reserved funds were already spent. diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 16b6a05bba..8ca69463ca 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -442,7 +442,7 @@ offerbook.warning.requireUpdateToNewVersion=Vaše verze Haveno již není kompat offerbook.warning.offerWasAlreadyUsedInTrade=Tuto nabídku nemůžete přijmout, protože jste ji již dříve využili. \ Je možné, že váš předchozí pokus o přijetí nabídky vyústil v neúspěšný obchod. -offerbook.warning.arbitratorNotValidated=Tuto nabídku nelze přijmout, protože rozhodce je neplatný +offerbook.warning.arbitratorNotValidated=Tuto nabídku nelze přijmout, protože arbitr není registrován. offerbook.warning.signatureNotValidated=Tuto nabídku nelze přijmout, protože rozhodce má neplatný podpis offerbook.info.sellAtMarketPrice=Budete prodávat za tržní cenu (aktualizováno každou minutu). diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 28034c72a3..d796e21e20 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -439,7 +439,7 @@ offerbook.warning.requireUpdateToNewVersion=Sizin Haveno sürümünüz artık ti offerbook.warning.offerWasAlreadyUsedInTrade=Bu teklifi alamazsınız çünkü daha önce aldınız. \ Önceki teklif alma girişiminiz başarısız bir ticaretle sonuçlanmış olabilir. -offerbook.warning.arbitratorNotValidated=Bu teklif, hakem geçersiz olduğu için alınamaz +offerbook.warning.arbitratorNotValidated=Bu teklif kabul edilemez çünkü hakem kayıtlı değil. offerbook.warning.signatureNotValidated=Bu teklif, hakemin imzası geçersiz olduğu için alınamaz offerbook.info.sellAtMarketPrice=Piyasa fiyatından satış yapacaksınız (her dakika güncellenir). From 115fa96daf8baf2217b7b216b0b18b78391207a1 Mon Sep 17 00:00:00 2001 From: PromptPunksFauxCough <200402670+PromptPunksFauxCough@users.noreply.github.com> Date: Wed, 28 May 2025 10:25:45 +0000 Subject: [PATCH 286/371] Update exec.sh to leverage Tails Stream-Isolation (#1746) https://tails.net/contribute/design/stream_isolation --- scripts/install_tails/assets/exec.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install_tails/assets/exec.sh b/scripts/install_tails/assets/exec.sh index ad0610a60b..55d821f386 100644 --- a/scripts/install_tails/assets/exec.sh +++ b/scripts/install_tails/assets/exec.sh @@ -59,4 +59,4 @@ fi echo_blue "Starting Haveno..." -/opt/haveno/bin/Haveno --torControlPort 951 --torControlCookieFile=/var/run/tor/control.authcookie --torControlUseSafeCookieAuth --userDataDir=${data_dir} --useTorForXmr=on --socks5ProxyXmrAddress=127.0.0.1:9050 +/opt/haveno/bin/Haveno --torControlPort 951 --torControlCookieFile=/var/run/tor/control.authcookie --torControlUseSafeCookieAuth --userDataDir=${data_dir} --useTorForXmr=on --socks5ProxyXmrAddress=127.0.0.1:9062 From ddab1702104493655c25758282757e10cb3d76f6 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 28 May 2025 08:56:34 -0400 Subject: [PATCH 287/371] remove currency codes from crypto names --- .../main/java/haveno/asset/tokens/TetherUSDERC20.java | 2 +- .../main/java/haveno/asset/tokens/TetherUSDTRC20.java | 2 +- .../main/java/haveno/asset/tokens/USDCoinERC20.java | 2 +- .../main/java/haveno/core/locale/CryptoCurrency.java | 2 +- .../src/main/java/haveno/core/locale/CurrencyUtil.java | 10 +++++----- .../java/haveno/core/locale/TraditionalCurrency.java | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java b/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java index 1afb7ff1f2..ffbcac2cd3 100644 --- a/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java +++ b/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java @@ -6,6 +6,6 @@ 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/haveno/core/provider/price/PriceProvider.java:getAll() - super("Tether USD (ERC20)", "USDT-ERC20"); + super("Tether USD", "USDT-ERC20"); } } diff --git a/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java b/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java index c5669d126a..c12bb37442 100644 --- a/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java +++ b/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java @@ -6,6 +6,6 @@ public class TetherUSDTRC20 extends Trc20Token { public TetherUSDTRC20() { // If you add a new USDT variant or want to change this ticker symbol you should also look here: // core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll() - super("Tether USD (TRC20)", "USDT-TRC20"); + super("Tether USD", "USDT-TRC20"); } } diff --git a/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java index a65c021df9..cb371bd221 100644 --- a/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java +++ b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java @@ -22,6 +22,6 @@ import haveno.asset.Erc20Token; public class USDCoinERC20 extends Erc20Token { public USDCoinERC20() { - super("USD Coin (ERC20)", "USDC-ERC20"); + super("USD Coin", "USDC-ERC20"); } } diff --git a/core/src/main/java/haveno/core/locale/CryptoCurrency.java b/core/src/main/java/haveno/core/locale/CryptoCurrency.java index 6c46c9d2b3..feabaf1943 100644 --- a/core/src/main/java/haveno/core/locale/CryptoCurrency.java +++ b/core/src/main/java/haveno/core/locale/CryptoCurrency.java @@ -54,7 +54,7 @@ public final class CryptoCurrency extends TradeCurrency { public static CryptoCurrency fromProto(protobuf.TradeCurrency proto) { return new CryptoCurrency(proto.getCode(), - proto.getName(), + CurrencyUtil.getNameByCode(proto.getCode()), proto.getCryptoCurrency().getIsAsset()); } diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java index 6ed42b6234..911bb65d96 100644 --- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java +++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java @@ -66,7 +66,7 @@ import static java.lang.String.format; @Slf4j public class CurrencyUtil { public static void setup() { - setBaseCurrencyCode("XMR"); + setBaseCurrencyCode(baseCurrencyCode); } private static final AssetRegistry assetRegistry = new AssetRegistry(); @@ -200,10 +200,10 @@ public class CurrencyUtil { result.add(new CryptoCurrency("BCH", "Bitcoin Cash")); result.add(new CryptoCurrency("ETH", "Ether")); result.add(new CryptoCurrency("LTC", "Litecoin")); - result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin (ERC20)")); - result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)")); - result.add(new CryptoCurrency("USDT-TRC20", "Tether USD (TRC20)")); - result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)")); + result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin")); + result.add(new CryptoCurrency("USDT-ERC20", "Tether USD")); + result.add(new CryptoCurrency("USDT-TRC20", "Tether USD")); + result.add(new CryptoCurrency("USDC-ERC20", "USD Coin")); result.sort(TradeCurrency::compareTo); return result; } diff --git a/core/src/main/java/haveno/core/locale/TraditionalCurrency.java b/core/src/main/java/haveno/core/locale/TraditionalCurrency.java index 1ab491467e..cc42342abb 100644 --- a/core/src/main/java/haveno/core/locale/TraditionalCurrency.java +++ b/core/src/main/java/haveno/core/locale/TraditionalCurrency.java @@ -86,7 +86,7 @@ public final class TraditionalCurrency extends TradeCurrency { } public static TraditionalCurrency fromProto(protobuf.TradeCurrency proto) { - return new TraditionalCurrency(proto.getCode(), proto.getName()); + return new TraditionalCurrency(proto.getCode(), CurrencyUtil.getNameByCode(proto.getCode())); } From a37654e116cf11c6c67e295610c97163b7109fa9 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 28 May 2025 09:18:20 -0400 Subject: [PATCH 288/371] arbitrator transactions show maker and taker fees --- core/src/main/resources/i18n/displayStrings.properties | 2 ++ .../main/funds/transactions/TransactionsListItem.java | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 8133a7b0f0..d3ce81f9e1 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1138,6 +1138,8 @@ funds.tx.disputeLost=Lost dispute case: {0} funds.tx.collateralForRefund=Refund collateral: {0} funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} funds.tx.refund=Refund from arbitration: {0} +funds.tx.makerTradeFee=Maker fee: {0} +funds.tx.takerTradeFee=Taker fee: {0} funds.tx.unknown=Unknown reason: {0} funds.tx.noFundsFromDispute=No refund from dispute funds.tx.receivedFunds=Received funds diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java index b325b04efa..11a013e42f 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java @@ -22,6 +22,7 @@ import com.google.common.base.Suppliers; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; +import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Tradable; import haveno.core.trade.Trade; @@ -159,6 +160,13 @@ public class TransactionsListItem { } } else { details = Res.get("funds.tx.unknown", tradeId); + if (trade instanceof ArbitratorTrade) { + if (txId.equals(trade.getMaker().getDepositTxHash())) { + details = Res.get("funds.tx.makerTradeFee", tradeId); + } else if (txId.equals(trade.getTaker().getDepositTxHash())) { + details = Res.get("funds.tx.takerTradeFee", tradeId); + } + } } } } From 5d5eb649c6dc5c6b2c6c19ce6d6ff800e319e978 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 28 May 2025 10:36:46 -0400 Subject: [PATCH 289/371] log possible DoS attack --- .../main/java/haveno/network/p2p/network/Connection.java | 6 +++--- .../network/p2p/peers/keepalive/KeepAliveHandler.java | 2 +- .../network/p2p/peers/peerexchange/PeerExchangeHandler.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/p2p/src/main/java/haveno/network/p2p/network/Connection.java b/p2p/src/main/java/haveno/network/p2p/network/Connection.java index 79df171470..02218f7249 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/Connection.java +++ b/p2p/src/main/java/haveno/network/p2p/network/Connection.java @@ -656,7 +656,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) { if (logReport) { - if (numThrottledInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", numThrottledInvalidRequestReports); + if (numThrottledInvalidRequestReports > 0) log.warn("Possible DoS attack detected. We received {} other reports of invalid requests since the last log entry", numThrottledInvalidRequestReports); numThrottledInvalidRequestReports = 0; lastLoggedInvalidRequestReportTs = System.currentTimeMillis(); } @@ -942,7 +942,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { boolean doLog = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; if (doLog) { log.warn(msg); - if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings); + if (numThrottledWarnings > 0) log.warn("Possible DoS attack detected. {} warnings were throttled since the last log entry", numThrottledWarnings); numThrottledWarnings = 0; lastLoggedWarningTs = System.currentTimeMillis(); } else { @@ -954,7 +954,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { boolean doLog = System.currentTimeMillis() - lastLoggedInfoTs > LOG_THROTTLE_INTERVAL_MS; if (doLog) { log.info(msg); - if (numThrottledInfos > 0) log.info("{} info logs were throttled since the last log entry", numThrottledInfos); + if (numThrottledInfos > 0) log.info("Possible DoS attack detected. {} info logs were throttled since the last log entry", numThrottledInfos); numThrottledInfos = 0; lastLoggedInfoTs = System.currentTimeMillis(); } else { diff --git a/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java index 07ea9397a5..bcc4100ff6 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java @@ -173,7 +173,7 @@ class KeepAliveHandler implements MessageListener { boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; if (logWarning) { log.warn(msg); - if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings); + if (numThrottledWarnings > 0) log.warn("Possible DoS attack detected. {} warnings were throttled since the last log entry", numThrottledWarnings); numThrottledWarnings = 0; lastLoggedWarningTs = System.currentTimeMillis(); } else { diff --git a/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java index 0b10307ccb..e0eb7a6f33 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java @@ -222,7 +222,7 @@ class PeerExchangeHandler implements MessageListener { boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; if (logWarning) { log.warn(msg); - if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings); + if (numThrottledWarnings > 0) log.warn("Possible DoS attack detected. {} warnings were throttled since the last log entry", numThrottledWarnings); numThrottledWarnings = 0; lastLoggedWarningTs = System.currentTimeMillis(); } else { From 8f310469da5fe1f725061870adabd9de118b2b9c Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Wed, 28 May 2025 10:28:58 -0400 Subject: [PATCH 290/371] fix npe with japan bank account --- .../main/java/haveno/core/payment/JapanBankData.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/haveno/core/payment/JapanBankData.java b/core/src/main/java/haveno/core/payment/JapanBankData.java index 9181e0f48a..c8919acec0 100644 --- a/core/src/main/java/haveno/core/payment/JapanBankData.java +++ b/core/src/main/java/haveno/core/payment/JapanBankData.java @@ -18,8 +18,7 @@ package haveno.core.payment; import com.google.common.collect.ImmutableMap; -import com.google.inject.Inject; -import haveno.core.user.Preferences; +import haveno.core.trade.HavenoUtils; import java.util.ArrayList; import java.util.List; @@ -47,13 +46,6 @@ import java.util.Map; public class JapanBankData { - private static String userLanguage; - - @Inject - JapanBankData(Preferences preferences) { - userLanguage = preferences.getUserLanguage(); - } - /* Returns the main list of ~500 banks in Japan with bank codes, but since 90%+ of people will be using one of ~30 major banks, @@ -793,7 +785,7 @@ public class JapanBankData { // don't localize these strings into all languages, // all we want is either Japanese or English here. public static String getString(String id) { - boolean ja = userLanguage.equals("ja"); + boolean ja = HavenoUtils.preferences.getUserLanguage().equals("ja"); switch (id) { case "bank": From 85ee6787cda0da511469e38279fd403bfce03442 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 29 May 2025 16:29:32 -0400 Subject: [PATCH 291/371] fix npe on report dispute button with null payment account --- .../desktop/main/support/dispute/DisputeView.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java index 5ac5f03328..5af027c16c 100644 --- a/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java @@ -739,11 +739,13 @@ public abstract class DisputeView extends ActivatableView implements .append(winner) .append(")\n"); - String buyerPaymentAccountPayload = Utilities.toTruncatedString( - firstDispute.getBuyerPaymentAccountPayload().getPaymentDetails(). + String buyerPaymentAccountPayload = firstDispute.getBuyerPaymentAccountPayload() == null ? null : + Utilities.toTruncatedString( + firstDispute.getBuyerPaymentAccountPayload().getPaymentDetails(). replace("\n", " ").replace(";", "."), 100); - String sellerPaymentAccountPayload = Utilities.toTruncatedString( - firstDispute.getSellerPaymentAccountPayload().getPaymentDetails() + String sellerPaymentAccountPayload = firstDispute.getSellerPaymentAccountPayload() == null ? null : + Utilities.toTruncatedString( + firstDispute.getSellerPaymentAccountPayload().getPaymentDetails() .replace("\n", " ").replace(";", "."), 100); String buyerNodeAddress = contract.getBuyerNodeAddress().getFullAddress(); String sellerNodeAddress = contract.getSellerNodeAddress().getFullAddress(); From dd65cdca13effc0fe899d637b115f806de08c83f Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 29 May 2025 17:07:20 -0400 Subject: [PATCH 292/371] fix custom amounts for dispute result --- .../windows/DisputeSummaryWindow.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java index ebf1c25309..5004d240d8 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -25,6 +25,7 @@ import haveno.common.handlers.ResultHandler; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.core.api.CoreDisputesService; +import haveno.core.api.CoreDisputesService.PayoutSuggestion; import haveno.core.locale.Res; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; @@ -138,6 +139,7 @@ public class DisputeSummaryWindow extends Overlay { public void show(Dispute dispute) { this.dispute = dispute; this.trade = tradeManager.getTrade(dispute.getTradeId()); + this.payoutSuggestion = null; rowIndex = -1; width = 1150; @@ -243,7 +245,6 @@ public class DisputeSummaryWindow extends Overlay { reasonWasPeerWasLateRadioButton.setDisable(true); reasonWasTradeAlreadySettledRadioButton.setDisable(true); - applyPayoutAmounts(tradeAmountToggleGroup.selectedToggleProperty().get()); applyTradeAmountRadioButtonStates(); } @@ -724,6 +725,10 @@ public class DisputeSummaryWindow extends Overlay { private void applyTradeAmountRadioButtonStates() { + if (payoutSuggestion == null) { + payoutSuggestion = getPayoutSuggestionFromDisputeResult(); + } + BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmountBeforeCost(); BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmountBeforeCost(); @@ -748,4 +753,20 @@ public class DisputeSummaryWindow extends Overlay { break; } } + + // TODO: Persist the payout suggestion to DisputeResult like Bisq upstream? + // That would be a better design, but it's not currently needed. + private PayoutSuggestion getPayoutSuggestionFromDisputeResult() { + if (disputeResult.getBuyerPayoutAmountBeforeCost().equals(BigInteger.ZERO)) { + return PayoutSuggestion.SELLER_GETS_ALL; + } else if (disputeResult.getSellerPayoutAmountBeforeCost().equals(BigInteger.ZERO)) { + return PayoutSuggestion.BUYER_GETS_ALL; + } else if (disputeResult.getBuyerPayoutAmountBeforeCost().equals(trade.getAmount().add(trade.getBuyer().getSecurityDeposit()))) { + return PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT; + } else if (disputeResult.getSellerPayoutAmountBeforeCost().equals(trade.getAmount().add(trade.getSeller().getSecurityDeposit()))) { + return PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT; + } else { + return PayoutSuggestion.CUSTOM; + } + } } From 5b04eb17a2f6ad259bebaeee5fb59697ea526734 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 30 May 2025 09:54:20 -0400 Subject: [PATCH 293/371] improve error messages when deposit txs are missing --- .../trade/protocol/tasks/ProcessPaymentReceivedMessage.java | 2 +- .../trade/protocol/tasks/ProcessPaymentSentMessage.java | 2 +- core/src/main/resources/i18n/displayStrings.properties | 6 +----- core/src/main/resources/i18n/displayStrings_cs.properties | 6 +----- core/src/main/resources/i18n/displayStrings_de.properties | 2 +- core/src/main/resources/i18n/displayStrings_es.properties | 2 +- core/src/main/resources/i18n/displayStrings_fa.properties | 2 +- core/src/main/resources/i18n/displayStrings_fr.properties | 2 +- core/src/main/resources/i18n/displayStrings_it.properties | 2 +- core/src/main/resources/i18n/displayStrings_ja.properties | 2 +- .../src/main/resources/i18n/displayStrings_pt-br.properties | 2 +- core/src/main/resources/i18n/displayStrings_pt.properties | 2 +- core/src/main/resources/i18n/displayStrings_ru.properties | 2 +- core/src/main/resources/i18n/displayStrings_th.properties | 2 +- core/src/main/resources/i18n/displayStrings_tr.properties | 6 +----- core/src/main/resources/i18n/displayStrings_vi.properties | 2 +- .../main/resources/i18n/displayStrings_zh-hans.properties | 2 +- .../main/resources/i18n/displayStrings_zh-hant.properties | 2 +- 18 files changed, 18 insertions(+), 30 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java index 5d55bbaea7..331494f767 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java @@ -85,7 +85,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask { if (!trade.isDepositsUnlocked()) { trade.syncAndPollWallet(); if (!trade.isDepositsUnlocked()) { - throw new RuntimeException("Cannot process PaymentReceivedMessage until wallet sees that deposits are unlocked for " + trade.getClass().getSimpleName() + " " + trade.getId()); + throw new RuntimeException("Cannot process PaymentReceivedMessage until the trade wallet sees that the deposits are unlocked for " + trade.getClass().getSimpleName() + " " + trade.getId()); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java index 1f99d64806..7bdd6659ea 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java @@ -52,7 +52,7 @@ public class ProcessPaymentSentMessage extends TradeTask { if (!trade.isDepositsConfirmed()) { trade.syncAndPollWallet(); if (!trade.isDepositsConfirmed()) { - throw new RuntimeException("Cannot process PaymentSentMessage until wallet sees that deposits are confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId()); + throw new RuntimeException("Cannot process PaymentSentMessage until the trade wallet sees that the deposits are confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId()); } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index d3ce81f9e1..c214a016dd 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -994,11 +994,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee trans Without this tx, the trade cannot be completed. No funds have been locked. \ Your offer is still available to other traders, so you have not lost the maker fee. \ You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\n\ - Without this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. \ - You can make a request to be reimbursed the trade fee here: \ - [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ - Feel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=A deposit transaction is missing.\n\nThis transaction is required to complete the trade. Please ensure your wallet is fully synchronized with the Monero blockchain.\n\nYou can move this trade to the "Failed Trades" section to deactivate it. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, \ but funds have been locked in the deposit transaction.\n\n\ Please do NOT send the traditional or cryptocurrency payment to the XMR seller, because without the delayed payout tx, arbitration \ diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 8ca69463ca..ce414afabd 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -959,11 +959,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=Chybí poplatek příjemce Bez tohoto tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky. Vaše nabídka je \ stále k dispozici dalším obchodníkům, takže jste neztratili poplatek za vytvoření. \ Tento obchod můžete přesunout do neúspěšných obchodů. -portfolio.pending.failedTrade.missingDepositTx=Vkladová transakce (transakce 2-of-2 multisig) chybí.\n\n\ - Bez této tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky, ale byl zaplacen váš obchodní poplatek. \ - Zde můžete požádat o vrácení obchodního poplatku: \ - [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ - Klidně můžete přesunout tento obchod do neúspěšných obchodů. +portfolio.pending.failedTrade.missingDepositTx=Chybí vkladová transakce.\n\nTato transakce je nutná k dokončení obchodu. Ujistěte se, že je vaše peněženka plně synchronizována s blockchainem Monero.\n\nTento obchod můžete přesunout do sekce „Neúspěšné obchody“ pro jeho deaktivaci. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Zpožděná výplatní transakce chybí, ale prostředky byly uzamčeny v vkladové transakci.\n\n\ Nezasílejte prosím fiat nebo crypto platbu prodejci XMR, protože bez odložené platby tx nelze zahájit arbitráž. \ diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 4663384a66..43b664b1cf 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -820,7 +820,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Sie haben bereits akzept portfolio.pending.failedTrade.taker.missingTakerFeeTx=Die Transaktion der Abnehmer-Gebühr fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt und keine Handelsgebühr wurde bezahlt. Sie können diesen Handel zu den fehlgeschlagenen Händeln verschieben. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Die Transaktion der Abnehmer-Gebühr fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt. Ihr Angebot ist für andere Händler weiterhin verfügbar. Sie haben die Ersteller-Gebühr also nicht verloren. Sie können diesen Handel zu den fehlgeschlagenen Händeln verschieben. -portfolio.pending.failedTrade.missingDepositTx=Die Einzahlungstransaktion (die 2-of-2 Multisig-Transaktion) fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt aber die Handels-Gebühr wurde bezahlt. Sie können eine Anfrage für eine Rückerstattung der Handels-Gebühr hier einreichen: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nSie können diesen Handel gerne zu den fehlgeschlagenen Händeln verschieben. +portfolio.pending.failedTrade.missingDepositTx=Eine Einzahlungstransaktion fehlt.\n\nDiese Transaktion ist erforderlich, um den Handel abzuschließen. Bitte stellen Sie sicher, dass Ihre Wallet vollständig mit der Monero-Blockchain synchronisiert ist.\n\nSie können diesen Handel in den Bereich „Fehlgeschlagene Trades“ verschieben, um ihn zu deaktivieren. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nBitte schicken Sie KEINE Geld-(Traditional-) oder Crypto-Zahlungen an den XMR Verkäufer, weil ohne die verzögerte Auszahlungstransaktion später kein Schlichtungsverfahren eröffnet werden kann. Stattdessen öffnen Sie ein Vermittlungs-Ticket mit Cmd/Strg+o. Der Vermittler sollte vorschlagen, dass beide Handelspartner ihre vollständige Sicherheitskaution zurückerstattet bekommen (und der Verkäufer auch seinen Handels-Betrag). Durch diese Vorgehensweise entsteht kein Sicherheitsrisiko und es geht ausschließlich die Handelsgebühr verloren.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nWenn dem Käufer die verzögerte Auszahlungstransaktion auch fehlt, wird er dazu aufgefordert die Bezahlung NICHT zu schicken und stattdessen ein Vermittlungs-Ticket zu eröffnen. Sie sollten auch ein Vermittlungs-Ticket mit Cmd/Strg+o öffnen.\n\nWenn der Käufer die Zahlung noch nicht geschickt hat, sollte der Vermittler vorschlagen, dass beide Handelspartner ihre Sicherheitskaution vollständig zurückerhalten (und der Verkäufer auch den Handels-Betrag). Anderenfalls sollte der Handels-Betrag an den Käufer gehen.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Während der Ausführung des Handel-Protokolls ist ein Fehler aufgetreten.\n\nFehler: {0}\n\nEs kann sein, dass dieser Fehler nicht gravierend ist und der Handel ganz normal abgeschlossen werden kann. Wenn Sie sich unsicher sind, öffnen Sie ein Vermittlungs-Ticket um den Rat eines Haveno Vermittlers zu erhalten.\n\nWenn der Fehler gravierend war, kann der Handel nicht abgeschlossen werden und Sie haben vielleicht die Handelsgebühr verloren. Sie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index a8f105d6c0..a4001cb075 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -820,7 +820,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Ya ha aceptado portfolio.pending.failedTrade.taker.missingTakerFeeTx=Falta la transacción de tasa de tomador\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos y no se ha pagado ninguna tasa de intercambio. Puede mover esta operación a intercambios fallidos. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Falta la transacción de tasa de tomador de su par.\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos. Su oferta aún está disponible para otros comerciantes, por lo que no ha perdido la tasa de tomador. Puede mover este intercambio a intercambios fallidos. -portfolio.pending.failedTrade.missingDepositTx=Falta la transacción de depósito (la transacción multifirma 2 de 2).\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos, pero se ha pagado su tarifa comercial. Puede hacer una solicitud para que se le reembolse la tarifa comercial aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues].\n\nSiéntase libre de mover esta operación a operaciones fallidas. +portfolio.pending.failedTrade.missingDepositTx=Falta una transacción de depósito.\n\nEsta transacción es necesaria para completar la operación. Por favor, asegúrate de que tu monedero esté completamente sincronizado con la cadena de bloques de Monero.\n\nPuedes mover esta operación a la sección de "Operaciones Fallidas" para desactivarla. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción de pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nNO envíe el pago traditional o crypto al vendedor de XMR, porque sin el tx de pago demorado, no se puede abrir el arbitraje. En su lugar, abra un ticket de mediación con Cmd / Ctrl + o. El mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De esta manera, no hay riesgo en la seguridad y solo se pierden las tarifas comerciales.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción del pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nSi al comprador también le falta la transacción de pago demorado, se le indicará que NO envíe el pago y abra un ticket de mediación. También debe abrir un ticket de mediación con Cmd / Ctrl + o.\n\nSi el comprador aún no ha enviado el pago, el mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De lo contrario, el monto comercial debe ir al comprador.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.errorMsgSet=Hubo un error durante la ejecución del protocolo de intercambio.\n\nError: {0}\n\nPuede ser que este error no sea crítico y que el intercambio se pueda completar normalmente. Si no está seguro, abra un ticket de mediación para obtener consejos de los mediadores de Haveno.\n\nSi el error fue crítico y la operación no se puede completar, es posible que haya perdido su tarifa de operación. Solicite un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:ttps://github.com/bisq-network/support/issues]. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index dd0cbee6d3..891dc297da 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -819,7 +819,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=یک تراکنش واریز مفقود است.\n\nاین تراکنش برای تکمیل معامله لازم است. لطفاً اطمینان حاصل کنید که کیف پول شما به‌طور کامل با بلاک‌چین مونرو همگام‌سازی شده است.\n\nمی‌توانید این معامله را به بخش «معاملات ناموفق» منتقل کنید تا غیرفعال شود. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index f4395ca9c4..0a0e1274ad 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -821,7 +821,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Vous avez déjà accept portfolio.pending.failedTrade.taker.missingTakerFeeTx=Le frais de transaction du preneur est manquant.\n\nSans ce tx, le trade ne peut être complété. Aucun fonds ont été verrouillés et aucun frais de trade a été payé. Vous pouvez déplacer ce trade vers les trade échoués. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Le frais de transaction du pair preneur est manquant.\n\nSans ce tx, le trade ne peut être complété. Aucun fonds ont été verrouillés. Votre offre est toujours valable pour les autres traders, vous n'avez donc pas perdu le frais de maker. Vous pouvez déplacer ce trade vers les trades échoués. -portfolio.pending.failedTrade.missingDepositTx=Cette transaction de marge (transaction multi-signature de 2 à 2) est manquante.\n\nSans ce tx, la transaction ne peut pas être complétée. Aucun fonds n'est bloqué, mais vos frais de transaction sont toujours payés. Vous pouvez lancer une demande de compensation des frais de transaction ici: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] \nN'hésitez pas à déplacer la transaction vers la transaction échouée. +portfolio.pending.failedTrade.missingDepositTx=Une transaction de dépôt est manquante.\n\nCette transaction est nécessaire pour compléter la transaction. Veuillez vous assurer que votre portefeuille est entièrement synchronisé avec la blockchain Monero.\n\nVous pouvez déplacer cette transaction dans la section « Transactions échouées » pour la désactiver. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nVeuillez NE PAS envoyer de Fiat ou d'crypto au vendeur de XMR, car avec le tx de paiement différé, le jugemenbt ne peut être ouvert. À la place, ouvrez un ticket de médiation avec Cmd/Ctrl+O. Le médiateur devrait suggérer que les deux pair reçoivent tous les deux le montant total de leurs dépôts de sécurité (le vendeur aussi doit reçevoir le montant total du trade). De cette manière, il n'y a pas de risque de non sécurité, et seuls les frais du trade sont perdus.\n\nVous pouvez demander le remboursement des frais de trade perdus ici;\n[LIEN:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nSi l'acheteur n'a pas non plus la transaction de paiement différée, il sera informé du fait de ne PAS envoyer le paiement et d'ouvrir un ticket de médiation à la place. Vous devriez aussi ouvrir un ticket de médiation avec Cmd/Ctrl+o.\n\nSi l'acheteur n'a pas encore envoyé le paiement, le médiateur devrait suggérer que les deux pairs reçoivent le montant total de leurs dépôts de sécurité (le vendeur doit aussi reçevoir le montant total du trade). Sinon, le montant du trade revient à l'acheteur.\n\nVous pouvez effectuer une demande de remboursement pour les frais de trade perdus ici: [LIEN:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Il y'a eu une erreur durant l'exécution du protocole de trade.\n\nErreur: {0}\n\nIl est possible que cette erreur ne soit pas critique, et que le trade puisse être complété normalement. Si vous n'en êtes pas sûr, ouvrez un ticket de médiation pour avoir des conseils de la part des médiateurs de Haveno.\n\nSi cette erreur est critique et que le trade ne peut être complété, il est possible que vous ayez perdu le frais du trade. Effectuez une demande de remboursement ici: [LIEN:https://github.com/haveno-dex/haveno/issues] diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 4a5d177c0a..7847646806 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -819,7 +819,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Hai già accettato portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=Manca una transazione di deposito.\n\nQuesta transazione è necessaria per completare lo scambio. Assicurati che il tuo portafoglio sia completamente sincronizzato con la blockchain di Monero.\n\nPuoi spostare questo scambio nella sezione "Scambi Falliti" per disattivarlo. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 3514d37343..9036bd8e2d 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -820,7 +820,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=すでに受け入れて portfolio.pending.failedTrade.taker.missingTakerFeeTx=欠測テイカー手数料のトランザクション。\n\nこのtxがなければ、トレードを完了できません。資金はロックされず、トレード手数料は支払いませんでした。「失敗トレード」へ送ることができます。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=ピアのテイカー手数料のトランザクションは欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでした。あなたのオファーがまだ他の取引者には有効ですので、メイカー手数料は失っていません。このトレードを「失敗トレード」へ送ることができます。 -portfolio.pending.failedTrade.missingDepositTx=入金トランザクション(2-of-2マルチシグトランザクション)は欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでしたが、トレード手数料は支払いました。トレード手数料の返済要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nこのトレードを「失敗トレード」へ送れます。 +portfolio.pending.failedTrade.missingDepositTx=入金トランザクションが見つかりません。\n\nこのトランザクションは取引を完了するために必要です。Moneroブロックチェーンとウォレットが完全に同期されていることを確認してください。\n\nこの取引を「失敗した取引」セクションに移動して、無効化することができます。 portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\nこの法定通貨・アルトコイン支払いをXMR売り手に送信しないで下さい。遅延支払いtxがなければ、係争仲裁は開始されることができません。代りに、「Cmd/Ctrl+o」で調停チケットをオープンして下さい。調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。このような方法でセキュリティーのリスクがなし、トレード手数料のみが失われます。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\n買い手の遅延支払いトランザクションが同じく欠測される場合、相手は支払いを送信せず調停チケットをオープンするように指示されます。同様に「Cmd/Ctrl+o」で調停チケットをオープンするのは賢明でしょう。\n\n買い手はまだ支払いを送信しなかった場合、調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。さもなければ、トレード金額は買い手に支払われるでしょう。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=トレードプロトコルの実行にはエラーが生じました。\n\nエラー: {0}\n\nクリティカル・エラーではない可能性はあり、トレードは普通に完了できるかもしれない。迷う場合は調停チケットをオープンして、Haveno調停者からアドバイスを受けることができます。\n\nクリティカル・エラーでトレードが完了できなかった場合はトレード手数料は失われた可能性があります。失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 03d90cc9fd..f6520e8436 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -822,7 +822,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=Uma transação de depósito está faltando.\n\nEssa transação é necessária para concluir a negociação. Por favor, certifique-se de que sua carteira esteja totalmente sincronizada com a blockchain do Monero.\n\nVocê pode mover esta negociação para a seção "Negociações Falhas" para desativá-la. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 0ec7c93184..c69236f8b1 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -819,7 +819,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=Uma transação de depósito está faltando.\n\nEssa transação é necessária para concluir a negociação. Certifique-se de que sua carteira esteja totalmente sincronizada com a blockchain do Monero.\n\nVocê pode mover esta negociação para a seção "Negociações com Falha" para desativá-la. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 46828f97b1..f5ded7cdb2 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -819,7 +819,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=Отсутствует транзакция депозита.\n\nЭта транзакция необходима для завершения сделки. Пожалуйста, убедитесь, что ваш кошелёк полностью синхронизирован с блокчейном Monero.\n\nВы можете переместить эту сделку в раздел "Неудачные сделки", чтобы деактивировать её. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 56fe9e67c9..d2d89f3b83 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -819,7 +819,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=ธุรกรรมเงินมัดจำหายไป\n\nธุรกรรมนี้จำเป็นสำหรับการดำเนินการซื้อขายให้เสร็จสมบูรณ์ กรุณาตรวจสอบให้แน่ใจว่า Wallet ของคุณได้ซิงค์กับบล็อกเชน Monero อย่างสมบูรณ์แล้ว\n\nคุณสามารถย้ายการซื้อขายนี้ไปยังส่วน "การซื้อขายที่ล้มเหลว" เพื่อปิดการใช้งานได้ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index d796e21e20..426c99500f 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -956,11 +956,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=Karşı tarafın alıcı Bu işlem olmadan, ticaret tamamlanamaz. Hiçbir fon kilitlenmedi. \ Teklifiniz diğer tüccarlar için hala mevcut, bu yüzden üretici ücretini kaybetmediniz. \ Bu ticareti başarısız ticaretler arasına taşıyabilirsiniz. -portfolio.pending.failedTrade.missingDepositTx=Para yatırma işlemi (2-of-2 multisig işlemi) eksik.\n\n\ - Bu işlem olmadan, ticaret tamamlanamaz. Hiçbir fon kilitlenmedi ancak ticaret ücretiniz ödendi. \ - Ticaret ücretinin geri ödenmesi için burada talepte bulunabilirsiniz: \ - [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ - Bu ticareti başarısız ticaretler arasına taşımakta özgürsünüz. +portfolio.pending.failedTrade.missingDepositTx=Bir teminat işlemi eksik.\n\nBu işlem, ticareti tamamlamak için gereklidir. Lütfen cüzdanınızın Monero blok zinciri ile tamamen senkronize olduğundan emin olun.\n\nBu ticareti devre dışı bırakmak için "Başarısız İşlemler" bölümüne taşıyabilirsiniz. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Gecikmiş ödeme işlemi eksik, \ ancak fonlar depozito işleminde kilitlendi.\n\n\ Lütfen geleneksel veya kripto para ödemesini XMR satıcısına göndermeyin, çünkü gecikmiş ödeme işlemi olmadan arabuluculuk \ diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index e6dd802dee..4df8f44b16 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -819,7 +819,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=Một giao dịch ký quỹ đang bị thiếu.\n\nGiao dịch này là bắt buộc để hoàn tất giao dịch. Vui lòng đảm bảo ví của bạn được đồng bộ hoàn toàn với blockchain Monero.\n\nBạn có thể chuyển giao dịch này đến mục "Giao dịch thất bại" để vô hiệu hóa nó. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index f1b086eec3..1e65527036 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -820,7 +820,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=您已经接受了。 portfolio.pending.failedTrade.taker.missingTakerFeeTx=吃单交易费未找到。\n\n如果没有 tx,交易不能完成。没有资金被锁定以及没有支付交易费用。你可以将交易移至失败的交易。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=挂单费交易未找到。\n\n如果没有 tx,交易不能完成。没有资金被锁定以及没有支付交易费用。你可以将交易移至失败的交易。 -portfolio.pending.failedTrade.missingDepositTx=这个保证金交易(2 对 2 多重签名交易)缺失\n\n没有该 tx,交易不能完成。没有资金被锁定但是您的交易手续费仍然已支出。您可以发起一个请求去赔偿改交易手续费在这里:https://github.com/haveno-dex/haveno/issues\n\n请随意的将该交易移至失败交易 +portfolio.pending.failedTrade.missingDepositTx=缺少一笔保证金交易。\n\n该交易是完成交易所必需的。请确保您的钱包已与 Monero 区块链完全同步。\n\n您可以将此交易移动到“失败的交易”部分以将其停用。 portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易缺失,但是资金仍然被锁定在保证金交易中。\n\n请不要给比特币卖家发送法币或数字货币,因为没有延迟交易 tx,不能开启仲裁。使用 Cmd/Ctrl+o开启调解协助。调解员应该建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。这样的话不会有任何的安全问题只会损失交易手续费。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易确实但是资金仍然被锁定在保证金交易中。\n\n如果卖家仍然缺失延迟支付交易,他会接到请勿付款的指示并开启一个调节帮助。你也应该使用 Cmd/Ctrl+O 去打开一个调节协助\n\n如果买家还没有发送付款,调解员应该会建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。否则交易额应该判给买方。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.errorMsgSet=在处理交易协议是发生了一个错误\n\n错误:{0}\n\n这应该不是致命错误,您可以正常的完成交易。如果你仍担忧,打开一个调解协助并从 Haveno 调解员处得到建议。\n\n如果这个错误是致命的那么这个交易就无法完成,你可能会损失交易费。可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 94554bc12f..05f8bb5e8d 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -820,7 +820,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=您已經接受了。 portfolio.pending.failedTrade.taker.missingTakerFeeTx=吃單交易費未找到。\n\n如果沒有 tx,交易不能完成。沒有資金被鎖定以及沒有支付交易費用。你可以將交易移至失敗的交易。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=掛單費交易未找到。\n\n如果沒有 tx,交易不能完成。沒有資金被鎖定以及沒有支付交易費用。你可以將交易移至失敗的交易。 -portfolio.pending.failedTrade.missingDepositTx=這個保證金交易(2 對 2 多重簽名交易)缺失\n\n沒有該 tx,交易不能完成。沒有資金被鎖定但是您的交易手續費仍然已支出。您可以發起一個請求去賠償改交易手續費在這裏:https://github.com/haveno-dex/haveno/issues\n\n請隨意的將該交易移至失敗交易 +portfolio.pending.failedTrade.missingDepositTx=缺少一筆保證金交易。\n\n此交易是完成交易所必需的。請確保您的錢包已與 Monero 區塊鏈完全同步。\n\n您可以將此交易移至「失敗的交易」區段以停用它。 portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易缺失,但是資金仍然被鎖定在保證金交易中。\n\n請不要給比特幣賣家發送法幣或數字貨幣,因為沒有延遲交易 tx,不能開啟仲裁。使用 Cmd/Ctrl+o開啟調解協助。調解員應該建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。這樣的話不會有任何的安全問題只會損失交易手續費。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易確實但是資金仍然被鎖定在保證金交易中。\n\n如果賣家仍然缺失延遲支付交易,他會接到請勿付款的指示並開啟一個調節幫助。你也應該使用 Cmd/Ctrl+O 去打開一個調節協助\n\n如果買家還沒有發送付款,調解員應該會建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。否則交易額應該判給買方。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.errorMsgSet=在處理交易協議是發生了一個錯誤\n\n錯誤:{0}\n\n這應該不是致命錯誤,您可以正常的完成交易。如果你仍擔憂,打開一個調解協助並從 Haveno 調解員處得到建議。\n\n如果這個錯誤是致命的那麼這個交易就無法完成,你可能會損失交易費。可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues From 9dd011afc887e71436434dba6da78c405460f184 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 31 May 2025 09:10:52 -0400 Subject: [PATCH 294/371] poll key images in batches --- .../core/xmr/wallet/XmrKeyImagePoller.java | 95 +++++++++++++------ 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java index 73332dc4fa..606ca29798 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -42,12 +43,15 @@ public class XmrKeyImagePoller { private MoneroDaemon daemon; private long refreshPeriodMs; + private Object lock = new Object(); private Map> keyImageGroups = new HashMap>(); + private LinkedHashSet keyImagePollQueue = new LinkedHashSet<>(); private Set listeners = new HashSet(); private TaskLooper looper; private Map lastStatuses = new HashMap(); private boolean isPolling = false; private Long lastLogPollErrorTimestamp; + private static final int MAX_POLL_SIZE = 200; /** * Construct the listener. @@ -74,8 +78,10 @@ public class XmrKeyImagePoller { * @param listener - the listener to add */ public void addListener(XmrKeyImageListener listener) { - listeners.add(listener); - refreshPolling(); + synchronized (lock) { + listeners.add(listener); + refreshPolling(); + } } /** @@ -84,9 +90,11 @@ public class XmrKeyImagePoller { * @param listener - the listener to remove */ public void removeListener(XmrKeyImageListener listener) { - if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered"); - listeners.remove(listener); - refreshPolling(); + synchronized (lock) { + if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered"); + listeners.remove(listener); + refreshPolling(); + } } /** @@ -140,10 +148,11 @@ public class XmrKeyImagePoller { * @param keyImages - key images to listen to */ public void addKeyImages(Collection keyImages, String groupId) { - synchronized (this.keyImageGroups) { + synchronized (lock) { if (!keyImageGroups.containsKey(groupId)) keyImageGroups.put(groupId, new HashSet()); Set keyImagesGroup = keyImageGroups.get(groupId); keyImagesGroup.addAll(keyImages); + keyImagePollQueue.addAll(keyImages); refreshPolling(); } } @@ -154,17 +163,16 @@ public class XmrKeyImagePoller { * @param keyImages - key images to unlisten to */ public void removeKeyImages(Collection keyImages, String groupId) { - synchronized (keyImageGroups) { + synchronized (lock) { Set keyImagesGroup = keyImageGroups.get(groupId); if (keyImagesGroup == null) return; keyImagesGroup.removeAll(keyImages); if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId); Set allKeyImages = getKeyImages(); - synchronized (lastStatuses) { - for (String keyImage : keyImages) { - if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) { - lastStatuses.remove(keyImage); - } + for (String keyImage : keyImages) { + if (!allKeyImages.contains(keyImage)) { + keyImagePollQueue.remove(keyImage); + lastStatuses.remove(keyImage); } } refreshPolling(); @@ -172,16 +180,15 @@ public class XmrKeyImagePoller { } public void removeKeyImages(String groupId) { - synchronized (keyImageGroups) { + synchronized (lock) { Set keyImagesGroup = keyImageGroups.get(groupId); if (keyImagesGroup == null) return; keyImageGroups.remove(groupId); Set allKeyImages = getKeyImages(); - synchronized (lastStatuses) { - for (String keyImage : keyImagesGroup) { - if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) { - lastStatuses.remove(keyImage); - } + for (String keyImage : keyImagesGroup) { + if (!allKeyImages.contains(keyImage)) { + keyImagePollQueue.remove(keyImage); + lastStatuses.remove(keyImage); } } refreshPolling(); @@ -192,11 +199,10 @@ public class XmrKeyImagePoller { * Clear the key images which stops polling. */ public void clearKeyImages() { - synchronized (keyImageGroups) { + synchronized (lock) { keyImageGroups.clear(); - synchronized (lastStatuses) { - lastStatuses.clear(); - } + keyImagePollQueue.clear(); + lastStatuses.clear(); refreshPolling(); } } @@ -208,7 +214,7 @@ public class XmrKeyImagePoller { * @return true if the key is spent, false if unspent, null if unknown */ public Boolean isSpent(String keyImage) { - synchronized (lastStatuses) { + synchronized (lock) { if (!lastStatuses.containsKey(keyImage)) return null; return XmrKeyImagePoller.isSpent(lastStatuses.get(keyImage)); } @@ -231,7 +237,7 @@ public class XmrKeyImagePoller { * @return the last known spent status of the key image */ public MoneroKeyImageSpentStatus getLastSpentStatus(String keyImage) { - synchronized (lastStatuses) { + synchronized (lock) { return lastStatuses.get(keyImage); } } @@ -244,7 +250,7 @@ public class XmrKeyImagePoller { // fetch spent statuses List spentStatuses = null; - List keyImages = new ArrayList(getKeyImages()); + List keyImages = new ArrayList(getNextKeyImageBatch()); try { spentStatuses = keyImages.isEmpty() ? new ArrayList() : daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter } catch (Exception e) { @@ -257,10 +263,20 @@ public class XmrKeyImagePoller { return; } - // collect changed statuses + // process spent statuses Map changedStatuses = new HashMap(); - synchronized (lastStatuses) { - for (int i = 0; i < spentStatuses.size(); i++) { + synchronized (lock) { + Set allKeyImages = getKeyImages(); + for (int i = 0; i < keyImages.size(); i++) { + + // skip if key image is removed + if (!allKeyImages.contains(keyImages.get(i))) continue; + + // move key image to the end of the queue + keyImagePollQueue.remove(keyImages.get(i)); + keyImagePollQueue.add(keyImages.get(i)); + + // update spent status if (spentStatuses.get(i) != lastStatuses.get(keyImages.get(i))) { lastStatuses.put(keyImages.get(i), spentStatuses.get(i)); changedStatuses.put(keyImages.get(i), spentStatuses.get(i)); @@ -270,14 +286,18 @@ public class XmrKeyImagePoller { // announce changes if (!changedStatuses.isEmpty()) { - for (XmrKeyImageListener listener : new ArrayList(listeners)) { + List listeners; + synchronized (lock) { + listeners = new ArrayList(this.listeners); + } + for (XmrKeyImageListener listener : listeners) { listener.onSpentStatusChanged(changedStatuses); } } } private void refreshPolling() { - synchronized (keyImageGroups) { + synchronized (lock) { setIsPolling(!getKeyImages().isEmpty() && listeners.size() > 0); } } @@ -296,11 +316,24 @@ public class XmrKeyImagePoller { private Set getKeyImages() { Set allKeyImages = new HashSet(); - synchronized (keyImageGroups) { + synchronized (lock) { for (Set keyImagesGroup : keyImageGroups.values()) { allKeyImages.addAll(keyImagesGroup); } } return allKeyImages; } + + private List getNextKeyImageBatch() { + synchronized (lock) { + List keyImageBatch = new ArrayList<>(); + int count = 0; + for (String keyImage : keyImagePollQueue) { + if (count >= MAX_POLL_SIZE) break; + keyImageBatch.add(keyImage); + count++; + } + return keyImageBatch; + } + } } From 98130499a730c9c4156497b679a272c7ba8d1522 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 31 May 2025 09:14:09 -0400 Subject: [PATCH 295/371] fix log of trade miner fee --- core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index f2d16943d1..5a191068e1 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -767,7 +767,7 @@ public class XmrWalletService extends XmrWalletBase { // verify miner fee BigInteger minerFeeEstimate = getFeeEstimate(tx.getWeight()); HavenoUtils.verifyMinerFee(minerFeeEstimate, tx.getFee()); - log.info("Trade miner fee {} is within tolerance"); + log.info("Trade miner fee {} is within tolerance", tx.getFee()); // verify proof to fee address BigInteger actualTradeFee = BigInteger.ZERO; From 3648c1eb0ed5f6d4e3912c3de4d1ba2ca576ee36 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 1 Jun 2025 15:15:35 -0400 Subject: [PATCH 296/371] skip polling if shut down started after acquiring lock --- .../main/java/haveno/core/trade/Trade.java | 3 +++ .../core/xmr/wallet/XmrWalletService.java | 21 +++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index aa2330a78f..29df35e45f 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2637,6 +2637,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // poll wallet try { + // skip if shut down started + if (isShutDownStarted) return; + // skip if payout unlocked if (isPayoutUnlocked()) return; diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 5a191068e1..26f300b7c0 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1994,6 +1994,9 @@ public class XmrWalletService extends XmrWalletBase { // poll wallet try { + // skip if shut down started + if (isShutDownStarted) return; + // skip if daemon not synced MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); if (lastInfo == null) { @@ -2059,16 +2062,16 @@ public class XmrWalletService extends XmrWalletBase { pollInProgress = false; } } + saveWalletWithDelay(); + } - // cache wallet info last - synchronized (walletLock) { - if (wallet != null && !isShutDownStarted) { - try { - cacheWalletInfo(); - saveWalletWithDelay(); - } catch (Exception e) { - log.warn("Error caching wallet info: " + e.getMessage() + "\n", e); - } + // cache wallet info last + synchronized (walletLock) { + if (wallet != null && !isShutDownStarted) { + try { + cacheWalletInfo(); + } catch (Exception e) { + log.warn("Error caching wallet info: " + e.getMessage() + "\n", e); } } } From fa375b3cbde403ddda3aa1b6ab9c7b52023ae841 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 1 Jun 2025 17:36:57 -0400 Subject: [PATCH 297/371] process connection messages on main run thread --- p2p/src/main/java/haveno/network/p2p/network/Connection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/src/main/java/haveno/network/p2p/network/Connection.java b/p2p/src/main/java/haveno/network/p2p/network/Connection.java index 02218f7249..2045c04c5b 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/Connection.java +++ b/p2p/src/main/java/haveno/network/p2p/network/Connection.java @@ -872,7 +872,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { log.info("We got a {} from a peer with yet unknown address on connection with uid={}", networkEnvelope.getClass().getSimpleName(), uid); } - ThreadUtils.execute(() -> onMessage(networkEnvelope, this), THREAD_ID); + onMessage(networkEnvelope, this); ThreadUtils.execute(() -> connectionStatistics.addReceivedMsgMetrics(System.currentTimeMillis() - ts, size), THREAD_ID); } } catch (InvalidClassException e) { From 501530d85f3e2fae98a18a766a6bfda4a99fc645 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 2 Jun 2025 08:48:03 -0400 Subject: [PATCH 298/371] read bip39 words once and synchronized --- .../java/haveno/core/trade/HavenoUtils.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index 74caf5d7f2..32a6d76824 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -123,6 +123,7 @@ public class HavenoUtils { private static final BigInteger XMR_AU_MULTIPLIER = new BigInteger("1000000000000"); public static final DecimalFormat XMR_FORMATTER = new DecimalFormat("##############0.000000000000", DECIMAL_FORMAT_SYMBOLS); public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); + private static List bip39Words = new ArrayList(); // shared references TODO: better way to share references? public static HavenoSetup havenoSetup; @@ -298,10 +299,7 @@ public class HavenoUtils { try { // load bip39 words - String fileName = "bip39_english.txt"; - File bip39File = new File(havenoSetup.getConfig().appDataDir, fileName); - if (!bip39File.exists()) FileUtil.resourceToFile(fileName, bip39File); - List bip39Words = Files.readAllLines(bip39File.toPath(), StandardCharsets.UTF_8); + loadBip39Words(); // select words randomly List passphraseWords = new ArrayList(); @@ -315,13 +313,26 @@ public class HavenoUtils { } } + private static synchronized void loadBip39Words() { + if (bip39Words.isEmpty()) { + try { + String fileName = "bip39_english.txt"; + File bip39File = new File(havenoSetup.getConfig().appDataDir, fileName); + if (!bip39File.exists()) FileUtil.resourceToFile(fileName, bip39File); + bip39Words = Files.readAllLines(bip39File.toPath(), StandardCharsets.UTF_8); + } catch (Exception e) { + throw new IllegalStateException("Failed to load BIP39 words", e); + } + } + } + public static String getChallengeHash(String challenge) { if (challenge == null) return null; // tokenize passphrase String[] words = challenge.toLowerCase().split(" "); - // collect first 4 letters of each word, which are unique in bip39 + // collect up to first 4 letters of each word, which are unique in bip39 List prefixes = new ArrayList(); for (String word : words) prefixes.add(word.substring(0, Math.min(word.length(), 4))); From 264e5f436e96ad26c06744565e22d49cbd9e6bed Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 2 Jun 2025 08:48:17 -0400 Subject: [PATCH 299/371] stricter validation for creating private, no deposit offers --- .../main/java/haveno/core/offer/CreateOfferService.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index fab646433b..161bf69a16 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -134,10 +134,12 @@ public class CreateOfferService { // must nullify empty string so contracts match if ("".equals(extraInfo)) extraInfo = null; - // verify buyer as taker security deposit + // verify config for private no deposit offers boolean isBuyerMaker = offerUtil.isBuyOffer(direction); - if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) { - throw new IllegalArgumentException("Buyer as taker deposit is required for public offers"); + if (buyerAsTakerWithoutDeposit || isPrivateOffer) { + if (isBuyerMaker) throw new IllegalArgumentException("Buyer must be taker for private offers without deposit"); + if (!buyerAsTakerWithoutDeposit) throw new IllegalArgumentException("Must set buyer as taker without deposit for private offers"); + if (!isPrivateOffer) throw new IllegalArgumentException("Must set offer to private for buyer as taker without deposit"); } // verify fixed price xor market price with margin From 33a91cf98016c6ffcfdbab652f9a93a4459dac12 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 1 Jun 2025 08:54:04 -0400 Subject: [PATCH 300/371] maker recreates reserve tx then cancels offer on trade nacks --- .../java/haveno/core/offer/OpenOffer.java | 4 + .../haveno/core/offer/OpenOfferManager.java | 6 +- .../java/haveno/core/trade/TradeManager.java | 2 +- .../core/trade/protocol/TradeProtocol.java | 74 ++++++++- .../tasks/MakerRecreateReserveTx.java | 147 ++++++++++++++++++ .../tasks/TakerReserveTradeFunds.java | 13 +- 6 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index f493b1b584..9c51dead66 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -276,6 +276,10 @@ public final class OpenOffer implements Tradable { return state == State.AVAILABLE; } + public boolean isReserved() { + return state == State.RESERVED; + } + public boolean isDeactivated() { return state == State.DEACTIVATED; } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 710cb92d54..49fa770ae2 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -661,7 +661,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe ErrorMessageHandler errorMessageHandler) { log.info("Canceling open offer: {}", openOffer.getId()); if (!offersToBeEdited.containsKey(openOffer.getId())) { - if (openOffer.isAvailable()) { + if (isOnOfferBook(openOffer)) { openOffer.setState(OpenOffer.State.CANCELED); offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), () -> { @@ -683,6 +683,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } + private boolean isOnOfferBook(OpenOffer openOffer) { + return openOffer.isAvailable() || openOffer.isReserved(); + } + public void editOpenOfferStart(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 9befbd6e82..a1f2b5d0b7 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -997,7 +997,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi log.warn("Unregistering {} {}", trade.getClass().getSimpleName(), trade.getId()); removeTrade(trade, true); removeFailedTrade(trade); - xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that. + if (!trade.isMaker()) xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that. requestPersistence(); } diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 5ff09324fd..4249e967a9 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -43,6 +43,7 @@ import haveno.common.handlers.ErrorMessageHandler; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.taskrunner.Task; import haveno.core.network.MessageState; +import haveno.core.offer.OpenOffer; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; @@ -55,13 +56,17 @@ import haveno.core.trade.messages.DepositRequest; import haveno.core.trade.messages.DepositResponse; import haveno.core.trade.messages.DepositsConfirmedMessage; import haveno.core.trade.messages.InitMultisigRequest; +import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.trade.messages.SignContractRequest; import haveno.core.trade.messages.SignContractResponse; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.FluentProtocol.Condition; +import haveno.core.trade.protocol.FluentProtocol.Event; import haveno.core.trade.protocol.tasks.ApplyFilter; +import haveno.core.trade.protocol.tasks.MakerRecreateReserveTx; +import haveno.core.trade.protocol.tasks.MakerSendInitTradeRequestToArbitrator; import haveno.core.trade.protocol.tasks.MaybeSendSignContractRequest; import haveno.core.trade.protocol.tasks.ProcessDepositResponse; import haveno.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage; @@ -110,6 +115,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private boolean depositsConfirmedTasksCalled; private int reprocessPaymentSentMessageCount; private int reprocessPaymentReceivedMessageCount; + private boolean makerInitTradeRequestNacked = false; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -758,6 +764,18 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D peer.setNodeAddress(sender); } + // TODO: arbitrator may nack maker's InitTradeRequest if reserve tx has become invalid (e.g. check_tx_key shows 0 funds received). recreate reserve tx in this case + if (!ackMessage.isSuccess() && trade.isMaker() && peer == trade.getArbitrator() && ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName())) { + if (makerInitTradeRequestNacked) { + handleSecondMakerInitTradeRequestNack(ackMessage); + // use default postprocessing + } else { + makerInitTradeRequestNacked = true; + handleFirstMakerInitTradeRequestNack(ackMessage); + return; + } + } + // handle nack of deposit request if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) { if (!ackMessage.isSuccess()) { @@ -774,12 +792,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // handle ack message for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) { - if (trade.getTradePeer(sender) == trade.getSeller()) { + if (peer == trade.getSeller()) { trade.getSeller().setPaymentSentAckMessage(ackMessage); if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); else trade.setState(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); processModel.getTradeManager().requestPersistence(); - } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { + } else if (peer == trade.getArbitrator()) { trade.getArbitrator().setPaymentSentAckMessage(ackMessage); processModel.getTradeManager().requestPersistence(); } else { @@ -792,7 +810,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D if (ackMessage.getSourceMsgClassName().equals(PaymentReceivedMessage.class.getSimpleName())) { // ack message from buyer - if (trade.getTradePeer(sender) == trade.getBuyer()) { + if (peer == trade.getBuyer()) { trade.getBuyer().setPaymentReceivedAckMessage(ackMessage); // handle successful ack @@ -819,7 +837,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } // ack message from arbitrator - else if (trade.getTradePeer(sender) == trade.getArbitrator()) { + else if (peer == trade.getArbitrator()) { trade.getArbitrator().setPaymentReceivedAckMessage(ackMessage); // handle nack @@ -856,6 +874,48 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D trade.onAckMessage(ackMessage, sender); } + private void handleFirstMakerInitTradeRequestNack(AckMessage ackMessage) { + log.warn("Maker received NACK to InitTradeRequest from arbitrator for {} {}, messageUid={}, errorMessage={}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); + ThreadUtils.execute(() -> { + Event event = new Event() { + @Override + public String name() { + return "MakerRecreateReserveTx"; + } + }; + synchronized (trade.getLock()) { + latchTrade(); + expect(phase(Trade.Phase.INIT) + .with(event)) + .setup(tasks( + MakerRecreateReserveTx.class, + MakerSendInitTradeRequestToArbitrator.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(); + unlatchTrade(); + }, + errorMessage -> { + handleError("Failed to re-send InitTradeRequest to arbitrator for " + trade.getClass().getSimpleName() + " " + trade.getId() + ": " + errorMessage); + })) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) + .executeTasks(true); + awaitTradeLatch(); + } + }, trade.getId()); + } + + private void handleSecondMakerInitTradeRequestNack(AckMessage ackMessage) { + log.warn("Maker received 2nd NACK to InitTradeRequest from arbitrator for {} {}, messageUid={}, errorMessage={}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); + String warningMessage = "Your offer (" + trade.getOffer().getShortId() + ") has been removed because there was a problem taking the trade.\n\nError message: " + ackMessage.getErrorMessage(); + OpenOffer openOffer = HavenoUtils.openOfferManager.getOpenOffer(trade.getId()).orElse(null); + if (openOffer != null) { + HavenoUtils.openOfferManager.cancelOpenOffer(openOffer, null, null); + HavenoUtils.setTopError(warningMessage); + } + log.warn(warningMessage); + } + private boolean isPaymentReceivedMessageAckedByEither() { if (trade.getBuyer().getPaymentReceivedMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return true; if (trade.getArbitrator().getPaymentReceivedMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return true; @@ -992,11 +1052,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage, String updatedMultisigHex) { log.error("Task runner failed with error {}. Triggered from {}. Monerod={}" , errorMessage, source, trade.getXmrWalletService().getXmrConnectionService().getConnection()); + handleError(errorMessage); + if (message != null) { sendAckMessage(ackReceiver, message, false, errorMessage, updatedMultisigHex); } - - handleError(errorMessage); } // these are not thread safe, so they must be used within a lock on the trade @@ -1006,9 +1066,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D log.error(errorMessage); trade.setErrorMessage(errorMessage); processModel.getTradeManager().requestPersistence(); + unlatchTrade(); if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessage); errorMessageHandler = null; - unlatchTrade(); } protected void latchTrade() { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java new file mode 100644 index 0000000000..05130dbbf3 --- /dev/null +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java @@ -0,0 +1,147 @@ +/* + * This file is part of Haveno. + * + * Haveno 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. + * + * Haveno 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 Haveno. If not, see . + */ + +package haveno.core.trade.protocol.tasks; + +import haveno.common.taskrunner.TaskRunner; +import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; +import haveno.core.offer.OpenOffer; +import haveno.core.trade.HavenoUtils; +import haveno.core.trade.MakerTrade; +import haveno.core.trade.Trade; +import haveno.core.trade.protocol.TradeProtocol; +import haveno.core.xmr.model.XmrAddressEntry; +import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroRpcConnection; +import monero.wallet.model.MoneroTxWallet; + +import java.math.BigInteger; + +@Slf4j +public class MakerRecreateReserveTx extends TradeTask { + + public MakerRecreateReserveTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // maker trade expected + if (!(trade instanceof MakerTrade)) { + throw new RuntimeException("Expected maker trade but was " + trade.getClass().getSimpleName() + " " + trade.getShortId() + ". That should never happen."); + } + + // get open offer + OpenOffer openOffer = HavenoUtils.openOfferManager.getOpenOffer(trade.getOffer().getId()).orElse(null); + if (openOffer == null) throw new RuntimeException("Open offer not found for " + trade.getClass().getSimpleName() + " " + trade.getId()); + Offer offer = openOffer.getOffer(); + + // reset reserve tx state + trade.getSelf().setReserveTxHex(null); + trade.getSelf().setReserveTxHash(null); + trade.getSelf().setReserveTxKey(null); + trade.getSelf().setReserveTxKeyImages(null); + + // recreate reserve tx + log.warn("Maker is recreating reserve tx for tradeId={}", trade.getShortId()); + MoneroTxWallet reserveTx = null; + synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { + + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId()); + trade.startProtocolTimeout(); + + // thaw reserved key images + log.info("Thawing reserve tx key images for tradeId={}", trade.getShortId()); + HavenoUtils.xmrWalletService.thawOutputs(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); + + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while thawing key images, tradeId=" + trade.getShortId()); + trade.startProtocolTimeout(); + + // collect relevant info + BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), offer.getPenaltyFeePct()); + BigInteger makerFee = offer.getMaxMakerFee(); + BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); + BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); + String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(); + XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); + Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex(); + + // attempt re-creating reserve tx + try { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); + try { + reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); + } catch (IllegalStateException e) { + log.warn("Illegal state creating reserve tx, tradeId={}, error={}", trade.getShortId(), i + 1, e.getMessage()); + throw e; + } catch (Exception e) { + log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + trade.getXmrWalletService().handleWalletError(e, sourceConnection); + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); + if (reserveTx != null) break; + } + } + } catch (Exception e) { + + // reset state + if (reserveTx != null) model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); + model.getXmrWalletService().freezeOutputs(offer.getOfferPayload().getReserveTxKeyImages()); + trade.getSelf().setReserveTxKeyImages(null); + throw e; + } + + // reset protocol timeout + trade.startProtocolTimeout(); + + // update state + trade.getSelf().setReserveTxHash(reserveTx.getHash()); + trade.getSelf().setReserveTxHex(reserveTx.getFullHex()); + trade.getSelf().setReserveTxKey(reserveTx.getKey()); + trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); + trade.getXmrWalletService().freezeOutputs(HavenoUtils.getInputKeyImages(reserveTx)); + } + + // save process state + processModel.setReserveTx(reserveTx); // TODO: remove this? how is it used? + processModel.getTradeManager().requestPersistence(); + complete(); + } catch (Throwable t) { + trade.setErrorMessage("An error occurred.\n" + + "Error message:\n" + + t.getMessage()); + failed(t); + } + } + + private boolean isTimedOut() { + return !processModel.getTradeManager().hasOpenTrade(trade); + } +} diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java index 9a461b2d83..f7116ee0db 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -88,7 +88,7 @@ public class TakerReserveTradeFunds extends TradeTask { } } catch (Exception e) { - // reset state with wallet lock + // reset state model.getXmrWalletService().swapPayoutAddressEntryToAvailable(trade.getId()); if (reserveTx != null) { model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); @@ -101,11 +101,12 @@ public class TakerReserveTradeFunds extends TradeTask { // reset protocol timeout trade.startProtocolTimeout(); - // update trade state - trade.getTaker().setReserveTxHash(reserveTx.getHash()); - trade.getTaker().setReserveTxHex(reserveTx.getFullHex()); - trade.getTaker().setReserveTxKey(reserveTx.getKey()); - trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); + // update state + trade.getSelf().setReserveTxHash(reserveTx.getHash()); + trade.getSelf().setReserveTxHex(reserveTx.getFullHex()); + trade.getSelf().setReserveTxKey(reserveTx.getKey()); + trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); + trade.getXmrWalletService().freezeOutputs(HavenoUtils.getInputKeyImages(reserveTx)); } } From a4345ae709a16c4d9cd59605e98312ff172a6c69 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 3 Jun 2025 21:44:14 -0400 Subject: [PATCH 301/371] arbitrator verifies offers are public xor no deposit from buyer/taker --- .../src/main/java/haveno/core/offer/OpenOfferManager.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 49fa770ae2..c3188da86c 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1618,6 +1618,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } else { + // verify public offer (remove to generally allow private offers) + if (offer.isPrivateOffer() || offer.getChallengeHash() != null) { + errorMessage = "Private offer " + request.offerId + " is not valid. It must have direction SELL, taker fee of 0, and a challenge hash."; + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + // verify maker's trade fee if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) { errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_PCT + " but got " + offer.getMakerFeePct(); From ea536bb4eeb7232c73d3035e947133801a39bca7 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 5 Jun 2025 07:00:49 -0400 Subject: [PATCH 302/371] fix npe on startup routine when monero node is not synced --- .../core/xmr/wallet/XmrWalletService.java | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 26f300b7c0..65cd28d3d8 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1394,10 +1394,10 @@ public class XmrWalletService extends XmrWalletBase { maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); } - private void maybeInitMainWallet(boolean sync, int numSyncAttempts) { + private void maybeInitMainWallet(boolean sync, int numSyncAttemptsRemaining) { ThreadUtils.execute(() -> { try { - doMaybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); + doMaybeInitMainWallet(sync, numSyncAttemptsRemaining); } catch (Exception e) { if (isShutDownStarted) return; log.warn("Error initializing main wallet: {}\n", e.getMessage(), e); @@ -1407,7 +1407,7 @@ public class XmrWalletService extends XmrWalletBase { }, THREAD_ID); } - private void doMaybeInitMainWallet(boolean sync, int numSyncAttempts) { + private void doMaybeInitMainWallet(boolean sync, int numSyncAttemptsRemaining) { synchronized (walletLock) { if (isShutDownStarted) return; @@ -1435,7 +1435,7 @@ public class XmrWalletService extends XmrWalletBase { // sync main wallet if applicable // TODO: error handling and re-initialization is jenky, refactor - if (sync && numSyncAttempts > 0) { + if (sync && numSyncAttemptsRemaining > 0) { try { // switch connection if disconnected @@ -1454,13 +1454,14 @@ public class XmrWalletService extends XmrWalletBase { if (wallet != null) log.warn("Error syncing wallet with progress on startup: " + e.getMessage()); forceCloseMainWallet(); requestSwitchToNextBestConnection(sourceConnection); - maybeInitMainWallet(true, numSyncAttempts - 1); // re-initialize wallet and sync again + maybeInitMainWallet(true, numSyncAttemptsRemaining - 1); // re-initialize wallet and sync again return; } log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); // poll wallet doPollWallet(true); + if (getBalance() == null) throw new RuntimeException("Balance is null after polling main wallet"); if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); // log wallet balances @@ -1488,9 +1489,9 @@ public class XmrWalletService extends XmrWalletBase { saveWallet(false); } catch (Exception e) { if (isClosingWallet || isShutDownStarted || HavenoUtils.havenoSetup.getWalletInitialized().get()) return; // ignore if wallet closing, shut down started, or app already initialized - log.warn("Error initially syncing main wallet: {}", e.getMessage()); - if (numSyncAttempts <= 1) { - log.warn("Failed to sync main wallet. Opening app without syncing", numSyncAttempts); + log.warn("Error initially syncing main wallet, numSyncAttemptsRemaining={}", numSyncAttemptsRemaining, e); + if (numSyncAttemptsRemaining <= 1) { + log.warn("Failed to sync main wallet. Opening app without syncing."); HavenoUtils.havenoSetup.getWalletInitialized().set(true); saveWallet(false); @@ -1501,7 +1502,7 @@ public class XmrWalletService extends XmrWalletBase { } else { log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000); UserThread.runAfter(() -> { - maybeInitMainWallet(true, numSyncAttempts - 1); + maybeInitMainWallet(true, numSyncAttemptsRemaining - 1); }, xmrConnectionService.getRefreshPeriodMs() / 1000); } } @@ -2063,15 +2064,15 @@ public class XmrWalletService extends XmrWalletBase { } } saveWalletWithDelay(); - } - // cache wallet info last - synchronized (walletLock) { - if (wallet != null && !isShutDownStarted) { - try { - cacheWalletInfo(); - } catch (Exception e) { - log.warn("Error caching wallet info: " + e.getMessage() + "\n", e); + // cache wallet info last + synchronized (walletLock) { + if (wallet != null && !isShutDownStarted) { + try { + cacheWalletInfo(); + } catch (Exception e) { + log.warn("Error caching wallet info: " + e.getMessage() + "\n", e); + } } } } From 8ee1bb372b51addaf60bff0cbce865991d028791 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:50:12 -0400 Subject: [PATCH 303/371] fix hanging of pending offer with scheduled txs --- core/src/main/java/haveno/core/offer/OpenOfferManager.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index c3188da86c..03327b1d09 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1175,9 +1175,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } else if (openOffer.getScheduledTxHashes() == null) { scheduleWithEarliestTxs(openOffers, openOffer); - resultHandler.handleResult(null); - return; } + + resultHandler.handleResult(null); + return; } } catch (Exception e) { if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e); From c239f9aac0c6f955c61783bfd6fa6f42d87fd030 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 5 Jun 2025 19:39:17 -0400 Subject: [PATCH 304/371] make updated multisig hex nullable in dispute closed message --- .../support/dispute/messages/DisputeClosedMessage.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/support/dispute/messages/DisputeClosedMessage.java b/core/src/main/java/haveno/core/support/dispute/messages/DisputeClosedMessage.java index 88a1b6a9df..e4f6828c19 100644 --- a/core/src/main/java/haveno/core/support/dispute/messages/DisputeClosedMessage.java +++ b/core/src/main/java/haveno/core/support/dispute/messages/DisputeClosedMessage.java @@ -35,6 +35,7 @@ import static com.google.common.base.Preconditions.checkArgument; public final class DisputeClosedMessage extends DisputeMessage { private final DisputeResult disputeResult; private final NodeAddress senderNodeAddress; + @Nullable private final String updatedMultisigHex; @Nullable private final String unsignedPayoutTxHex; @@ -44,7 +45,7 @@ public final class DisputeClosedMessage extends DisputeMessage { NodeAddress senderNodeAddress, String uid, SupportType supportType, - String updatedMultisigHex, + @Nullable String updatedMultisigHex, @Nullable String unsignedPayoutTxHex, boolean deferPublishPayout) { this(disputeResult, @@ -85,9 +86,9 @@ public final class DisputeClosedMessage extends DisputeMessage { .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setUid(uid) .setType(SupportType.toProtoMessage(supportType)) - .setUpdatedMultisigHex(updatedMultisigHex) .setDeferPublishPayout(deferPublishPayout); Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex)); + Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); return getNetworkEnvelopeBuilder().setDisputeClosedMessage(builder).build(); } @@ -98,7 +99,7 @@ public final class DisputeClosedMessage extends DisputeMessage { proto.getUid(), messageVersion, SupportType.fromProto(proto.getType()), - proto.getUpdatedMultisigHex(), + ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()), proto.getDeferPublishPayout()); } From aa1eb70d9a9fb7e13b1ba0b6f72dda4c23f02c3d Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 6 Jun 2025 08:18:51 -0400 Subject: [PATCH 305/371] major update to look and feel of desktop ui (#1733) --- .../java/haveno/core/util/VolumeUtil.java | 32 + .../resources/i18n/displayStrings.properties | 9 +- .../i18n/displayStrings_cs.properties | 5 +- .../i18n/displayStrings_de.properties | 4 +- .../i18n/displayStrings_es.properties | 4 +- .../i18n/displayStrings_fa.properties | 8 +- .../i18n/displayStrings_fr.properties | 4 +- .../i18n/displayStrings_it.properties | 6 +- .../i18n/displayStrings_ja.properties | 4 +- .../i18n/displayStrings_pt-br.properties | 6 +- .../i18n/displayStrings_pt.properties | 6 +- .../i18n/displayStrings_ru.properties | 8 +- .../i18n/displayStrings_th.properties | 8 +- .../i18n/displayStrings_tr.properties | 6 +- .../i18n/displayStrings_vi.properties | 8 +- .../i18n/displayStrings_zh-hans.properties | 4 +- .../i18n/displayStrings_zh-hant.properties | 6 +- .../java/haveno/desktop/CandleStickChart.css | 4 + .../java/haveno/desktop/app/HavenoApp.java | 4 + .../haveno/desktop/app/HavenoAppMain.java | 5 +- .../desktop/components/AddressTextField.java | 26 +- .../desktop/components/AutoTooltipButton.java | 6 +- .../components/AutocompleteComboBox.java | 27 + .../desktop/components/BalanceTextField.java | 1 + .../components/ExplorerAddressTextField.java | 25 +- .../desktop/components/FundsTextField.java | 20 +- .../desktop/components/HavenoTextField.java | 4 +- .../desktop/components/InfoTextField.java | 4 +- .../desktop/components/InputTextField.java | 2 + .../desktop/components/PeerInfoIcon.java | 5 +- .../components/TextFieldWithCopyIcon.java | 32 +- .../desktop/components/TextFieldWithIcon.java | 5 +- .../desktop/components/TxIdTextField.java | 40 +- .../controlsfx/control/PopOver.java | 2 +- .../desktop/components/list/FilterBox.java | 12 +- .../components/paymentmethods/AssetsForm.java | 3 + .../src/main/java/haveno/desktop/haveno.css | 797 +++++++++++++----- .../src/main/java/haveno/desktop/images.css | 86 +- .../java/haveno/desktop/main/MainView.java | 176 +++- .../desktop/main/account/AccountView.java | 12 +- .../cryptoaccounts/CryptoAccountsView.java | 2 +- .../ManageMarketAlertsWindow.java | 2 + .../TraditionalAccountsView.java | 2 +- .../haveno/desktop/main/funds/FundsView.java | 6 +- .../main/funds/deposit/DepositView.java | 49 +- .../desktop/main/funds/locked/LockedView.java | 3 +- .../main/funds/reserved/ReservedView.java | 4 +- .../funds/transactions/TransactionsView.fxml | 10 +- .../funds/transactions/TransactionsView.java | 14 +- .../main/funds/withdrawal/WithdrawalView.java | 2 +- .../desktop/main/market/MarketView.java | 8 +- .../market/offerbook/OfferBookChartView.java | 82 +- .../offerbook/OfferBookChartViewModel.java | 42 +- .../main/market/spread/SpreadView.java | 8 +- .../main/market/trades/TradesChartsView.java | 7 +- .../desktop/main/offer/MutableOfferView.java | 67 +- .../main/offer/MutableOfferViewModel.java | 6 +- .../haveno/desktop/main/offer/OfferView.java | 8 +- .../main/offer/offerbook/OfferBookView.java | 69 +- .../offer/signedoffer/SignedOfferView.java | 6 +- .../main/offer/takeoffer/TakeOfferView.java | 107 ++- .../offer/takeoffer/TakeOfferViewModel.java | 2 +- .../haveno/desktop/main/overlays/Overlay.java | 76 +- .../main/overlays/editor/PasswordPopup.java | 2 +- .../overlays/notifications/Notification.java | 5 +- .../notifications/NotificationCenter.java | 4 +- .../main/overlays/windows/ContractWindow.java | 35 +- .../windows/DisputeSummaryWindow.java | 4 +- .../windows/GenericMessageWindow.java | 17 +- .../overlays/windows/OfferDetailsWindow.java | 140 ++- .../main/overlays/windows/QRCodeWindow.java | 40 +- .../windows/SignPaymentAccountsWindow.java | 2 +- .../windows/SignSpecificWitnessWindow.java | 1 + .../windows/SignUnsignedPubKeysWindow.java | 1 + .../overlays/windows/TradeDetailsWindow.java | 66 +- .../overlays/windows/TradeFeedbackWindow.java | 13 +- .../overlays/windows/TxDetailsWindow.java | 15 +- .../VerifyDisputeResultSignatureWindow.java | 1 + .../desktop/main/portfolio/PortfolioView.java | 6 +- .../closedtrades/ClosedTradesView.java | 8 +- .../failedtrades/FailedTradesView.java | 4 +- .../portfolio/openoffer/OpenOffersView.fxml | 1 - .../portfolio/openoffer/OpenOffersView.java | 15 +- .../pendingtrades/PendingTradesView.java | 9 +- .../pendingtrades/steps/TradeStepView.java | 1 + .../steps/buyer/BuyerStep4View.java | 2 +- .../desktop/main/settings/SettingsView.java | 6 +- .../main/settings/about/AboutView.java | 12 +- .../settings/network/NetworkSettingsView.fxml | 10 +- .../settings/network/NetworkSettingsView.java | 18 +- .../settings/preferences/PreferencesView.java | 12 +- .../haveno/desktop/main/shared/ChatView.java | 45 +- .../desktop/main/support/SupportView.java | 14 +- .../main/support/dispute/DisputeView.java | 12 +- .../dispute/agent/DisputeAgentView.java | 2 - .../main/java/haveno/desktop/theme-dark.css | 267 +++--- .../main/java/haveno/desktop/theme-light.css | 71 +- .../java/haveno/desktop/util/CssTheme.java | 4 + .../haveno/desktop/util/CurrencyList.java | 11 +- .../java/haveno/desktop/util/FormBuilder.java | 54 +- .../java/haveno/desktop/util/GUIUtil.java | 438 +++++++++- .../main/java/haveno/desktop/util/Layout.java | 7 +- .../java/haveno/desktop/util/Transitions.java | 6 +- desktop/src/main/resources/images/account.png | Bin 0 -> 1014 bytes .../src/main/resources/images/alert_round.png | Bin 895 -> 4964 bytes .../src/main/resources/images/bch_logo.png | Bin 0 -> 30750 bytes ...{blue_circle.png => blue_circle_solid.png} | Bin ...circle@2x.png => blue_circle_solid@2x.png} | Bin .../src/main/resources/images/btc_logo.png | Bin 0 -> 21730 bytes .../main/resources/images/connection/tor.png | Bin 3583 -> 3381 bytes .../resources/images/connection/tor@2x.png | Bin 7469 -> 0 bytes .../images/connection/tor_color@2x.png | Bin 3331 -> 0 bytes .../main/resources/images/dai-erc20_logo.png | Bin 0 -> 73562 bytes .../resources/images/dark_mode_toggle.png | Bin 0 -> 1760 bytes .../src/main/resources/images/eth_logo.png | Bin 0 -> 49471 bytes .../main/resources/images/green_circle.png | Bin 3351 -> 5120 bytes .../resources/images/green_circle_solid.png | Bin 0 -> 3351 bytes ...ircle@2x.png => green_circle_solid@2x.png} | Bin .../resources/images/light_mode_toggle.png | Bin 0 -> 2013 bytes desktop/src/main/resources/images/lock@2x.png | Bin 721 -> 629 bytes .../src/main/resources/images/lock_circle.png | Bin 0 -> 629 bytes .../resources/images/logo_landscape_dark.png | Bin 0 -> 49306 bytes .../resources/images/logo_landscape_light.png | Bin 0 -> 22767 bytes .../src/main/resources/images/logo_splash.png | Bin 21326 -> 0 bytes ...ogo_splash@2x.png => logo_splash_dark.png} | Bin .../resources/images/logo_splash_light.png | Bin 0 -> 30317 bytes .../resources/images/logo_splash_testnet.png | Bin 15693 -> 0 bytes .../images/logo_splash_testnet_dark.png | Bin 0 -> 24129 bytes ...t@2x.png => logo_splash_testnet_light.png} | Bin .../src/main/resources/images/ltc_logo.png | Bin 0 -> 77610 bytes .../resources/images/red_circle_solid.png | Bin 0 -> 895 bytes ...t_round@2x.png => red_circle_solid@2x.png} | Bin .../src/main/resources/images/settings.png | Bin 0 -> 1193 bytes desktop/src/main/resources/images/support.png | Bin 0 -> 728 bytes .../main/resources/images/usdc-erc20_logo.png | Bin 0 -> 120909 bytes .../main/resources/images/usdt-erc20_logo.png | Bin 0 -> 61105 bytes .../main/resources/images/usdt-trc20_logo.png | Bin 0 -> 61105 bytes .../src/main/resources/images/xmr_logo.png | Bin 0 -> 37040 bytes .../main/resources/images/yellow_circle.png | Bin 510 -> 4887 bytes .../resources/images/yellow_circle_solid.png | Bin 0 -> 510 bytes .../java/haveno/desktop/ComponentsDemo.java | 1 + 141 files changed, 2395 insertions(+), 995 deletions(-) create mode 100644 desktop/src/main/resources/images/account.png create mode 100644 desktop/src/main/resources/images/bch_logo.png rename desktop/src/main/resources/images/{blue_circle.png => blue_circle_solid.png} (100%) rename desktop/src/main/resources/images/{blue_circle@2x.png => blue_circle_solid@2x.png} (100%) create mode 100644 desktop/src/main/resources/images/btc_logo.png delete mode 100644 desktop/src/main/resources/images/connection/tor@2x.png delete mode 100644 desktop/src/main/resources/images/connection/tor_color@2x.png create mode 100644 desktop/src/main/resources/images/dai-erc20_logo.png create mode 100644 desktop/src/main/resources/images/dark_mode_toggle.png create mode 100644 desktop/src/main/resources/images/eth_logo.png create mode 100644 desktop/src/main/resources/images/green_circle_solid.png rename desktop/src/main/resources/images/{green_circle@2x.png => green_circle_solid@2x.png} (100%) create mode 100644 desktop/src/main/resources/images/light_mode_toggle.png create mode 100644 desktop/src/main/resources/images/lock_circle.png create mode 100644 desktop/src/main/resources/images/logo_landscape_dark.png create mode 100644 desktop/src/main/resources/images/logo_landscape_light.png delete mode 100644 desktop/src/main/resources/images/logo_splash.png rename desktop/src/main/resources/images/{logo_splash@2x.png => logo_splash_dark.png} (100%) create mode 100644 desktop/src/main/resources/images/logo_splash_light.png delete mode 100644 desktop/src/main/resources/images/logo_splash_testnet.png create mode 100644 desktop/src/main/resources/images/logo_splash_testnet_dark.png rename desktop/src/main/resources/images/{logo_splash_testnet@2x.png => logo_splash_testnet_light.png} (100%) create mode 100644 desktop/src/main/resources/images/ltc_logo.png create mode 100644 desktop/src/main/resources/images/red_circle_solid.png rename desktop/src/main/resources/images/{alert_round@2x.png => red_circle_solid@2x.png} (100%) create mode 100644 desktop/src/main/resources/images/settings.png create mode 100644 desktop/src/main/resources/images/support.png create mode 100644 desktop/src/main/resources/images/usdc-erc20_logo.png create mode 100644 desktop/src/main/resources/images/usdt-erc20_logo.png create mode 100644 desktop/src/main/resources/images/usdt-trc20_logo.png create mode 100644 desktop/src/main/resources/images/xmr_logo.png create mode 100644 desktop/src/main/resources/images/yellow_circle_solid.png diff --git a/core/src/main/java/haveno/core/util/VolumeUtil.java b/core/src/main/java/haveno/core/util/VolumeUtil.java index b74a70f226..050b7b1a4d 100644 --- a/core/src/main/java/haveno/core/util/VolumeUtil.java +++ b/core/src/main/java/haveno/core/util/VolumeUtil.java @@ -51,6 +51,7 @@ import org.bitcoinj.utils.MonetaryFormat; import java.math.BigInteger; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.util.Collection; import java.util.Locale; public class VolumeUtil { @@ -187,4 +188,35 @@ public class VolumeUtil { private static MonetaryFormat getMonetaryFormat(String currencyCode) { return CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode) ? VOLUME_FORMAT_UNIT : VOLUME_FORMAT_PRECISE; } + + public static Volume sum(Collection volumes) { + if (volumes == null || volumes.isEmpty()) { + return null; + } + Volume sum = null; + for (Volume volume : volumes) { + if (sum == null) { + sum = volume; + } else { + if (!sum.getCurrencyCode().equals(volume.getCurrencyCode())) { + throw new IllegalArgumentException("Cannot sum volumes with different currencies"); + } + sum = add(sum, volume); + } + } + return sum; + } + + public static Volume add(Volume volume1, Volume volume2) { + if (volume1 == null) return volume2; + if (volume2 == null) return volume1; + if (!volume1.getCurrencyCode().equals(volume2.getCurrencyCode())) { + throw new IllegalArgumentException("Cannot add volumes with different currencies"); + } + if (volume1.getMonetary() instanceof CryptoMoney) { + return new Volume(((CryptoMoney) volume1.getMonetary()).add((CryptoMoney) volume2.getMonetary())); + } else { + return new Volume(((TraditionalMoney) volume1.getMonetary()).add((TraditionalMoney) volume2.getMonetary())); + } + } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index c214a016dd..a0fd2962b1 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -315,9 +315,6 @@ market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Trades -# OfferBookView -market.offerBook.filterPrompt=Filter - # OfferBookChartView market.offerBook.sellOffersHeaderLabel=Sell {0} to market.offerBook.buyOffersHeaderLabel=Buy {0} from @@ -410,8 +407,10 @@ shared.notSigned.noNeedAlts=Cryptocurrency accounts do not feature signing or ag offerbook.nrOffers=No. of offers: {0} offerbook.volume={0} (min - max) +offerbook.volumeTotal={0} {1} offerbook.deposit=Deposit XMR (%) offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. +offerbook.XMRTotal=XMR ({0}) offerbook.createNewOffer=Create offer to {0} {1} offerbook.createOfferDisabled.tooltip=You can only create one offer at a time @@ -1162,8 +1161,6 @@ support.tab.refund.support=Refund support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Search disputes -support.filter.prompt=Enter trade ID, date, onion address or account data support.tab.SignedOffers=Signed Offers support.prompt.signedOffer.penalty.msg=This will charge the maker a penalty fee and return the remaining trade funds to their wallet. Are you sure you want to send?\n\n\ Offer ID: {0}\n\ @@ -1337,6 +1334,7 @@ setting.preferences.displayOptions=Display options setting.preferences.showOwnOffers=Show my own offers in offer book setting.preferences.useAnimations=Use animations setting.preferences.useDarkMode=Use dark mode +setting.preferences.useLightMode=Use light mode setting.preferences.sortWithNumOffers=Sort market lists with no. of offers/trades setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -2012,6 +2010,7 @@ offerDetailsWindow.confirm.takerCrypto=Confirm: Take offer to {0} {1} offerDetailsWindow.creationDate=Creation date offerDetailsWindow.makersOnion=Maker's onion address offerDetailsWindow.challenge=Offer passphrase +offerDetailsWindow.challenge.copy=Copy passphrase to share with your peer qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index ce414afabd..53658b4038 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -315,7 +315,6 @@ market.tabs.spreadPayment=Nabídky podle způsobů platby market.tabs.trades=Obchody # OfferBookView -market.offerBook.filterPrompt=Filtr # OfferBookChartView market.offerBook.sellOffersHeaderLabel=Prodat {0} kupujícímu @@ -1125,8 +1124,6 @@ support.tab.refund.support=Vrácení peněz support.tab.arbitration.support=Arbitráž support.tab.legacyArbitration.support=Starší arbitráž support.tab.ArbitratorsSupportTickets=Úkoly pro {0} -support.filter=Hledat spory -support.filter.prompt=Zadejte ID obchodu, datum, onion adresu nebo údaje o účtu support.tab.SignedOffers=Podepsané nabídky support.prompt.signedOffer.penalty.msg=Tím se tvůrci účtuje sankční poplatek a zbývající prostředky z obchodu se vrátí do jeho peněženky. Jste si jisti, že chcete odeslat?\n\n\ ID nabídky: {0}\n\ @@ -1300,6 +1297,7 @@ setting.preferences.displayOptions=Zobrazit možnosti setting.preferences.showOwnOffers=Zobrazit mé vlastní nabídky v seznamu nabídek setting.preferences.useAnimations=Použít animace setting.preferences.useDarkMode=Použít tmavý režim +setting.preferences.useLightMode=Použijte světlý režim setting.preferences.sortWithNumOffers=Seřadit seznamy trhů s počtem nabídek/obchodů setting.preferences.onlyShowPaymentMethodsFromAccount=Skrýt nepodporované způsoby platby setting.preferences.denyApiTaker=Odmítat příjemce, kteří používají API @@ -1975,6 +1973,7 @@ offerDetailsWindow.confirm.takerCrypto=Potvrďte: Přijmout nabídku {0} {1} offerDetailsWindow.creationDate=Datum vzniku offerDetailsWindow.makersOnion=Onion adresa tvůrce offerDetailsWindow.challenge=Passphrase nabídky +offerDetailsWindow.challenge.copy=Zkopírujte přístupovou frázi pro sdílení s protějškem qRCodeWindow.headline=QR Kód qRCodeWindow.msg=Použijte tento QR kód k financování vaší peněženky Haveno z vaší externí peněženky. diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 43b664b1cf..57fd93df67 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -927,8 +927,6 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Vermittlung support.tab.legacyArbitration.support=Legacy-Vermittlung support.tab.ArbitratorsSupportTickets={0} Tickets -support.filter=Konflikte durchsuchen -support.filter.prompt=Tragen sie Handel ID, Datum, Onion Adresse oder Kontodaten support.sigCheck.button=Signatur überprüfen support.sigCheck.popup.info=Fügen Sie die Zusammenfassungsnachricht des Schiedsverfahrens ein. Mit diesem Tool kann jeder Benutzer überprüfen, ob die Unterschrift des Schiedsrichters mit der Zusammenfassungsnachricht übereinstimmt. @@ -1035,6 +1033,7 @@ setting.preferences.displayOptions=Darstellungsoptionen setting.preferences.showOwnOffers=Eigenen Angebote im Angebotsbuch zeigen setting.preferences.useAnimations=Animationen abspielen setting.preferences.useDarkMode=Nacht-Modus benutzen +setting.preferences.useLightMode=Leichtmodus verwenden setting.preferences.sortWithNumOffers=Marktlisten nach Anzahl der Angebote/Trades sortieren setting.preferences.onlyShowPaymentMethodsFromAccount=Nicht unterstützte Zahlungsmethoden ausblenden setting.preferences.denyApiTaker=Taker die das API nutzen vermeiden @@ -1477,6 +1476,7 @@ offerDetailsWindow.confirm.taker=Bestätigen: Angebot annehmen monero zu {0} offerDetailsWindow.creationDate=Erstellungsdatum offerDetailsWindow.makersOnion=Onion-Adresse des Erstellers offerDetailsWindow.challenge=Angebots-Passphrase +offerDetailsWindow.challenge.copy=Passphrase kopieren, um sie mit Ihrem Handelspartner zu teilen qRCodeWindow.headline=QR Code qRCodeWindow.msg=Bitte nutzen Sie diesen QR Code um Ihr Haveno Wallet von Ihrem externen Wallet aufzuladen. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index a4001cb075..b5754592dd 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -928,8 +928,6 @@ 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=Buscar disputas -support.filter.prompt=Introduzca ID de transacción, fecha, dirección onion o datos de cuenta. support.sigCheck.button=Comprobar firma support.sigCheck.popup.info=Pegue el mensaje resumido del proceso de arbitraje. Con esta herramienta, cualquier usuario puede verificar si la firma del árbitro coincide con el mensaje resumido. @@ -1036,6 +1034,7 @@ setting.preferences.displayOptions=Mostrar opciones setting.preferences.showOwnOffers=Mostrar mis propias ofertas en el libro de ofertas setting.preferences.useAnimations=Usar animaciones setting.preferences.useDarkMode=Usar modo oscuro +setting.preferences.useLightMode=Usar modo claro setting.preferences.sortWithNumOffers=Ordenar listas de mercado por número de ofertas/intercambios setting.preferences.onlyShowPaymentMethodsFromAccount=Ocultar métodos de pago no soportados setting.preferences.denyApiTaker=Denegar tomadores usando la misma API @@ -1478,6 +1477,7 @@ offerDetailsWindow.confirm.taker=Confirmar: Tomar oferta {0} monero offerDetailsWindow.creationDate=Fecha de creación offerDetailsWindow.makersOnion=Dirección onion del creador offerDetailsWindow.challenge=Frase de contraseña de la oferta +offerDetailsWindow.challenge.copy=Copiar frase de contraseña para compartir con tu contraparte qRCodeWindow.headline=Código QR qRCodeWindow.msg=Por favor, utilice este código QR para fondear su billetera Haveno desde su billetera externa. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 891dc297da..44693d7e01 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -218,7 +218,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=فیلتر shared.enabled=Enabled @@ -926,8 +926,6 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Search disputes -support.filter.prompt=Enter trade ID, date, onion address or account data support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -1031,7 +1029,8 @@ setting.preferences.addCrypto=افزودن آلتکوین setting.preferences.displayOptions=نمایش گزینه‌ها setting.preferences.showOwnOffers=نمایش پیشنهادهای من در دفتر پیشنهاد setting.preferences.useAnimations=استفاده از انیمیشن‌ها -setting.preferences.useDarkMode=Use dark mode +setting.preferences.useDarkMode=حالت تاریک را استفاده کنید +setting.preferences.useLightMode=حالت روشن را استفاده کنید setting.preferences.sortWithNumOffers=مرتب سازی لیست‌ها با تعداد معاملات/پیشنهادها setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1473,6 +1472,7 @@ offerDetailsWindow.confirm.taker=تأیید: پیشنهاد را به {0} بپذ offerDetailsWindow.creationDate=تاریخ ایجاد offerDetailsWindow.makersOnion=آدرس Onion سفارش گذار offerDetailsWindow.challenge=Passphrase de l'offre +offerDetailsWindow.challenge.copy=عبارت عبور را برای به اشتراک‌گذاری با همتا کپی کنید qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 0a0e1274ad..478ab4c6c3 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -929,8 +929,6 @@ 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=Chercher les litiges -support.filter.prompt=Saisissez l'ID du trade, la date, l'adresse "onion" ou les données du compte. support.sigCheck.button=Vérifier la signature support.sigCheck.popup.info=Collez le message récapitulatif du processus d'arbitrage. Avec cet outil, n'importe quel utilisateur peut vérifier si la signature de l'arbitre correspond au message récapitulatif. @@ -1037,6 +1035,7 @@ setting.preferences.displayOptions=Afficher les options setting.preferences.showOwnOffers=Montrer mes ordres dans le livre des ordres setting.preferences.useAnimations=Utiliser des animations setting.preferences.useDarkMode=Utiliser le mode sombre +setting.preferences.useLightMode=Utiliser le mode clair setting.preferences.sortWithNumOffers=Trier les listes de marché avec le nombre d'ordres/de transactions setting.preferences.onlyShowPaymentMethodsFromAccount=Masquer les méthodes de paiement non supportées setting.preferences.denyApiTaker=Refuser les preneurs utilisant l'API @@ -1479,6 +1478,7 @@ offerDetailsWindow.confirm.taker=Confirmer: Acceptez l''ordre de {0} monero offerDetailsWindow.creationDate=Date de création offerDetailsWindow.makersOnion=Adresse onion du maker offerDetailsWindow.challenge=Phrase secrète de l'offre +offerDetailsWindow.challenge.copy=Copier la phrase secrète à partager avec votre pair qRCodeWindow.headline=QR Code qRCodeWindow.msg=Veuillez utiliser le code QR pour recharger du portefeuille externe au portefeuille Haveno. diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 7847646806..784b65234f 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -218,7 +218,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=Al momento, hai troppe transazioni non confermate. Per favore riprova più tardi. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=Filtro shared.enabled=Enabled @@ -927,8 +927,6 @@ support.tab.mediation.support=Mediazione support.tab.arbitration.support=Arbitrato support.tab.legacyArbitration.support=Arbitrato Legacy support.tab.ArbitratorsSupportTickets=I ticket di {0} -support.filter=Search disputes -support.filter.prompt=Inserisci ID commerciale, data, indirizzo onion o dati dell'account support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -1034,6 +1032,7 @@ setting.preferences.displayOptions=Mostra opzioni setting.preferences.showOwnOffers=Mostra le mie offerte nel libro delle offerte setting.preferences.useAnimations=Usa animazioni setting.preferences.useDarkMode=Usa modalità notte +setting.preferences.useLightMode=Usa la modalità chiara setting.preferences.sortWithNumOffers=Ordina le liste di mercato con n. di offerte/scambi setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1476,6 +1475,7 @@ offerDetailsWindow.confirm.taker=Conferma: Accetta l'offerta a {0} monero offerDetailsWindow.creationDate=Data di creazione offerDetailsWindow.makersOnion=Indirizzo .onion del maker offerDetailsWindow.challenge=Passphrase dell'offerta +offerDetailsWindow.challenge.copy=Copia la frase segreta da condividere con il tuo interlocutore qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 9036bd8e2d..ccc24ab657 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -927,8 +927,6 @@ support.tab.mediation.support=調停 support.tab.arbitration.support=仲裁 support.tab.legacyArbitration.support=レガシー仲裁 support.tab.ArbitratorsSupportTickets={0} のチケット -support.filter=係争を検索 -support.filter.prompt=トレードID、日付、onionアドレスまたはアカウントデータを入力してください support.sigCheck.button=Check signature support.sigCheck.popup.info=仲裁プロセスの要約メッセージを貼り付けてください。このツールを使用すると、どんなユーザーでも仲裁者の署名が要約メッセージと一致するかどうかを確認できます。 @@ -1035,6 +1033,7 @@ setting.preferences.displayOptions=表示設定 setting.preferences.showOwnOffers=オファーブックに自分のオファーを表示 setting.preferences.useAnimations=アニメーションを使用 setting.preferences.useDarkMode=ダークモードを利用 +setting.preferences.useLightMode=ライトモードを使用する setting.preferences.sortWithNumOffers=市場リストをオファー/トレードの数で並び替える setting.preferences.onlyShowPaymentMethodsFromAccount=サポートされていない支払い方法を非表示にする setting.preferences.denyApiTaker=APIを使用するテイカーを拒否する @@ -1477,6 +1476,7 @@ offerDetailsWindow.confirm.taker=承認: ビットコインを{0}オファーを offerDetailsWindow.creationDate=作成日 offerDetailsWindow.makersOnion=メイカーのonionアドレス offerDetailsWindow.challenge=オファーパスフレーズ +offerDetailsWindow.challenge.copy=ピアと共有するためにパスフレーズをコピーする qRCodeWindow.headline=QRコード qRCodeWindow.msg=外部ウォレットからHavenoウォレットへ送金するのに、このQRコードを利用して下さい。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index f6520e8436..c28a530456 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -221,7 +221,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=No momento, você possui muitas transações não-confirmadas. Tente novamente mais tarde. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=Filtro shared.enabled=Enabled @@ -929,8 +929,6 @@ 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=Search disputes -support.filter.prompt=Insira ID da negociação. data. endereço onion ou dados da conta support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -1036,6 +1034,7 @@ setting.preferences.displayOptions=Opções de exibição setting.preferences.showOwnOffers=Exibir minhas ofertas no livro de ofertas setting.preferences.useAnimations=Usar animações setting.preferences.useDarkMode=Usar modo escuro +setting.preferences.useLightMode=Usar modo claro setting.preferences.sortWithNumOffers=Ordenar pelo nº de ofertas/negociações setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1480,6 +1479,7 @@ offerDetailsWindow.confirm.taker=Confirmar: Aceitar oferta de {0} monero offerDetailsWindow.creationDate=Criada em offerDetailsWindow.makersOnion=Endereço onion do ofertante offerDetailsWindow.challenge=Passphrase da oferta +offerDetailsWindow.challenge.copy=Copiar frase secreta para compartilhar com seu par qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index c69236f8b1..d14b41668c 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -218,7 +218,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=Filtro shared.enabled=Enabled @@ -926,8 +926,6 @@ 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=Search disputes -support.filter.prompt=Insira o ID do negócio, data, endereço onion ou dados da conta support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -1033,6 +1031,7 @@ setting.preferences.displayOptions=Mostrar opções setting.preferences.showOwnOffers=Mostrar as minhas próprias ofertas no livro de ofertas setting.preferences.useAnimations=Usar animações setting.preferences.useDarkMode=Usar o modo escuro +setting.preferences.useLightMode=Usar modo claro setting.preferences.sortWithNumOffers=Ordenar listas de mercado por nº de ofertas/negociações: setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1473,6 +1472,7 @@ offerDetailsWindow.confirm.taker=Confirmar: Aceitar oferta de {0} monero offerDetailsWindow.creationDate=Data de criação offerDetailsWindow.makersOnion=Endereço onion do ofertante offerDetailsWindow.challenge=Passphrase da oferta +offerDetailsWindow.challenge.copy=Copiar frase secreta para compartilhar com seu parceiro qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index f5ded7cdb2..848b765ae2 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -218,7 +218,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=Фильтр shared.enabled=Enabled @@ -926,8 +926,6 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Search disputes -support.filter.prompt=Введите идентификатор сделки, дату, onion-адрес или данные учётной записи support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -1031,7 +1029,8 @@ setting.preferences.addCrypto=Добавить альткойн setting.preferences.displayOptions=Параметры отображения setting.preferences.showOwnOffers=Показать мои предложения в списке предложений setting.preferences.useAnimations=Использовать анимацию -setting.preferences.useDarkMode=Use dark mode +setting.preferences.useDarkMode=Использовать тёмный режим +setting.preferences.useLightMode=Использовать светлый режим setting.preferences.sortWithNumOffers=Сортировать списки по кол-ву предложений/сделок setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1474,6 +1473,7 @@ offerDetailsWindow.confirm.taker=Подтвердите: принять пред offerDetailsWindow.creationDate=Дата создания offerDetailsWindow.makersOnion=Onion-адрес мейкера offerDetailsWindow.challenge=Пароль предложения +offerDetailsWindow.challenge.copy=Скопируйте кодовую фразу, чтобы поделиться с партнёром qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index d2d89f3b83..df37fc3f28 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -218,7 +218,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=ตัวกรอง shared.enabled=Enabled @@ -926,8 +926,6 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Search disputes -support.filter.prompt=Enter trade ID, date, onion address or account data support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -1031,7 +1029,8 @@ setting.preferences.addCrypto=เพิ่ม crypto setting.preferences.displayOptions=แสดงตัวเลือกเพิ่มเติม setting.preferences.showOwnOffers=แสดงข้อเสนอของฉันเองในสมุดข้อเสนอ setting.preferences.useAnimations=ใช้ภาพเคลื่อนไหว -setting.preferences.useDarkMode=Use dark mode +setting.preferences.useDarkMode=ใช้โหมดมืด +setting.preferences.useLightMode=ใช้โหมดสว่าง setting.preferences.sortWithNumOffers=จัดเรียงรายการโดยเลขของข้อเสนอ / การซื้อขาย setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1474,6 +1473,7 @@ offerDetailsWindow.confirm.taker=ยืนยัน: รับข้อเสน offerDetailsWindow.creationDate=วันที่สร้าง offerDetailsWindow.makersOnion=ที่อยู่ onion ของผู้สร้าง offerDetailsWindow.challenge=รหัสผ่านสำหรับข้อเสนอ +offerDetailsWindow.challenge.copy=คัดลอกวลีรหัสเพื่อแชร์กับเพื่อนของคุณ qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 426c99500f..9b7f12e466 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -231,7 +231,7 @@ shared.delayedPayoutTxId=Gecikmiş ödeme işlem kimliği shared.delayedPayoutTxReceiverAddress=Gecikmiş ödeme işlemi gönderildi shared.unconfirmedTransactionsLimitReached=Şu anda çok fazla onaylanmamış işleminiz var. Lütfen daha sonra tekrar deneyin. shared.numItemsLabel=Girdi sayısı: {0} -shared.filter=Filtrele +shared.filter=Filtre shared.enabled=Etkin shared.pending=Beklemede shared.me=Ben @@ -1120,8 +1120,6 @@ support.tab.refund.support=Geri Ödeme support.tab.arbitration.support=Arbitraj support.tab.legacyArbitration.support=Eski Arbitraj support.tab.ArbitratorsSupportTickets={0}'nin biletleri -support.filter=Uyuşmazlıkları ara -support.filter.prompt=İşlem ID'si, tarih, onion adresi veya hesap verilerini girin support.tab.SignedOffers=İmzalı Teklifler support.prompt.signedOffer.penalty.msg=Bu, üreticiden bir ceza ücreti alacak ve kalan işlem fonlarını cüzdanına iade edecektir. Göndermek istediğinizden emin misiniz?\n\n\ Teklif ID'si: {0}\n\ @@ -1295,6 +1293,7 @@ setting.preferences.displayOptions=Görüntüleme seçenekleri setting.preferences.showOwnOffers=Teklif defterinde kendi tekliflerini göster setting.preferences.useAnimations=Animasyonları kullan setting.preferences.useDarkMode=Karanlık modu kullan +setting.preferences.useLightMode=Aydınlık modu kullan setting.preferences.sortWithNumOffers=Piyasaları teklif sayısına göre sırala setting.preferences.onlyShowPaymentMethodsFromAccount=Olmayan ödeme yöntemlerini gizle setting.preferences.denyApiTaker=API kullanan alıcıları reddet @@ -1970,6 +1969,7 @@ offerDetailsWindow.confirm.takerCrypto=Onayla: {0} {1} teklifi al offerDetailsWindow.creationDate=Oluşturma tarihi offerDetailsWindow.makersOnion=Yapıcı'nın onion adresi offerDetailsWindow.challenge=Teklif şifresi +offerDetailsWindow.challenge.copy=Parolanızı eşinizle paylaşmak için kopyalayın qRCodeWindow.headline=QR Kodu qRCodeWindow.msg=Harici cüzdanınızdan Haveno cüzdanınızı finanse etmek için bu QR kodunu kullanın. diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 4df8f44b16..52854a4208 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -218,7 +218,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=Bộ lọc shared.enabled=Enabled @@ -928,8 +928,6 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -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.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -1033,7 +1031,8 @@ setting.preferences.addCrypto=Bổ sung crypto setting.preferences.displayOptions=Hiển thị các phương án setting.preferences.showOwnOffers=Hiển thị Báo giá của tôi trong danh mục Báo giá setting.preferences.useAnimations=Sử dụng hoạt ảnh -setting.preferences.useDarkMode=Use dark mode +setting.preferences.useDarkMode=Sử dụng chế độ tối +setting.preferences.useLightMode=Sử dụng chế độ sáng setting.preferences.sortWithNumOffers=Sắp xếp danh sách thị trường với số chào giá/giao dịch setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1476,6 +1475,7 @@ offerDetailsWindow.confirm.taker=Xác nhận: Nhận chào giáo cho {0} monero offerDetailsWindow.creationDate=Ngày tạo offerDetailsWindow.makersOnion=Địa chỉ onion của người tạo offerDetailsWindow.challenge=Mã bảo vệ giao dịch +offerDetailsWindow.challenge.copy=Sao chép cụm mật khẩu để chia sẻ với đối tác của bạn qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 1e65527036..7e6b9be4d5 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -927,8 +927,6 @@ support.tab.mediation.support=调解 support.tab.arbitration.support=仲裁 support.tab.legacyArbitration.support=历史仲裁 support.tab.ArbitratorsSupportTickets={0} 的工单 -support.filter=查找纠纷 -support.filter.prompt=输入 交易 ID、日期、洋葱地址或账户信息 support.sigCheck.button=Check signature support.sigCheck.popup.info=请粘贴仲裁过程的摘要信息。使用这个工具,任何用户都可以检查仲裁者的签名是否与摘要信息相符。 @@ -1035,6 +1033,7 @@ setting.preferences.displayOptions=显示选项 setting.preferences.showOwnOffers=在报价列表中显示我的报价 setting.preferences.useAnimations=使用动画 setting.preferences.useDarkMode=使用夜间模式 +setting.preferences.useLightMode=使用浅色模式 setting.preferences.sortWithNumOffers=使用“报价ID/交易ID”筛选列表 setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1478,6 +1477,7 @@ offerDetailsWindow.confirm.taker=确定:下单买入 {0} 比特币 offerDetailsWindow.creationDate=创建时间 offerDetailsWindow.makersOnion=卖家的匿名地址 offerDetailsWindow.challenge=提供密码 +offerDetailsWindow.challenge.copy=复制助记词以与您的交易对手共享 qRCodeWindow.headline=二维码 qRCodeWindow.msg=请使用二维码从外部钱包充值至 Haveno 钱包 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 05f8bb5e8d..a1694e3999 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -218,7 +218,7 @@ shared.delayedPayoutTxId=延遲支付交易 ID shared.delayedPayoutTxReceiverAddress=延遲交易交易已發送至 shared.unconfirmedTransactionsLimitReached=你現在有過多的未確認交易。請稍後嘗試 shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=篩選 shared.enabled=啟用 @@ -927,8 +927,6 @@ support.tab.mediation.support=調解 support.tab.arbitration.support=仲裁 support.tab.legacyArbitration.support=歷史仲裁 support.tab.ArbitratorsSupportTickets={0} 的工單 -support.filter=查找糾紛 -support.filter.prompt=輸入 交易 ID、日期、洋葱地址或賬户信息 support.sigCheck.button=Check signature support.sigCheck.popup.info=請貼上仲裁程序的摘要訊息。利用這個工具,任何使用者都可以檢查仲裁者的簽名是否與摘要訊息相符。 @@ -1035,6 +1033,7 @@ setting.preferences.displayOptions=顯示選項 setting.preferences.showOwnOffers=在報價列表中顯示我的報價 setting.preferences.useAnimations=使用動畫 setting.preferences.useDarkMode=使用夜間模式 +setting.preferences.useLightMode=使用淺色模式 setting.preferences.sortWithNumOffers=使用“報價ID/交易ID”篩選列表 setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1478,6 +1477,7 @@ offerDetailsWindow.confirm.taker=確定:下單買入 {0} 比特幣 offerDetailsWindow.creationDate=創建時間 offerDetailsWindow.makersOnion=賣家的匿名地址 offerDetailsWindow.challenge=提供密碼 +offerDetailsWindow.challenge.copy=複製密語以與對方分享 qRCodeWindow.headline=二維碼 qRCodeWindow.msg=請使用二維碼從外部錢包充值至 Haveno 錢包 diff --git a/desktop/src/main/java/haveno/desktop/CandleStickChart.css b/desktop/src/main/java/haveno/desktop/CandleStickChart.css index 376e0db666..4e5a05d7ea 100644 --- a/desktop/src/main/java/haveno/desktop/CandleStickChart.css +++ b/desktop/src/main/java/haveno/desktop/CandleStickChart.css @@ -62,6 +62,8 @@ -demo-bar-fill: -bs-sell; -fx-background-color: -demo-bar-fill; -fx-background-insets: 0; + -fx-background-radius: 2px; + -fx-border-radius: 2px; } .candlestick-bar.close-above-open { @@ -80,6 +82,8 @@ -fx-padding: 5; -fx-background-color: -bs-volume-transparent; -fx-background-insets: 0; + -fx-background-radius: 2px; + -fx-border-radius: 2px; } .chart-alternative-row-fill { diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java index 16370c2cdb..41b5ad09bc 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java @@ -74,6 +74,7 @@ import javafx.scene.Scene; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; import javafx.stage.Modality; import javafx.stage.Screen; import javafx.stage.Stage; @@ -223,6 +224,9 @@ public class HavenoApp extends Application implements UncaughtExceptionHandler { CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), config.useDevModeHeader); }); CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), config.useDevModeHeader); + + // set initial background color + scene.setFill(CssTheme.isDarkTheme() ? Color.BLACK : Color.WHITE); return scene; } diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java index ed7e956eba..73e78ab6d7 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java @@ -216,7 +216,10 @@ public class HavenoAppMain extends HavenoExecutable { // Set the dialog content VBox vbox = new VBox(10); - vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), passwordField, errorMessageField, versionField); + ImageView logoImageView = new ImageView(ImageUtil.getImageByPath("logo_splash_light.png")); + logoImageView.setFitWidth(342); + logoImageView.setPreserveRatio(true); + vbox.getChildren().addAll(logoImageView, passwordField, errorMessageField, versionField); vbox.setAlignment(Pos.TOP_CENTER); getDialogPane().setContent(vbox); diff --git a/desktop/src/main/java/haveno/desktop/components/AddressTextField.java b/desktop/src/main/java/haveno/desktop/components/AddressTextField.java index ec6f284379..8edfbd222a 100644 --- a/desktop/src/main/java/haveno/desktop/components/AddressTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/AddressTextField.java @@ -20,14 +20,17 @@ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; +import haveno.common.UserThread; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.GUIUtil; +import haveno.desktop.util.Layout; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.layout.AnchorPane; @@ -55,6 +58,7 @@ public class AddressTextField extends AnchorPane { textField.setId("address-text-field"); textField.setEditable(false); textField.setLabelFloat(true); + textField.getStyleClass().add("label-float"); textField.setPromptText(label); textField.textProperty().bind(address); @@ -70,28 +74,32 @@ public class AddressTextField extends AnchorPane { textField.focusTraversableProperty().set(focusTraversableProperty().get()); Label extWalletIcon = new Label(); - extWalletIcon.setLayoutY(3); + extWalletIcon.setLayoutY(Layout.FLOATING_ICON_Y); extWalletIcon.getStyleClass().addAll("icon", "highlight"); extWalletIcon.setTooltip(new Tooltip(tooltipText)); AwesomeDude.setIcon(extWalletIcon, AwesomeIcon.SIGNIN); extWalletIcon.setOnMouseClicked(e -> openWallet()); - Label copyIcon = new Label(); - copyIcon.setLayoutY(3); - copyIcon.getStyleClass().addAll("icon", "highlight"); - Tooltip.install(copyIcon, new Tooltip(Res.get("addressTextField.copyToClipboard"))); - AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); - copyIcon.setOnMouseClicked(e -> { + Label copyLabel = new Label(); + copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); + copyLabel.getStyleClass().addAll("icon", "highlight"); + Tooltip.install(copyLabel, new Tooltip(Res.get("addressTextField.copyToClipboard"))); + copyLabel.setGraphic(GUIUtil.getCopyIcon()); + copyLabel.setOnMouseClicked(e -> { if (address.get() != null && address.get().length() > 0) Utilities.copyToClipboard(address.get()); + Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); + Node node = (Node) e.getSource(); + UserThread.runAfter(() -> tp.hide(), 1); + tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); }); - AnchorPane.setRightAnchor(copyIcon, 30.0); + AnchorPane.setRightAnchor(copyLabel, 30.0); AnchorPane.setRightAnchor(extWalletIcon, 5.0); AnchorPane.setRightAnchor(textField, 55.0); AnchorPane.setLeftAnchor(textField, 0.0); - getChildren().addAll(textField, copyIcon, extWalletIcon); + getChildren().addAll(textField, copyLabel, extWalletIcon); } private void openWallet() { diff --git a/desktop/src/main/java/haveno/desktop/components/AutoTooltipButton.java b/desktop/src/main/java/haveno/desktop/components/AutoTooltipButton.java index 83261ba212..11521e93a1 100644 --- a/desktop/src/main/java/haveno/desktop/components/AutoTooltipButton.java +++ b/desktop/src/main/java/haveno/desktop/components/AutoTooltipButton.java @@ -31,15 +31,15 @@ public class AutoTooltipButton extends JFXButton { } public AutoTooltipButton(String text) { - super(text.toUpperCase()); + super(text); } public AutoTooltipButton(String text, Node graphic) { - super(text.toUpperCase(), graphic); + super(text, graphic); } public void updateText(String text) { - setText(text.toUpperCase()); + setText(text); } @Override diff --git a/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java b/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java index 2b1adc5769..e6701cdb4c 100644 --- a/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java +++ b/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java @@ -24,6 +24,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.Event; import javafx.event.EventHandler; +import javafx.scene.control.ListView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import org.apache.commons.lang3.StringUtils; @@ -44,6 +45,7 @@ public class AutocompleteComboBox extends JFXComboBox { private List extendedList; private List matchingList; private JFXComboBoxListViewSkin comboBoxListViewSkin; + private boolean selectAllShortcut = false; public AutocompleteComboBox() { this(FXCollections.observableArrayList()); @@ -153,6 +155,27 @@ public class AutocompleteComboBox extends JFXComboBox { private void reactToQueryChanges() { getEditor().addEventHandler(KeyEvent.KEY_RELEASED, (KeyEvent event) -> { + + // ignore ctrl and command keys + if (event.getCode() == KeyCode.CONTROL || event.getCode() == KeyCode.COMMAND || event.getCode() == KeyCode.META) { + event.consume(); + return; + } + + // handle select all + boolean isSelectAll = event.getCode() == KeyCode.A && (event.isControlDown() || event.isMetaDown()); + if (isSelectAll) { + getEditor().selectAll(); + selectAllShortcut = true; + event.consume(); + return; + } + if (event.getCode() == KeyCode.A && selectAllShortcut) { // 'A' can be received after ctrl/cmd + selectAllShortcut = false; + event.consume(); + return; + } + UserThread.execute(() -> { String query = getEditor().getText(); var exactMatch = list.stream().anyMatch(item -> asString(item).equalsIgnoreCase(query)); @@ -180,6 +203,10 @@ public class AutocompleteComboBox extends JFXComboBox { if (matchingListSize() > 0) { comboBoxListViewSkin.getPopupContent().autosize(); show(); + if (comboBoxListViewSkin.getPopupContent() instanceof ListView listView) { + listView.applyCss(); + listView.layout(); + } } else { hide(); } diff --git a/desktop/src/main/java/haveno/desktop/components/BalanceTextField.java b/desktop/src/main/java/haveno/desktop/components/BalanceTextField.java index 4e00789dc8..7775b0b812 100644 --- a/desktop/src/main/java/haveno/desktop/components/BalanceTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/BalanceTextField.java @@ -47,6 +47,7 @@ public class BalanceTextField extends AnchorPane { public BalanceTextField(String label) { textField = new HavenoTextField(); textField.setLabelFloat(true); + textField.getStyleClass().add("label-float"); textField.setPromptText(label); textField.setFocusTraversable(false); textField.setEditable(false); diff --git a/desktop/src/main/java/haveno/desktop/components/ExplorerAddressTextField.java b/desktop/src/main/java/haveno/desktop/components/ExplorerAddressTextField.java index 7dff009b9e..7e31272ca8 100644 --- a/desktop/src/main/java/haveno/desktop/components/ExplorerAddressTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/ExplorerAddressTextField.java @@ -23,6 +23,7 @@ import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.user.Preferences; +import haveno.desktop.util.GUIUtil; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; @@ -38,19 +39,19 @@ public class ExplorerAddressTextField extends AnchorPane { @Getter private final TextField textField; - private final Label copyIcon, missingAddressWarningIcon; + private final Label copyLabel, missingAddressWarningIcon; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public ExplorerAddressTextField() { - copyIcon = new Label(); - copyIcon.setLayoutY(3); - copyIcon.getStyleClass().addAll("icon", "highlight"); - copyIcon.setTooltip(new Tooltip(Res.get("explorerAddressTextField.copyToClipboard"))); - AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); - AnchorPane.setRightAnchor(copyIcon, 30.0); + copyLabel = new Label(); + copyLabel.setLayoutY(3); + copyLabel.getStyleClass().addAll("icon", "highlight"); + copyLabel.setTooltip(new Tooltip(Res.get("explorerAddressTextField.copyToClipboard"))); + copyLabel.setGraphic(GUIUtil.getCopyIcon()); + AnchorPane.setRightAnchor(copyLabel, 30.0); Tooltip tooltip = new Tooltip(Res.get("explorerAddressTextField.blockExplorerIcon.tooltip")); @@ -71,27 +72,27 @@ public class ExplorerAddressTextField extends AnchorPane { AnchorPane.setRightAnchor(textField, 80.0); AnchorPane.setLeftAnchor(textField, 0.0); textField.focusTraversableProperty().set(focusTraversableProperty().get()); - getChildren().addAll(textField, missingAddressWarningIcon, copyIcon); + getChildren().addAll(textField, missingAddressWarningIcon, copyLabel); } public void setup(@Nullable String address) { if (address == null) { textField.setText(Res.get("shared.na")); textField.setId("address-text-field-error"); - copyIcon.setVisible(false); - copyIcon.setManaged(false); + copyLabel.setVisible(false); + copyLabel.setManaged(false); missingAddressWarningIcon.setVisible(true); missingAddressWarningIcon.setManaged(true); return; } textField.setText(address); - copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(address)); + copyLabel.setOnMouseClicked(e -> Utilities.copyToClipboard(address)); } public void cleanup() { textField.setOnMouseClicked(null); - copyIcon.setOnMouseClicked(null); + copyLabel.setOnMouseClicked(null); textField.setText(""); } } diff --git a/desktop/src/main/java/haveno/desktop/components/FundsTextField.java b/desktop/src/main/java/haveno/desktop/components/FundsTextField.java index 0804750972..aa01093469 100644 --- a/desktop/src/main/java/haveno/desktop/components/FundsTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/FundsTextField.java @@ -17,9 +17,10 @@ package haveno.desktop.components; -import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.util.Utilities; import haveno.core.locale.Res; +import haveno.desktop.util.GUIUtil; +import haveno.desktop.util.Layout; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; @@ -29,8 +30,6 @@ import javafx.scene.layout.AnchorPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static haveno.desktop.util.FormBuilder.getIcon; - public class FundsTextField extends InfoTextField { public static final Logger log = LoggerFactory.getLogger(FundsTextField.class); @@ -46,11 +45,12 @@ public class FundsTextField extends InfoTextField { textField.textProperty().unbind(); textField.textProperty().bind(Bindings.concat(textProperty())); // TODO: removed `, " ", fundsStructure` for haveno to fix "Funds needed: .123 XMR (null)" bug - Label copyIcon = getIcon(AwesomeIcon.COPY); - copyIcon.setLayoutY(5); - copyIcon.getStyleClass().addAll("icon", "highlight"); - Tooltip.install(copyIcon, new Tooltip(Res.get("shared.copyToClipboard"))); - copyIcon.setOnMouseClicked(e -> { + Label copyLabel = new Label(); + copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); + copyLabel.getStyleClass().addAll("icon", "highlight"); + Tooltip.install(copyLabel, new Tooltip(Res.get("shared.copyToClipboard"))); + copyLabel.setGraphic(GUIUtil.getCopyIcon()); + copyLabel.setOnMouseClicked(e -> { String text = getText(); if (text != null && text.length() > 0) { String copyText; @@ -64,11 +64,11 @@ public class FundsTextField extends InfoTextField { } }); - AnchorPane.setRightAnchor(copyIcon, 30.0); + AnchorPane.setRightAnchor(copyLabel, 30.0); AnchorPane.setRightAnchor(infoIcon, 62.0); AnchorPane.setRightAnchor(textField, 55.0); - getChildren().add(copyIcon); + getChildren().add(copyLabel); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/haveno/desktop/components/HavenoTextField.java b/desktop/src/main/java/haveno/desktop/components/HavenoTextField.java index 96c9e2571d..bb8c24eecf 100644 --- a/desktop/src/main/java/haveno/desktop/components/HavenoTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/HavenoTextField.java @@ -1,16 +1,18 @@ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; +import haveno.desktop.util.GUIUtil; import javafx.scene.control.Skin; public class HavenoTextField extends JFXTextField { public HavenoTextField(String value) { super(value); + GUIUtil.applyFilledStyle(this); } public HavenoTextField() { - super(); + this(null); } @Override diff --git a/desktop/src/main/java/haveno/desktop/components/InfoTextField.java b/desktop/src/main/java/haveno/desktop/components/InfoTextField.java index beafcf9494..7e47b9339f 100644 --- a/desktop/src/main/java/haveno/desktop/components/InfoTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/InfoTextField.java @@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.desktop.components.controlsfx.control.PopOver; +import haveno.desktop.util.Layout; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Node; @@ -51,13 +52,14 @@ public class InfoTextField extends AnchorPane { arrowLocation = PopOver.ArrowLocation.RIGHT_TOP; textField = new HavenoTextField(); textField.setLabelFloat(true); + textField.getStyleClass().add("label-float"); textField.setEditable(false); textField.textProperty().bind(text); textField.setFocusTraversable(false); textField.setId("info-field"); infoIcon = getIcon(AwesomeIcon.INFO_SIGN); - infoIcon.setLayoutY(5); + infoIcon.setLayoutY(Layout.FLOATING_ICON_Y - 2); infoIcon.getStyleClass().addAll("icon", "info"); AnchorPane.setRightAnchor(infoIcon, 7.0); diff --git a/desktop/src/main/java/haveno/desktop/components/InputTextField.java b/desktop/src/main/java/haveno/desktop/components/InputTextField.java index 8c4ace02b8..b46e7f84b1 100644 --- a/desktop/src/main/java/haveno/desktop/components/InputTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/InputTextField.java @@ -20,6 +20,7 @@ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import haveno.core.util.validation.InputValidator; +import haveno.desktop.util.GUIUtil; import haveno.desktop.util.validation.JFXInputValidator; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -67,6 +68,7 @@ public class InputTextField extends JFXTextField { public InputTextField() { super(); + GUIUtil.applyFilledStyle(this); getValidators().add(jfxValidationWrapper); diff --git a/desktop/src/main/java/haveno/desktop/components/PeerInfoIcon.java b/desktop/src/main/java/haveno/desktop/components/PeerInfoIcon.java index da8dcb4a70..af854c437f 100644 --- a/desktop/src/main/java/haveno/desktop/components/PeerInfoIcon.java +++ b/desktop/src/main/java/haveno/desktop/components/PeerInfoIcon.java @@ -124,7 +124,8 @@ public class PeerInfoIcon extends Group { numTradesPane.relocate(scaleFactor * 18, scaleFactor * 14); numTradesPane.setMouseTransparent(true); ImageView numTradesCircle = new ImageView(); - numTradesCircle.setId("image-green_circle"); + numTradesCircle.setId("image-green_circle_solid"); + numTradesLabel = new AutoTooltipLabel(); numTradesLabel.relocate(scaleFactor * 5, scaleFactor * 1); numTradesLabel.setId("ident-num-label"); @@ -134,7 +135,7 @@ public class PeerInfoIcon extends Group { tagPane.relocate(Math.round(scaleFactor * 18), scaleFactor * -2); tagPane.setMouseTransparent(true); ImageView tagCircle = new ImageView(); - tagCircle.setId("image-blue_circle"); + tagCircle.setId("image-blue_circle_solid"); tagLabel = new AutoTooltipLabel(); tagLabel.relocate(Math.round(scaleFactor * 5), scaleFactor * 1); tagLabel.setId("ident-num-label"); diff --git a/desktop/src/main/java/haveno/desktop/components/TextFieldWithCopyIcon.java b/desktop/src/main/java/haveno/desktop/components/TextFieldWithCopyIcon.java index d337916da3..1dbd47eaaa 100644 --- a/desktop/src/main/java/haveno/desktop/components/TextFieldWithCopyIcon.java +++ b/desktop/src/main/java/haveno/desktop/components/TextFieldWithCopyIcon.java @@ -18,12 +18,15 @@ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; -import de.jensd.fx.fontawesome.AwesomeDude; -import de.jensd.fx.fontawesome.AwesomeIcon; + +import haveno.common.UserThread; import haveno.common.util.Utilities; import haveno.core.locale.Res; +import haveno.desktop.util.GUIUtil; +import haveno.desktop.util.Layout; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; @@ -45,12 +48,13 @@ public class TextFieldWithCopyIcon extends AnchorPane { } public TextFieldWithCopyIcon(String customStyleClass) { - Label copyIcon = new Label(); - copyIcon.setLayoutY(3); - copyIcon.getStyleClass().addAll("icon", "highlight"); - copyIcon.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); - AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); - copyIcon.setOnMouseClicked(e -> { + Label copyLabel = new Label(); + copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); + copyLabel.getStyleClass().addAll("icon", "highlight"); + if (customStyleClass != null) copyLabel.getStyleClass().add(customStyleClass + "-icon"); + copyLabel.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); + copyLabel.setGraphic(GUIUtil.getCopyIcon()); + copyLabel.setOnMouseClicked(e -> { String text = getText(); if (text != null && text.length() > 0) { String copyText; @@ -70,17 +74,25 @@ public class TextFieldWithCopyIcon extends AnchorPane { copyText = text; } Utilities.copyToClipboard(copyText); + Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); + Node node = (Node) e.getSource(); + UserThread.runAfter(() -> tp.hide(), 1); + tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); } }); textField = new JFXTextField(); textField.setEditable(false); if (customStyleClass != null) textField.getStyleClass().add(customStyleClass); textField.textProperty().bindBidirectional(text); - AnchorPane.setRightAnchor(copyIcon, 5.0); + AnchorPane.setRightAnchor(copyLabel, 5.0); AnchorPane.setRightAnchor(textField, 30.0); AnchorPane.setLeftAnchor(textField, 0.0); + AnchorPane.setTopAnchor(copyLabel, 0.0); + AnchorPane.setBottomAnchor(copyLabel, 0.0); + AnchorPane.setTopAnchor(textField, 0.0); + AnchorPane.setBottomAnchor(textField, 0.0); textField.focusTraversableProperty().set(focusTraversableProperty().get()); - getChildren().addAll(textField, copyIcon); + getChildren().addAll(textField, copyLabel); } public void setPromptText(String value) { diff --git a/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java b/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java index 2e66a0e026..1db14cf73a 100644 --- a/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java +++ b/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java @@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.UserThread; +import haveno.desktop.util.Layout; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.TextField; @@ -53,10 +54,10 @@ public class TextFieldWithIcon extends AnchorPane { iconLabel = new Label(); iconLabel.setLayoutX(0); - iconLabel.setLayoutY(3); + iconLabel.setLayoutY(Layout.FLOATING_ICON_Y); dummyTextField.widthProperty().addListener((observable, oldValue, newValue) -> { - iconLabel.setLayoutX(dummyTextField.widthProperty().get() + 20); + iconLabel.setLayoutX(dummyTextField.widthProperty().get() + 20 + Layout.FLOATING_ICON_Y); }); getChildren().addAll(textField, dummyTextField, iconLabel); diff --git a/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java b/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java index e9ced56cdf..2a55dba7b0 100644 --- a/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java @@ -29,7 +29,10 @@ import haveno.core.user.Preferences; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.components.indicator.TxConfidenceIndicator; import haveno.desktop.util.GUIUtil; +import haveno.desktop.util.Layout; import javafx.beans.value.ChangeListener; +import javafx.scene.Cursor; +import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; @@ -51,7 +54,7 @@ public class TxIdTextField extends AnchorPane { private final TextField textField; private final Tooltip progressIndicatorTooltip; private final TxConfidenceIndicator txConfidenceIndicator; - private final Label copyIcon, blockExplorerIcon, missingTxWarningIcon; + private final Label copyLabel, blockExplorerIcon, missingTxWarningIcon; private MoneroWalletListener walletListener; private ChangeListener tradeListener; @@ -70,16 +73,17 @@ public class TxIdTextField extends AnchorPane { txConfidenceIndicator.setProgress(0); txConfidenceIndicator.setVisible(false); AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0); - AnchorPane.setTopAnchor(txConfidenceIndicator, 3.0); + AnchorPane.setTopAnchor(txConfidenceIndicator, Layout.FLOATING_ICON_Y); progressIndicatorTooltip = new Tooltip("-"); txConfidenceIndicator.setTooltip(progressIndicatorTooltip); - copyIcon = new Label(); - copyIcon.setLayoutY(3); - copyIcon.getStyleClass().addAll("icon", "highlight"); - copyIcon.setTooltip(new Tooltip(Res.get("txIdTextField.copyIcon.tooltip"))); - AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); - AnchorPane.setRightAnchor(copyIcon, 30.0); + copyLabel = new Label(); + copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); + copyLabel.getStyleClass().addAll("icon", "highlight"); + copyLabel.setTooltip(new Tooltip(Res.get("txIdTextField.copyIcon.tooltip"))); + copyLabel.setGraphic(GUIUtil.getCopyIcon()); + copyLabel.setCursor(Cursor.HAND); + AnchorPane.setRightAnchor(copyLabel, 30.0); Tooltip tooltip = new Tooltip(Res.get("txIdTextField.blockExplorerIcon.tooltip")); @@ -89,7 +93,7 @@ public class TxIdTextField extends AnchorPane { AwesomeDude.setIcon(blockExplorerIcon, AwesomeIcon.EXTERNAL_LINK); blockExplorerIcon.setMinWidth(20); AnchorPane.setRightAnchor(blockExplorerIcon, 52.0); - AnchorPane.setTopAnchor(blockExplorerIcon, 4.0); + AnchorPane.setTopAnchor(blockExplorerIcon, Layout.FLOATING_ICON_Y); missingTxWarningIcon = new Label(); missingTxWarningIcon.getStyleClass().addAll("icon", "error-icon"); @@ -97,7 +101,7 @@ public class TxIdTextField extends AnchorPane { missingTxWarningIcon.setTooltip(new Tooltip(Res.get("txIdTextField.missingTx.warning.tooltip"))); missingTxWarningIcon.setMinWidth(20); AnchorPane.setRightAnchor(missingTxWarningIcon, 52.0); - AnchorPane.setTopAnchor(missingTxWarningIcon, 4.0); + AnchorPane.setTopAnchor(missingTxWarningIcon, Layout.FLOATING_ICON_Y); missingTxWarningIcon.setVisible(false); missingTxWarningIcon.setManaged(false); @@ -108,7 +112,7 @@ public class TxIdTextField extends AnchorPane { AnchorPane.setRightAnchor(textField, 80.0); AnchorPane.setLeftAnchor(textField, 0.0); textField.focusTraversableProperty().set(focusTraversableProperty().get()); - getChildren().addAll(textField, missingTxWarningIcon, blockExplorerIcon, copyIcon, txConfidenceIndicator); + getChildren().addAll(textField, missingTxWarningIcon, blockExplorerIcon, copyLabel, txConfidenceIndicator); } public void setup(@Nullable String txId) { @@ -131,8 +135,8 @@ public class TxIdTextField extends AnchorPane { textField.setId("address-text-field-error"); blockExplorerIcon.setVisible(false); blockExplorerIcon.setManaged(false); - copyIcon.setVisible(false); - copyIcon.setManaged(false); + copyLabel.setVisible(false); + copyLabel.setManaged(false); txConfidenceIndicator.setVisible(false); missingTxWarningIcon.setVisible(true); missingTxWarningIcon.setManaged(true); @@ -158,7 +162,13 @@ public class TxIdTextField extends AnchorPane { textField.setText(txId); textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); - copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(txId)); + copyLabel.setOnMouseClicked(e -> { + Utilities.copyToClipboard(txId); + Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); + Node node = (Node) e.getSource(); + UserThread.runAfter(() -> tp.hide(), 1); + tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); + }); txConfidenceIndicator.setVisible(true); // update off main thread @@ -177,7 +187,7 @@ public class TxIdTextField extends AnchorPane { trade = null; textField.setOnMouseClicked(null); blockExplorerIcon.setOnMouseClicked(null); - copyIcon.setOnMouseClicked(null); + copyLabel.setOnMouseClicked(null); textField.setText(""); } diff --git a/desktop/src/main/java/haveno/desktop/components/controlsfx/control/PopOver.java b/desktop/src/main/java/haveno/desktop/components/controlsfx/control/PopOver.java index f04dee5960..85b7eaceba 100644 --- a/desktop/src/main/java/haveno/desktop/components/controlsfx/control/PopOver.java +++ b/desktop/src/main/java/haveno/desktop/components/controlsfx/control/PopOver.java @@ -494,7 +494,7 @@ public class PopOver extends PopupControl { * @since 1.0 */ public final void hide(Duration fadeOutDuration) { - log.info("hide:" + fadeOutDuration.toString()); + log.debug("hide:" + fadeOutDuration.toString()); //We must remove EventFilter in order to prevent memory leak. if (ownerWindow != null) { ownerWindow.removeEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, diff --git a/desktop/src/main/java/haveno/desktop/components/list/FilterBox.java b/desktop/src/main/java/haveno/desktop/components/list/FilterBox.java index 9bed491f49..adb4707564 100644 --- a/desktop/src/main/java/haveno/desktop/components/list/FilterBox.java +++ b/desktop/src/main/java/haveno/desktop/components/list/FilterBox.java @@ -17,13 +17,10 @@ package haveno.desktop.components.list; -import haveno.core.locale.Res; -import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.InputTextField; import haveno.desktop.util.filtering.FilterableListItem; import javafx.beans.value.ChangeListener; import javafx.collections.transformation.FilteredList; -import javafx.geometry.Insets; import javafx.scene.control.TableView; import javafx.scene.layout.HBox; @@ -37,13 +34,10 @@ public class FilterBox extends HBox { super(); setSpacing(5.0); - AutoTooltipLabel label = new AutoTooltipLabel(Res.get("shared.filter")); - HBox.setMargin(label, new Insets(5.0, 0, 0, 10.0)); - textField = new InputTextField(); textField.setMinWidth(500); - getChildren().addAll(label, textField); + getChildren().addAll(textField); } public void initialize(FilteredList filteredList, @@ -67,4 +61,8 @@ public class FilterBox extends HBox { private void applyFilteredListPredicate(String filterString) { filteredList.setPredicate(item -> item.match(filterString)); } + + public void setPromptText(String promptText) { + textField.setPromptText(promptText); + } } diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java index f57c3f154f..7baa76c564 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java @@ -36,6 +36,7 @@ import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.FormBuilder; +import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.geometry.Insets; import javafx.scene.control.CheckBox; @@ -202,6 +203,8 @@ public class AssetsForm extends PaymentMethodForm { CurrencyUtil.getActiveSortedCryptoCurrencies(filterManager)); currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 10)); + currencyComboBox.setCellFactory(GUIUtil.getTradeCurrencyCellFactoryNameAndCode()); + currencyComboBox.setConverter(new StringConverter<>() { @Override public String toString(TradeCurrency tradeCurrency) { diff --git a/desktop/src/main/java/haveno/desktop/haveno.css b/desktop/src/main/java/haveno/desktop/haveno.css index e3cfac8c0e..7c24526aaf 100644 --- a/desktop/src/main/java/haveno/desktop/haveno.css +++ b/desktop/src/main/java/haveno/desktop/haveno.css @@ -39,7 +39,7 @@ -fx-text-fill: -bs-color-primary; } -.highlight, .highlight-static { +.highlight, .highlight-static, .highlight.label .glyph-icon { -fx-text-fill: -fx-accent; -fx-fill: -fx-accent; } @@ -105,6 +105,11 @@ -fx-font-size: 1.077em; -fx-font-family: "IBM Plex Mono"; -fx-padding: 0 !important; + -fx-border-width: 0; + -fx-text-fill: -bs-rd-font-dark-gray !important; +} + +.confirmation-text-field-as-label-icon { } /* Other UI Elements */ @@ -150,9 +155,9 @@ -fx-text-fill: -bs-rd-font-dark-gray; -fx-font-size: 0.923em; -fx-font-weight: normal; - -fx-background-radius: 2px; - -fx-pref-height: 32; - -fx-min-height: -fx-pref-height; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-min-height: 32; -fx-padding: 0 40 0 40; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 2, 0, 0, 0, 1); -fx-cursor: hand; @@ -171,8 +176,12 @@ -fx-text-fill: -bs-background-color; } -.compact-button, .table-cell .jfx-button, .action-button.compact-button { - -fx-padding: 0 10 0 10; +.action-button.compact-button, .compact-button { + -fx-padding: 0 15 0 15; +} + +.table-cell .jfx-button { + -fx-padding: 0 7 0 7; } .tiny-button, @@ -217,10 +226,59 @@ -fx-border-width: 1; } +.jfx-combo-box, .jfx-text-field, .jfx-text-area, .jfx-password-field, .toggle-button-no-slider { + -fx-padding: 7 14 7 14; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-border-color: transparent; +} + .jfx-combo-box { - -jfx-focus-color: -bs-color-primary; - -jfx-unfocus-color: -bs-color-gray-line; - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-form-field; +} + +.input-line, .input-focused-line { + -fx-background-color: transparent; + visibility: hidden; + -fx-max-height: 0; +} + +.jfx-text-field { + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-background-color: -bs-color-background-form-field; +} + +.jfx-text-field.label-float .prompt-container { + -fx-translate-y: 0px; +} + +.jfx-text-field.filled.label-float .prompt-container, +.jfx-text-field.label-float:focused .prompt-container, +.jfx-combo-box.filled.label-float .prompt-container, +.jfx-combo-box.label-float:focused .prompt-container, +.jfx-password-field.filled.label-float .prompt-container, +.jfx-password-field.label-float:focused .prompt-container { + -fx-translate-x: -14px; + -fx-translate-y: -5.5px; +} + +.jfx-combo-box .arrow-button { + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-padding: 0 0 0 10; +} + +.jfx-combo-box:hover { + -fx-cursor: hand; +} + +.jfx-combo-box:editable:hover { + -fx-cursor: null; +} + +.jfx-combo-box .arrow-button:hover { + -fx-cursor: hand; } .jfx-combo-box > .list-cell { @@ -228,15 +286,66 @@ -fx-font-family: "IBM Plex Sans Medium"; } +/* TODO: otherwise combo box with "odd" class is opacity 0.4? */ +.jfx-combo-box > .list-cell:odd, .jfx-combo-box > .list-cell:even { + -fx-opacity: 1.0; +} + +.jfx-combo-box > .list-cell, +.jfx-combo-box > .text-field { + -fx-padding: 0 !important; +} + .jfx-combo-box > .arrow-button > .arrow { -fx-background-color: null; - -fx-border-color: -jfx-unfocus-color; + -fx-border-color: -bs-color-gray-line; -fx-shape: "M 0 0 l 3.5 4 l 3.5 -4"; } +.combo-box-popup { + -fx-background-color: -bs-color-background-pane; + -fx-background-radius: 15; + -fx-border-radius: 15; + -fx-padding: 5; +} + +.combo-box-popup .scroll-pane { + -fx-background-color: -bs-color-background-pane; + -fx-background-radius: 15; + -fx-border-radius: 15; + -fx-padding: 5; +} + +.combo-box-popup > .list-view { + -fx-background-color: -bs-color-background-pane; + -fx-border-color: -bs-color-border-form-field; + -fx-translate-y: 4; + -fx-background-radius: 15; + -fx-border-radius: 15; + -fx-padding: 5; +} + +/* Rounds the first and last list cells to create full round illusion */ +.combo-box-popup .list-cell:first-child { + -fx-background-radius: 10 10 0 0; +} +.combo-box-popup .list-cell:last-child { + -fx-background-radius: 0 0 10 10; +} + +.combo-box-popup .list-cell:hover { + -fx-background-radius: 8; +} + +.combo-box-popup > .list-view:hover { + -fx-cursor: hand; +} + .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected { -fx-background: -fx-selection-bar; -fx-background-color: -fx-selection-bar; + -fx-background-radius: 15; + -fx-border-radius: 15; } .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:hover, @@ -301,19 +410,12 @@ tree-table-view:focused { -fx-background-insets: 0; } -.jfx-text-field { - -jfx-focus-color: -bs-color-primary; - -fx-background-color: -bs-background-color; - -fx-background-radius: 3 3 0 0; - -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; +/* combo box list view */ +.combo-box .list-view .list-cell:odd { + -fx-background-color: -bs-color-background-pane; } - -.jfx-text-field > .input-line { - -fx-translate-x: -0.333333em; -} - -.jfx-text-field > .input-focused-line { - -fx-translate-x: -0.333333em; +.combo-box .list-view .list-cell:even { + -fx-background-color: -bs-color-background-pane; } .jfx-text-field-top-label { @@ -321,8 +423,7 @@ tree-table-view:focused { } .jfx-text-field:readonly, .hyperlink-with-icon { - -fx-background-color: -bs-color-gray-1; - -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; + -fx-background-color: -bs-color-background-form-field-readonly; } .jfx-text-field:readonly > .input-line { @@ -348,23 +449,16 @@ tree-table-view:focused { } .jfx-password-field { - -fx-background-color: -bs-background-color; - -fx-background-radius: 3 3 0 0; - -jfx-focus-color: -bs-color-primary; - -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; + -fx-background-color: -bs-color-background-form-field; } .jfx-password-field > .input-line { -fx-translate-x: -0.333333em; } -.jfx-password-field > .input-focused-line { - -fx-translate-x: -0.333333em; -} - -.jfx-text-field:error, .jfx-password-field:error, .jfx-text-area:error { - -jfx-focus-color: -bs-rd-error-red; - -jfx-unfocus-color: -bs-rd-error-red; +.jfx-combo-box:error, +.jfx-text-field:error { + -fx-text-fill: -bs-rd-error-red; } .jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label { @@ -378,58 +472,69 @@ tree-table-view:focused { -fx-font-size: 1em; } -.input-with-border { - -fx-background-color: -bs-background-color; - -fx-border-width: 1; +.offer-input { + -fx-background-color: -bs-color-background-form-field; -fx-border-color: -bs-background-gray; - -fx-border-radius: 3; -fx-pref-height: 43; -fx-pref-width: 310; - -fx-effect: innershadow(gaussian, -bs-text-color-transparent, 3, 0, 0, 1); -} - -.input-with-border .text-field { - -fx-alignment: center-right; - -fx-pref-height: 43; - -fx-font-size: 1.385em; -} - -.input-with-border > .input-label { - -fx-font-size: 0.692em; - -fx-min-width: 60; - -fx-padding: 16; + -fx-effect: innershadow(gaussian, -bs-text-color-transparent, 3, 0, 0, 0); + -fx-background-radius: 999; + -fx-border-radius: 999; -fx-alignment: center; } -.input-with-border .icon { +.offer-input .text-field { + -fx-alignment: center-right; + -fx-pref-height: 44; + -fx-font-size: 1.385em; + -fx-background-radius: 999 0 0 999; + -fx-border-radius: 999 0 0 999; + -fx-background-color: -bs-color-background-form-field; + -fx-border-color: transparent; +} + +.offer-input > .input-label { + -fx-font-size: 0.692em; + -fx-min-width: 45; + -fx-padding: 8; + -fx-alignment: center; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-background-color: derive(-bs-color-background-form-field, 15%); +} + +.offer-input .icon { -fx-padding: 10; } -.input-with-border-readonly { +.offer-input-readonly { -fx-background-color: -bs-color-gray-1; -fx-border-width: 0; -fx-pref-width: 300; + -fx-background-radius: 999; + -fx-border-radius: 999; } -.input-with-border-readonly .text-field { +.offer-input-readonly .text-field { -fx-alignment: center-right; -fx-font-size: 1em; -fx-background-color: -bs-color-gray-1; + -fx-border-width: 0; } -.input-with-border-readonly .text-field > .input-line { +.offer-input-readonly .text-field > .input-line { -fx-background-color: transparent; } -.input-with-border-readonly > .input-label { +.offer-input-readonly > .input-label { -fx-font-size: 0.692em; -fx-min-width: 30; -fx-padding: 8; -fx-alignment: center; } -.input-with-border-readonly .icon { - -fx-padding: 2; +.offer-input-readonly .icon { + -fx-padding: 3; } .jfx-badge .badge-pane { @@ -456,7 +561,8 @@ tree-table-view:focused { } .jfx-badge { - -fx-padding: -3 0 0 0; + -fx-padding: -2 0 0 0; + -fx-border-insets: 0 0 0 0; } .jfx-toggle-button, @@ -469,11 +575,15 @@ tree-table-view:focused { -jfx-size: 8; } +.jfx-toggle-button:hover { + -fx-cursor: hand; +} + .jfx-text-area { - -jfx-focus-color: -bs-color-primary; - -jfx-unfocus-color: -bs-color-gray-line; - -fx-background-color: -bs-background-color; - -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; + -fx-background-color: -bs-color-background-form-field; + -fx-padding: 9 9 9 9; + -fx-background-radius: 15; + -fx-border-radius: 15; } .jfx-text-area:readonly { @@ -484,8 +594,10 @@ tree-table-view:focused { -fx-translate-x: -0.333333em; } -.jfx-text-area > .input-focused-line { - -fx-translate-x: -0.333333em; +.text-area .viewport { + -fx-background-color: transparent; + -fx-background-radius: 15; + -fx-border-radius: 15; } .wallet-seed-words { @@ -501,16 +613,18 @@ tree-table-view:focused { -jfx-default-color: -bs-color-primary; } -.jfx-date-picker .jfx-text-field .jfx-text-area { +.jfx-date-picker { -fx-padding: 0.333333em 0em 0.333333em 0em; } .jfx-date-picker .jfx-text-field .jfx-text-area > .input-line { -fx-translate-x: 0em; + -fx-background-color: transparent; } .jfx-date-picker .jfx-text-field .jfx-text-area > .input-focused-line { -fx-translate-x: 0em; + -fx-background-color: transparent; } .jfx-date-picker > .arrow-button > .arrow { @@ -535,14 +649,14 @@ tree-table-view:focused { .scroll-bar:horizontal .track, .scroll-bar:vertical .track { - -fx-background-color: -bs-background-color; - -fx-border-color: -bs-background-color; + -fx-background-color: -bs-color-background-pane; + -fx-border-color: -bs-color-background-pane; -fx-background-radius: 0; } .scroll-bar:vertical .track-background, .scroll-bar:horizontal .track-background { - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-pane; -fx-background-insets: 0; -fx-background-radius: 0; } @@ -573,7 +687,7 @@ tree-table-view:focused { .scroll-bar:vertical .decrement-button, .scroll-bar:horizontal .increment-button, .scroll-bar:horizontal .decrement-button { - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-pane; -fx-padding: 1; } @@ -582,12 +696,12 @@ tree-table-view:focused { .scroll-bar:horizontal .decrement-arrow, .scroll-bar:vertical .decrement-arrow { -fx-shape: null; - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-pane; } .scroll-bar:vertical:focused, .scroll-bar:horizontal:focused { - -fx-background-color: -bs-background-color, -bs-color-gray-ccc, -bs-color-gray-ddd; + -fx-background-color: -bs-color-background-pane; } /* Behavior */ @@ -632,7 +746,7 @@ tree-table-view:focused { /* Main UI */ #base-content-container { - -fx-background-color: -bs-background-gray; + -fx-background-color: -bs-color-gray-background; } .content-pane { @@ -659,8 +773,8 @@ tree-table-view:focused { -fx-background-color: -bs-rd-nav-background; -fx-border-width: 0 0 1 0; -fx-border-color: -bs-rd-nav-primary-border; - -fx-pref-height: 57; - -fx-padding: 0 11 0 0; + -fx-background-radius: 999; + -fx-border-radius: 999; } .top-navigation .separator:vertical .line { @@ -669,50 +783,54 @@ tree-table-view:focused { -fx-border-insets: 0 0 0 1; } +.nav-logo { + -fx-max-width: 190; + -fx-min-width: 155; +} + .nav-primary { -fx-background-color: -bs-rd-nav-primary-background; - -fx-padding: 0 11 0 11; - -fx-border-width: 0 1 0 0; + -fx-border-width: 0 0 0 0; -fx-border-color: -bs-rd-nav-primary-border; - -fx-min-width: 410; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-padding: 9 0 9 20; } .nav-secondary { - -fx-padding: 0 11 0 11; - -fx-min-width: 296; + -fx-padding: 0 14 0 0; } -.nav-price-balance { - -fx-background-color: -bs-color-gray-background; - -fx-background-radius: 3; - -fx-effect: innershadow(gaussian, -bs-text-color-transparent, 3, 0, 0, 1); - -fx-pref-height: 41; - -fx-padding: 0 10 0 0; -} - -.nav-price-balance .separator:vertical .line { +.nav-separator { + -fx-max-width: 1; + -fx-min-width: 1; -fx-border-color: transparent transparent transparent -bs-rd-separator-dark; -fx-border-width: 1; -fx-border-insets: 0 0 0 1; } -.nav-price-balance .jfx-combo-box > .input-line { - -fx-pref-height: 0px; +.nav-spacer { + -fx-max-width: 10; + -fx-min-width: 10; } -.jfx-badge > .nav-button { + +.jfx-badge > .nav-button, +.jfx-badge > .nav-secondary-button { -fx-translate-y: 1; } .nav-button { -fx-cursor: hand; -fx-background-color: transparent; - -fx-padding: 11; + -fx-padding: 9 15; + -fx-background-radius: 999; + -fx-border-radius: 999; } .nav-button .text { - -fx-font-size: 0.769em; - -fx-font-weight: bold; + -fx-font-size: 0.95em; + -fx-font-weight: 500; -fx-fill: -bs-rd-nav-deselected; } @@ -722,23 +840,84 @@ tree-table-view:focused { .nav-button:selected { -fx-background-color: -bs-background-color; - -fx-border-radius: 4; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } +.top-navigation .nav-button:hover { + -fx-background-color: -bs-rd-nav-button-hover; +} + +.nav-primary .nav-button:hover { + -fx-background-color: -bs-rd-nav-primary-button-hover; +} + .nav-button:selected .text { -fx-fill: -bs-rd-nav-selected; } +.nav-secondary-button { + -fx-cursor: hand; + -fx-padding: 9 2 9 2; + -fx-border-insets: 0 12 1 12; + -fx-border-color: transparent; + -fx-border-width: 0 0 1px 0; +} + +.nav-secondary-button .text { + -fx-font-size: 0.95em; + -fx-font-weight: 500; + -fx-fill: -bs-rd-nav-secondary-deselected; +} + +.nav-secondary-button-japanese .text { + -fx-font-size: 1em; +} + +.nav-secondary-button:selected { + -fx-border-color: transparent transparent -bs-rd-nav-secondary-selected transparent; + -fx-border-width: 0 0 1px 0; +} + +.nav-secondary-button:hover { +} + +.nav-secondary-button:selected .text { + -fx-fill: -bs-rd-nav-secondary-selected; +} + .nav-balance-display { -fx-alignment: center-left; -fx-text-fill: -bs-rd-font-balance; } +.nav-price-balance { + -fx-background-color: -bs-rd-nav-background; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-padding: 0 20 0 20; +} + +.nav-price-balance .separator:vertical .line { + -fx-border-color: transparent transparent transparent -bs-rd-separator-dark; + -fx-border-width: 1; + -fx-border-insets: 0 0 0 1; +} + +.nav-price-balance .jfx-combo-box { + -fx-border-color: transparent; + -fx-padding: 0; + -fx-pref-width: 180; +} + +.nav-price-balance .jfx-combo-box > .input-line { + -fx-pref-height: 0px; +} + .nav-balance-label { -fx-font-size: 0.769em; -fx-alignment: center-left; -fx-text-fill: -bs-rd-font-balance-label; + -fx-padding: 0; } #nav-alert-label { @@ -794,6 +973,10 @@ tree-table-view:focused { -fx-text-fill: -bs-background-color; } +.copy-icon-disputes.label .glyph-icon { + -fx-fill: -bs-background-color; +} + .copy-icon:hover { -fx-text-fill: -bs-text-color; } @@ -948,12 +1131,18 @@ textfield */ * * ******************************************************************************/ .table-view .table-row-cell:even .table-cell { - -fx-background-color: derive(-bs-background-color, 5%); - -fx-border-color: derive(-bs-background-color,5%); + -fx-background-color: -bs-color-background-row-even; + -fx-border-color: -bs-color-background-row-even; } .table-view .table-row-cell:odd .table-cell { - -fx-background-color: derive(-bs-background-color,-5%); - -fx-border-color: derive(-bs-background-color,-5%); + -fx-background-color: -bs-color-background-row-odd; + -fx-border-color: -bs-color-background-row-odd; +} +.table-view .table-row-cell.row-faded .table-cell .text { + -fx-fill: -bs-color-table-cell-dim; +} +.cell-faded { + -fx-opacity: 0.4; } .table-view .table-row-cell:hover .table-cell, .table-view .table-row-cell:selected .table-cell { @@ -975,42 +1164,41 @@ textfield */ .table-view .table-cell { -fx-alignment: center-left; - -fx-padding: 2 0 2 0; + -fx-padding: 6 0 4 0; + -fx-text-fill: -bs-text-color; /*-fx-padding: 3 0 2 0;*/ } .table-view .table-cell.last-column { - -fx-alignment: center-right; - -fx-padding: 2 10 2 0; + -fx-padding: 6 0 4 0; } -.table-view .table-cell.last-column.avatar-column { - -fx-alignment: center; - -fx-padding: 2 0 2 0; -} - -.table-view .column-header.last-column { - -fx-padding: 0 10 0 0; -} - -.table-view .column-header.last-column .label { - -fx-alignment: center-right; -} - -.table-view .column-header.last-column.avatar-column { - -fx-padding: 0; -} - -.table-view .column-header.last-column.avatar-column .label { +.table-view .table-cell.avatar-column { -fx-alignment: center; + -fx-padding: 6 0 4 0; } .table-view .table-cell.first-column { - -fx-padding: 2 0 2 10; + -fx-padding: 6 0 4 0; +} + +.table-view .column-header.last-column .label { } .table-view .column-header.first-column { - -fx-padding: 0 0 0 10; + -fx-padding: 0 0 0 0; +} + +.table-view .column-header.last-column { + -fx-padding: 0 0 0 0; +} + +.table-view .column-header.avatar-column { + -fx-padding: 0; +} + +.table-view .column-header.avatar-column .label { + -fx-alignment: center; } .number-column.table-cell { @@ -1019,31 +1207,31 @@ textfield */ } .table-view .filler { - -fx-background-color: -bs-color-gray-0; + -fx-background-color: transparent; } .table-view { -fx-control-inner-background-alt: -fx-control-inner-background; + -fx-padding: 0; +} + +.table-view .column-header-background { + -fx-background-color: -bs-color-background-pane; + -fx-border-color: -bs-color-border-form-field; + -fx-border-width: 0 0 1 0; } .table-view .column-header .label { -fx-alignment: center-left; -fx-font-weight: normal; -fx-font-size: 0.923em; - -fx-padding: 0; + -fx-padding: 6 0 6 0; + -fx-text-fill: -bs-text-color; } .table-view .column-header { - -fx-background-color: -bs-color-gray-0; - -fx-padding: 0; -} - -.table-view .focus { - -fx-alignment: center-left; -} - -.table-view .text { - -fx-fill: -bs-text-color; + -fx-border-color: transparent; + -fx-background-color: -bs-color-background-pane; } /* horizontal scrollbars are never needed and are flickering at scaling so lets turn them off */ @@ -1051,17 +1239,6 @@ textfield */ -fx-opacity: 0; } -.table-view:focused { - -fx-background-color: -fx-box-border, -fx-control-inner-background; - -fx-background-insets: 0, 1; - -fx-padding: 1; -} - -.table-view:focused .table-row-cell:focused { - -fx-background-color: -fx-table-cell-border-color, -fx-background; - -fx-background-insets: 0, 0 0 1 0; -} - .offer-table .table-row-cell { -fx-border-color: -bs-background-color; -fx-table-cell-border-color: -bs-background-color; @@ -1141,6 +1318,46 @@ textfield */ -fx-cell-size: 47px; } +.table-view.offer-table { + -fx-background-radius: 0; + -fx-border-radius: 0; +} + +.table-view.offer-table .column-header.first-column { + -fx-background-radius: 0; + -fx-border-radius: 0; +} + +.table-view.offer-table .column-header.last-column { + -fx-background-radius: 0; + -fx-border-radius: 0; +} + +.table-view.offer-table .table-row-cell { + -fx-background: -fx-accent; + -fx-background-color: -bs-color-gray-6; +} + +.offer-table-top { + -fx-background-color: -bs-color-background-pane; + -fx-padding: 15 15 5 15; + -fx-background-radius: 15 15 0 0; + -fx-border-radius: 15 15 0 0; + -fx-border-width: 0 0 0 0; +} + +.offer-table-top .label { + -fx-text-fill: -bs-text-color; + -fx-font-size: 1.1em; + -fx-font-weight: bold; +} + +.offer-table-top .jfx-button { + -fx-pref-width: 300px; + -fx-min-height: 35px; + -fx-padding: 5 25 5 25; +} + /******************************************************************************* * * * Icons * @@ -1204,17 +1421,28 @@ textfield */ -fx-border-color: -bs-background-gray; } -.text-area-no-border { - -fx-border-color: -bs-background-color; +.text-area-popup { + -fx-border-color: -bs-color-background-popup-blur; } -.text-area-no-border .content { - -fx-background-color: -bs-background-color; +.text-area-popup .content { + -fx-background-color: -bs-color-background-popup-blur; } -.text-area-no-border:focused { - -fx-focus-color: -bs-background-color; - -fx-faint-focus-color: -bs-background-color; +.text-area-popup:focused { + -fx-faint-focus-color: -bs-color-background-popup-blur; +} + +.notification-popup-bg .text-area-popup, .peer-info-popup-bg .text-area-popup { + -fx-border-color: -bs-color-background-popup; +} + +.notification-popup-bg .text-area-popup .content, .peer-info-popup-bg .text-area-popup .content{ + -fx-background-color: -bs-color-background-popup; +} + +.notification-popup-bg .text-area-popup:focused, .peer-info-popup-bg .text-area-popup:focused { + -fx-faint-focus-color: -bs-color-background-popup; } /******************************************************************************* @@ -1238,7 +1466,7 @@ textfield */ } .jfx-tab-pane .headers-region .tab .tab-container .tab-close-button .jfx-rippler { - -jfx-rippler-fill: -fx-accent; + -jfx-rippler-fill: none; } .tab:disabled .jfx-rippler { @@ -1256,7 +1484,7 @@ textfield */ } .jfx-tab-pane .headers-region .tab .tab-container .tab-close-button { - -fx-padding: 0 0 2 0; + -fx-padding: 0 0 0 0; } .jfx-tab-pane .headers-region .tab:selected .tab-container .tab-close-button > .jfx-svg-glyph { @@ -1276,8 +1504,8 @@ textfield */ .jfx-tab-pane .headers-region .tab .tab-container .tab-label { -fx-text-fill: -bs-rd-font-light; - -fx-padding: 14; - -fx-font-size: 0.769em; + -fx-padding: 9 14; + -fx-font-size: .95em; -fx-font-weight: normal; -fx-cursor: hand; } @@ -1291,7 +1519,7 @@ textfield */ } .jfx-tab-pane .headers-region > .tab > .jfx-rippler { - -jfx-rippler-fill: -fx-accent; + -jfx-rippler-fill: none; } .jfx-tab-pane .headers-region .tab:closable { @@ -1395,7 +1623,7 @@ textfield */ } #payment-info { - -fx-background-color: -bs-content-background-gray; + -fx-background-color: -bs-color-gray-fafa; } .toggle-button-active { @@ -1406,6 +1634,19 @@ textfield */ -fx-background-color: -bs-color-gray-1; } +.toggle-button-no-slider { + -fx-border-width: 1px; + -fx-border-color: -bs-color-border-form-field; + -fx-background-insets: 0; + -fx-pref-height: 36px; + -fx-focus-color: transparent; + -fx-faint-focus-color: transparent; +} + +.toggle-button-no-slider:hover { + -fx-cursor: hand; +} + #trade-fee-textfield { -fx-font-size: 0.9em; -fx-alignment: center-right; @@ -1425,7 +1666,7 @@ textfield */ .combo-box-editor-bold { -fx-font-weight: bold; - -fx-padding: 5 8 5 8 !important; + -fx-padding: 0 !important; -fx-text-fill: -bs-text-color; -fx-font-family: "IBM Plex Sans Medium"; } @@ -1456,6 +1697,15 @@ textfield */ -fx-pref-height: 35px; } +.offer-label { + -fx-background-color: rgb(50, 95, 182); + -fx-text-fill: white; + -fx-font-weight: normal; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-padding: 0 6 0 6; +} + /* Offer */ .percentage-label { -fx-alignment: center; @@ -1619,7 +1869,7 @@ textfield */ .titled-group-bg, .titled-group-bg-active { -fx-body-color: -bs-color-gray-background; -fx-border-color: -bs-rd-separator; - -fx-border-width: 0 0 1 0; + -fx-border-width: 0 0 0 0; -fx-background-color: transparent; -fx-background-insets: 0; } @@ -1729,11 +1979,23 @@ textfield */ * * ******************************************************************************/ .grid-pane { - -fx-background-color: -bs-content-background-gray; - -fx-background-radius: 5; - -fx-effect: null; - -fx-effect: dropshadow(gaussian, -bs-color-gray-10, 10, 0, 0, 0); + -fx-background-color: -bs-color-background-popup-blur; -fx-background-insets: 10; + -fx-background-radius: 15; + -fx-border-radius: 15; + -fx-padding: 35, 40, 30, 40; +} + +.grid-pane-separator { + -fx-border-color: -bs-rd-separator; + -fx-border-width: 0 0 1 0; + -fx-translate-y: -2; +} + +.grid-pane .text-area { + -fx-border-width: 1; + -fx-border-color: -bs-color-border-form-field; + -fx-text-fill: -bs-text-color; } /******************************************************************************************************************** @@ -1762,24 +2024,26 @@ textfield */ -fx-text-alignment: center; } -#charts .chart-plot-background, #charts-dao .chart-plot-background { - -fx-background-color: -bs-background-color; +.chart-pane, .chart-plot-background, #charts .chart-plot-background { + -fx-background-color: transparent; } #charts .default-color0.chart-area-symbol { - -fx-background-color: -bs-sell, -bs-background-color; -} - -#charts .default-color1.chart-area-symbol, #charts-dao .default-color0.chart-area-symbol { -fx-background-color: -bs-buy, -bs-background-color; } +#charts .default-color1.chart-area-symbol, #charts-dao .default-color0.chart-area-symbol { + -fx-background-color: -bs-sell, -bs-background-color; +} + #charts .default-color0.chart-series-area-line { - -fx-stroke: -bs-sell; + -fx-stroke: -bs-buy; + -fx-stroke-width: 2px; } #charts .default-color1.chart-series-area-line, #charts-dao .default-color0.chart-series-area-line { - -fx-stroke: -bs-buy; + -fx-stroke: -bs-sell; + -fx-stroke-width: 2px; } /* The .chart-line-symbol rules change the color of the legend symbol */ @@ -1907,13 +2171,6 @@ textfield */ -fx-stroke-width: 2px; } -#charts .default-color0.chart-series-area-fill { - -fx-fill: -bs-sell-transparent; -} - -#charts .default-color1.chart-series-area-fill, #charts-dao .default-color0.chart-series-area-fill { - -fx-fill: -bs-buy-transparent; -} .chart-vertical-grid-lines { -fx-stroke: transparent; } @@ -2013,22 +2270,41 @@ textfield */ -fx-text-fill: -bs-rd-error-red; } -.popup-bg, .notification-popup-bg, .peer-info-popup-bg { +.popup-headline-information.label .glyph-icon, +.popup-headline-warning.label .glyph-icon, +.popup-icon-information.label .glyph-icon, +.popup-icon-warning.label .glyph-icon { + -fx-fill: -bs-color-primary; +} + +.popup-bg { -fx-font-size: 1.077em; - -fx-text-fill: -bs-rd-font-dark; - -fx-background-color: -bs-background-color; - -fx-background-radius: 0; + -fx-background-color: -bs-color-background-popup-blur; -fx-background-insets: 44; - -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, 0, 0); + -fx-background-radius: 15; + -fx-border-radius: 15; +} + +.notification-popup-bg, .peer-info-popup-bg { + -fx-font-size: 0.846em; + -fx-text-fill: -bs-rd-font-dark; + -fx-background-color: -bs-color-background-popup; + -fx-background-insets: 44; + -fx-effect: dropshadow(gaussian, -bs-text-color-dropshadow-light-mode, 44, 0, 0, 0); + -fx-background-radius: 15; + -fx-border-radius: 15; } .popup-bg-top { -fx-font-size: 1.077em; -fx-text-fill: -bs-rd-font-dark; - -fx-background-color: -bs-background-color; - -fx-background-radius: 0; + -fx-background-color: -bs-color-background-popup-blur; -fx-background-insets: 44; - -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, 0, 0); + -fx-background-radius: 0 0 15px 15px; +} + +.popup-dropshadow { + -fx-effect: dropshadow(gaussian, -bs-text-color-dropshadow, 20, 0, 0, 0); } .notification-popup-headline, peer-info-popup-headline { @@ -2037,18 +2313,6 @@ textfield */ -fx-text-fill: -bs-color-primary; } -.notification-popup-bg { - -fx-font-size: 0.846em; - -fx-background-insets: 44; - -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, -1, 3); -} - -.peer-info-popup-bg { - -fx-font-size: 0.846em; - -fx-background-insets: 44; - -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, -1, 3); -} - .account-status-title { -fx-font-size: 0.769em; -fx-font-family: "IBM Plex Sans Medium"; @@ -2069,7 +2333,7 @@ textfield */ } #price-feed-combo > .list-cell { - -fx-text-fill: -bs-text-color; + -fx-text-fill: -bs-rd-font-balance; -fx-font-family: "IBM Plex Sans"; } @@ -2089,42 +2353,48 @@ textfield */ } #toggle-left { - -fx-border-radius: 4 0 0 4; -fx-border-color: -bs-rd-separator-dark; + -fx-border-radius: 4 0 0 4; -fx-border-style: solid; - -fx-border-width: 0 1 0 0; + -fx-border-width: 1 1 1 1; -fx-background-radius: 4 0 0 4; + -fx-border-insets: 0; + -fx-background-insets: 1 1 1 1; -fx-background-color: -bs-background-color; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } #toggle-center { - -fx-border-radius: 0; -fx-border-color: -bs-rd-separator-dark; + -fx-border-radius: 0; -fx-border-style: solid; - -fx-border-width: 0 1 0 0; + -fx-border-width: 1 1 1 0; -fx-border-insets: 0; - -fx-background-insets: 0; + -fx-background-insets: 1 1 1 0; -fx-background-radius: 0; -fx-background-color: -bs-background-color; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } -#toggle-center:selected, #toggle-left:selected, #toggle-right:selected { - -fx-text-fill: -bs-background-color; - -fx-background-color: -bs-toggle-selected; -} - #toggle-right { + -fx-border-color: -bs-rd-separator-dark; -fx-border-radius: 0 4 4 0; - -fx-border-width: 0; + -fx-border-width: 1 1 1 0; + -fx-border-insets: 0; + -fx-background-insets: 1 1 1 0; -fx-background-radius: 0 4 4 0; -fx-background-color: -bs-background-color; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } +#toggle-center:selected, #toggle-left:selected, #toggle-right:selected { + -fx-text-fill: white; + -fx-background-color: -bs-toggle-selected; +} + #toggle-left:hover, #toggle-right:hover, #toggle-center:hover { -fx-background-color: -bs-toggle-selected; + -fx-cursor: hand; } /******************************************************************************************************************** @@ -2136,10 +2406,18 @@ textfield */ -fx-text-fill: -bs-text-color; } +.message.label .glyph-icon { + -fx-fill: -bs-text-color; +} + .my-message { -fx-text-fill: -bs-background-color; } +.my-message.label .glyph-icon { + -fx-fill: -bs-background-color; +} + .message-header { -fx-text-fill: -bs-color-gray-3; -fx-font-size: 0.846em; @@ -2308,18 +2586,103 @@ textfield */ /******************************************************************************************************************** * * - * Popover * + * Popover * * * ********************************************************************************************************************/ .popover > .content { -fx-padding: 10; + -fx-background-color: -bs-color-background-popup; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-background-insets: 1; } .popover > .content .default-text { -fx-text-fill: -bs-text-color; } -.popover > .border { - -fx-stroke: linear-gradient(to bottom, -bs-text-color-transparent, -bs-text-color-transparent-dark) !important; - -fx-fill: -bs-background-color !important; +.popover > .content .text-field { + -fx-background-color: -bs-color-background-form-field-readonly !important; + -fx-border-radius: 4; + -fx-background-radius: 4; +} + +.popover > .border { + -fx-stroke: linear-gradient(to bottom, -bs-text-color-transparent, -bs-text-color-dropshadow) !important; + -fx-fill: -bs-color-background-popup !important; +} + +/******************************************************************************************************************** + * * + * Other * + * * + ********************************************************************************************************************/ +.input-with-border { + -fx-border-width: 1; + -fx-border-color: -bs-color-border-form-field; + -fx-border-insets: 1 0 1 0; + -fx-background-insets: 1 0 1 0; +} + +.table-view.non-interactive-table .column-header .label { + -fx-text-fill: -bs-text-color-dim2; +} + +.highlight-text { + -fx-text-fill: -fx-dark-text-color !important; +} + +.grid-pane .text-area, +.flat-text-area-with-border { + -fx-background-radius: 8; + -fx-border-radius: 8; + -fx-font-size: 1.077em; + -fx-font-family: "IBM Plex Sans"; + -fx-font-weight: normal; + -fx-text-fill: -bs-rd-font-dark-gray !important; + -fx-border-width: 1; + -fx-border-color: -bs-color-border-form-field !important; +} + +.grid-pane .text-area:readonly, +.flat-text-area-with-border { + -fx-background-color: transparent !important; +} + +.grid-pane .text-area { + -fx-max-height: 150 !important; +} + +.passphrase-copy-box { + -fx-border-width: 1; + -fx-border-color: -bs-color-border-form-field; + -fx-background-radius: 8; + -fx-border-radius: 8; + -fx-padding: 13; + -fx-background-insets: 0; +} + +.passphrase-copy-box > .jfx-text-field { + -fx-padding: 0; + -fx-background-color: transparent; + -fx-border-width: 0; +} + +.passphrase-copy-box .label { + -fx-text-fill: white; + -fx-padding: 0; +} + +.passphrase-copy-box .jfx-button { + -fx-padding: 5 15 5 15; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-min-height: 0; + -fx-font-size: 1.077em; + -fx-font-family: "IBM Plex Sans"; + -fx-font-weight: normal; +} + +.popup-with-input { + -fx-background-color: -bs-color-background-popup-input; } diff --git a/desktop/src/main/java/haveno/desktop/images.css b/desktop/src/main/java/haveno/desktop/images.css index aacd4c6b1b..1e36427ee9 100644 --- a/desktop/src/main/java/haveno/desktop/images.css +++ b/desktop/src/main/java/haveno/desktop/images.css @@ -1,16 +1,3 @@ -/* splash screen */ -/*noinspection CssUnknownTarget*/ -#image-splash-logo { - -fx-image: url("../../images/logo_splash.png"); -} - -/* splash screen testnet */ -/*noinspection CssUnknownTarget*/ -#image-splash-testnet-logo { - -fx-image: url("../../images/logo_splash_testnet.png"); -} - -/* shared*/ #image-info { -fx-image: url("../../images/info.png"); } @@ -23,16 +10,29 @@ -fx-image: url("../../images/alert_round.png"); } +#image-red_circle_solid { + -fx-image: url("../../images/red_circle_solid.png"); +} + + #image-green_circle { -fx-image: url("../../images/green_circle.png"); } +#image-green_circle_solid { + -fx-image: url("../../images/green_circle_solid.png"); +} + #image-yellow_circle { -fx-image: url("../../images/yellow_circle.png"); } -#image-blue_circle { - -fx-image: url("../../images/blue_circle.png"); +#image-yellow_circle_solid { + -fx-image: url("../../images/yellow_circle_solid.png"); +} + +#image-blue_circle_solid { + -fx-image: url("../../images/blue_circle_solid.png"); } #image-remove { @@ -300,3 +300,59 @@ #image-new-trade-protocol-screenshot { -fx-image: url("../../images/new_trade_protocol_screenshot.png"); } + +#image-support { + -fx-image: url("../../images/support.png"); +} + +#image-account { + -fx-image: url("../../images/account.png"); +} + +#image-settings { + -fx-image: url("../../images/settings.png"); +} + +#image-btc-logo { + -fx-image: url("../../images/btc_logo.png"); +} + +#image-bch-logo { + -fx-image: url("../../images/bch_logo.png"); +} + +#image-dai-erc20-logo { + -fx-image: url("../../images/dai-erc20_logo.png"); +} + +#image-eth-logo { + -fx-image: url("../../images/eth_logo.png"); +} + +#image-ltc-logo { + -fx-image: url("../../images/ltc_logo.png"); +} + +#image-usdc-erc20-logo { + -fx-image: url("../../images/usdc-erc20_logo.png"); +} + +#image-usdt-erc20-logo { + -fx-image: url("../../images/usdt-erc20_logo.png"); +} + +#image-usdt-trc20-logo { + -fx-image: url("../../images/usdt-trc20_logo.png"); +} + +#image-xmr-logo { + -fx-image: url("../../images/xmr_logo.png"); +} + +#image-dark-mode-toggle { + -fx-image: url("../../images/dark_mode_toggle.png"); +} + +#image-light-mode-toggle { + -fx-image: url("../../images/light_mode_toggle.png"); +} diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index 7856a2021f..ee7434324c 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -32,6 +32,7 @@ import haveno.core.locale.GlobalSettings; import haveno.core.locale.LanguageUtil; import haveno.core.locale.Res; import haveno.core.provider.price.MarketPrice; +import haveno.core.user.Preferences; import haveno.desktop.Navigation; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.FxmlView; @@ -73,6 +74,7 @@ import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; import javafx.geometry.Orientation; import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; @@ -92,6 +94,7 @@ import static javafx.scene.layout.AnchorPane.setRightAnchor; import static javafx.scene.layout.AnchorPane.setTopAnchor; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; @@ -125,17 +128,19 @@ public class MainView extends InitializableView { private Label xmrSplashInfo; private Popup p2PNetworkWarnMsgPopup, xmrNetworkWarnMsgPopup; private final TorNetworkSettingsWindow torNetworkSettingsWindow; + private final Preferences preferences; + private static final int networkIconSize = 20; public static StackPane getRootContainer() { return MainView.rootContainer; } public static void blurLight() { - transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 5); + transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 15); } public static void blurUltraLight() { - transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 2); + transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 15); } public static void darken() { @@ -151,12 +156,14 @@ public class MainView extends InitializableView { CachingViewLoader viewLoader, Navigation navigation, Transitions transitions, - TorNetworkSettingsWindow torNetworkSettingsWindow) { + TorNetworkSettingsWindow torNetworkSettingsWindow, + Preferences preferences) { super(model); this.viewLoader = viewLoader; this.navigation = navigation; MainView.transitions = transitions; this.torNetworkSettingsWindow = torNetworkSettingsWindow; + this.preferences = preferences; } @Override @@ -165,15 +172,15 @@ public class MainView extends InitializableView { if (LanguageUtil.isDefaultLanguageRTL()) MainView.rootContainer.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT); - ToggleButton marketButton = new NavButton(MarketView.class, Res.get("mainView.menu.market").toUpperCase()); - ToggleButton buyButton = new NavButton(BuyOfferView.class, Res.get("mainView.menu.buyXmr").toUpperCase()); - ToggleButton sellButton = new NavButton(SellOfferView.class, Res.get("mainView.menu.sellXmr").toUpperCase()); - ToggleButton portfolioButton = new NavButton(PortfolioView.class, Res.get("mainView.menu.portfolio").toUpperCase()); - ToggleButton fundsButton = new NavButton(FundsView.class, Res.get("mainView.menu.funds").toUpperCase()); + ToggleButton marketButton = new NavButton(MarketView.class, Res.get("mainView.menu.market")); + ToggleButton buyButton = new NavButton(BuyOfferView.class, Res.get("mainView.menu.buyXmr")); + ToggleButton sellButton = new NavButton(SellOfferView.class, Res.get("mainView.menu.sellXmr")); + ToggleButton portfolioButton = new NavButton(PortfolioView.class, Res.get("mainView.menu.portfolio")); + ToggleButton fundsButton = new NavButton(FundsView.class, Res.get("mainView.menu.funds")); - ToggleButton supportButton = new NavButton(SupportView.class, Res.get("mainView.menu.support")); - ToggleButton accountButton = new NavButton(AccountView.class, Res.get("mainView.menu.account")); - ToggleButton settingsButton = new NavButton(SettingsView.class, Res.get("mainView.menu.settings")); + ToggleButton supportButton = new SecondaryNavButton(SupportView.class, Res.get("mainView.menu.support"), "image-support"); + ToggleButton accountButton = new SecondaryNavButton(AccountView.class, Res.get("mainView.menu.account"), "image-account"); + ToggleButton settingsButton = new SecondaryNavButton(SettingsView.class, Res.get("mainView.menu.settings"), "image-settings"); JFXBadge portfolioButtonWithBadge = new JFXBadge(portfolioButton); JFXBadge supportButtonWithBadge = new JFXBadge(supportButton); @@ -298,47 +305,56 @@ public class MainView extends InitializableView { } }); - HBox primaryNav = new HBox(marketButton, getNavigationSeparator(), buyButton, getNavigationSeparator(), - sellButton, getNavigationSeparator(), portfolioButtonWithBadge, getNavigationSeparator(), fundsButton); + HBox primaryNav = new HBox(getLogoPane(), marketButton, getNavigationSpacer(), buyButton, getNavigationSpacer(), + sellButton, getNavigationSpacer(), portfolioButtonWithBadge, getNavigationSpacer(), fundsButton); primaryNav.setAlignment(Pos.CENTER_LEFT); primaryNav.getStyleClass().add("nav-primary"); HBox.setHgrow(primaryNav, Priority.SOMETIMES); - HBox secondaryNav = new HBox(supportButtonWithBadge, getNavigationSpacer(), accountButton, - getNavigationSpacer(), settingsButtonWithBadge, getNavigationSpacer()); - secondaryNav.getStyleClass().add("nav-secondary"); - HBox.setHgrow(secondaryNav, Priority.SOMETIMES); - - secondaryNav.setAlignment(Pos.CENTER); - HBox priceAndBalance = new HBox(marketPriceBox.second, getNavigationSeparator(), availableBalanceBox.second, getNavigationSeparator(), pendingBalanceBox.second, getNavigationSeparator(), reservedBalanceBox.second); - priceAndBalance.setMaxHeight(41); priceAndBalance.setAlignment(Pos.CENTER); - priceAndBalance.setSpacing(9); + priceAndBalance.setSpacing(12); priceAndBalance.getStyleClass().add("nav-price-balance"); - HBox navPane = new HBox(primaryNav, secondaryNav, getNavigationSpacer(), - priceAndBalance) {{ - setLeftAnchor(this, 0d); - setRightAnchor(this, 0d); - setTopAnchor(this, 0d); + HBox navPane = new HBox(primaryNav, priceAndBalance) {{ + setLeftAnchor(this, 25d); + setRightAnchor(this, 25d); + setTopAnchor(this, 20d); setPadding(new Insets(0, 0, 0, 0)); getStyleClass().add("top-navigation"); }}; navPane.setAlignment(Pos.CENTER); + HBox secondaryNav = new HBox(supportButtonWithBadge, accountButton, settingsButtonWithBadge); + secondaryNav.getStyleClass().add("nav-secondary"); + secondaryNav.setAlignment(Pos.CENTER_RIGHT); + secondaryNav.setPickOnBounds(false); + HBox.setHgrow(secondaryNav, Priority.ALWAYS); + AnchorPane.setLeftAnchor(secondaryNav, 0.0); + AnchorPane.setRightAnchor(secondaryNav, 0.0); + AnchorPane.setTopAnchor(secondaryNav, 0.0); + + AnchorPane secondaryNavContainer = new AnchorPane() {{ + setId("nav-secondary-container"); + setLeftAnchor(this, 0d); + setRightAnchor(this, 0d); + setTopAnchor(this, 94d); + }}; + secondaryNavContainer.setPickOnBounds(false); + secondaryNavContainer.getChildren().add(secondaryNav); + AnchorPane contentContainer = new AnchorPane() {{ getStyleClass().add("content-pane"); setLeftAnchor(this, 0d); setRightAnchor(this, 0d); - setTopAnchor(this, 57d); + setTopAnchor(this, 95d); setBottomAnchor(this, 0d); }}; - AnchorPane applicationContainer = new AnchorPane(navPane, contentContainer) {{ + AnchorPane applicationContainer = new AnchorPane(navPane, contentContainer, secondaryNavContainer) {{ setId("application-container"); }}; @@ -398,15 +414,32 @@ public class MainView extends InitializableView { private Separator getNavigationSeparator() { final Separator separator = new Separator(Orientation.VERTICAL); HBox.setHgrow(separator, Priority.ALWAYS); - separator.setMaxHeight(22); separator.setMaxWidth(Double.MAX_VALUE); + separator.getStyleClass().add("nav-separator"); return separator; } + @NotNull + private Pane getLogoPane() { + ImageView logo = new ImageView(); + logo.setId("image-logo-landscape"); + logo.setPreserveRatio(true); + logo.setFitHeight(40); + logo.setSmooth(true); + logo.setCache(true); + + final Pane pane = new Pane(); + HBox.setHgrow(pane, Priority.ALWAYS); + pane.getStyleClass().add("nav-logo"); + pane.getChildren().add(logo); + return pane; + } + @NotNull private Region getNavigationSpacer() { final Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); + spacer.getStyleClass().add("nav-spacer"); return spacer; } @@ -447,7 +480,6 @@ public class MainView extends InitializableView { priceComboBox.setVisibleRowCount(12); priceComboBox.setFocusTraversable(false); priceComboBox.setId("price-feed-combo"); - priceComboBox.setPadding(new Insets(0, -4, -4, 0)); priceComboBox.setCellFactory(p -> getPriceFeedComboBoxListCell()); ListCell buttonCell = getPriceFeedComboBoxListCell(); buttonCell.setId("price-feed-combo"); @@ -458,7 +490,6 @@ public class MainView extends InitializableView { updateMarketPriceLabel(marketPriceLabel); marketPriceLabel.getStyleClass().add("nav-balance-label"); - marketPriceLabel.setPadding(new Insets(-2, 0, 4, 9)); marketPriceBox.getChildren().addAll(priceComboBox, marketPriceLabel); @@ -509,7 +540,10 @@ public class MainView extends InitializableView { vBox.setId("splash"); ImageView logo = new ImageView(); - logo.setId(Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_MAINNET ? "image-splash-logo" : "image-splash-testnet-logo"); + logo.setId(Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_MAINNET ? "image-logo-splash" : "image-logo-splash-testnet"); + logo.setFitWidth(400); + logo.setPreserveRatio(true); + logo.setSmooth(true); // createBitcoinInfoBox xmrSplashInfo = new AutoTooltipLabel(); @@ -552,7 +586,7 @@ public class MainView extends InitializableView { // create P2PNetworkBox splashP2PNetworkLabel = new AutoTooltipLabel(); splashP2PNetworkLabel.setWrapText(true); - splashP2PNetworkLabel.setMaxWidth(500); + splashP2PNetworkLabel.setMaxWidth(700); splashP2PNetworkLabel.setTextAlignment(TextAlignment.CENTER); splashP2PNetworkLabel.getStyleClass().add("sub-info"); splashP2PNetworkLabel.textProperty().bind(model.getP2PNetworkInfo()); @@ -587,9 +621,11 @@ public class MainView extends InitializableView { ImageView splashP2PNetworkIcon = new ImageView(); splashP2PNetworkIcon.setId("image-connection-tor"); + splashP2PNetworkIcon.setFitWidth(networkIconSize); + splashP2PNetworkIcon.setFitHeight(networkIconSize); splashP2PNetworkIcon.setVisible(false); splashP2PNetworkIcon.setManaged(false); - HBox.setMargin(splashP2PNetworkIcon, new Insets(0, 0, 5, 0)); + HBox.setMargin(splashP2PNetworkIcon, new Insets(0, 0, 0, 0)); splashP2PNetworkIcon.setOnMouseClicked(e -> { torNetworkSettingsWindow.show(); }); @@ -603,6 +639,8 @@ public class MainView extends InitializableView { splashP2PNetworkIcon.setId(newValue); splashP2PNetworkIcon.setVisible(true); splashP2PNetworkIcon.setManaged(true); + splashP2PNetworkIcon.setFitWidth(networkIconSize); + splashP2PNetworkIcon.setFitHeight(networkIconSize); // if we can connect in 10 sec. we know that tor is working showTorNetworkSettingsTimer.stop(); @@ -725,15 +763,39 @@ public class MainView extends InitializableView { setRightAnchor(versionBox, 10d); setBottomAnchor(versionBox, 7d); + // Dark mode toggle + ImageView useDarkModeIcon = new ImageView(); + useDarkModeIcon.setId(preferences.getCssTheme() == 1 ? "image-dark-mode-toggle" : "image-light-mode-toggle"); + useDarkModeIcon.setFitHeight(networkIconSize); + useDarkModeIcon.setPreserveRatio(true); + useDarkModeIcon.setPickOnBounds(true); + useDarkModeIcon.setCursor(Cursor.HAND); + setRightAnchor(useDarkModeIcon, 8d); + setBottomAnchor(useDarkModeIcon, 6d); + Tooltip modeToolTip = new Tooltip(); + Tooltip.install(useDarkModeIcon, modeToolTip); + useDarkModeIcon.setOnMouseEntered(e -> modeToolTip.setText(Res.get(preferences.getCssTheme() == 1 ? "setting.preferences.useLightMode" : "setting.preferences.useDarkMode"))); + useDarkModeIcon.setOnMouseClicked(e -> { + preferences.setCssTheme(preferences.getCssTheme() != 1); + }); + preferences.getCssThemeProperty().addListener((observable, oldValue, newValue) -> { + useDarkModeIcon.setId(preferences.getCssTheme() == 1 ? "image-dark-mode-toggle" : "image-light-mode-toggle"); + }); + // P2P Network Label p2PNetworkLabel = new AutoTooltipLabel(); p2PNetworkLabel.setId("footer-pane"); p2PNetworkLabel.textProperty().bind(model.getP2PNetworkInfo()); + double networkIconRightAnchor = 54d; ImageView p2PNetworkIcon = new ImageView(); - setRightAnchor(p2PNetworkIcon, 10d); - setBottomAnchor(p2PNetworkIcon, 5d); + setRightAnchor(p2PNetworkIcon, networkIconRightAnchor); + setBottomAnchor(p2PNetworkIcon, 6d); + p2PNetworkIcon.setPickOnBounds(true); + p2PNetworkIcon.setCursor(Cursor.HAND); p2PNetworkIcon.setOpacity(0.4); + p2PNetworkIcon.setFitWidth(networkIconSize); + p2PNetworkIcon.setFitHeight(networkIconSize); p2PNetworkIcon.idProperty().bind(model.getP2PNetworkIconId()); p2PNetworkLabel.idProperty().bind(model.getP2pNetworkLabelId()); model.getP2pNetworkWarnMsg().addListener((ov, oldValue, newValue) -> { @@ -749,8 +811,12 @@ public class MainView extends InitializableView { }); ImageView p2PNetworkStatusIcon = new ImageView(); - setRightAnchor(p2PNetworkStatusIcon, 30d); - setBottomAnchor(p2PNetworkStatusIcon, 7d); + p2PNetworkStatusIcon.setPickOnBounds(true); + p2PNetworkStatusIcon.setCursor(Cursor.HAND); + p2PNetworkStatusIcon.setFitWidth(networkIconSize); + p2PNetworkStatusIcon.setFitHeight(networkIconSize); + setRightAnchor(p2PNetworkStatusIcon, networkIconRightAnchor + 22); + setBottomAnchor(p2PNetworkStatusIcon, 6d); Tooltip p2pNetworkStatusToolTip = new Tooltip(); Tooltip.install(p2PNetworkStatusIcon, p2pNetworkStatusToolTip); p2PNetworkStatusIcon.setOnMouseEntered(e -> p2pNetworkStatusToolTip.setText(model.getP2pConnectionSummary())); @@ -791,10 +857,10 @@ public class MainView extends InitializableView { VBox vBox = new VBox(); vBox.setAlignment(Pos.CENTER_RIGHT); vBox.getChildren().addAll(p2PNetworkLabel, p2pNetworkProgressBar); - setRightAnchor(vBox, 53d); + setRightAnchor(vBox, networkIconRightAnchor + 45); setBottomAnchor(vBox, 5d); - return new AnchorPane(separator, xmrInfoLabel, versionBox, vBox, p2PNetworkStatusIcon, p2PNetworkIcon) {{ + return new AnchorPane(separator, xmrInfoLabel, versionBox, vBox, p2PNetworkStatusIcon, p2PNetworkIcon, useDarkModeIcon) {{ setId("footer-pane"); setMinHeight(30); setMaxHeight(30); @@ -825,6 +891,9 @@ public class MainView extends InitializableView { this.setToggleGroup(navButtons); this.getStyleClass().add("nav-button"); + this.setMinWidth(Region.USE_PREF_SIZE); // prevent squashing content + this.setPrefWidth(Region.USE_COMPUTED_SIZE); + // Japanese fonts are dense, increase top nav button text size if (model.getPreferences() != null && "ja".equals(model.getPreferences().getUserLanguage())) { this.getStyleClass().add("nav-button-japanese"); @@ -836,4 +905,29 @@ public class MainView extends InitializableView { } } + + private class SecondaryNavButton extends NavButton { + + SecondaryNavButton(Class viewClass, String title, String iconId) { + super(viewClass, title); + this.getStyleClass().setAll("nav-secondary-button"); + + // Japanese fonts are dense, increase top nav button text size + if (model.getPreferences() != null && "ja".equals(model.getPreferences().getUserLanguage())) { + this.getStyleClass().setAll("nav-secondary-button-japanese"); + } + + // add icon + ImageView imageView = new ImageView(); + imageView.setId(iconId); + imageView.setFitWidth(15); + imageView.setPreserveRatio(true); + setGraphicTextGap(10); + setGraphic(imageView); + + // show cursor hand on any hover + this.setPickOnBounds(true); + } + + } } diff --git a/desktop/src/main/java/haveno/desktop/main/account/AccountView.java b/desktop/src/main/java/haveno/desktop/main/account/AccountView.java index 30c434cccd..028276a83a 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/AccountView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/AccountView.java @@ -86,12 +86,12 @@ public class AccountView extends ActivatableView { root.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS); - traditionalAccountsTab.setText(Res.get("account.menu.paymentAccount").toUpperCase()); - cryptoAccountsTab.setText(Res.get("account.menu.altCoinsAccountView").toUpperCase()); - passwordTab.setText(Res.get("account.menu.password").toUpperCase()); - seedWordsTab.setText(Res.get("account.menu.seedWords").toUpperCase()); - //walletInfoTab.setText(Res.get("account.menu.walletInfo").toUpperCase()); - backupTab.setText(Res.get("account.menu.backup").toUpperCase()); + traditionalAccountsTab.setText(Res.get("account.menu.paymentAccount")); + cryptoAccountsTab.setText(Res.get("account.menu.altCoinsAccountView")); + passwordTab.setText(Res.get("account.menu.password")); + seedWordsTab.setText(Res.get("account.menu.seedWords")); + //walletInfoTab.setText(Res.get("account.menu.walletInfo")); + backupTab.setText(Res.get("account.menu.backup")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(AccountView.class) == 1) { diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java index 8abcd98a9d..bdadfffbda 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java @@ -216,7 +216,7 @@ public class CryptoAccountsView extends PaymentAccountsView private void addContent() { TableView tableView = new TableView<>(); + GUIUtil.applyTableStyle(tableView); GridPane.setRowIndex(tableView, ++rowIndex); GridPane.setColumnSpan(tableView, 2); GridPane.setMargin(tableView, new Insets(10, 0, 0, 0)); diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java index a5f7332c81..79243691bc 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java @@ -535,7 +535,7 @@ public class TraditionalAccountsView extends PaymentAccountsView { @Override public void initialize() { - depositTab.setText(Res.get("funds.tab.deposit").toUpperCase()); - withdrawalTab.setText(Res.get("funds.tab.withdrawal").toUpperCase()); - transactionsTab.setText(Res.get("funds.tab.transactions").toUpperCase()); + depositTab.setText(Res.get("funds.tab.deposit")); + withdrawalTab.setText(Res.get("funds.tab.withdrawal")); + transactionsTab.setText(Res.get("funds.tab.transactions")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(FundsView.class) == 1) diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java index 884df454e7..72f7945f65 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java @@ -40,6 +40,7 @@ import com.google.inject.name.Named; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.app.DevEnv; +import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; @@ -89,10 +90,9 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.util.Callback; -import monero.common.MoneroUtils; -import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroWalletListener; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; @@ -111,6 +111,7 @@ public class DepositView extends ActivatableView { @FXML TableColumn addressColumn, balanceColumn, confirmationsColumn, usageColumn; private ImageView qrCodeImageView; + private StackPane qrCodePane; private AddressTextField addressTextField; private Button generateNewAddressButton; private TitledGroupBg titledGroupBg; @@ -144,6 +145,7 @@ public class DepositView extends ActivatableView { @Override public void initialize() { + GUIUtil.applyTableStyle(tableView); paymentLabelString = Res.get("funds.deposit.fundHavenoWallet"); addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); @@ -154,6 +156,7 @@ public class DepositView extends ActivatableView { // set loading placeholder Label placeholderLabel = new Label("Loading..."); tableView.setPlaceholder(placeholderLabel); + tableView.getStyleClass().add("non-interactive-table"); ThreadUtils.execute(() -> { @@ -190,19 +193,19 @@ public class DepositView extends ActivatableView { titledGroupBg = addTitledGroupBg(gridPane, gridRow, 4, Res.get("funds.deposit.fundWallet")); titledGroupBg.getStyleClass().add("last"); - qrCodeImageView = new ImageView(); - qrCodeImageView.setFitHeight(150); - qrCodeImageView.setFitWidth(150); - qrCodeImageView.getStyleClass().add("qr-code"); - Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); - qrCodeImageView.setOnMouseClicked(e -> UserThread.runAfter( + Tuple2 qrCodeTuple = GUIUtil.getSmallXmrQrCodePane(); + qrCodePane = qrCodeTuple.first; + qrCodeImageView = qrCodeTuple.second; + + Tooltip.install(qrCodePane, new Tooltip(Res.get("shared.openLargeQRWindow"))); + qrCodePane.setOnMouseClicked(e -> UserThread.runAfter( () -> new QRCodeWindow(getPaymentUri()).show(), 200, TimeUnit.MILLISECONDS)); - GridPane.setRowIndex(qrCodeImageView, gridRow); - GridPane.setRowSpan(qrCodeImageView, 4); - GridPane.setColumnIndex(qrCodeImageView, 1); - GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 10)); - gridPane.getChildren().add(qrCodeImageView); + GridPane.setRowIndex(qrCodePane, gridRow); + GridPane.setRowSpan(qrCodePane, 4); + GridPane.setColumnIndex(qrCodePane, 1); + GridPane.setMargin(qrCodePane, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 10)); + gridPane.getChildren().add(qrCodePane); addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.address"), Layout.FIRST_ROW_DISTANCE); addressTextField.setPaymentLabel(paymentLabelString); @@ -213,8 +216,8 @@ public class DepositView extends ActivatableView { titledGroupBg.setVisible(false); titledGroupBg.setManaged(false); - qrCodeImageView.setVisible(false); - qrCodeImageView.setManaged(false); + qrCodePane.setVisible(false); + qrCodePane.setManaged(false); addressTextField.setVisible(false); addressTextField.setManaged(false); amountTextField.setManaged(false); @@ -310,8 +313,8 @@ public class DepositView extends ActivatableView { private void fillForm(String address) { titledGroupBg.setVisible(true); titledGroupBg.setManaged(true); - qrCodeImageView.setVisible(true); - qrCodeImageView.setManaged(true); + qrCodePane.setVisible(true); + qrCodePane.setManaged(true); addressTextField.setVisible(true); addressTextField.setManaged(true); amountTextField.setManaged(true); @@ -366,10 +369,7 @@ public class DepositView extends ActivatableView { @NotNull private String getPaymentUri() { - return MoneroUtils.getPaymentUri(new MoneroTxConfig() - .setAddress(addressTextField.getAddress()) - .setAmount(HavenoUtils.coinToAtomicUnits(getAmount())) - .setNote(paymentLabelString)); + return GUIUtil.getMoneroURI(addressTextField.getAddress(), HavenoUtils.coinToAtomicUnits(getAmount()), paymentLabelString); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -377,7 +377,6 @@ public class DepositView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void setUsageColumnCellFactory() { - usageColumn.getStyleClass().add("last-column"); usageColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); usageColumn.setCellFactory(new Callback<>() { @@ -390,7 +389,9 @@ public class DepositView extends ActivatableView { public void updateItem(final DepositListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { - setGraphic(new AutoTooltipLabel(item.getUsage())); + Label usageLabel = new AutoTooltipLabel(item.getUsage()); + usageLabel.getStyleClass().add("highlight-text"); + setGraphic(usageLabel); } else { setGraphic(null); } @@ -401,7 +402,6 @@ public class DepositView extends ActivatableView { } private void setAddressColumnCellFactory() { - addressColumn.getStyleClass().add("first-column"); addressColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); addressColumn.setCellFactory( @@ -434,6 +434,7 @@ public class DepositView extends ActivatableView { private void setBalanceColumnCellFactory() { balanceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + balanceColumn.getStyleClass().add("highlight-text"); balanceColumn.setCellFactory(new Callback<>() { @Override diff --git a/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java b/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java index 8cf5700cc6..5570ca1bef 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java @@ -122,6 +122,7 @@ public class LockedView extends ActivatableView { addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); balanceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.balanceWithCur", Res.getBaseCurrencyCode()))); + GUIUtil.applyTableStyle(tableView); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.locked.noFunds"))); @@ -250,7 +251,6 @@ public class LockedView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void setDateColumnCellFactory() { - dateColumn.getStyleClass().add("first-column"); dateColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); dateColumn.setCellFactory(new Callback<>() { @@ -342,7 +342,6 @@ public class LockedView extends ActivatableView { } private void setBalanceColumnCellFactory() { - balanceColumn.getStyleClass().add("last-column"); balanceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); balanceColumn.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java b/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java index bcef7e6488..3ed2dfea85 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java @@ -122,6 +122,7 @@ public class ReservedView extends ActivatableView { addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); balanceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.balanceWithCur", Res.getBaseCurrencyCode()))); + GUIUtil.applyTableStyle(tableView); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.reserved.noFunds"))); @@ -249,7 +250,6 @@ public class ReservedView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void setDateColumnCellFactory() { - dateColumn.getStyleClass().add("first-column"); dateColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); dateColumn.setCellFactory(new Callback<>() { @@ -313,7 +313,6 @@ public class ReservedView extends ActivatableView { } private void setAddressColumnCellFactory() { - addressColumn.getStyleClass().add("last-column"); addressColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); addressColumn.setCellFactory( @@ -341,7 +340,6 @@ public class ReservedView extends ActivatableView { } private void setBalanceColumnCellFactory() { - balanceColumn.getStyleClass().add("last-column"); balanceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); balanceColumn.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml index 24d8f121d7..3f8aa752fd 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml @@ -32,14 +32,14 @@ - - - + + + - - + + diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java index b882782212..66434a8663 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java @@ -127,6 +127,8 @@ public class TransactionsView extends ActivatableView { @Override public void initialize() { + GUIUtil.applyTableStyle(tableView); + dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); detailsColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.details"))); addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); @@ -139,6 +141,7 @@ public class TransactionsView extends ActivatableView { tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.tx.noTxAvailable"))); + tableView.getStyleClass().add("non-interactive-table"); setDateColumnCellFactory(); setDetailsColumnCellFactory(); @@ -169,11 +172,6 @@ public class TransactionsView extends ActivatableView { keyEventEventHandler = event -> { // Not intended to be public to users as the feature is not well tested if (Utilities.isAltOrCtrlPressed(KeyCode.R, event)) { - if (revertTxColumn.isVisible()) { - confidenceColumn.getStyleClass().remove("last-column"); - } else { - confidenceColumn.getStyleClass().add("last-column"); - } revertTxColumn.setVisible(!revertTxColumn.isVisible()); } }; @@ -265,7 +263,6 @@ public class TransactionsView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void setDateColumnCellFactory() { - dateColumn.getStyleClass().add("first-column"); dateColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); dateColumn.setMaxWidth(200); @@ -400,6 +397,7 @@ public class TransactionsView extends ActivatableView { private void setAmountColumnCellFactory() { amountColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + amountColumn.getStyleClass().add("highlight-text"); amountColumn.setCellFactory( new Callback<>() { @@ -427,6 +425,7 @@ public class TransactionsView extends ActivatableView { private void setTxFeeColumnCellFactory() { txFeeColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + txFeeColumn.getStyleClass().add("highlight-text"); txFeeColumn.setCellFactory( new Callback<>() { @@ -453,6 +452,7 @@ public class TransactionsView extends ActivatableView { private void setMemoColumnCellFactory() { memoColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + memoColumn.getStyleClass().add("highlight-text"); memoColumn.setCellFactory( new Callback<>() { @@ -477,7 +477,6 @@ public class TransactionsView extends ActivatableView { } private void setConfidenceColumnCellFactory() { - confidenceColumn.getStyleClass().add("last-column"); confidenceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); confidenceColumn.setCellFactory( @@ -504,7 +503,6 @@ public class TransactionsView extends ActivatableView { } private void setRevertTxColumnCellFactory() { - revertTxColumn.getStyleClass().add("last-column"); revertTxColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); revertTxColumn.setCellFactory( diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java index 730b47adf6..6217bb1e04 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java @@ -144,7 +144,7 @@ public class WithdrawalView extends ActivatableView { amountLabel = feeTuple3.first; amountTextField = feeTuple3.second; - amountTextField.setMinWidth(180); + amountTextField.setMinWidth(200); HyperlinkWithIcon sendMaxLink = feeTuple3.third; withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex, diff --git a/desktop/src/main/java/haveno/desktop/main/market/MarketView.java b/desktop/src/main/java/haveno/desktop/main/market/MarketView.java index 98f5e75490..fc72de153a 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/MarketView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/MarketView.java @@ -88,10 +88,10 @@ public class MarketView extends ActivatableView { @Override public void initialize() { - offerBookTab.setText(Res.get("market.tabs.offerBook").toUpperCase()); - spreadTab.setText(Res.get("market.tabs.spreadCurrency").toUpperCase()); - spreadTabPaymentMethod.setText(Res.get("market.tabs.spreadPayment").toUpperCase()); - tradesTab.setText(Res.get("market.tabs.trades").toUpperCase()); + offerBookTab.setText(Res.get("market.tabs.offerBook")); + spreadTab.setText(Res.get("market.tabs.spreadCurrency")); + spreadTabPaymentMethod.setText(Res.get("market.tabs.spreadPayment")); + tradesTab.setText(Res.get("market.tabs.trades")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(MarketView.class) == 1) diff --git a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java index 3d2ec6d884..e89c78dac9 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java @@ -26,6 +26,7 @@ import haveno.common.util.Tuple3; import haveno.common.util.Tuple4; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; +import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.util.FormattingUtils; @@ -65,13 +66,13 @@ import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.SingleSelectionModel; import javafx.scene.control.Tab; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; -import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; @@ -95,7 +96,10 @@ public class OfferBookChartView extends ActivatableViewAndModel currencyComboBox; private Subscription tradeCurrencySubscriber; - private final StringProperty volumeColumnLabel = new SimpleStringProperty(); + private final StringProperty volumeSellColumnLabel = new SimpleStringProperty(); + private final StringProperty volumeBuyColumnLabel = new SimpleStringProperty(); + private final StringProperty amountSellColumnLabel = new SimpleStringProperty(); + private final StringProperty amountBuyColumnLabel = new SimpleStringProperty(); private final StringProperty priceColumnLabel = new SimpleStringProperty(); private AutoTooltipButton sellButton; private AutoTooltipButton buyButton; @@ -106,10 +110,11 @@ public class OfferBookChartView extends ActivatableViewAndModel changeListener; private ListChangeListener currencyListItemsListener; private final double dataLimitFactor = 3; - private final double initialOfferTableViewHeight = 121; + private final double initialOfferTableViewHeight = 78; // decrease as MainView's content-pane's top anchor increases + private final double offerTableExtraMarginBottom = 0; private final Function offerTableViewHeight = (screenSize) -> { // initial visible row count=5, header height=30 - double pixelsPerOfferTableRow = (initialOfferTableViewHeight - 30) / 5.0; + double pixelsPerOfferTableRow = (initialOfferTableViewHeight - offerTableExtraMarginBottom) / 5.0; int extraRows = screenSize <= INITIAL_WINDOW_HEIGHT ? 0 : (int) ((screenSize - INITIAL_WINDOW_HEIGHT) / pixelsPerOfferTableRow); return extraRows == 0 ? initialOfferTableViewHeight : Math.ceil(initialOfferTableViewHeight + ((extraRows + 1) * pixelsPerOfferTableRow)); }; @@ -136,6 +141,7 @@ public class OfferBookChartView extends ActivatableViewAndModel { String code = tradeCurrency.getCode(); - volumeColumnLabel.set(Res.get("offerbook.volume", code)); xAxis.setTickLabelFormatter(new StringConverter<>() { final int cryptoPrecision = 3; final DecimalFormat df = new DecimalFormat(",###"); @@ -230,15 +235,21 @@ public class OfferBookChartView extends ActivatableViewAndModel model.goToOfferView(model.isCrypto() ? OfferDirection.SELL : OfferDirection.BUY)); + sellButton.setId("sell-button-big"); buyHeaderLabel.setText(Res.get("market.offerBook.buyOffersHeaderLabel", viewBaseCurrencyCode)); - buyButton.updateText(Res.get("shared.buyCurrency", viewBaseCurrencyCode, viewPriceCurrencyCode)); + buyButton.updateText(Res.get( "shared.buyCurrency", viewBaseCurrencyCode)); + buyButton.setGraphic(GUIUtil.getCurrencyIconWithBorder(viewBaseCurrencyCode)); + buyButton.setOnAction(e -> model.goToOfferView(model.isCrypto() ? OfferDirection.BUY : OfferDirection.SELL)); + buyButton.setId("buy-button-big"); priceColumnLabel.set(Res.get("shared.priceWithCur", viewPriceCurrencyCode)); @@ -288,8 +299,8 @@ public class OfferBookChartView extends ActivatableViewAndModel model.goToOfferView(OfferDirection.BUY); sellTableRowSelectionListener = (observable, oldValue, newValue) -> model.goToOfferView(OfferDirection.SELL); + buyTableRowSelectionListener = (observable, oldValue, newValue) -> model.goToOfferView(OfferDirection.BUY); havenoWindowVerticalSizeListener = (observable, oldValue, newValue) -> layout(); } @@ -345,12 +356,27 @@ public class OfferBookChartView extends ActivatableViewAndModel, VBox, Button, Label> getOfferTable(OfferDirection direction) { TableView tableView = new TableView<>(); + GUIUtil.applyTableStyle(tableView, false); tableView.setMinHeight(initialOfferTableViewHeight); tableView.setPrefHeight(initialOfferTableViewHeight); tableView.setMinWidth(480); - tableView.getStyleClass().add("offer-table"); + tableView.getStyleClass().addAll("offer-table", "non-interactive-table"); // price TableColumn priceColumn = new TableColumn<>(); @@ -484,12 +511,14 @@ public class OfferBookChartView extends ActivatableViewAndModel volumeColumn = new TableColumn<>(); volumeColumn.setMinWidth(115); volumeColumn.setSortable(false); - volumeColumn.textProperty().bind(volumeColumnLabel); - volumeColumn.getStyleClass().addAll("number-column", "first-column"); + volumeColumn.textProperty().bind(isSellTable ? volumeSellColumnLabel : volumeBuyColumnLabel); + volumeColumn.getStyleClass().addAll("number-column"); volumeColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); volumeColumn.setCellFactory( new Callback<>() { @@ -546,7 +575,8 @@ public class OfferBookChartView extends ActivatableViewAndModel amountColumn = new AutoTooltipTableColumn<>(Res.get("shared.XMRMinMax")); + TableColumn amountColumn = new TableColumn<>(); + amountColumn.textProperty().bind(isSellTable ? amountSellColumnLabel : amountBuyColumnLabel); amountColumn.setMinWidth(115); amountColumn.setSortable(false); amountColumn.getStyleClass().add("number-column"); @@ -570,10 +600,8 @@ public class OfferBookChartView extends ActivatableViewAndModel avatarColumn = new AutoTooltipTableColumn<>(isSellOffer ? + TableColumn avatarColumn = new AutoTooltipTableColumn<>(isSellTable ? Res.get("shared.sellerUpperCase") : Res.get("shared.buyerUpperCase")) { { setMinWidth(80); @@ -582,7 +610,7 @@ public class OfferBookChartView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); avatarColumn.setCellFactory( new Callback<>() { @@ -629,20 +657,16 @@ public class OfferBookChartView extends ActivatableViewAndModel model.goToOfferView(direction)); Region spacer = new Region(); @@ -653,9 +677,9 @@ public class OfferBookChartView extends ActivatableViewAndModel(tableView, vBox, button, titleLabel); diff --git a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java index c7d0c91277..3e8b52184a 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java @@ -26,6 +26,7 @@ import haveno.core.locale.CurrencyUtil; import haveno.core.locale.GlobalSettings; import haveno.core.locale.TradeCurrency; import haveno.core.monetary.Price; +import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOfferManager; @@ -58,6 +59,7 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.chart.XYChart; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -212,10 +214,42 @@ class OfferBookChartViewModel extends ActivatableViewModel { } public boolean isSellOffer(OfferDirection direction) { - // for cryptocurrency, buy direction is to buy XMR, so we need sell offers - // for traditional currency, buy direction is to sell XMR, so we need buy offers - boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(getCurrencyCode()); - return isCryptoCurrency ? direction == OfferDirection.BUY : direction == OfferDirection.SELL; + return direction == OfferDirection.SELL; + } + + public double getTotalAmount(OfferDirection direction) { + synchronized (offerBookListItems) { + List offerList = offerBookListItems.stream() + .map(OfferBookListItem::getOffer) + .filter(e -> e.getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) + && e.getDirection().equals(direction)) + .collect(Collectors.toList()); + BigInteger sum = BigInteger.ZERO; + for (Offer offer : offerList) sum = sum.add(offer.getAmount()); + return HavenoUtils.atomicUnitsToXmr(sum); + } + } + + public Volume getTotalVolume(OfferDirection direction) { + synchronized (offerBookListItems) { + List volumes = offerBookListItems.stream() + .map(OfferBookListItem::getOffer) + .filter(e -> e.getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) + && e.getDirection().equals(direction)) + .map(Offer::getVolume) + .collect(Collectors.toList()); + try { + return VolumeUtil.sum(volumes); + } catch (Exception e) { + // log.error("Cannot compute total volume because prices are unavailable, currency={}, direction={}", + // selectedTradeCurrencyProperty.get().getCode(), direction); + return null; // expected before prices are available + } + } + } + + public boolean isCrypto() { + return CurrencyUtil.isCryptoCurrency(getCurrencyCode()); } public boolean isMyOffer(Offer offer) { diff --git a/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadView.java b/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadView.java index 9adf595ad4..150b7c888a 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadView.java @@ -65,6 +65,8 @@ public class SpreadView extends ActivatableViewAndModel(); + GUIUtil.applyTableStyle(tableView); + tableView.getStyleClass().add("non-interactive-table"); int gridRow = 0; GridPane.setRowIndex(tableView, gridRow); @@ -144,7 +146,7 @@ public class SpreadView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @@ -259,7 +261,7 @@ public class SpreadView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @@ -289,7 +291,7 @@ public class SpreadView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java index 97fcce06d1..7470cf1c4b 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java @@ -686,6 +686,7 @@ public class TradesChartsView extends ActivatableViewAndModel(); + GUIUtil.applyTableStyle(tableView); VBox.setVgrow(tableView, Priority.ALWAYS); + tableView.getStyleClass().add("non-interactive-table"); // date TableColumn dateColumn = new AutoTooltipTableColumn<>(Res.get("shared.dateTime")) { @@ -739,7 +742,7 @@ public class TradesChartsView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); dateColumn.setCellFactory( new Callback<>() { @@ -865,7 +868,7 @@ public class TradesChartsView extends ActivatableViewAndModel paymentMethodColumn = new AutoTooltipTableColumn<>(Res.get("shared.paymentMethod")); - paymentMethodColumn.getStyleClass().add("number-column"); + paymentMethodColumn.getStyleClass().addAll("number-column"); paymentMethodColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); paymentMethodColumn.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index e28ecff72b..a580d24802 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -85,6 +85,7 @@ import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.util.StringConverter; @@ -149,7 +150,8 @@ public abstract class MutableOfferView> exten private ComboBox paymentAccountsComboBox; private ComboBox currencyComboBox; private ImageView qrCodeImageView; - private VBox currencySelection, fixedPriceBox, percentagePriceBox, currencyTextFieldBox, triggerPriceVBox; + private StackPane qrCodePane; + private VBox paymentAccountsSelection, currencySelection, fixedPriceBox, percentagePriceBox, currencyTextFieldBox, triggerPriceVBox; private HBox fundingHBox, firstRowHBox, secondRowHBox, placeOfferBox, amountValueCurrencyBox, priceAsPercentageValueCurrencyBox, volumeValueCurrencyBox, priceValueCurrencyBox, minAmountValueCurrencyBox, securityDepositAndFeeBox, triggerPriceHBox; @@ -308,7 +310,7 @@ public abstract class MutableOfferView> exten if (!result) { new Popup().headLine(Res.get("popup.warning.noTradingAccountSetup.headline")) .instruction(Res.get("popup.warning.noTradingAccountSetup.msg")) - .actionButtonTextWithGoTo("navigation.account") + .actionButtonTextWithGoTo("mainView.menu.account") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); @@ -425,7 +427,8 @@ public abstract class MutableOfferView> exten totalToPayTextField.setContentForInfoPopOver(createInfoPopover()); }); - paymentAccountsComboBox.setDisable(true); + paymentAccountsSelection.setDisable(true); + currencySelection.setDisable(true); editOfferElements.forEach(node -> { node.setMouseTransparent(true); @@ -449,8 +452,8 @@ public abstract class MutableOfferView> exten private void updateOfferElementsStyle() { GridPane.setColumnSpan(firstRowHBox, 2); - String activeInputStyle = "input-with-border"; - String readOnlyInputStyle = "input-with-border-readonly"; + String activeInputStyle = "offer-input"; + String readOnlyInputStyle = "offer-input-readonly"; amountValueCurrencyBox.getStyleClass().remove(activeInputStyle); amountValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); priceAsPercentageValueCurrencyBox.getStyleClass().remove(activeInputStyle); @@ -709,7 +712,11 @@ public abstract class MutableOfferView> exten }; extraInfoFocusedListener = (observable, oldValue, newValue) -> { model.onFocusOutExtraInfoTextArea(oldValue, newValue); - extraInfoTextArea.setText(model.extraInfo.get()); + + // avoid setting text area to empty text because blinking caret does not appear + if (model.extraInfo.get() != null && !model.extraInfo.get().isEmpty()) { + extraInfoTextArea.setText(model.extraInfo.get()); + } }; errorMessageListener = (o, oldValue, newValue) -> { @@ -749,7 +756,7 @@ public abstract class MutableOfferView> exten UserThread.runAfter(() -> new Popup().headLine(Res.get("createOffer.success.headline")) .feedback(Res.get("createOffer.success.info")) .dontShowAgainId(key) - .actionButtonTextWithGoTo("navigation.portfolio.myOpenOffers") + .actionButtonTextWithGoTo("portfolio.tab.openOffers") .onAction(this::closeAndGoToOpenOffers) .onClose(this::closeAndGoToOpenOffers) .show(), @@ -973,7 +980,7 @@ public abstract class MutableOfferView> exten private void addGridPane() { gridPane = new GridPane(); gridPane.getStyleClass().add("content-pane"); - gridPane.setPadding(new Insets(25, 25, -1, 25)); + gridPane.setPadding(new Insets(25, 25, 25, 25)); gridPane.setHgap(5); gridPane.setVgap(5); GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane); @@ -995,8 +1002,9 @@ public abstract class MutableOfferView> exten final Tuple3> currencyBoxTuple = addTopLabelComboBox( Res.get("shared.currency"), Res.get("list.currency.select")); + paymentAccountsSelection = tradingAccountBoxTuple.first; currencySelection = currencyBoxTuple.first; - paymentGroupBox.getChildren().addAll(tradingAccountBoxTuple.first, currencySelection); + paymentGroupBox.getChildren().addAll(paymentAccountsSelection, currencySelection); GridPane.setRowIndex(paymentGroupBox, gridRow); GridPane.setColumnSpan(paymentGroupBox, 2); @@ -1007,11 +1015,13 @@ public abstract class MutableOfferView> exten paymentAccountsComboBox = tradingAccountBoxTuple.third; paymentAccountsComboBox.setMinWidth(tradingAccountBoxTuple.first.getMinWidth()); paymentAccountsComboBox.setPrefWidth(tradingAccountBoxTuple.first.getMinWidth()); - editOfferElements.add(tradingAccountBoxTuple.first); + paymentAccountsComboBox.getStyleClass().add("input-with-border"); + editOfferElements.add(paymentAccountsSelection); // we display either currencyComboBox (multi currency account) or currencyTextField (single) currencyComboBox = currencyBoxTuple.third; currencyComboBox.setMaxWidth(tradingAccountBoxTuple.first.getMinWidth() / 2); + currencyComboBox.getStyleClass().add("input-with-border"); editOfferElements.add(currencySelection); currencyComboBox.setConverter(new StringConverter<>() { @Override @@ -1094,6 +1104,7 @@ public abstract class MutableOfferView> exten GridPane.setColumnSpan(extraInfoTitledGroupBg, 3); extraInfoTextArea = new InputTextArea(); + extraInfoTextArea.setText(""); extraInfoTextArea.setPromptText(Res.get("payment.shared.extraInfo.prompt.offer")); extraInfoTextArea.getStyleClass().add("text-area"); extraInfoTextArea.setWrapText(true); @@ -1179,8 +1190,8 @@ public abstract class MutableOfferView> exten totalToPayTextField.setManaged(false); addressTextField.setVisible(false); addressTextField.setManaged(false); - qrCodeImageView.setVisible(false); - qrCodeImageView.setManaged(false); + qrCodePane.setVisible(false); + qrCodePane.setManaged(false); balanceTextField.setVisible(false); balanceTextField.setManaged(false); cancelButton2.setVisible(false); @@ -1196,8 +1207,8 @@ public abstract class MutableOfferView> exten totalToPayTextField.setManaged(true); addressTextField.setVisible(true); addressTextField.setManaged(true); - qrCodeImageView.setVisible(true); - qrCodeImageView.setManaged(true); + qrCodePane.setVisible(true); + qrCodePane.setManaged(true); balanceTextField.setVisible(true); balanceTextField.setManaged(true); cancelButton2.setVisible(true); @@ -1239,21 +1250,23 @@ public abstract class MutableOfferView> exten totalToPayTextField.setVisible(false); GridPane.setMargin(totalToPayTextField, new Insets(60 + heightAdjustment, 10, 0, 0)); - qrCodeImageView = new ImageView(); - qrCodeImageView.setVisible(false); - qrCodeImageView.setFitHeight(150); - qrCodeImageView.setFitWidth(150); - qrCodeImageView.getStyleClass().add("qr-code"); - Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); - qrCodeImageView.setOnMouseClicked(e -> UserThread.runAfter( + Tuple2 qrCodeTuple = GUIUtil.getSmallXmrQrCodePane(); + qrCodePane = qrCodeTuple.first; + qrCodeImageView = qrCodeTuple.second; + + Tooltip.install(qrCodePane, new Tooltip(Res.get("shared.openLargeQRWindow"))); + qrCodePane.setOnMouseClicked(e -> UserThread.runAfter( () -> new QRCodeWindow(getMoneroURI()).show(), 200, TimeUnit.MILLISECONDS)); - GridPane.setRowIndex(qrCodeImageView, gridRow); - GridPane.setColumnIndex(qrCodeImageView, 1); - GridPane.setRowSpan(qrCodeImageView, 3); - GridPane.setValignment(qrCodeImageView, VPos.BOTTOM); - GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_DISTANCE - 9, 0, 0, 10)); - gridPane.getChildren().add(qrCodeImageView); + GridPane.setRowIndex(qrCodePane, gridRow); + GridPane.setColumnIndex(qrCodePane, 1); + GridPane.setRowSpan(qrCodePane, 3); + GridPane.setValignment(qrCodePane, VPos.BOTTOM); + GridPane.setMargin(qrCodePane, new Insets(Layout.FIRST_ROW_DISTANCE - 9, 0, 0, 10)); + gridPane.getChildren().add(qrCodePane); + + qrCodePane.setVisible(false); + qrCodePane.setManaged(false); addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.tradeWalletAddress")); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index 821f9e8a5c..c29781c3bf 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -504,7 +504,7 @@ public abstract class MutableOfferViewModel ext extraInfoStringListener = (ov, oldValue, newValue) -> { if (newValue != null) { - extraInfo.set(newValue); + extraInfo.set(newValue.trim()); UserThread.execute(() -> onExtraInfoTextAreaChanged()); } }; @@ -727,7 +727,7 @@ public abstract class MutableOfferViewModel ext new Popup().warning(Res.get("shared.notEnoughFunds", HavenoUtils.formatXmr(dataModel.totalToPayAsProperty().get(), true), HavenoUtils.formatXmr(dataModel.getTotalBalance(), true))) - .actionButtonTextWithGoTo("navigation.funds.depositFunds") + .actionButtonTextWithGoTo("funds.tab.deposit") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) .show(); } @@ -1056,7 +1056,7 @@ public abstract class MutableOfferViewModel ext FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent()))) .actionButtonText(Res.get("createOffer.changePrice")) .onAction(popup::hide) - .closeButtonTextWithGoTo("navigation.settings.preferences") + .closeButtonTextWithGoTo("settings.tab.preferences") .onClose(() -> navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class)) .show(); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/OfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/OfferView.java index 6f6aba7cbc..dd037593fa 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/OfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/OfferView.java @@ -220,14 +220,14 @@ public abstract class OfferView extends ActivatableView { labelTab.setClosable(false); Label offerLabel = new Label(getOfferLabel()); // use overlay for label for custom formatting offerLabel.getStyleClass().add("titled-group-bg-label"); - offerLabel.setStyle("-fx-font-size: 1.4em;"); + offerLabel.setStyle("-fx-font-size: 1.3em;"); labelTab.setGraphic(offerLabel); - fiatOfferBookTab = new Tab(Res.get("shared.fiat").toUpperCase()); + fiatOfferBookTab = new Tab(Res.get("shared.fiat")); fiatOfferBookTab.setClosable(false); - cryptoOfferBookTab = new Tab(Res.get("shared.crypto").toUpperCase()); + cryptoOfferBookTab = new Tab(Res.get("shared.crypto")); cryptoOfferBookTab.setClosable(false); - otherOfferBookTab = new Tab(Res.get("shared.other").toUpperCase()); + otherOfferBookTab = new Tab(Res.get("shared.other")); otherOfferBookTab.setClosable(false); tabPane.getTabs().addAll(labelTab, fiatOfferBookTab, cryptoOfferBookTab, otherOfferBookTab); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index 0248b9c522..e3ed68ccf1 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -91,7 +91,6 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.TextAlignment; import javafx.util.Callback; @@ -179,29 +178,29 @@ abstract public class OfferBookView> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByPaymentMethod")); paymentMethodComboBox = paymentBoxTuple.third; paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory()); paymentMethodComboBox.setPrefWidth(250); - - matchingOffersToggleButton = AwesomeDude.createIconToggleButton(AwesomeIcon.USER, null, "1.5em", null); - matchingOffersToggleButton.getStyleClass().add("toggle-button-no-slider"); - matchingOffersToggleButton.setPrefHeight(27); - Tooltip matchingOffersTooltip = new Tooltip(Res.get("offerbook.matchingOffers")); - Tooltip.install(matchingOffersToggleButton, matchingOffersTooltip); + paymentMethodComboBox.getStyleClass().add("input-with-border"); noDepositOffersToggleButton = new ToggleButton(Res.get("offerbook.filterNoDeposit")); noDepositOffersToggleButton.getStyleClass().add("toggle-button-no-slider"); - noDepositOffersToggleButton.setPrefHeight(27); Tooltip noDepositOffersTooltip = new Tooltip(Res.get("offerbook.noDepositOffers")); Tooltip.install(noDepositOffersToggleButton, noDepositOffersTooltip); + matchingOffersToggleButton = AwesomeDude.createIconToggleButton(AwesomeIcon.USER, null, "1.5em", null); + matchingOffersToggleButton.getStyleClass().add("toggle-button-no-slider"); + Tooltip matchingOffersTooltip = new Tooltip(Res.get("offerbook.matchingOffers")); + Tooltip.install(matchingOffersToggleButton, matchingOffersTooltip); + createOfferButton = new AutoTooltipButton(""); createOfferButton.setMinHeight(40); createOfferButton.setGraphicTextGap(10); - createOfferButton.setStyle("-fx-padding: 0 15 0 15;"); + createOfferButton.setStyle("-fx-padding: 7 25 7 25;"); disabledCreateOfferButtonTooltip = new Label(""); disabledCreateOfferButtonTooltip.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); disabledCreateOfferButtonTooltip.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); @@ -211,15 +210,17 @@ abstract public class OfferBookView autoToolTipTextField = addTopLabelAutoToolTipTextField(""); VBox filterBox = autoToolTipTextField.first; filterInputField = autoToolTipTextField.third; - filterInputField.setPromptText(Res.get("market.offerBook.filterPrompt")); + filterInputField.setPromptText(Res.get("shared.filter")); + filterInputField.getStyleClass().add("input-with-border"); offerToolsBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first, - filterBox, matchingOffersToggleButton, noDepositOffersToggleButton, getSpacer(), createOfferButtonStack); + filterBox, noDepositOffersToggleButton, matchingOffersToggleButton, getSpacer(), createOfferVBox); GridPane.setHgrow(offerToolsBox, Priority.ALWAYS); GridPane.setRowIndex(offerToolsBox, gridRow); @@ -228,6 +229,7 @@ abstract public class OfferBookView(); + GUIUtil.applyTableStyle(tableView); GridPane.setRowIndex(tableView, ++gridRow); GridPane.setColumnIndex(tableView, 0); @@ -405,14 +407,12 @@ abstract public class OfferBookView { log.debug(Res.get("offerbook.removeOffer.success")); if (DontShowAgainLookup.showAgain(key)) - new Popup().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("navigation.funds.availableForWithdrawal"))) - .actionButtonTextWithGoTo("navigation.funds.availableForWithdrawal") + new Popup().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("funds.tab.withdrawal"))) + .actionButtonTextWithGoTo("funds.tab.withdrawal") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class)) .dontShowAgainId(key) .show(); @@ -769,7 +768,7 @@ abstract public class OfferBookView { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, accountViewClass); @@ -812,7 +811,7 @@ abstract public class OfferBookView new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @@ -1065,7 +1064,6 @@ abstract public class OfferBookView new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @@ -1116,7 +1114,12 @@ abstract public class OfferBookView onTakeOffer(offer)); button2.setManaged(false); @@ -1179,8 +1186,8 @@ abstract public class OfferBookView new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @@ -1280,8 +1287,8 @@ abstract public class OfferBookView paymentAccountsComboBox; private TextArea extraInfoTextArea; private Label amountDescriptionLabel, @@ -142,7 +143,7 @@ public class TakeOfferView extends ActivatableViewAndModel extraInfoTuple = addCompactTopLabelTextArea(gridPane, ++gridRowNoFundingRequired, Res.get("payment.shared.extraInfo.noDeposit"), ""); + extraInfoLabel = extraInfoTuple.first; + extraInfoLabel.setVisible(false); + extraInfoLabel.setManaged(false); + extraInfoTextArea = extraInfoTuple.second; + extraInfoTextArea.setVisible(false); + extraInfoTextArea.setManaged(false); + extraInfoTextArea.setText(offer.getCombinedExtraInfo().trim()); + extraInfoTextArea.getStyleClass().addAll("text-area", "flat-text-area-with-border"); extraInfoTextArea.setWrapText(true); - extraInfoTextArea.setPrefHeight(75); - extraInfoTextArea.setMinHeight(75); - extraInfoTextArea.setMaxHeight(150); + extraInfoTextArea.setMaxHeight(300); extraInfoTextArea.setEditable(false); - GridPane.setRowIndex(extraInfoTextArea, lastGridRowNoFundingRequired); + GUIUtil.adjustHeightAutomatically(extraInfoTextArea); + GridPane.setRowIndex(extraInfoTextArea, gridRowNoFundingRequired); GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING); GridPane.setColumnIndex(extraInfoTextArea, 0); // move up take offer buttons - GridPane.setRowIndex(takeOfferBox, lastGridRowNoFundingRequired + 1); + GridPane.setRowIndex(takeOfferBox, gridRowNoFundingRequired + 1); GridPane.setMargin(takeOfferBox, new Insets(15, 0, 0, 0)); } } @@ -490,19 +500,33 @@ public class TakeOfferView extends ActivatableViewAndModel new Popup().warning(newValue + "\n\n" + Res.get("takeOffer.alreadyPaidInFunds")) - .actionButtonTextWithGoTo("navigation.funds.availableForWithdrawal") + .actionButtonTextWithGoTo("funds.tab.withdrawal") .onAction(() -> { errorPopupDisplayed.set(true); model.resetOfferWarning(); @@ -663,6 +687,7 @@ public class TakeOfferView extends ActivatableViewAndModel new Popup().headLine(Res.get("takeOffer.success.headline")) .feedback(Res.get("takeOffer.success.info")) - .actionButtonTextWithGoTo("navigation.portfolio.pending") + .actionButtonTextWithGoTo("portfolio.tab.pendingTrades") .dontShowAgainId(key) .onAction(() -> { UserThread.runAfter( @@ -755,7 +780,7 @@ public class TakeOfferView extends ActivatableViewAndModel tuple = add2ButtonsWithBox(gridPane, ++gridRow, Res.get("shared.nextStep"), Res.get("shared.cancel"), 15, true); - buttonBox = tuple.third; + nextButtonBox = tuple.third; nextButton = tuple.first; nextButton.setMaxWidth(200); @@ -870,7 +895,7 @@ public class TakeOfferView extends ActivatableViewAndModel UserThread.runAfter( + Tuple2 qrCodeTuple = GUIUtil.getSmallXmrQrCodePane(); + qrCodePane = qrCodeTuple.first; + qrCodeImageView = qrCodeTuple.second; + + Tooltip.install(qrCodePane, new Tooltip(Res.get("shared.openLargeQRWindow"))); + qrCodePane.setOnMouseClicked(e -> UserThread.runAfter( () -> new QRCodeWindow(getMoneroURI()).show(), 200, TimeUnit.MILLISECONDS)); - GridPane.setRowIndex(qrCodeImageView, gridRow); - GridPane.setColumnIndex(qrCodeImageView, 1); - GridPane.setRowSpan(qrCodeImageView, 3); - GridPane.setValignment(qrCodeImageView, VPos.BOTTOM); - GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_DISTANCE - 9, 0, 0, 10)); - gridPane.getChildren().add(qrCodeImageView); + GridPane.setRowIndex(qrCodePane, gridRow); + GridPane.setColumnIndex(qrCodePane, 1); + GridPane.setRowSpan(qrCodePane, 3); + GridPane.setValignment(qrCodePane, VPos.BOTTOM); + GridPane.setMargin(qrCodePane, new Insets(Layout.FIRST_ROW_DISTANCE - 9, 0, 0, 10)); + gridPane.getChildren().add(qrCodePane); + + qrCodePane.setVisible(false); + qrCodePane.setManaged(false); addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.tradeWalletAddress")); addressTextField.setVisible(false); + addressTextField.setManaged(false); balanceTextField = addBalanceTextField(gridPane, ++gridRow, Res.get("shared.tradeWalletBalance")); balanceTextField.setVisible(false); + balanceTextField.setManaged(false); fundingHBox = new HBox(); fundingHBox.setVisible(false); @@ -1000,6 +1033,7 @@ public class TakeOfferView extends ActivatableViewAndModel { new GenericMessageWindow() .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getCombinedExtraInfo()) + .instruction(offer.getCombinedExtraInfo().trim()) .actionButtonText(Res.get("shared.iConfirm")) .closeButtonText(Res.get("shared.close")) .width(Layout.INITIAL_WINDOW_WIDTH) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java index c73ce988c6..1ad0f9547a 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java @@ -256,7 +256,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im new Popup().warning(Res.get("shared.notEnoughFunds", HavenoUtils.formatXmr(dataModel.getTotalToPay().get(), true), HavenoUtils.formatXmr(dataModel.getTotalAvailableBalance(), true))) - .actionButtonTextWithGoTo("navigation.funds.depositFunds") + .actionButtonTextWithGoTo("funds.tab.deposit") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) .show(); return false; diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java b/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java index e216b14ed9..ea29d4b7ec 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java @@ -19,6 +19,8 @@ package haveno.desktop.main.overlays; import com.google.common.reflect.TypeToken; import de.jensd.fx.fontawesome.AwesomeIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.Config; @@ -42,8 +44,6 @@ import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; @@ -52,6 +52,7 @@ import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.PerspectiveCamera; import javafx.scene.Scene; @@ -122,7 +123,7 @@ public abstract class Overlay> { Notification(AnimationType.SlideFromRightTop, ChangeBackgroundType.BlurLight), BackgroundInfo(AnimationType.SlideDownFromCenterTop, ChangeBackgroundType.BlurUltraLight), - Feedback(AnimationType.SlideDownFromCenterTop, ChangeBackgroundType.Darken), + Feedback(AnimationType.SlideDownFromCenterTop, ChangeBackgroundType.BlurLight), Information(AnimationType.FadeInAtCenter, ChangeBackgroundType.BlurLight), Instruction(AnimationType.ScaleFromCenter, ChangeBackgroundType.BlurLight), @@ -141,6 +142,9 @@ public abstract class Overlay> { } } + private static int numCenterOverlays = 0; + private static int numBlurEffects = 0; + protected final static double DEFAULT_WIDTH = 668; protected Stage stage; protected GridPane gridPane; @@ -168,7 +172,7 @@ public abstract class Overlay> { protected boolean showScrollPane = false; protected TextArea messageTextArea; - protected Label headlineIcon, copyIcon, headLineLabel; + protected Label headlineIcon, copyLabel, headLineLabel; protected String headLine, message, closeButtonText, actionButtonText, secondaryActionButtonText, dontShowAgainId, dontShowAgainText, truncatedMessage; @@ -249,6 +253,7 @@ public abstract class Overlay> { protected void animateHide() { animateHide(() -> { + if (isCentered()) numCenterOverlays--; removeEffectFromBackground(); if (stage != null) @@ -541,6 +546,14 @@ public abstract class Overlay> { layout(); + // add dropshadow if light mode or multiple centered overlays + if (isCentered()) { + numCenterOverlays++; + } + if (!CssTheme.isDarkTheme() || numCenterOverlays > 1) { + getRootContainer().getStyleClass().add("popup-dropshadow"); + } + addEffectToBackground(); // On Linux the owner stage does not move the child stage as it does on Mac @@ -739,6 +752,8 @@ public abstract class Overlay> { } protected void addEffectToBackground() { + numBlurEffects++; + if (numBlurEffects > 1) return; if (type.changeBackgroundType == ChangeBackgroundType.BlurUltraLight) MainView.blurUltraLight(); else if (type.changeBackgroundType == ChangeBackgroundType.BlurLight) @@ -758,12 +773,14 @@ public abstract class Overlay> { if (headLineLabel != null) { - if (copyIcon != null) { - copyIcon.getStyleClass().add("popup-icon-information"); - copyIcon.setManaged(true); - copyIcon.setVisible(true); - FormBuilder.getIconForLabel(AwesomeIcon.COPY, copyIcon, "1.1em"); - copyIcon.addEventHandler(MOUSE_CLICKED, mouseEvent -> { + if (copyLabel != null) { + copyLabel.getStyleClass().add("popup-icon-information"); + copyLabel.setManaged(true); + copyLabel.setVisible(true); + MaterialDesignIconView copyIcon = new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "1.2em"); + copyLabel.setGraphic(copyIcon); + copyLabel.setCursor(Cursor.HAND); + copyLabel.addEventHandler(MOUSE_CLICKED, mouseEvent -> { if (message != null) { Utilities.copyToClipboard(getClipboardText()); Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); @@ -808,6 +825,8 @@ public abstract class Overlay> { } protected void removeEffectFromBackground() { + numBlurEffects--; + if (numBlurEffects > 0) return; MainView.removeEffect(); } @@ -828,15 +847,15 @@ public abstract class Overlay> { headLineLabel.setStyle(headlineStyle); if (message != null) { - copyIcon = new Label(); - copyIcon.setManaged(false); - copyIcon.setVisible(false); - copyIcon.setPadding(new Insets(3)); - copyIcon.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); + copyLabel = new Label(); + copyLabel.setManaged(false); + copyLabel.setVisible(false); + copyLabel.setPadding(new Insets(3)); + copyLabel.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); final Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); spacer.setMinSize(Layout.PADDING, 1); - hBox.getChildren().addAll(headlineIcon, headLineLabel, spacer, copyIcon); + hBox.getChildren().addAll(headlineIcon, headLineLabel, spacer, copyLabel); } else { hBox.getChildren().addAll(headlineIcon, headLineLabel); } @@ -852,23 +871,8 @@ public abstract class Overlay> { if (message != null) { messageTextArea = new TextArea(truncatedMessage); messageTextArea.setEditable(false); - messageTextArea.getStyleClass().add("text-area-no-border"); - messageTextArea.sceneProperty().addListener((o, oldScene, newScene) -> { - if (newScene != null) { - // avoid javafx css warning - CssTheme.loadSceneStyles(newScene, CssTheme.CSS_THEME_LIGHT, false); - messageTextArea.applyCss(); - var text = messageTextArea.lookup(".text"); - - messageTextArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() -> { - return messageTextArea.getFont().getSize() + text.getBoundsInLocal().getHeight(); - }, text.boundsInLocalProperty())); - - text.boundsInLocalProperty().addListener((observableBoundsAfter, boundsBefore, boundsAfter) -> { - Platform.runLater(() -> messageTextArea.requestLayout()); - }); - } - }); + messageTextArea.getStyleClass().add("text-area-popup"); + GUIUtil.adjustHeightAutomatically(messageTextArea); messageTextArea.setWrapText(true); Region messageRegion; @@ -1093,4 +1097,10 @@ public abstract class Overlay> { ", message='" + message + '\'' + '}'; } + + private boolean isCentered() { + if (type.animationType == AnimationType.SlideDownFromCenterTop) return false; + if (type.animationType == AnimationType.SlideFromRightTop) return false; + return true; + } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java b/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java index bd169b3a4b..0af066c9aa 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java @@ -71,7 +71,7 @@ public class PasswordPopup extends Overlay { @Override public void show() { - actionButtonText("CONFIRM"); + actionButtonText("Confirm"); createGridPane(); addHeadLine(); addContent(); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/notifications/Notification.java b/desktop/src/main/java/haveno/desktop/main/overlays/notifications/Notification.java index 57a9550e3a..ebfcb4dcd4 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/notifications/Notification.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/notifications/Notification.java @@ -42,6 +42,7 @@ public class Notification extends Overlay { private boolean hasBeenDisplayed; private boolean autoClose; private Timer autoCloseTimer; + private static final int BORDER_PADDING = 10; public Notification() { width = 413; // 320 visible bg because of insets @@ -205,8 +206,8 @@ public class Notification extends Overlay { Window window = owner.getScene().getWindow(); double titleBarHeight = window.getHeight() - owner.getScene().getHeight(); double shadowInset = 44; - stage.setX(Math.round(window.getX() + window.getWidth() + shadowInset - stage.getWidth())); - stage.setY(Math.round(window.getY() + titleBarHeight - shadowInset)); + stage.setX(Math.round(window.getX() + window.getWidth() + shadowInset - stage.getWidth() - BORDER_PADDING)); + stage.setY(Math.round(window.getY() + titleBarHeight - shadowInset + BORDER_PADDING)); } @Override diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/notifications/NotificationCenter.java b/desktop/src/main/java/haveno/desktop/main/overlays/notifications/NotificationCenter.java index 829a0640d5..f9211cd17f 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/notifications/NotificationCenter.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/notifications/NotificationCenter.java @@ -216,7 +216,7 @@ public class NotificationCenter { if (DontShowAgainLookup.showAgain(key)) { Notification notification = new Notification().tradeHeadLine(trade.getShortId()).message(message); if (navigation.getCurrentPath() != null && !navigation.getCurrentPath().contains(PendingTradesView.class)) { - notification.actionButtonTextWithGoTo("navigation.portfolio.pending") + notification.actionButtonTextWithGoTo("portfolio.tab.pendingTrades") .onAction(() -> { DontShowAgainLookup.dontShowAgain(key, true); navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class); @@ -318,7 +318,7 @@ public class NotificationCenter { private void goToSupport(Trade trade, String message, Class viewClass) { Notification notification = new Notification().disputeHeadLine(trade.getShortId()).message(message); if (navigation.getCurrentPath() != null && !navigation.getCurrentPath().contains(viewClass)) { - notification.actionButtonTextWithGoTo("navigation.support") + notification.actionButtonTextWithGoTo("mainView.menu.support") .onAction(() -> navigation.navigateTo(MainView.class, SupportView.class, viewClass)) .show(); } else { diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java index 6a238e56ee..7f72fb2e10 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java @@ -46,6 +46,7 @@ import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextField; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addLabelExplorerAddressTextField; import static haveno.desktop.util.FormBuilder.addLabelTxIdTextField; +import static haveno.desktop.util.FormBuilder.addSeparator; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.Layout; import haveno.network.p2p.NodeAddress; @@ -137,15 +138,20 @@ public class ContractWindow extends Overlay { addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("contractWindow.title")); addConfirmationLabelTextField(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(), Layout.TWICE_FIRST_ROW_DISTANCE); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.dates"), DisplayUtils.formatDateTime(offer.getDate()) + " / " + DisplayUtils.formatDateTime(dispute.getTradeDate())); String currencyCode = offer.getCurrencyCode(); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.offerType"), DisplayUtils.getDirectionBothSides(offer.getDirection(), offer.isPrivateOffer())); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradePrice"), FormattingUtils.formatPrice(contract.getPrice())); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradeAmount"), HavenoUtils.formatXmr(contract.getTradeAmount(), true)); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, VolumeUtil.formatVolumeLabel(currencyCode, ":"), @@ -157,16 +163,20 @@ public class ContractWindow extends Overlay { Res.getWithColAndCap("shared.seller") + " " + HavenoUtils.formatXmr(offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(contract.getTradeAmount()), true); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.xmrAddresses"), contract.getBuyerPayoutAddressString() + " / " + contract.getSellerPayoutAddressString()); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.onions"), contract.getBuyerNodeAddress().getFullAddress() + " / " + contract.getSellerNodeAddress().getFullAddress()); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.accountAge"), @@ -176,16 +186,19 @@ public class ContractWindow extends Overlay { DisputeManager> disputeManager = getDisputeManager(dispute); String nrOfDisputesAsBuyer = disputeManager != null ? disputeManager.getNrOfDisputes(true, contract) : ""; String nrOfDisputesAsSeller = disputeManager != null ? disputeManager.getNrOfDisputes(false, contract) : ""; + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.numDisputes"), nrOfDisputesAsBuyer + " / " + nrOfDisputesAsSeller); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.buyer")), dispute.getBuyerPaymentAccountPayload() != null ? dispute.getBuyerPaymentAccountPayload().getPaymentDetails() : "NA"); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.seller")), @@ -219,6 +232,7 @@ public class ContractWindow extends Overlay { NodeAddress agentNodeAddress = disputeManager.getAgentNodeAddress(dispute); if (agentNodeAddress != null) { String value = agentMatrixUserName + " (" + agentNodeAddress.getFullAddress() + ")"; + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, title, value); } } @@ -232,40 +246,53 @@ public class ContractWindow extends Overlay { countries = CountryUtil.getCodesString(acceptedCountryCodes); tooltip = new Tooltip(CountryUtil.getNamesByCodesString(acceptedCountryCodes)); } + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.acceptedTakerCountries"), countries) .second.setTooltip(tooltip); } if (showAcceptedBanks) { if (offer.getPaymentMethod().equals(PaymentMethod.SAME_BANK)) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.bankName"), acceptedBanks.get(0)); } else if (offer.getPaymentMethod().equals(PaymentMethod.SPECIFIC_BANKS)) { String value = Joiner.on(", ").join(acceptedBanks); Tooltip tooltip = new Tooltip(Res.get("shared.acceptedBanks") + value); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.acceptedBanks"), value) .second.setTooltip(tooltip); } } + addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.makerDepositTransactionId"), contract.getMakerDepositTxHash()); - if (contract.getTakerDepositTxHash() != null) + if (contract.getTakerDepositTxHash() != null) { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerDepositTransactionId"), contract.getTakerDepositTxHash()); + } - if (dispute.getDelayedPayoutTxId() != null) + if (dispute.getDelayedPayoutTxId() != null) { + addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxId"), dispute.getDelayedPayoutTxId()); + } if (dispute.getDonationAddressOfDelayedPayoutTx() != null) { + addSeparator(gridPane, ++rowIndex); addLabelExplorerAddressTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxReceiverAddress"), dispute.getDonationAddressOfDelayedPayoutTx()); } - if (dispute.getPayoutTxSerialized() != null) + if (dispute.getPayoutTxSerialized() != null) { + addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), dispute.getPayoutTxId()); + } - if (dispute.getContractHash() != null) + if (dispute.getContractHash() != null) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("contractWindow.contractHash"), Utils.HEX.encode(dispute.getContractHash())).second.setMouseTransparent(false); + } + addSeparator(gridPane, ++rowIndex); Button viewContractButton = addConfirmationLabelButton(gridPane, ++rowIndex, Res.get("shared.contractAsJson"), Res.get("shared.viewContractAsJson"), 0).second; viewContractButton.setDefaultButton(false); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java index 5004d240d8..49931e202b 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -188,7 +188,7 @@ public class DisputeSummaryWindow extends Overlay { protected void createGridPane() { super.createGridPane(); gridPane.setPadding(new Insets(35, 40, 30, 40)); - gridPane.getStyleClass().add("grid-pane"); + gridPane.getStyleClass().addAll("grid-pane", "popup-with-input"); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); gridPane.setPrefWidth(width); } @@ -413,11 +413,13 @@ public class DisputeSummaryWindow extends Overlay { private void addPayoutAmountTextFields() { buyerPayoutAmountInputTextField = new InputTextField(); buyerPayoutAmountInputTextField.setLabelFloat(true); + buyerPayoutAmountInputTextField.getStyleClass().add("label-float"); buyerPayoutAmountInputTextField.setEditable(false); buyerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.buyer")); sellerPayoutAmountInputTextField = new InputTextField(); sellerPayoutAmountInputTextField.setLabelFloat(true); + sellerPayoutAmountInputTextField.getStyleClass().add("label-float"); sellerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.seller")); sellerPayoutAmountInputTextField.setEditable(false); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/GenericMessageWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/GenericMessageWindow.java index 01d119a479..9ee42ae27c 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/GenericMessageWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/GenericMessageWindow.java @@ -18,6 +18,7 @@ package haveno.desktop.main.overlays.windows; import haveno.desktop.main.overlays.Overlay; +import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.scene.control.Label; import javafx.scene.control.TextArea; @@ -28,6 +29,7 @@ import static haveno.desktop.util.FormBuilder.addTextArea; public class GenericMessageWindow extends Overlay { private String preamble; + private static final double MAX_TEXT_AREA_HEIGHT = 250; public GenericMessageWindow() { super(); @@ -54,20 +56,11 @@ public class GenericMessageWindow extends Overlay { } checkNotNull(message, "message must not be null"); TextArea textArea = addTextArea(gridPane, ++rowIndex, "", 10); + textArea.getStyleClass().add("flat-text-area-with-border"); textArea.setText(message); textArea.setEditable(false); textArea.setWrapText(true); - // sizes the textArea to fit within its parent container - double verticalSizePercentage = ensureRange(countLines(message) / 20.0, 0.2, 0.7); - textArea.setPrefSize(Layout.INITIAL_WINDOW_WIDTH, Layout.INITIAL_WINDOW_HEIGHT * verticalSizePercentage); - } - - private static int countLines(String str) { - String[] lines = str.split("\r\n|\r|\n"); - return lines.length; - } - - private static double ensureRange(double value, double min, double max) { - return Math.min(Math.max(value, min), max); + textArea.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); + GUIUtil.adjustHeightAutomatically(textArea, MAX_TEXT_AREA_HEIGHT); } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index dd645246f7..b0c00ca60f 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -20,10 +20,16 @@ package haveno.desktop.main.overlays.windows; import com.google.common.base.Joiner; import com.google.inject.Inject; import com.google.inject.name.Named; +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXTextField; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.UserThread; import haveno.common.crypto.KeyRing; import haveno.common.util.Tuple2; import haveno.common.util.Tuple4; +import haveno.common.util.Utilities; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.monetary.Price; @@ -45,30 +51,37 @@ import haveno.desktop.components.BusyAnimation; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.editor.PasswordPopup; import haveno.desktop.main.overlays.popups.Popup; -import haveno.desktop.util.CssTheme; import haveno.desktop.util.DisplayUtils; import static haveno.desktop.util.FormBuilder.addButtonAfterGroup; import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; import static haveno.desktop.util.FormBuilder.addConfirmationLabelLabel; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextArea; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextFieldWithCopyIcon; +import static haveno.desktop.util.FormBuilder.addLabel; +import static haveno.desktop.util.FormBuilder.addSeparator; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import java.math.BigInteger; import java.util.List; import java.util.Optional; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; import javafx.geometry.HPos; import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.VPos; +import javafx.scene.Node; import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; + import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.slf4j.Logger; @@ -115,7 +128,7 @@ public class OfferDetailsWindow extends Overlay { this.tradePrice = tradePrice; rowIndex = -1; - width = 1118; + width = Layout.DETAILS_WINDOW_WIDTH; createGridPane(); addContent(); display(); @@ -124,7 +137,7 @@ public class OfferDetailsWindow extends Overlay { public void show(Offer offer) { this.offer = offer; rowIndex = -1; - width = 1118; + width = Layout.DETAILS_WINDOW_WIDTH; createGridPane(); addContent(); display(); @@ -194,7 +207,7 @@ public class OfferDetailsWindow extends Overlay { rows++; } - addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get(offer.isPrivateOffer() ? "shared.Offer" : "shared.Offer")); + addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.Offer")); String counterCurrencyDirectionInfo = ""; String xmrDirectionInfo = ""; @@ -218,17 +231,22 @@ public class OfferDetailsWindow extends Overlay { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, DisplayUtils.getDirectionBothSides(direction, offer.isPrivateOffer()), firstRowDistance); } + String amount = Res.get("shared.xmrAmount"); + addSeparator(gridPane, ++rowIndex); if (takeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, ++rowIndex, amount + xmrDirectionInfo, HavenoUtils.formatXmr(tradeAmount, true)); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, VolumeUtil.formatVolumeLabel(currencyCode) + counterCurrencyDirectionInfo, VolumeUtil.formatVolumeWithCode(offer.getVolumeByAmount(tradeAmount))); } else { addConfirmationLabelLabel(gridPane, ++rowIndex, amount + xmrDirectionInfo, HavenoUtils.formatXmr(offer.getAmount(), true)); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.minXmrAmount"), HavenoUtils.formatXmr(offer.getMinAmount(), true)); + addSeparator(gridPane, ++rowIndex); String volume = VolumeUtil.formatVolumeWithCode(offer.getVolume()); String minVolume = ""; if (offer.getVolume() != null && offer.getMinVolume() != null && @@ -239,6 +257,7 @@ public class OfferDetailsWindow extends Overlay { } String priceLabel = Res.get("shared.price"); + addSeparator(gridPane, ++rowIndex); if (takeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, ++rowIndex, priceLabel, FormattingUtils.formatPrice(tradePrice)); } else { @@ -264,6 +283,7 @@ public class OfferDetailsWindow extends Overlay { final PaymentAccount myPaymentAccount = user.getPaymentAccount(makerPaymentAccountId); String countryCode = offer.getCountryCode(); boolean isMyOffer = offer.isMyOffer(keyRing); + addSeparator(gridPane, ++rowIndex); if (isMyOffer && makerPaymentAccountId != null && myPaymentAccount != null) { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.myTradingAccount"), myPaymentAccount.getAccountName()); } else { @@ -272,6 +292,7 @@ public class OfferDetailsWindow extends Overlay { } if (showXmrAutoConf) { + addSeparator(gridPane, ++rowIndex); String isAutoConf = offer.isXmrAutoConf() ? Res.get("shared.yes") : Res.get("shared.no"); @@ -280,8 +301,10 @@ public class OfferDetailsWindow extends Overlay { if (showAcceptedBanks) { if (paymentMethod.equals(PaymentMethod.SAME_BANK)) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.bankId"), acceptedBanks.get(0)); } else if (isSpecificBanks) { + addSeparator(gridPane, ++rowIndex); String value = Joiner.on(", ").join(acceptedBanks); String acceptedBanksLabel = Res.get("shared.acceptedBanks"); Tooltip tooltip = new Tooltip(acceptedBanksLabel + " " + value); @@ -291,6 +314,7 @@ public class OfferDetailsWindow extends Overlay { } } if (showAcceptedCountryCodes) { + addSeparator(gridPane, ++rowIndex); String countries; Tooltip tooltip = null; if (CountryUtil.containsAllSepaEuroCountries(acceptedCountryCodes)) { @@ -313,29 +337,16 @@ public class OfferDetailsWindow extends Overlay { } if (isF2F) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("payment.f2f.city"), offer.getF2FCity()); } if (showOfferExtraInfo) { + addSeparator(gridPane, ++rowIndex); TextArea textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("payment.shared.extraInfo"), "", 0).second; - textArea.setText(offer.getCombinedExtraInfo()); - textArea.setMaxHeight(200); - textArea.sceneProperty().addListener((o, oldScene, newScene) -> { - if (newScene != null) { - // avoid javafx css warning - CssTheme.loadSceneStyles(newScene, CssTheme.CSS_THEME_LIGHT, false); - textArea.applyCss(); - var text = textArea.lookup(".text"); - - textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() -> { - return textArea.getFont().getSize() + text.getBoundsInLocal().getHeight(); - }, text.boundsInLocalProperty())); - - text.boundsInLocalProperty().addListener((observableBoundsAfter, boundsBefore, boundsAfter) -> { - Platform.runLater(() -> textArea.requestLayout()); - }); - } - }); + textArea.setText(offer.getCombinedExtraInfo().trim()); + textArea.setMaxHeight(Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); textArea.setEditable(false); + GUIUtil.adjustHeightAutomatically(textArea, Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); } // get amount reserved for the offer @@ -355,13 +366,16 @@ public class OfferDetailsWindow extends Overlay { if (offerChallenge != null) rows++; - addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE); + addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.COMPACT_GROUP_DISTANCE); addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(), - Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); + Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.makersOnion"), offer.getMakerNodeAddress().getFullAddress()); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.creationDate"), DisplayUtils.formatDateTime(offer.getDate())); + addSeparator(gridPane, ++rowIndex); String value = Res.getWithColAndCap("shared.buyer") + " " + HavenoUtils.formatXmr(takeOfferHandlerOptional.isPresent() ? offer.getOfferPayload().getBuyerSecurityDepositForTradeAmount(tradeAmount) : offer.getOfferPayload().getMaxBuyerSecurityDeposit(), true) + @@ -372,27 +386,75 @@ public class OfferDetailsWindow extends Overlay { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), value); if (reservedAmount != null) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.reservedAmount"), HavenoUtils.formatXmr(reservedAmount, true)); } - if (countryCode != null && !isF2F) + if (countryCode != null && !isF2F) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.countryBank"), CountryUtil.getNameAndCode(countryCode)); + } - if (offerChallenge != null) - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.challenge"), offerChallenge); + if (offerChallenge != null) { + addSeparator(gridPane, ++rowIndex); + + // add label + Label label = addLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.challenge"), 0); + label.getStyleClass().addAll("confirmation-label", "regular-text-color"); + GridPane.setHalignment(label, HPos.LEFT); + GridPane.setValignment(label, VPos.TOP); + + // add vbox with passphrase and copy button + VBox vbox = new VBox(13); + vbox.setAlignment(Pos.TOP_CENTER); + VBox.setVgrow(vbox, Priority.ALWAYS); + vbox.getStyleClass().addAll("passphrase-copy-box"); + + // add passphrase + JFXTextField centerLabel = new JFXTextField(offerChallenge); + centerLabel.getStyleClass().add("confirmation-value"); + centerLabel.setAlignment(Pos.CENTER); + centerLabel.setFocusTraversable(false); + + // add copy button + Label copyLabel = new Label(); + copyLabel.getStyleClass().addAll("icon"); + copyLabel.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); + MaterialDesignIconView copyIcon = new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "1.2em"); + copyIcon.setFill(Color.WHITE); + copyLabel.setGraphic(copyIcon); + JFXButton copyButton = new JFXButton(Res.get("offerDetailsWindow.challenge.copy"), copyLabel); + copyButton.setContentDisplay(ContentDisplay.LEFT); + copyButton.setGraphicTextGap(8); + copyButton.setOnMouseClicked(e -> { + Utilities.copyToClipboard(offerChallenge); + Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); + Node node = (Node) e.getSource(); + UserThread.runAfter(() -> tp.hide(), 1); + tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); + }); + copyButton.setId("buy-button"); + copyButton.setFocusTraversable(false); + vbox.getChildren().addAll(centerLabel, copyButton); + + // add vbox to grid pane in next column + GridPane.setRowIndex(vbox, rowIndex); + GridPane.setColumnIndex(vbox, 1); + gridPane.getChildren().add(vbox); + } if (placeOfferHandlerOptional.isPresent()) { - addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("offerDetailsWindow.commitment"), Layout.GROUP_DISTANCE); + addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("offerDetailsWindow.commitment"), Layout.COMPACT_GROUP_DISTANCE); final Tuple2 labelLabelTuple2 = addConfirmationLabelLabel(gridPane, rowIndex, Res.get("offerDetailsWindow.agree"), Res.get("createOffer.tac"), - Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); + Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); labelLabelTuple2.second.setWrapText(true); addConfirmAndCancelButtons(true); } else if (takeOfferHandlerOptional.isPresent()) { - addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("shared.contract"), Layout.GROUP_DISTANCE); + addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("shared.contract"), Layout.COMPACT_GROUP_DISTANCE); final Tuple2 labelLabelTuple2 = addConfirmationLabelLabel(gridPane, rowIndex, Res.get("offerDetailsWindow.tac"), Res.get("takeOffer.tac"), - Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); + Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); labelLabelTuple2.second.setWrapText(true); addConfirmAndCancelButtons(false); @@ -418,9 +480,6 @@ public class OfferDetailsWindow extends Overlay { Res.get("offerDetailsWindow.confirm.taker", Res.get("shared.buy")) : Res.get("offerDetailsWindow.confirm.taker", Res.get("shared.sell")); - ImageView iconView = new ImageView(); - iconView.setId(isBuyerRole ? "image-buy-white" : "image-sell-white"); - Tuple4 placeOfferTuple = addButtonBusyAnimationLabelAfterGroup(gridPane, ++rowIndex, 1, isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); @@ -428,11 +487,18 @@ public class OfferDetailsWindow extends Overlay { AutoTooltipButton confirmButton = (AutoTooltipButton) placeOfferTuple.first; confirmButton.setMinHeight(40); confirmButton.setPadding(new Insets(0, 20, 0, 20)); - confirmButton.setGraphic(iconView); confirmButton.setGraphicTextGap(10); confirmButton.setId(isBuyerRole ? "buy-button-big" : "sell-button-big"); confirmButton.updateText(isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); + if (offer.hasBuyerAsTakerWithoutDeposit()) { + confirmButton.setGraphic(GUIUtil.getLockLabel()); + } else { + ImageView iconView = new ImageView(); + iconView.setId(isBuyerRole ? "image-buy-white" : "image-sell-white"); + confirmButton.setGraphic(iconView); + } + busyAnimation = placeOfferTuple.second; Label spinnerInfoLabel = placeOfferTuple.third; diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java index 8bca62d143..21809a7501 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java @@ -17,9 +17,12 @@ package haveno.desktop.main.overlays.windows; +import haveno.common.util.Tuple2; +import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.main.overlays.Overlay; +import haveno.desktop.util.GUIUtil; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.control.Label; @@ -27,31 +30,35 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; +import java.net.URI; public class QRCodeWindow extends Overlay { private static final Logger log = LoggerFactory.getLogger(QRCodeWindow.class); - private final ImageView qrCodeImageView; + private final StackPane qrCodePane; private final String moneroUri; - public QRCodeWindow(String bitcoinURI) { - this.moneroUri = bitcoinURI; + public QRCodeWindow(String moneroUri) { + this.moneroUri = moneroUri; + + Tuple2 qrCodeTuple = GUIUtil.getBigXmrQrCodePane(); + qrCodePane = qrCodeTuple.first; + ImageView qrCodeImageView = qrCodeTuple.second; + final byte[] imageBytes = QRCode - .from(bitcoinURI) + .from(moneroUri) .withSize(300, 300) .to(ImageType.PNG) .stream() .toByteArray(); Image qrImage = new Image(new ByteArrayInputStream(imageBytes)); - qrCodeImageView = new ImageView(qrImage); - qrCodeImageView.setFitHeight(250); - qrCodeImageView.setFitWidth(250); - qrCodeImageView.getStyleClass().add("qr-code"); + qrCodeImageView.setImage(qrImage); type = Type.Information; width = 468; @@ -65,10 +72,11 @@ public class QRCodeWindow extends Overlay { addHeadLine(); addMessage(); - GridPane.setRowIndex(qrCodeImageView, ++rowIndex); - GridPane.setColumnSpan(qrCodeImageView, 2); - GridPane.setHalignment(qrCodeImageView, HPos.CENTER); - gridPane.getChildren().add(qrCodeImageView); + qrCodePane.setOnMouseClicked(event -> openWallet()); + GridPane.setRowIndex(qrCodePane, ++rowIndex); + GridPane.setColumnSpan(qrCodePane, 2); + GridPane.setHalignment(qrCodePane, HPos.CENTER); + gridPane.getChildren().add(qrCodePane); String request = moneroUri.replace("%20", " ").replace("?", "\n?").replace("&", "\n&"); Label infoLabel = new AutoTooltipLabel(Res.get("qRCodeWindow.request", request)); @@ -91,4 +99,12 @@ public class QRCodeWindow extends Overlay { public String getClipboardText() { return moneroUri; } + + private void openWallet() { + try { + Utilities.openURI(URI.create(moneroUri)); + } catch (Exception e) { + log.warn(e.getMessage()); + } + } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/SignPaymentAccountsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/SignPaymentAccountsWindow.java index fe8b8c9009..3bf7be6b14 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/SignPaymentAccountsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/SignPaymentAccountsWindow.java @@ -113,7 +113,7 @@ public class SignPaymentAccountsWindow extends Overlay { this.trade = trade; rowIndex = -1; - width = 918; + width = Layout.DETAILS_WINDOW_WIDTH; createGridPane(); addContent(); display(); @@ -127,7 +126,6 @@ public class TradeDetailsWindow extends Overlay { @Override protected void createGridPane() { super.createGridPane(); - gridPane.setPadding(new Insets(35, 40, 30, 40)); gridPane.getStyleClass().add("grid-pane"); } @@ -135,7 +133,7 @@ public class TradeDetailsWindow extends Overlay { Offer offer = trade.getOffer(); Contract contract = trade.getContract(); - int rows = 5; + int rows = 9; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("tradeDetailsWindow.headline")); boolean myOffer = tradeManager.isMyOffer(offer); @@ -156,18 +154,22 @@ public class TradeDetailsWindow extends Overlay { xmrDirectionInfo = toSpend; } + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.xmrAmount") + xmrDirectionInfo, HavenoUtils.formatXmr(trade.getAmount(), true)); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, VolumeUtil.formatVolumeLabel(offer.getCurrencyCode()) + counterCurrencyDirectionInfo, VolumeUtil.formatVolumeWithCode(trade.getVolume())); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradePrice"), FormattingUtils.formatPrice(trade.getPrice())); String paymentMethodText = Res.get(offer.getPaymentMethod().getId()); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), paymentMethodText); // second group - rows = 7; + rows = 5; if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) rows++; @@ -200,9 +202,10 @@ public class TradeDetailsWindow extends Overlay { if (trade.getTradePeerNodeAddress() != null) rows++; - addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE); + addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.COMPACT_GROUP_DISTANCE); addConfirmationLabelTextField(gridPane, rowIndex, Res.get("shared.tradeId"), - trade.getId(), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); + trade.getId(), Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradeDate"), DisplayUtils.formatDateTime(trade.getDate())); String securityDeposit = Res.getWithColAndCap("shared.buyer") + @@ -212,40 +215,30 @@ public class TradeDetailsWindow extends Overlay { Res.getWithColAndCap("shared.seller") + " " + HavenoUtils.formatXmr(trade.getSellerSecurityDepositBeforeMiningFee(), true); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); NodeAddress arbitratorNodeAddress = trade.getArbitratorNodeAddress(); if (arbitratorNodeAddress != null) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.agentAddresses"), arbitratorNodeAddress.getFullAddress()); } - if (trade.getTradePeerNodeAddress() != null) + if (trade.getTradePeerNodeAddress() != null) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradePeersOnion"), trade.getTradePeerNodeAddress().getFullAddress()); + } if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) { + addSeparator(gridPane, ++rowIndex); TextArea textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("payment.shared.extraInfo.offer"), "", 0).second; - textArea.setText(offer.getCombinedExtraInfo()); - textArea.setMaxHeight(200); - textArea.sceneProperty().addListener((o, oldScene, newScene) -> { - if (newScene != null) { - // avoid javafx css warning - CssTheme.loadSceneStyles(newScene, CssTheme.CSS_THEME_LIGHT, false); - textArea.applyCss(); - var text = textArea.lookup(".text"); - - textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() -> { - return textArea.getFont().getSize() + text.getBoundsInLocal().getHeight(); - }, text.boundsInLocalProperty())); - - text.boundsInLocalProperty().addListener((observableBoundsAfter, boundsBefore, boundsAfter) -> { - Platform.runLater(() -> textArea.requestLayout()); - }); - } - }); + textArea.setText(offer.getCombinedExtraInfo().trim()); + textArea.setMaxHeight(Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); textArea.setEditable(false); + GUIUtil.adjustHeightAutomatically(textArea, Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); } if (contract != null) { @@ -254,6 +247,7 @@ public class TradeDetailsWindow extends Overlay { if (buyerPaymentAccountPayload != null) { String paymentDetails = buyerPaymentAccountPayload.getPaymentDetails(); String postFix = " / " + buyersAccountAge; + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.buyer")), paymentDetails + postFix).second.setTooltip(new Tooltip(paymentDetails + postFix)); @@ -261,21 +255,27 @@ public class TradeDetailsWindow extends Overlay { if (sellerPaymentAccountPayload != null) { String paymentDetails = sellerPaymentAccountPayload.getPaymentDetails(); String postFix = " / " + sellersAccountAge; + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.seller")), paymentDetails + postFix).second.setTooltip(new Tooltip(paymentDetails + postFix)); } - if (buyerPaymentAccountPayload == null && sellerPaymentAccountPayload == null) + if (buyerPaymentAccountPayload == null && sellerPaymentAccountPayload == null) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), Res.get(contract.getPaymentMethodId())); + } } - if (trade.getMaker().getDepositTxHash() != null) + if (trade.getMaker().getDepositTxHash() != null) { + addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.makerDepositTransactionId"), trade.getMaker().getDepositTxHash()); - if (trade.getTaker().getDepositTxHash() != null) + } + if (trade.getTaker().getDepositTxHash() != null) { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerDepositTransactionId"), trade.getTaker().getDepositTxHash()); + } if (showDisputedTx) { @@ -287,6 +287,7 @@ public class TradeDetailsWindow extends Overlay { } if (trade.hasFailed()) { + addSeparator(gridPane, ++rowIndex); textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("shared.errorMessage"), "", 0).second; textArea.setText(trade.getErrorMessage()); textArea.setEditable(false); @@ -302,6 +303,7 @@ public class TradeDetailsWindow extends Overlay { textArea.scrollTopProperty().addListener(changeListener); textArea.setScrollTop(30); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradePhase"), trade.getPhase().name()); } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeFeedbackWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeFeedbackWindow.java index 76d414b380..a9202f9b8a 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeFeedbackWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeFeedbackWindow.java @@ -40,7 +40,7 @@ public class TradeFeedbackWindow extends Overlay { @Override public void show() { headLine(Res.get("tradeFeedbackWindow.title")); - message(Res.get("tradeFeedbackWindow.msg.part1")); + //message(Res.get("tradeFeedbackWindow.msg.part1")); // TODO: this message part has padding which remaining message does not have hideCloseButton(); actionButtonText(Res.get("shared.close")); @@ -51,6 +51,17 @@ public class TradeFeedbackWindow extends Overlay { protected void addMessage() { super.addMessage(); + AutoTooltipLabel messageLabel1 = new AutoTooltipLabel(Res.get("tradeFeedbackWindow.msg.part1")); + messageLabel1.setMouseTransparent(true); + messageLabel1.setWrapText(true); + GridPane.setHalignment(messageLabel1, HPos.LEFT); + GridPane.setHgrow(messageLabel1, Priority.ALWAYS); + GridPane.setRowIndex(messageLabel1, ++rowIndex); + GridPane.setColumnIndex(messageLabel1, 0); + GridPane.setColumnSpan(messageLabel1, 2); + gridPane.getChildren().add(messageLabel1); + GridPane.setMargin(messageLabel1, new Insets(10, 0, 10, 0)); + AutoTooltipLabel messageLabel2 = new AutoTooltipLabel(Res.get("tradeFeedbackWindow.msg.part2")); messageLabel2.setMouseTransparent(true); messageLabel2.setWrapText(true); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxDetailsWindow.java index 1af5d97fbf..417e5c0043 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxDetailsWindow.java @@ -30,7 +30,6 @@ import static haveno.desktop.util.FormBuilder.addConfirmationLabelLabel; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addLabelTxIdTextField; import static haveno.desktop.util.FormBuilder.addMultilineLabel; -import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import java.math.BigInteger; @@ -50,21 +49,20 @@ public class TxDetailsWindow extends Overlay { this.item = item; rowIndex = -1; width = 918; + if (headLine == null) + headLine = Res.get("txDetailsWindow.headline"); createGridPane(); gridPane.setHgap(15); addHeadLine(); addContent(); addButtons(); - addDontShowAgainCheckBox(); applyStyles(); display(); } protected void addContent() { - int rows = 10; MoneroTxWallet tx = item.getTx(); String memo = tx.getNote(); - if (memo != null && !"".equals(memo)) rows++; String txKey = null; boolean isOutgoing = tx.getOutgoingTransfer() != null; if (isOutgoing) { @@ -74,18 +72,11 @@ public class TxDetailsWindow extends Overlay { // TODO (monero-java): wallet.getTxKey() should return null if key does not exist instead of throwing exception } } - if (txKey != null && !"".equals(txKey)) rows++; - - // add title - addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("txDetailsWindow.headline")); - Region spacer = new Region(); - spacer.setMinHeight(15); - gridPane.add(spacer, 0, ++rowIndex); // add sent or received note String resKey = isOutgoing ? "txDetailsWindow.xmr.noteSent" : "txDetailsWindow.xmr.noteReceived"; GridPane.setColumnSpan(addMultilineLabel(gridPane, ++rowIndex, Res.get(resKey), 0), 2); - spacer = new Region(); + Region spacer = new Region(); spacer.setMinHeight(15); gridPane.add(spacer, 0, ++rowIndex); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java index fd60d07193..a8bd993219 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java @@ -77,6 +77,7 @@ public class VerifyDisputeResultSignatureWindow extends Overlay { root.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS); failedTradesTab.setClosable(false); - openOffersTab.setText(Res.get("portfolio.tab.openOffers").toUpperCase()); - pendingTradesTab.setText(Res.get("portfolio.tab.pendingTrades").toUpperCase()); - closedTradesTab.setText(Res.get("portfolio.tab.history").toUpperCase()); + openOffersTab.setText(Res.get("portfolio.tab.openOffers")); + pendingTradesTab.setText(Res.get("portfolio.tab.pendingTrades")); + closedTradesTab.setText(Res.get("portfolio.tab.history")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(PortfolioView.class) == 1) diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java index ab92f845db..aa7c6bb1c5 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java @@ -156,6 +156,8 @@ public class ClosedTradesView extends ActivatableViewAndModel onWidthChange((double) newValue); tradeFeeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRADE_FEE.toString().replace(" BTC", ""))); buyerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.BUYER_SEC.toString())); @@ -252,6 +254,7 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offerListItem.getValue())); tradeIdColumn.setCellFactory( new Callback<>() { @@ -463,7 +465,7 @@ public class ClosedTradesView extends ActivatableViewAndModel setAvatarColumnCellFactory() { - avatarColumn.getStyleClass().addAll("last-column", "avatar-column"); + avatarColumn.getStyleClass().add("avatar-column"); avatarColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); avatarColumn.setCellFactory( new Callback<>() { @@ -696,7 +698,7 @@ public class ClosedTradesView extends ActivatableViewAndModel onRevertTrade(trade)); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java index 35fe31dc22..f92dc4c0b8 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java @@ -115,6 +115,8 @@ public class FailedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offerListItem.getValue())); tradeIdColumn.setCellFactory( new Callback<>() { @@ -455,7 +456,6 @@ public class FailedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(trade.getValue())); stateColumn.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml index 035ec5fbdc..44b26f1bd3 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml @@ -35,7 +35,6 @@ - diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java index 3282ab078a..ec19daccdd 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -116,8 +116,6 @@ public class OpenOffersView extends ActivatableViewAndModel onWidthChange((double) newValue); groupIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.GROUP_ID.toString())); paymentMethodColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PAYMENT_METHOD.toString())); @@ -231,8 +231,7 @@ public class OpenOffersView extends ActivatableViewAndModel applyFilteredListPredicate(filterTextField.getText()); searchBox.setSpacing(5); HBox.setHgrow(searchBoxSpacer, Priority.ALWAYS); @@ -470,8 +469,8 @@ public class OpenOffersView extends ActivatableViewAndModel navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class)) .dontShowAgainId(key) .show(); @@ -527,7 +526,7 @@ public class OpenOffersView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(openOfferListItem.getValue())); - offerIdColumn.getStyleClass().addAll("number-column", "first-column"); + offerIdColumn.getStyleClass().addAll("number-column"); offerIdColumn.setCellFactory( new Callback<>() { @@ -903,7 +902,7 @@ public class OpenOffersView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offerListItem.getValue())); removeItemColumn.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 645fbb5014..70b588bb4e 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -55,6 +55,7 @@ import haveno.desktop.main.shared.ChatView; import haveno.desktop.util.CssTheme; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.FormBuilder; +import haveno.desktop.util.GUIUtil; import haveno.network.p2p.NodeAddress; import java.util.Comparator; import java.util.HashMap; @@ -171,6 +172,8 @@ public class PendingTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(pendingTradesListItem.getValue())); tradeIdColumn.setCellFactory( new Callback<>() { @@ -821,7 +824,7 @@ public class PendingTradesView extends ActivatableViewAndModel setAvatarColumnCellFactory() { avatarColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); - avatarColumn.getStyleClass().addAll("last-column", "avatar-column"); + avatarColumn.getStyleClass().add("avatar-column"); avatarColumn.setCellFactory( new Callback<>() { @@ -860,7 +863,7 @@ public class PendingTradesView extends ActivatableViewAndModel setChatColumnCellFactory() { chatColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); - chatColumn.getStyleClass().addAll("last-column", "avatar-column"); + chatColumn.getStyleClass().addAll("avatar-column"); chatColumn.setSortable(false); chatColumn.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index 3af019e6d0..b7ac1b1886 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -125,6 +125,7 @@ public abstract class TradeStepView extends AnchorPane { gridPane.setHgap(Layout.GRID_GAP); gridPane.setVgap(Layout.GRID_GAP); + gridPane.setPadding(new Insets(0, 0, 25, 0)); ColumnConstraints columnConstraints1 = new ColumnConstraints(); columnConstraints1.setHgrow(Priority.ALWAYS); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java index e589443301..9a328695df 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java @@ -142,7 +142,7 @@ public class BuyerStep4View extends TradeStepView { if (!DevEnv.isDevMode()) { UserThread.runAfter(() -> new Popup().headLine(Res.get("portfolio.pending.step5_buyer.tradeCompleted.headline")) .feedback(Res.get("portfolio.pending.step5_buyer.tradeCompleted.msg")) - .actionButtonTextWithGoTo("navigation.portfolio.closedTrades") + .actionButtonTextWithGoTo("portfolio.tab.history") .onAction(() -> model.dataModel.navigation.navigateTo(MainView.class, PortfolioView.class, ClosedTradesView.class)) .dontShowAgainId("tradeCompleteWithdrawCompletedInfo") .show(), 500, TimeUnit.MILLISECONDS); diff --git a/desktop/src/main/java/haveno/desktop/main/settings/SettingsView.java b/desktop/src/main/java/haveno/desktop/main/settings/SettingsView.java index ee147ad376..e2a67428d9 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/SettingsView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/SettingsView.java @@ -56,9 +56,9 @@ public class SettingsView extends ActivatableView { @Override public void initialize() { - preferencesTab.setText(Res.get("settings.tab.preferences").toUpperCase()); - networkTab.setText(Res.get("settings.tab.network").toUpperCase()); - aboutTab.setText(Res.get("settings.tab.about").toUpperCase()); + preferencesTab.setText(Res.get("settings.tab.preferences")); + networkTab.setText(Res.get("settings.tab.network")); + aboutTab.setText(Res.get("settings.tab.about")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(SettingsView.class) == 1) diff --git a/desktop/src/main/java/haveno/desktop/main/settings/about/AboutView.java b/desktop/src/main/java/haveno/desktop/main/settings/about/AboutView.java index e543837f5d..73e51a47da 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/about/AboutView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/about/AboutView.java @@ -19,6 +19,7 @@ package haveno.desktop.main.settings.about; import com.google.inject.Inject; import haveno.common.app.Version; +import haveno.core.filter.FilterManager; import haveno.core.locale.Res; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; @@ -35,16 +36,18 @@ import javafx.scene.layout.GridPane; @FxmlView public class AboutView extends ActivatableView { +private final FilterManager filterManager; private int gridRow = 0; @Inject - public AboutView() { + public AboutView(FilterManager filterManager) { super(); + this.filterManager = filterManager; } @Override public void initialize() { - addTitledGroupBg(root, gridRow, 4, Res.get("setting.about.aboutHaveno")); + addTitledGroupBg(root, gridRow, 5, Res.get("setting.about.aboutHaveno")); Label label = addLabel(root, gridRow, Res.get("setting.about.about"), Layout.TWICE_FIRST_ROW_DISTANCE); label.setWrapText(true); @@ -77,8 +80,11 @@ public class AboutView extends ActivatableView { if (isXmr) addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.feeEstimation.label"), "Monero node"); - addTitledGroupBg(root, ++gridRow, 2, Res.get("setting.about.versionDetails"), Layout.GROUP_DISTANCE); + String minVersion = filterManager.getDisableTradeBelowVersion() == null ? Res.get("shared.none") : filterManager.getDisableTradeBelowVersion(); + + addTitledGroupBg(root, ++gridRow, 3, Res.get("setting.about.versionDetails"), Layout.GROUP_DISTANCE); addCompactTopLabelTextField(root, gridRow, Res.get("setting.about.version"), Version.VERSION, Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(root, ++gridRow, Res.get("filterWindow.disableTradeBelowVersion"), minVersion); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.subsystems.label"), Res.get("setting.about.subsystems.val", diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml index bbfb4c6e05..d4dcf14085 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml @@ -53,7 +53,7 @@ - + @@ -108,6 +108,9 @@ + + + @@ -159,10 +162,7 @@ - - - + diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java index a767d032bc..4d1a6cde01 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java @@ -75,7 +75,7 @@ public class NetworkSettingsView extends ActivatableView { @FXML InputTextField xmrNodesInputTextField; @FXML - TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField, minVersionForTrading; + TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField; @FXML Label p2PPeersLabel, moneroConnectionsLabel; @FXML @@ -149,6 +149,14 @@ public class NetworkSettingsView extends ActivatableView { @Override public void initialize() { + GUIUtil.applyTableStyle(p2pPeersTableView); + GUIUtil.applyTableStyle(moneroConnectionsTableView); + + onionAddress.getStyleClass().add("label-float"); + sentDataTextField.getStyleClass().add("label-float"); + receivedDataTextField.getStyleClass().add("label-float"); + chainHeightTextField.getStyleClass().add("label-float"); + btcHeader.setText(Res.get("settings.net.xmrHeader")); p2pHeader.setText(Res.get("settings.net.p2pHeader")); onionAddress.setPromptText(Res.get("settings.net.onionAddressLabel")); @@ -160,7 +168,6 @@ public class NetworkSettingsView extends ActivatableView { useTorForXmrOnRadio.setText(Res.get("settings.net.useTorForXmrOnRadio")); moneroNodesLabel.setText(Res.get("settings.net.moneroNodesLabel")); moneroConnectionAddressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); - moneroConnectionAddressColumn.getStyleClass().add("first-column"); moneroConnectionConnectedColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.connection"))); localhostXmrNodeInfoLabel.setText(Res.get("settings.net.localhostXmrNodeInfo")); useProvidedNodesRadio.setText(Res.get("settings.net.useProvidedNodesRadio")); @@ -170,18 +177,15 @@ public class NetworkSettingsView extends ActivatableView { rescanOutputsButton.updateText(Res.get("settings.net.rescanOutputsButton")); p2PPeersLabel.setText(Res.get("settings.net.p2PPeersLabel")); onionAddressColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.onionAddressColumn"))); - onionAddressColumn.getStyleClass().add("first-column"); creationDateColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.creationDateColumn"))); connectionTypeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.connectionTypeColumn"))); sentDataTextField.setPromptText(Res.get("settings.net.sentDataLabel")); receivedDataTextField.setPromptText(Res.get("settings.net.receivedDataLabel")); chainHeightTextField.setPromptText(Res.get("settings.net.chainHeightLabel")); - minVersionForTrading.setPromptText(Res.get("filterWindow.disableTradeBelowVersion")); roundTripTimeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.roundTripTimeColumn"))); sentBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.sentBytesColumn"))); receivedBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.receivedBytesColumn"))); peerTypeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.peerTypeColumn"))); - peerTypeColumn.getStyleClass().add("last-column"); openTorSettingsButton.updateText(Res.get("settings.net.openTorSettingsButton")); // TODO: hiding button to rescan outputs until supported @@ -504,10 +508,6 @@ public class NetworkSettingsView extends ActivatableView { selectMoneroPeersToggle(); onMoneroPeersToggleSelected(false); } - - // set min version for trading - String minVersion = filterManager.getDisableTradeBelowVersion(); - minVersionForTrading.textProperty().setValue(minVersion == null ? Res.get("shared.none") : minVersion); } private boolean isPublicNodesDisabled() { diff --git a/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java index da71490b9a..e12e4d4bc0 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java @@ -102,7 +102,7 @@ import org.apache.commons.lang3.StringUtils; @FxmlView public class PreferencesView extends ActivatableViewAndModel { private final User user; - private TextField btcExplorerTextField; + private TextField xmrExplorerTextField; private ComboBox userLanguageComboBox; private ComboBox userCountryComboBox; private ComboBox preferredTradeCurrencyComboBox; @@ -220,9 +220,9 @@ public class PreferencesView extends ActivatableViewAndModel btcExp = addTextFieldWithEditButton(root, ++gridRow, Res.get("setting.preferences.explorer")); - btcExplorerTextField = btcExp.first; - editCustomBtcExplorer = btcExp.second; + Tuple2 xmrExp = addTextFieldWithEditButton(root, ++gridRow, Res.get("setting.preferences.explorer")); + xmrExplorerTextField = xmrExp.first; + editCustomBtcExplorer = xmrExp.second; // deviation deviationInputTextField = addInputTextField(root, ++gridRow, @@ -688,7 +688,7 @@ public class PreferencesView extends ActivatableViewAndModel { preferences.setBlockChainExplorer(urlWindow.getEditedBlockChainExplorer()); - btcExplorerTextField.setText(preferences.getBlockChainExplorer().name); + xmrExplorerTextField.setText(preferences.getBlockChainExplorer().name); }) .closeButtonText(Res.get("shared.cancel")) .onClose(urlWindow::hide) diff --git a/desktop/src/main/java/haveno/desktop/main/shared/ChatView.java b/desktop/src/main/java/haveno/desktop/main/shared/ChatView.java index 236f8471e9..e2ea0bb1c9 100644 --- a/desktop/src/main/java/haveno/desktop/main/shared/ChatView.java +++ b/desktop/src/main/java/haveno/desktop/main/shared/ChatView.java @@ -26,7 +26,7 @@ import haveno.desktop.main.overlays.notifications.Notification; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; - +import haveno.desktop.util.Layout; import haveno.core.locale.Res; import haveno.core.support.SupportManager; import haveno.core.support.SupportSession; @@ -43,7 +43,8 @@ import com.google.common.io.ByteStreams; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; - +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import javafx.stage.FileChooser; import javafx.scene.Node; @@ -204,6 +205,7 @@ public class ChatView extends AnchorPane { inputTextArea = new HavenoTextArea(); inputTextArea.setPrefHeight(70); inputTextArea.setWrapText(true); + inputTextArea.getStyleClass().add("input-with-border"); if (!supportSession.isDisputeAgent()) { inputTextArea.setPromptText(Res.get("support.input.prompt")); @@ -271,7 +273,7 @@ public class ChatView extends AnchorPane { ImageView arrow = new ImageView(); Label headerLabel = new AutoTooltipLabel(); Label messageLabel = new AutoTooltipLabel(); - Label copyIcon = new Label(); + Label copyLabel = new Label(); HBox attachmentsBox = new HBox(); AnchorPane messageAnchorPane = new AnchorPane(); Label statusIcon = new Label(); @@ -292,10 +294,10 @@ public class ChatView extends AnchorPane { statusIcon.getStyleClass().add("small-text"); statusInfoLabel.getStyleClass().add("small-text"); statusInfoLabel.setPadding(new Insets(3, 0, 0, 0)); - copyIcon.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); + copyLabel.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); statusHBox.setSpacing(5); statusHBox.getChildren().addAll(statusIcon, statusInfoLabel); - messageAnchorPane.getChildren().addAll(bg, arrow, headerLabel, messageLabel, copyIcon, attachmentsBox, statusHBox); + messageAnchorPane.getChildren().addAll(bg, arrow, headerLabel, messageLabel, copyLabel, attachmentsBox, statusHBox); } @Override @@ -303,7 +305,13 @@ public class ChatView extends AnchorPane { UserThread.execute(() -> { super.updateItem(message, empty); if (message != null && !empty) { - copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(messageLabel.getText())); + copyLabel.setOnMouseClicked(e -> { + Utilities.copyToClipboard(messageLabel.getText()); + Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); + Node node = (Node) e.getSource(); + UserThread.runAfter(() -> tp.hide(), 1); + tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); + }); messageLabel.setOnMouseClicked(event -> { if (2 > event.getClickCount()) { return; @@ -319,7 +327,7 @@ public class ChatView extends AnchorPane { AnchorPane.clearConstraints(headerLabel); AnchorPane.clearConstraints(arrow); AnchorPane.clearConstraints(messageLabel); - AnchorPane.clearConstraints(copyIcon); + AnchorPane.clearConstraints(copyLabel); AnchorPane.clearConstraints(statusHBox); AnchorPane.clearConstraints(attachmentsBox); @@ -328,7 +336,7 @@ public class ChatView extends AnchorPane { AnchorPane.setTopAnchor(headerLabel, 0d); AnchorPane.setBottomAnchor(arrow, bottomBorder + 5d); AnchorPane.setTopAnchor(messageLabel, 25d); - AnchorPane.setTopAnchor(copyIcon, 25d); + AnchorPane.setTopAnchor(copyLabel, 25d); AnchorPane.setBottomAnchor(attachmentsBox, bottomBorder + 10); boolean senderIsTrader = message.isSenderIsTrader(); @@ -341,20 +349,20 @@ public class ChatView extends AnchorPane { headerLabel.getStyleClass().removeAll("message-header", "my-message-header", "success-text", "highlight-static"); messageLabel.getStyleClass().removeAll("my-message", "message"); - copyIcon.getStyleClass().removeAll("my-message", "message"); + copyLabel.getStyleClass().removeAll("my-message", "message"); if (message.isSystemMessage()) { headerLabel.getStyleClass().addAll("message-header", "success-text"); bg.setId("message-bubble-green"); messageLabel.getStyleClass().add("my-message"); - copyIcon.getStyleClass().add("my-message"); + copyLabel.getStyleClass().add("my-message"); message.addWeakMessageStateListener(() -> UserThread.execute(() -> updateMsgState(message))); updateMsgState(message); } else if (isMyMsg) { headerLabel.getStyleClass().add("my-message-header"); bg.setId("message-bubble-blue"); messageLabel.getStyleClass().add("my-message"); - copyIcon.getStyleClass().add("my-message"); + copyLabel.getStyleClass().add("my-message"); if (supportSession.isClient()) arrow.setId("bubble_arrow_blue_left"); else @@ -375,7 +383,7 @@ public class ChatView extends AnchorPane { headerLabel.getStyleClass().add("message-header"); bg.setId("message-bubble-grey"); messageLabel.getStyleClass().add("message"); - copyIcon.getStyleClass().add("message"); + copyLabel.getStyleClass().add("message"); if (supportSession.isClient()) arrow.setId("bubble_arrow_grey_right"); else @@ -389,7 +397,7 @@ public class ChatView extends AnchorPane { AnchorPane.setRightAnchor(bg, border); AnchorPane.setLeftAnchor(messageLabel, padding); AnchorPane.setRightAnchor(messageLabel, msgLabelPaddingRight); - AnchorPane.setRightAnchor(copyIcon, padding); + AnchorPane.setRightAnchor(copyLabel, padding); AnchorPane.setLeftAnchor(attachmentsBox, padding); AnchorPane.setRightAnchor(attachmentsBox, padding); AnchorPane.setLeftAnchor(statusHBox, padding); @@ -400,7 +408,7 @@ public class ChatView extends AnchorPane { AnchorPane.setLeftAnchor(arrow, border); AnchorPane.setLeftAnchor(messageLabel, padding + arrowWidth); AnchorPane.setRightAnchor(messageLabel, msgLabelPaddingRight); - AnchorPane.setRightAnchor(copyIcon, padding); + AnchorPane.setRightAnchor(copyLabel, padding); AnchorPane.setLeftAnchor(attachmentsBox, padding + arrowWidth); AnchorPane.setRightAnchor(attachmentsBox, padding); AnchorPane.setRightAnchor(statusHBox, padding); @@ -411,7 +419,7 @@ public class ChatView extends AnchorPane { AnchorPane.setRightAnchor(arrow, border); AnchorPane.setLeftAnchor(messageLabel, padding); AnchorPane.setRightAnchor(messageLabel, msgLabelPaddingRight + arrowWidth); - AnchorPane.setRightAnchor(copyIcon, padding + arrowWidth); + AnchorPane.setRightAnchor(copyLabel, padding + arrowWidth); AnchorPane.setLeftAnchor(attachmentsBox, padding); AnchorPane.setRightAnchor(attachmentsBox, padding + arrowWidth); AnchorPane.setLeftAnchor(statusHBox, padding); @@ -454,8 +462,9 @@ public class ChatView extends AnchorPane { } // Need to set it here otherwise style is not correct - AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY, "16.0"); - copyIcon.getStyleClass().addAll("icon", "copy-icon-disputes"); + copyLabel.getStyleClass().addAll("icon", "copy-icon-disputes"); + MaterialDesignIconView copyIcon = new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "16.0"); + copyLabel.setGraphic(copyIcon); // TODO There are still some cell rendering issues on updates setGraphic(messageAnchorPane); @@ -465,7 +474,7 @@ public class ChatView extends AnchorPane { messageAnchorPane.prefWidthProperty().unbind(); - copyIcon.setOnMouseClicked(null); + copyLabel.setOnMouseClicked(null); messageLabel.setOnMouseClicked(null); setGraphic(null); } diff --git a/desktop/src/main/java/haveno/desktop/main/support/SupportView.java b/desktop/src/main/java/haveno/desktop/main/support/SupportView.java index 42503f315f..12a000c00b 100644 --- a/desktop/src/main/java/haveno/desktop/main/support/SupportView.java +++ b/desktop/src/main/java/haveno/desktop/main/support/SupportView.java @@ -139,9 +139,9 @@ public class SupportView extends ActivatableView { // Has to be called before loadView updateAgentTabs(); - tradersMediationDisputesTab.setText(Res.get("support.tab.mediation.support").toUpperCase()); - tradersRefundDisputesTab.setText(Res.get("support.tab.refund.support").toUpperCase()); - tradersArbitrationDisputesTab.setText(Res.get("support.tab.arbitration.support").toUpperCase()); + tradersMediationDisputesTab.setText(Res.get("support.tab.mediation.support")); + tradersRefundDisputesTab.setText(Res.get("support.tab.refund.support")); + tradersArbitrationDisputesTab.setText(Res.get("support.tab.arbitration.support")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(SupportView.class) == 1) @@ -221,16 +221,16 @@ public class SupportView extends ActivatableView { // We might get that method called before we have the map is filled in the arbitratorManager if (arbitratorTab != null) { - arbitratorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.arbitrator")).toUpperCase()); + arbitratorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.arbitrator"))); } if (signedOfferTab != null) { - signedOfferTab.setText(Res.get("support.tab.SignedOffers").toUpperCase()); + signedOfferTab.setText(Res.get("support.tab.SignedOffers")); } if (mediatorTab != null) { - mediatorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.mediator")).toUpperCase()); + mediatorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.mediator"))); } if (refundAgentTab != null) { - refundAgentTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.refundAgentForSupportStaff")).toUpperCase()); + refundAgentTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.refundAgentForSupportStaff"))); } } diff --git a/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java index 5af027c16c..87c05c997c 100644 --- a/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java @@ -223,12 +223,8 @@ public abstract class DisputeView extends ActivatableView implements @Override public void initialize() { - Label label = new AutoTooltipLabel(Res.get("support.filter")); - HBox.setMargin(label, new Insets(5, 0, 0, 0)); - HBox.setHgrow(label, Priority.NEVER); - filterTextField = new InputTextField(); - filterTextField.setPromptText(Res.get("support.filter.prompt")); + filterTextField.setPromptText(Res.get("shared.filter")); Tooltip tooltip = new Tooltip(); tooltip.setShowDelay(Duration.millis(100)); tooltip.setShowDuration(Duration.seconds(10)); @@ -298,8 +294,7 @@ public abstract class DisputeView extends ActivatableView implements HBox filterBox = new HBox(); filterBox.setSpacing(5); - filterBox.getChildren().addAll(label, - filterTextField, + filterBox.getChildren().addAll(filterTextField, alertIconLabel, spacer, reOpenButton, @@ -311,6 +306,7 @@ public abstract class DisputeView extends ActivatableView implements VBox.setVgrow(filterBox, Priority.NEVER); tableView = new TableView<>(); + GUIUtil.applyTableStyle(tableView); VBox.setVgrow(tableView, Priority.SOMETIMES); tableView.setMinHeight(150); @@ -957,7 +953,6 @@ public abstract class DisputeView extends ActivatableView implements { setMaxWidth(80); setMinWidth(65); - getStyleClass().addAll("first-column", "avatar-column"); setSortable(false); } }; @@ -1354,7 +1349,6 @@ public abstract class DisputeView extends ActivatableView implements setMinWidth(50); } }; - column.getStyleClass().add("last-column"); column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/support/dispute/agent/DisputeAgentView.java b/desktop/src/main/java/haveno/desktop/main/support/dispute/agent/DisputeAgentView.java index 10c657dce7..5901d48769 100644 --- a/desktop/src/main/java/haveno/desktop/main/support/dispute/agent/DisputeAgentView.java +++ b/desktop/src/main/java/haveno/desktop/main/support/dispute/agent/DisputeAgentView.java @@ -208,7 +208,6 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo protected void setupTable() { super.setupTable(); - stateColumn.getStyleClass().remove("last-column"); tableView.getColumns().add(getAlertColumn()); } @@ -243,7 +242,6 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo setMinWidth(50); } }; - column.getStyleClass().add("last-column"); column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( c -> new TableCell<>() { diff --git a/desktop/src/main/java/haveno/desktop/theme-dark.css b/desktop/src/main/java/haveno/desktop/theme-dark.css index d045e3b5b0..1e56814261 100644 --- a/desktop/src/main/java/haveno/desktop/theme-dark.css +++ b/desktop/src/main/java/haveno/desktop/theme-dark.css @@ -20,10 +20,10 @@ /* haveno main colors */ -bs-color-primary: #0b65da; -bs-color-primary-dark: #0c59bd; - -bs-text-color: #dadada; - -bs-background-color: #29292a; - -bs-background-gray: #2B2B2B; - -bs-content-background-gray: #1F1F1F; + -bs-text-color: white; + -bs-background-color: black; + -bs-background-gray: transparent; + -bs-content-background-gray: black; /* fifty shades of gray */ -bs-color-gray-13: #bbb; @@ -43,7 +43,19 @@ -bs-color-gray-bbb: #5a5a5a; -bs-color-gray-aaa: #29292a; -bs-color-gray-fafa: #0a0a0a; - -bs-color-gray-background: #1F1F1F; + -bs-color-gray-background: black; + -bs-color-background-popup: rgb(38, 38, 38); + -bs-color-background-popup-blur: rgb(9, 9, 9); + -bs-color-background-popup-input: rgb(9, 9, 9); + -bs-color-background-form-field: rgb(26, 26, 26); + -bs-color-background-form-field-readonly: rgb(18, 18, 18); + -bs-color-border-form-field: rgb(65, 65, 65); + -bs-color-background-pane: rgb(15, 15, 15); + -bs-color-background-row-even: rgb(19, 19, 19); + -bs-color-background-row-odd: rgb(9, 9, 9); + -bs-color-table-cell-dim: -bs-color-gray-ccc; + -bs-text-color-dim1: rgb(87, 87, 87); + -bs-text-color-dim2: rgb(130, 130, 130); /* lesser used colors */ -bs-color-blue-5: #0a4576; @@ -70,11 +82,15 @@ -bs-rd-nav-border: #535353; -bs-rd-nav-primary-border: rgba(0, 0, 0, 0); -bs-rd-nav-border-color: rgba(255, 255, 255, 0.1); - -bs-rd-nav-background: #141414; - -bs-rd-nav-primary-background: rgba(255, 255, 255, 0.015); - -bs-rd-nav-selected: #fff; - -bs-rd-nav-deselected: rgba(255, 255, 255, 0.45); - -bs-rd-nav-button-hover: rgba(255, 255, 255, 0.03); + -bs-rd-nav-background: rgb(15, 15, 15); + -bs-rd-nav-primary-background: rgb(15, 15, 15); + -bs-rd-nav-selected: black; + -bs-rd-nav-deselected: rgba(255, 255, 255, 1); + -bs-rd-nav-secondary-selected: -fx-accent; + -bs-rd-nav-secondary-deselected: -bs-rd-font-light; + -bs-rd-nav-button-hover: derive(-bs-rd-nav-background, 10%); + -bs-rd-nav-primary-button-hover: derive(-bs-rd-nav-primary-background, 10%); + -bs-rd-nav-hover-text: black; -bs-content-pane-bg-top: #212121; -bs-rd-tab-border: rgba(255, 255, 255, 0.00); @@ -90,7 +106,7 @@ -bs-footer-pane-text: #cfcecf; -bs-footer-pane-line: #29292a; - -bs-rd-font-balance: #bbbbbb; + -bs-rd-font-balance: white; -bs-rd-font-dark-gray: #d4d4d4; -bs-rd-font-dark: #cccccc; -bs-rd-font-light: #b4b4b4; @@ -99,28 +115,28 @@ -bs-rd-font-confirmation-label: #504f52; -bs-rd-font-balance-label: #999999; - -bs-text-color-transparent-dark: rgba(29, 29, 33, 0.54); + -bs-text-color-dropshadow: rgba(45, 45, 49, .75); + -bs-text-color-dropshadow-light-mode: transparent; -bs-text-color-transparent: rgba(29, 29, 33, 0.2); -bs-color-gray-line: #504f52; -bs-rd-separator: #1F1F1F; - -bs-rd-separator-dark: #1F1F1F; + -bs-rd-separator-dark: rgb(255, 255, 255, 0.1); -bs-rd-error-red: #d83431; -bs-rd-error-field: #521C1C; -bs-rd-message-bubble: #0086c6; -bs-rd-tooltip-truncated: #afaeb0; - -bs-toggle-selected: #25b135; + /*-bs-toggle-selected: rgb(12, 89, 189);*/ + -bs-toggle-selected: rgb(12, 89, 190); -bs-warning: #db6300; - -bs-buy: #006600; - -bs-buy-focus: black; - -bs-buy-hover: #237b2d; - -bs-buy-transparent: rgba(46, 163, 60, 0.3); - -bs-sell: #660000; - -bs-sell-focus: #090202; - -bs-sell-hover: #b42522; - -bs-sell-transparent: rgba(216, 52, 49, 0.3); - -bs-volume-transparent: rgba(37, 177, 54, 0.5); + -bs-buy: rgb(80, 180, 90); + -bs-buy-focus: derive(-bs-buy, -50%); + -bs-buy-hover: derive(-bs-buy, -10%); + -bs-sell: rgb(213, 63, 46); + -bs-sell-focus: derive(-bs-sell, -50%); + -bs-sell-hover: derive(-bs-sell, -10%); + -bs-volume-transparent: -bs-buy; -bs-candle-stick-average-line: rgba(21, 188, 29, 0.8); -bs-candle-stick-loss: #ee6563; -bs-candle-stick-won: #15bc1d; @@ -154,7 +170,7 @@ /* Monero orange color code */ -xmr-orange: #f26822; - -bs-support-chat-background: #cccccc; + -bs-support-chat-background: rgb(125, 125, 125); } /* table view */ @@ -164,7 +180,7 @@ } .table-view .column-header { - -fx-background-color: derive(-bs-background-color,-50%); + -fx-background-color: -bs-color-background-pane; -fx-border-width: 0; } @@ -173,21 +189,31 @@ -fx-border-width: 0; } +/** These must be set to override default styles */ .table-view .table-row-cell:even .table-cell { - -fx-background-color: derive(-bs-background-color, -5%); - -fx-border-color: derive(-bs-background-color, -5%); + -fx-background-color: -bs-color-background-row-even; + -fx-border-color: -bs-color-background-row-even; } - .table-view .table-row-cell:odd .table-cell { - -fx-background-color: derive(-bs-background-color,-30%); - -fx-border-color: derive(-bs-background-color,-30%); + -fx-background-color: -bs-color-background-row-odd; + -fx-border-color: -bs-color-background-row-odd; } - .table-view .table-row-cell:selected .table-cell { -fx-background: -fx-accent; -fx-background-color: -fx-selection-bar; -fx-border-color: -fx-selection-bar; } +.table-view .table-row-cell:selected .table-cell, +.table-view .table-row-cell:selected .table-cell .label, +.table-view .table-row-cell:selected .table-cell .text { + -fx-text-fill: -fx-dark-text-color; +} +.table-view .table-row-cell:selected .table-cell .hyperlink, +.table-view .table-row-cell:selected .table-cell .hyperlink .text, +.table-view .table-row-cell:selected .table-cell .hyperlink-with-icon, +.table-view .table-row-cell:selected .table-cell .hyperlink-with-icon .text { + -fx-fill: -fx-dark-text-color; +} .table-row-cell { -fx-border-color: -bs-background-color; @@ -208,35 +234,49 @@ -fx-background-color: -bs-tab-content-area; } -.jfx-tab-pane .viewport { - -fx-background-color: -bs-viewport-background; + +.jfx-tab-pane .headers-region .tab:selected .tab-container .tab-label { + -fx-text-fill: white; } -.jfx-tab-pane .tab-header-background { - -fx-background-color: derive(-bs-color-gray-background, -20%); +.nav-secondary-button:selected .text { + -fx-fill: white; +} + +.jfx-tab-pane .headers-region > .tab > .jfx-rippler { + -jfx-rippler-fill: none; +} + +.jfx-tab-pane .viewport { + -fx-background-color: -bs-viewport-background; } /* text field */ .jfx-text-field, .jfx-text-area, .jfx-combo-box, .jfx-combo-box > .list-cell { - -fx-background-color: derive(-bs-background-color, 15%); -fx-prompt-text-fill: -bs-color-gray-6; -fx-text-fill: -bs-color-gray-12; } -.jfx-text-area:readonly, .jfx-text-field:readonly, +.jfx-text-area:readonly, +.jfx-text-field:readonly, .hyperlink-with-icon { -fx-background: -bs-background-color; - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-form-field-readonly; -fx-prompt-text-fill: -bs-color-gray-2; -fx-text-fill: -bs-color-gray-3; } + +.popover > .content .text-field { + -fx-background-color: -bs-color-background-form-field !important; +} + .jfx-combo-box > .text, .jfx-text-field-top-label, .jfx-text-area-top-label { -fx-text-fill: -bs-color-gray-11; } -.input-with-border { +.offer-input { -fx-border-color: -bs-color-gray-2; -fx-border-width: 0 0 10 0; } @@ -254,11 +294,6 @@ -fx-text-fill: -fx-dark-text-color; } -.chart-pane, .chart-plot-background, -#charts .chart-plot-background, -#charts-dao .chart-plot-background { - -fx-background-color: transparent; -} .axis:top, .axis:right, .axis:bottom, .axis:left { -fx-border-color: transparent transparent transparent transparent; } @@ -332,7 +367,7 @@ } .combo-box-popup > .list-view{ - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-pane; } .jfx-combo-box > .arrow-button > .arrow { @@ -352,7 +387,6 @@ } .list-view .list-cell:odd, .list-view .list-cell:even { - -fx-background-color: -bs-background-color; -fx-border-width: 0; } @@ -371,18 +405,6 @@ -fx-border-width: 0; } -.jfx-text-field { - -fx-background-radius: 4; -} - -.jfx-text-field > .input-line { - -fx-translate-x: 0; -} - -.jfx-text-field > .input-focused-line { - -fx-translate-x: 0; -} - .jfx-text-field-top-label { -fx-text-fill: -bs-color-gray-dim; } @@ -394,14 +416,13 @@ -fx-background-color: derive(-bs-background-color, 15%); } .jfx-combo-box:error, -.jfx-text-field:error{ +.jfx-text-field:error { -fx-text-fill: -bs-rd-error-red; -fx-background-color: -bs-rd-error-field; } .jfx-combo-box:error:focused, .jfx-text-field:error:focused{ - -fx-text-fill: -bs-rd-error-red; -fx-background-color: derive(-bs-rd-error-field, -5%); } @@ -417,11 +438,7 @@ -jfx-disable-animation: true; } -.jfx-password-field { - -fx-background-color: derive(-bs-background-color, -15%); -} - -.input-with-border { +.offer-input { -fx-border-width: 0; -fx-border-color: -bs-background-color; } @@ -448,11 +465,6 @@ -jfx-disable-animation: true; } -.top-navigation { - -fx-border-width: 0 0 0 0; - -fx-padding: 0 7 0 0; -} - .nav-price-balance { -fx-effect: null; } @@ -462,37 +474,17 @@ } .nav-button:selected { - -fx-background-color: derive(-bs-color-primary-dark, -10%); + -fx-background-color: white; -fx-effect: null; } -.nav-button:hover { - -fx-background-color: -bs-rd-nav-button-hover; -} - .nav-primary .nav-button:selected { - -fx-background-color: derive(-bs-color-primary-dark, -5%); + -fx-background-color: derive(white, -5%); } .table-view { -fx-border-color: transparent; } -.table-view .table-cell { - -fx-padding: 6 0 4 0; - -fx-text-fill: -bs-text-color; -} -.table-view .table-cell.last-column { - -fx-padding: 6 10 4 0; -} - -.table-view .table-cell.last-column.avatar-column { - -fx-padding: 6 0 4 0; -} - -.table-view .table-cell.first-column { - -fx-padding: 6 0 4 10; -} - .jfx-tab-pane .headers-region .tab .tab-container .tab-label { -fx-cursor: hand; -jfx-disable-animation: true; @@ -559,12 +551,95 @@ } .toggle-button-no-slider { - -fx-focus-color: transparent; - -fx-faint-focus-color: transparent; - -fx-background-radius: 3; - -fx-background-insets: 0, 1; + -fx-background-color: -bs-color-background-form-field; } .toggle-button-no-slider:selected { + -fx-text-fill: white; + -fx-background-color: -bs-color-gray-ccc; + -fx-border-color: -bs-color-gray-ccc; + -fx-border-width: 1px; +} + +.toggle-button-no-slider:hover { + -fx-cursor: hand; + -fx-background-color: -bs-color-gray-ddd; + -fx-border-color: -bs-color-gray-ddd; +} + +.toggle-button-no-slider:selected:hover { + -fx-cursor: hand; + -fx-background-color: -bs-color-gray-3; + -fx-border-color: -bs-color-gray-3; +} + +.toggle-button-no-slider:pressed, .toggle-button-no-slider:selected:hover:pressed { -fx-background-color: -bs-color-gray-bbb; } + +#image-logo-splash { + -fx-image: url("../../images/logo_splash_dark.png"); +} + +#image-logo-splash-testnet { + -fx-image: url("../../images/logo_splash_testnet_dark.png"); +} + +#image-logo-landscape { + -fx-image: url("../../images/logo_landscape_dark.png"); +} + +.table-view .placeholder { + -fx-background-color: -bs-color-background-pane; +} + +#charts .default-color0.chart-series-area-fill { + -fx-fill: linear-gradient(to bottom, + rgba(80, 181, 90, 0.45) 0%, + rgba(80, 181, 90, 0.0) 100% + ); +} + +#charts .default-color1.chart-series-area-fill { + -fx-fill: linear-gradient(to bottom, + rgba(213, 63, 46, 0.45) 0%, + rgba(213, 63, 46, 0.0) 100% + ); +} + +.table-view .table-row-cell .label { + -fx-text-fill: -bs-text-color; +} + +.table-view.non-interactive-table .table-cell, +.table-view.non-interactive-table .table-cell .label, +.table-view.non-interactive-table .label, +.table-view.non-interactive-table .text, +.table-view.non-interactive-table .hyperlink, +.table-view.non-interactive-table .hyperlink-with-icon, +.table-view.non-interactive-table .table-row-cell .hyperlink .text { + -fx-text-fill: -bs-color-gray-dim; +} + +.table-view.non-interactive-table .hyperlink, +.table-view.non-interactive-table .hyperlink-with-icon, +.table-view.non-interactive-table .table-row-cell .hyperlink .text { + -fx-fill: -bs-color-gray-dim; +} + +.table-view.non-interactive-table .table-cell.highlight-text, +.table-view.non-interactive-table .table-cell.highlight-text .label, +.table-view.non-interactive-table .table-cell.highlight-text .text, +.table-view.non-interactive-table .table-cell.highlight-text .hyperlink, +.table-view.non-interactive-table .table-cell.highlight-text .hyperlink .text { + -fx-text-fill: -fx-dark-text-color; +} + +/* Match specificity to override. */ +.table-view.non-interactive-table .table-cell.highlight-text .zero-decimals { + -fx-text-fill: -bs-color-gray-3; +} + +.regular-text-color { + -fx-text-fill: -bs-text-color; +} diff --git a/desktop/src/main/java/haveno/desktop/theme-light.css b/desktop/src/main/java/haveno/desktop/theme-light.css index 7605eb1819..160a21e3c3 100644 --- a/desktop/src/main/java/haveno/desktop/theme-light.css +++ b/desktop/src/main/java/haveno/desktop/theme-light.css @@ -41,12 +41,17 @@ -bs-rd-green: #0b65da; -bs-rd-green-dark: #3EA34A; -bs-rd-nav-selected: #0b65da; - -bs-rd-nav-deselected: rgba(255, 255, 255, 0.75); - -bs-rd-nav-background: #0c59bd; + -bs-rd-nav-deselected: rgba(255, 255, 255, 1); + -bs-rd-nav-secondary-selected: -fx-accent; + -bs-rd-nav-secondary-deselected: -bs-rd-font-light; + -bs-rd-nav-background: #0b65da; -bs-rd-nav-primary-background: #0b65da; + -bs-rd-nav-button-hover: derive(-bs-rd-nav-background, 10%); + -bs-rd-nav-primary-button-hover: derive(-bs-rd-nav-primary-background, 10%); -bs-rd-nav-primary-border: #0B65DA; -bs-rd-nav-border: #535353; -bs-rd-nav-border-color: rgba(255, 255, 255, 0.31); + -bs-rd-nav-hover-text: white; -bs-rd-tab-border: #e2e0e0; -bs-tab-content-area: #ffffff; -bs-color-gray-background: #f2f2f2; @@ -58,32 +63,31 @@ -bs-footer-pane-background: #dddddd; -bs-footer-pane-text: #4b4b4b; -bs-footer-pane-line: #bbb; - -bs-rd-font-balance: #4f4f4f; + -bs-rd-font-balance: white; -bs-rd-font-dark-gray: #3c3c3c; -bs-rd-font-dark: #4b4b4b; -bs-rd-font-light: #8d8d8d; -bs-rd-font-lighter: #a7a7a7; -bs-rd-font-confirmation-label: #504f52; - -bs-rd-font-balance-label: #8e8e8e; - -bs-text-color-transparent-dark: rgba(0, 0, 0, 0.54); + -bs-rd-font-balance-label: #bbbbbb; + -bs-text-color-dropshadow: rgba(0, 0, 0, 0.54); + -bs-text-color-dropshadow-light-mode: rgba(0, 0, 0, 0.54); -bs-text-color-transparent: rgba(0, 0, 0, 0.2); -bs-color-gray-line: #979797; -bs-rd-separator: #dbdbdb; - -bs-rd-separator-dark: #d5e0d6; + -bs-rd-separator-dark: rgb(255, 255, 255, 0.1); -bs-rd-error-red: #dd0000; -bs-rd-message-bubble: #0086c6; -bs-toggle-selected: #7b7b7b; -bs-rd-tooltip-truncated: #0a0a0a; -bs-warning: #ff8a2b; - -bs-buy: #3ea34a; + -bs-buy: rgb(80, 180, 90); -bs-buy-focus: derive(-bs-buy, -50%); -bs-buy-hover: derive(-bs-buy, -10%); - -bs-buy-transparent: rgba(62, 163, 74, 0.3); - -bs-sell: #d73030; + -bs-sell: rgb(213, 63, 46); -bs-sell-focus: derive(-bs-sell, -50%); -bs-sell-hover: derive(-bs-sell, -10%); - -bs-sell-transparent: rgba(215, 48, 48, 0.3); - -bs-volume-transparent: rgba(37, 177, 53, 0.3); + -bs-volume-transparent: -bs-buy; -bs-candle-stick-average-line: -bs-rd-green; -bs-candle-stick-loss: #fe3001; -bs-candle-stick-won: #20b221; @@ -104,6 +108,19 @@ -bs-prompt-text: -fx-control-inner-background; -bs-soft-red: #aa4c3b; -bs-turquoise-light: #11eeee; + -bs-color-border-form-field: -bs-background-gray; + -bs-color-background-form-field-readonly: -bs-color-gray-1; + -bs-color-background-pane: -bs-background-color; + -bs-color-background-row-even: -bs-color-background-pane; + -bs-color-background-row-odd: derive(-bs-color-background-pane, -6%); + -bs-color-table-cell-dim: -bs-color-gray-ccc; + -bs-color-background-popup: white; + -bs-color-background-popup-blur: white; + -bs-color-background-popup-input: -bs-color-gray-background; + -bs-color-background-form-field: white; + -bs-text-color-dim1: black; + -bs-text-color-dim2: black; + /* Monero orange color code */ -xmr-orange: #f26822; @@ -126,7 +143,33 @@ -fx-background-color: -bs-color-gray-3; } -.toggle-button-no-slider { - -fx-focus-color: transparent; - -fx-faint-focus-color: transparent; +#image-logo-splash { + -fx-image: url("../../images/logo_splash_light.png"); +} + +#image-logo-splash-testnet { + -fx-image: url("../../images/logo_splash_testnet_light.png"); +} + +#image-logo-landscape { + -fx-image: url("../../images/logo_landscape_light.png"); +} + +#charts .default-color0.chart-series-area-fill { + -fx-fill: linear-gradient(to bottom, + rgba(62, 163, 74, 0.45) 0%, + rgba(62, 163, 74, 0.0) 100% + ); +} + +#charts .default-color1.chart-series-area-fill { + -fx-fill: linear-gradient(to bottom, + rgba(215, 48, 48, 0.45) 0%, + rgba(215, 48, 48, 0.0) 100% + ); +} + +/* All inputs have border in light mode. */ +.jfx-combo-box, .jfx-text-field, .jfx-text-area, .jfx-password-field { + -fx-border-color: -bs-color-border-form-field; } diff --git a/desktop/src/main/java/haveno/desktop/util/CssTheme.java b/desktop/src/main/java/haveno/desktop/util/CssTheme.java index 1e1c547607..4648d07eeb 100644 --- a/desktop/src/main/java/haveno/desktop/util/CssTheme.java +++ b/desktop/src/main/java/haveno/desktop/util/CssTheme.java @@ -58,6 +58,10 @@ public class CssTheme { scene.getStylesheets().add(cssThemeFolder + "theme-dev.css"); } + public static int getCurrentTheme() { + return currentCSSTheme; + } + public static boolean isDarkTheme() { return currentCSSTheme == CSS_THEME_DARK; } diff --git a/desktop/src/main/java/haveno/desktop/util/CurrencyList.java b/desktop/src/main/java/haveno/desktop/util/CurrencyList.java index 3e68ccf876..88bef5ab1c 100644 --- a/desktop/src/main/java/haveno/desktop/util/CurrencyList.java +++ b/desktop/src/main/java/haveno/desktop/util/CurrencyList.java @@ -18,6 +18,8 @@ package haveno.desktop.util; import com.google.common.collect.Lists; + +import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.user.Preferences; import javafx.collections.FXCollections; @@ -92,14 +94,13 @@ public class CurrencyList { } private Comparator getComparator() { - Comparator result; if (preferences.isSortMarketCurrenciesNumerically()) { - Comparator byCount = Comparator.comparingInt(left -> left.numTrades); - result = byCount.reversed(); + return Comparator + .comparingInt((CurrencyListItem item) -> item.numTrades).reversed() + .thenComparing(item -> CurrencyUtil.isCryptoCurrency(item.tradeCurrency.getCode()) ? item.tradeCurrency.getName() : item.tradeCurrency.getCode()); } else { - result = Comparator.comparing(item -> item.tradeCurrency); + return Comparator.comparing(item -> CurrencyUtil.isCryptoCurrency(item.tradeCurrency.getCode()) ? item.tradeCurrency.getName() : item.tradeCurrency.getCode()); } - return result; } private Map countTrades(List currencies) { diff --git a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java index ae3e7ed266..dbee100f06 100644 --- a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java @@ -135,6 +135,24 @@ public class FormBuilder { return titledGroupBg; } + /////////////////////////////////////////////////////////////////////////////////////////// + // Divider + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Region addSeparator(GridPane gridPane, int rowIndex) { + Region separator = new Region(); + separator.getStyleClass().add("grid-pane-separator"); + separator.setPrefHeight(1); + separator.setMinHeight(1); + separator.setMaxHeight(1); + GridPane.setRowIndex(separator, rowIndex); + GridPane.setColumnIndex(separator, 0); + GridPane.setColumnSpan(separator, 2); + gridPane.getChildren().add(separator); + separator.setPrefHeight(1); + GridPane.setMargin(separator, new Insets(0, 0, 3, 0)); + return separator; + } /////////////////////////////////////////////////////////////////////////////////////////// // Label @@ -361,7 +379,7 @@ public class FormBuilder { textField.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); Button button = new AutoTooltipButton("..."); - button.setStyle("-fx-min-width: 26; -fx-pref-height: 26; -fx-padding: 0 0 10 0; -fx-background-color: -fx-background;"); + button.setStyle("-fx-min-width: 32; -fx-padding: 0 0 10 0; -fx-background-color: -fx-background;"); button.managedProperty().bind(button.visibleProperty()); HBox hbox = new HBox(textField, button); @@ -369,6 +387,7 @@ public class FormBuilder { hbox.setSpacing(8); VBox vbox = getTopLabelVBox(0); + vbox.setSpacing(2); vbox.getChildren().addAll(getTopLabel(title), hbox); gridPane.getChildren().add(vbox); @@ -490,6 +509,7 @@ public class FormBuilder { GridPane.setColumnIndex(textArea, 1); GridPane.setMargin(label, new Insets(top, 0, 0, 0)); GridPane.setHalignment(label, HPos.LEFT); + GridPane.setValignment(label, VPos.TOP); GridPane.setMargin(textArea, new Insets(top, 0, 0, 0)); return new Tuple2<>(label, textArea); @@ -617,6 +637,7 @@ public class FormBuilder { JFXTextArea textArea = new HavenoTextArea(); textArea.setPromptText(prompt); textArea.setLabelFloat(true); + textArea.getStyleClass().add("label-float"); textArea.setWrapText(true); GridPane.setRowIndex(textArea, rowIndex); @@ -805,9 +826,9 @@ public class FormBuilder { } public static InputTextField addInputTextField(GridPane gridPane, int rowIndex, String title, double top) { - InputTextField inputTextField = new InputTextField(); inputTextField.setLabelFloat(true); + inputTextField.getStyleClass().add("label-float"); inputTextField.setPromptText(title); GridPane.setRowIndex(inputTextField, rowIndex); GridPane.setColumnIndex(inputTextField, 0); @@ -884,6 +905,8 @@ public class FormBuilder { public static PasswordTextField addPasswordTextField(GridPane gridPane, int rowIndex, String title, double top) { PasswordTextField passwordField = new PasswordTextField(); + passwordField.getStyleClass().addAll("label-float"); + GUIUtil.applyFilledStyle(passwordField); passwordField.setPromptText(title); GridPane.setRowIndex(passwordField, rowIndex); GridPane.setColumnIndex(passwordField, 0); @@ -1006,8 +1029,10 @@ public class FormBuilder { InputTextField inputTextField1 = new InputTextField(); inputTextField1.setPromptText(title1); inputTextField1.setLabelFloat(true); + inputTextField1.getStyleClass().add("label-float"); InputTextField inputTextField2 = new InputTextField(); inputTextField2.setLabelFloat(true); + inputTextField2.getStyleClass().add("label-float"); inputTextField2.setPromptText(title2); HBox hBox = new HBox(); @@ -1228,6 +1253,7 @@ public class FormBuilder { public static ComboBox addComboBox(GridPane gridPane, int rowIndex, int top) { final JFXComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); GridPane.setRowIndex(comboBox, rowIndex); GridPane.setMargin(comboBox, new Insets(top, 0, 0, 0)); @@ -1264,7 +1290,9 @@ public class FormBuilder { VBox vBox = getTopLabelVBox(top); final JFXComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); comboBox.setPromptText(prompt); + comboBox.setPadding(new Insets(top, 0, 0, 12)); vBox.getChildren().addAll(label, comboBox); @@ -1389,7 +1417,9 @@ public class FormBuilder { public static ComboBox addComboBox(GridPane gridPane, int rowIndex, String title, double top) { JFXComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); comboBox.setLabelFloat(true); + comboBox.getStyleClass().add("label-float"); comboBox.setPromptText(title); comboBox.setMaxWidth(Double.MAX_VALUE); @@ -1399,6 +1429,7 @@ public class FormBuilder { GridPane.setRowIndex(comboBox, rowIndex); GridPane.setColumnIndex(comboBox, 0); + comboBox.setPadding(new Insets(0, 0, 0, 12)); GridPane.setMargin(comboBox, new Insets(top + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(comboBox); @@ -1407,7 +1438,9 @@ public class FormBuilder { public static AutocompleteComboBox addAutocompleteComboBox(GridPane gridPane, int rowIndex, String title, double top) { var comboBox = new AutocompleteComboBox(); + GUIUtil.applyFilledStyle(comboBox); comboBox.setLabelFloat(true); + comboBox.getStyleClass().add("label-float"); comboBox.setPromptText(title); comboBox.setMaxWidth(Double.MAX_VALUE); @@ -1469,6 +1502,7 @@ public class FormBuilder { AutocompleteComboBox comboBox = new AutocompleteComboBox<>(); comboBox.setPromptText(titleCombobox); comboBox.setLabelFloat(true); + comboBox.getStyleClass().add("label-float"); topLabelVBox2.getChildren().addAll(topLabel2, comboBox); hBox.getChildren().addAll(topLabelVBox1, topLabelVBox2); @@ -1498,7 +1532,9 @@ public class FormBuilder { hBox.setSpacing(10); ComboBox comboBox1 = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox1); ComboBox comboBox2 = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox2); hBox.getChildren().addAll(comboBox1, comboBox2); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, top); @@ -1526,8 +1562,10 @@ public class FormBuilder { hBox.setSpacing(10); JFXComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); comboBox.setPromptText(titleCombobox); comboBox.setLabelFloat(true); + comboBox.getStyleClass().add("label-float"); TextField textField = new HavenoTextField(); @@ -1570,6 +1608,7 @@ public class FormBuilder { button.setDefaultButton(true); ComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); hBox.getChildren().addAll(comboBox, button); @@ -1604,6 +1643,7 @@ public class FormBuilder { hBox.setSpacing(10); ComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); TextField textField = new TextField(textFieldText); textField.setEditable(false); textField.setMouseTransparent(true); @@ -1797,6 +1837,7 @@ public class FormBuilder { return new Tuple2<>(label, textFieldWithCopyIcon); } + /////////////////////////////////////////////////////////////////////////////////////////// // Label + AddressTextField /////////////////////////////////////////////////////////////////////////////////////////// @@ -2181,11 +2222,13 @@ public class FormBuilder { Label label = new AutoTooltipLabel(Res.getBaseCurrencyCode()); label.getStyleClass().add("input-label"); + HBox.setMargin(label, new Insets(0, 8, 0, 0)); HBox box = new HBox(); HBox.setHgrow(input, Priority.ALWAYS); input.setMaxWidth(Double.MAX_VALUE); - box.getStyleClass().add("input-with-border"); + box.setAlignment(Pos.CENTER_LEFT); + box.getStyleClass().add("offer-input"); box.getChildren().addAll(input, label); return new Tuple3<>(box, input, label); } @@ -2197,11 +2240,13 @@ public class FormBuilder { Label label = new AutoTooltipLabel(Res.getBaseCurrencyCode()); label.getStyleClass().add("input-label"); + HBox.setMargin(label, new Insets(0, 8, 0, 0)); HBox box = new HBox(); HBox.setHgrow(infoInputTextField, Priority.ALWAYS); infoInputTextField.setMaxWidth(Double.MAX_VALUE); - box.getStyleClass().add("input-with-border"); + box.setAlignment(Pos.CENTER_LEFT); + box.getStyleClass().add("offer-input"); box.getChildren().addAll(infoInputTextField, label); return new Tuple3<>(box, infoInputTextField, label); } @@ -2444,6 +2489,7 @@ public class FormBuilder { if (groupStyle != null) titledGroupBg.getStyleClass().add(groupStyle); TableView tableView = new TableView<>(); + GUIUtil.applyTableStyle(tableView); GridPane.setRowIndex(tableView, rowIndex); GridPane.setMargin(tableView, new Insets(top + 30, -10, 5, -10)); gridPane.getChildren().add(tableView); diff --git a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java index e825e29e6e..3097780c1a 100644 --- a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java @@ -25,7 +25,10 @@ import com.googlecode.jcsv.CSVStrategy; import com.googlecode.jcsv.writer.CSVEntryConverter; import com.googlecode.jcsv.writer.CSVWriter; import com.googlecode.jcsv.writer.internal.CSVWriterBuilder; + +import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.CorruptedStorageFileHandler; @@ -64,9 +67,17 @@ import haveno.desktop.main.account.AccountView; import haveno.desktop.main.account.content.traditionalaccounts.TraditionalAccountsView; import haveno.desktop.main.overlays.popups.Popup; import haveno.network.p2p.P2PService; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; import javafx.geometry.HPos; +import javafx.geometry.Insets; import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.ComboBox; @@ -76,14 +87,21 @@ import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.ScrollBar; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.Rectangle; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Modality; @@ -131,7 +149,9 @@ public class GUIUtil { public final static int NUM_DECIMALS_PRECISE = 7; public final static int AMOUNT_DECIMALS_WITH_ZEROS = 3; public final static int AMOUNT_DECIMALS = 4; + public static final double NUM_OFFERS_TRANSLATE_X = -13.0; + public static final boolean disablePaymentUriLabel = true; // universally disable payment uri labels, allowing bigger xmr logo overlays private static Preferences preferences; public static void setPreferences(Preferences preferences) { @@ -304,30 +324,42 @@ public class GUIUtil { HBox box = new HBox(); box.setSpacing(20); - Label currencyType = new AutoTooltipLabel( - CurrencyUtil.isTraditionalCurrency(code) ? Res.get("shared.traditional") : Res.get("shared.crypto")); + box.setAlignment(Pos.CENTER_LEFT); + Label label1 = new AutoTooltipLabel(getCurrencyType(code)); + label1.getStyleClass().add("currency-label-small"); + Label label2 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? item.tradeCurrency.getNameAndCode() : code); + label2.getStyleClass().add("currency-label"); + Label label3 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? "" : item.tradeCurrency.getName()); + if (!CurrencyUtil.isCryptoCurrency(code)) label3.getStyleClass().add("currency-label"); + Label label4 = new AutoTooltipLabel(); - currencyType.getStyleClass().add("currency-label-small"); - Label currency = new AutoTooltipLabel(code); - currency.getStyleClass().add("currency-label"); - Label offers = new AutoTooltipLabel(item.tradeCurrency.getName()); - offers.getStyleClass().add("currency-label"); - - box.getChildren().addAll(currencyType, currency, offers); + box.getChildren().addAll(label1, label2, label3); + if (!CurrencyUtil.isCryptoCurrency(code)) box.getChildren().add(label4); switch (code) { case GUIUtil.SHOW_ALL_FLAG: - currencyType.setText(Res.get("shared.all")); - currency.setText(Res.get("list.currency.showAll")); + label1.setText(Res.get("shared.all")); + label2.setText(Res.get("list.currency.showAll")); break; case GUIUtil.EDIT_FLAG: - currencyType.setText(Res.get("shared.edit")); - currency.setText(Res.get("list.currency.editList")); + label1.setText(Res.get("shared.edit")); + label2.setText(Res.get("list.currency.editList")); break; default: - if (preferences.isSortMarketCurrenciesNumerically()) { - offers.setText(offers.getText() + " (" + item.numTrades + " " + - (item.numTrades == 1 ? postFixSingle : postFixMulti) + ")"); + + // use icons for crypto + if (CurrencyUtil.isCryptoCurrency(code)) { + label1.setText(""); + StackPane iconWrapper = new StackPane(getCurrencyIcon(code)); // TODO: icon must be wrapped in StackPane for reliable rendering on linux + label1.setGraphic(iconWrapper); + } + + if (preferences.isSortMarketCurrenciesNumerically() && item.numTrades > 0) { + boolean isCrypto = CurrencyUtil.isCryptoCurrency(code); + Label offersTarget = isCrypto ? label3 : label4; + HBox.setMargin(offersTarget, new Insets(0, 0, 0, NUM_OFFERS_TRANSLATE_X)); + offersTarget.getStyleClass().add("offer-label"); + offersTarget.setText(item.numTrades + " " + (item.numTrades == 1 ? postFixSingle : postFixMulti)); } } @@ -430,33 +462,47 @@ public class GUIUtil { HBox box = new HBox(); box.setSpacing(20); - Label currencyType = new AutoTooltipLabel( - CurrencyUtil.isTraditionalCurrency(item.getCode()) ? Res.get("shared.traditional") : Res.get("shared.crypto")); + box.setAlignment(Pos.CENTER_LEFT); - currencyType.getStyleClass().add("currency-label-small"); - Label currency = new AutoTooltipLabel(item.getCode()); - currency.getStyleClass().add("currency-label"); - Label offers = new AutoTooltipLabel(item.getName()); - offers.getStyleClass().add("currency-label"); - - box.getChildren().addAll(currencyType, currency, offers); + Label label1 = new AutoTooltipLabel(getCurrencyType(item.getCode())); + label1.getStyleClass().add("currency-label-small"); + Label label2 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? item.getNameAndCode() : code); + label2.getStyleClass().add("currency-label"); + Label label3 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? "" : item.getName()); + if (!CurrencyUtil.isCryptoCurrency(code)) label3.getStyleClass().add("currency-label"); + Label label4 = new AutoTooltipLabel(); Optional offerCountOptional = Optional.ofNullable(offerCounts.get(code)); switch (code) { case GUIUtil.SHOW_ALL_FLAG: - currencyType.setText(Res.get("shared.all")); - currency.setText(Res.get("list.currency.showAll")); + label1.setText(Res.get("shared.all")); + label2.setText(Res.get("list.currency.showAll")); break; case GUIUtil.EDIT_FLAG: - currencyType.setText(Res.get("shared.edit")); - currency.setText(Res.get("list.currency.editList")); + label1.setText(Res.get("shared.edit")); + label2.setText(Res.get("list.currency.editList")); break; default: - offerCountOptional.ifPresent(numOffer -> offers.setText(offers.getText() + " (" + numOffer + " " + - (numOffer == 1 ? postFixSingle : postFixMulti) + ")")); + + // use icons for crypto + if (CurrencyUtil.isCryptoCurrency(item.getCode())) { + label1.setText(""); + label1.setGraphic(getCurrencyIcon(item.getCode())); + } + + boolean isCrypto = CurrencyUtil.isCryptoCurrency(code); + Label offersTarget = isCrypto ? label3 : label4; + offerCountOptional.ifPresent(numOffers -> { + HBox.setMargin(offersTarget, new Insets(0, 0, 0, NUM_OFFERS_TRANSLATE_X)); + offersTarget.getStyleClass().add("offer-label"); + offersTarget.setText(numOffers + " " + (numOffers == 1 ? postFixSingle : postFixMulti)); + }); } + box.getChildren().addAll(label1, label2, label3); + if (!CurrencyUtil.isCryptoCurrency(code)) box.getChildren().add(label4); + setGraphic(box); } else { @@ -466,6 +512,55 @@ public class GUIUtil { }; } + public static Callback, ListCell> getTradeCurrencyCellFactoryNameAndCode() { + return p -> new ListCell<>() { + @Override + protected void updateItem(TradeCurrency item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + + HBox box = new HBox(); + box.setSpacing(10); + + Label label1 = new AutoTooltipLabel(getCurrencyType(item.getCode())); + label1.getStyleClass().add("currency-label-small"); + Label label2 = new AutoTooltipLabel(item.getNameAndCode()); + label2.getStyleClass().add("currency-label"); + + // use icons for crypto + if (CurrencyUtil.isCryptoCurrency(item.getCode())) { + label1.setText(""); + label1.setGraphic(getCurrencyIcon(item.getCode())); + } + + box.getChildren().addAll(label1, label2); + + setGraphic(box); + + } else { + setGraphic(null); + } + } + }; + } + + private static String getCurrencyType(String code) { + if (CurrencyUtil.isFiatCurrency(code)) { + return Res.get("shared.fiat"); + } else if (CurrencyUtil.isTraditionalCurrency(code)) { + return Res.get("shared.traditional"); + } else if (CurrencyUtil.isCryptoCurrency(code)) { + return Res.get("shared.crypto"); + } else { + return ""; + } + } + + private static String getCurrencyType(PaymentMethod method) { + return method.isTraditional() ? Res.get("shared.traditional") : Res.get("shared.crypto"); + } + public static ListCell getPaymentMethodButtonCell() { return new ListCell<>() { @@ -501,9 +596,7 @@ public class GUIUtil { HBox box = new HBox(); box.setSpacing(20); - Label paymentType = new AutoTooltipLabel( - method.isTraditional() ? Res.get("shared.traditional") : Res.get("shared.crypto")); - + Label paymentType = new AutoTooltipLabel(getCurrencyType(method)); paymentType.getStyleClass().add("currency-label-small"); Label paymentMethod = new AutoTooltipLabel(Res.get(id)); paymentMethod.getStyleClass().add("currency-label"); @@ -673,7 +766,7 @@ public class GUIUtil { String currencyName = Config.baseCurrencyNetwork().getCurrencyName(); new Popup().information(Res.get("payment.fasterPayments.newRequirements.info", currencyName)) .width(900) - .actionButtonTextWithGoTo("navigation.account") + .actionButtonTextWithGoTo("mainView.menu.account") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); @@ -683,10 +776,10 @@ public class GUIUtil { } public static String getMoneroURI(String address, BigInteger amount, String label) { - return MoneroUtils.getPaymentUri(new MoneroTxConfig() - .setAddress(address) - .setAmount(amount) - .setNote(label)); + MoneroTxConfig txConfig = new MoneroTxConfig().setAddress(address); + if (amount != null) txConfig.setAmount(amount); + if (label != null && !label.isEmpty() && !disablePaymentUriLabel) txConfig.setNote(label); + return MoneroUtils.getPaymentUri(txConfig); } public static boolean isBootstrappedOrShowPopup(P2PService p2PService) { @@ -742,7 +835,7 @@ public class GUIUtil { if (user.currentPaymentAccountProperty().get() == null) { new Popup().headLine(Res.get("popup.warning.noTradingAccountSetup.headline")) .instruction(Res.get("popup.warning.noTradingAccountSetup.msg")) - .actionButtonTextWithGoTo("navigation.account") + .actionButtonTextWithGoTo("mainView.menu.account") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); @@ -1033,4 +1126,269 @@ public class GUIUtil { columnConstraints2.setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); } + + public static void applyFilledStyle(TextField textField) { + textField.textProperty().addListener((observable, oldValue, newValue) -> { + updateFilledStyle(textField); + }); + } + + private static void updateFilledStyle(TextField textField) { + if (textField.getText() != null && !textField.getText().isEmpty()) { + if (!textField.getStyleClass().contains("filled")) { + textField.getStyleClass().add("filled"); + } + } else { + textField.getStyleClass().remove("filled"); + } + } + + public static void applyFilledStyle(ComboBox comboBox) { + comboBox.valueProperty().addListener((observable, oldValue, newValue) -> { + updateFilledStyle(comboBox); + }); + } + + private static void updateFilledStyle(ComboBox comboBox) { + if (comboBox.getValue() != null) { + if (!comboBox.getStyleClass().contains("filled")) { + comboBox.getStyleClass().add("filled"); + } + } else { + comboBox.getStyleClass().remove("filled"); + } + } + + public static void applyTableStyle(TableView tableView) { + applyTableStyle(tableView, true); + } + + public static void applyTableStyle(TableView tableView, boolean applyRoundedArc) { + if (applyRoundedArc) applyRoundedArc(tableView); + addSpacerColumns(tableView); + applyEdgeColumnStyleClasses(tableView); + } + + private static void applyRoundedArc(TableView tableView) { + Rectangle clip = new Rectangle(); + clip.setArcWidth(Layout.ROUNDED_ARC); + clip.setArcHeight(Layout.ROUNDED_ARC); + tableView.setClip(clip); + tableView.layoutBoundsProperty().addListener((obs, oldVal, newVal) -> { + clip.setWidth(newVal.getWidth()); + clip.setHeight(newVal.getHeight()); + }); + } + + private static void addSpacerColumns(TableView tableView) { + TableColumn leftSpacer = new TableColumn<>(); + TableColumn rightSpacer = new TableColumn<>(); + + configureSpacerColumn(leftSpacer); + configureSpacerColumn(rightSpacer); + + tableView.getColumns().add(0, leftSpacer); + tableView.getColumns().add(rightSpacer); + } + + private static void configureSpacerColumn(TableColumn column) { + column.setPrefWidth(15); + column.setMaxWidth(15); + column.setMinWidth(15); + column.setReorderable(false); + column.setResizable(false); + column.setSortable(false); + column.setCellFactory(col -> new TableCell<>()); // empty cell + } + + private static void applyEdgeColumnStyleClasses(TableView tableView) { + ListChangeListener> columnListener = change -> { + UserThread.execute(() -> { + updateEdgeColumnStyleClasses(tableView); + }); + }; + + tableView.getColumns().addListener(columnListener); + tableView.skinProperty().addListener((obs, oldSkin, newSkin) -> { + if (newSkin != null) { + UserThread.execute(() -> { + updateEdgeColumnStyleClasses(tableView); + }); + } + }); + + // react to size changes + ChangeListener sizeListener = (obs, oldVal, newVal) -> updateEdgeColumnStyleClasses(tableView); + tableView.heightProperty().addListener(sizeListener); + tableView.widthProperty().addListener(sizeListener); + + updateEdgeColumnStyleClasses(tableView); + } + + private static void updateEdgeColumnStyleClasses(TableView tableView) { + ObservableList> columns = tableView.getColumns(); + + // find columns with "first-column" and "last-column" classes + TableColumn firstCol = null; + TableColumn lastCol = null; + for (TableColumn col : columns) { + if (col.getStyleClass().contains("first-column")) { + firstCol = col; + } else if (col.getStyleClass().contains("last-column")) { + lastCol = col; + } + } + + // handle if columns do not exist + if (firstCol == null || lastCol == null) { + if (firstCol != null) throw new IllegalStateException("Missing column with 'last-column'"); + if (lastCol != null) throw new IllegalStateException("Missing column with 'first-column'"); + + // remove all classes + for (TableColumn col : columns) { + col.getStyleClass().removeAll("first-column", "last-column"); + } + + // apply first and last classes + if (!columns.isEmpty()) { + TableColumn first = columns.get(0); + TableColumn last = columns.get(columns.size() - 1); + + if (!first.getStyleClass().contains("first-column")) { + first.getStyleClass().add("first-column"); + } + + if (!last.getStyleClass().contains("last-column")) { + last.getStyleClass().add("last-column"); + } + } + } else { + + // done if correct order + if (columns.get(0) == firstCol && columns.get(columns.size() - 1) == lastCol) { + return; + } + + // set first and last columns + if (columns.get(0) != firstCol) { + columns.remove(firstCol); + columns.add(0, firstCol); + } + if (columns.get(columns.size() - 1) != lastCol) { + columns.remove(lastCol); + columns.add(firstCol == lastCol ? columns.size() - 1 : columns.size(), lastCol); + } + } + } + + public static ImageView getCurrencyIcon(String currencyCode) { + return getCurrencyIcon(currencyCode, 24); + } + + public static ImageView getCurrencyIcon(String currencyCode, double size) { + if (currencyCode == null) return null; + ImageView iconView = new ImageView(); + iconView.setFitWidth(size); + iconView.setPreserveRatio(true); + iconView.setSmooth(true); + iconView.setCache(true); + iconView.setId(getImageId(currencyCode)); + return iconView; + } + + public static StackPane getCurrencyIconWithBorder(String currencyCode) { + return getCurrencyIconWithBorder(currencyCode, 25, 1); + } + + public static StackPane getCurrencyIconWithBorder(String currencyCode, double size, double borderWidth) { + if (currencyCode == null) return null; + + ImageView icon = getCurrencyIcon(currencyCode, size); + icon.setFitWidth(size - 2 * borderWidth); + icon.setFitHeight(size - 2 * borderWidth); + icon.setPreserveRatio(true); + icon.setSmooth(true); + icon.setCache(true); + + StackPane circleWrapper = new StackPane(icon); + circleWrapper.setPrefSize(size, size); + circleWrapper.setMaxSize(size, size); + circleWrapper.setMinSize(size, size); + + circleWrapper.setStyle( + "-fx-background-color: white;" + + "-fx-background-radius: 50%;" + + "-fx-border-radius: 50%;" + + "-fx-border-color: white;" + + "-fx-border-width: " + borderWidth + "px;" + ); + + StackPane.setAlignment(icon, Pos.CENTER); + + return circleWrapper; + } + + private static String getImageId(String currencyCode) { + if (currencyCode == null) return null; + return "image-" + currencyCode.toLowerCase() + "-logo"; + } + + public static void adjustHeightAutomatically(TextArea textArea) { + adjustHeightAutomatically(textArea, null); + } + + public static void adjustHeightAutomatically(TextArea textArea, Double maxHeight) { + textArea.sceneProperty().addListener((o, oldScene, newScene) -> { + if (newScene != null) { + // avoid javafx css warning + CssTheme.loadSceneStyles(newScene, CssTheme.getCurrentTheme(), false); + textArea.applyCss(); + var text = textArea.lookup(".text"); + + textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() -> { + Insets padding = textArea.getInsets(); + double topBottomPadding = padding.getTop() + padding.getBottom(); + double prefHeight = textArea.getFont().getSize() + text.getBoundsInLocal().getHeight() + topBottomPadding; + return maxHeight == null ? prefHeight : Math.min(prefHeight, maxHeight); + }, text.boundsInLocalProperty())); + + text.boundsInLocalProperty().addListener((observableBoundsAfter, boundsBefore, boundsAfter) -> { + Platform.runLater(() -> textArea.requestLayout()); + }); + } + }); + } + + public static Label getLockLabel() { + Label lockLabel = FormBuilder.getIcon(AwesomeIcon.LOCK, "16px"); + lockLabel.setStyle(lockLabel.getStyle() + " -fx-text-fill: white;"); + return lockLabel; + } + + public static MaterialDesignIconView getCopyIcon() { + return new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "1.35em"); + } + + + public static Tuple2 getSmallXmrQrCodePane() { + return getXmrQrCodePane(150, disablePaymentUriLabel ? 32 : 28, 2); + } + + public static Tuple2 getBigXmrQrCodePane() { + return getXmrQrCodePane(250, disablePaymentUriLabel ? 47 : 45, 3); + } + + private static Tuple2 getXmrQrCodePane(int qrCodeSize, int logoSize, int logoBorderWidth) { + ImageView qrCodeImageView = new ImageView(); + qrCodeImageView.setFitHeight(qrCodeSize); + qrCodeImageView.setFitWidth(qrCodeSize); + qrCodeImageView.getStyleClass().add("qr-code"); + + StackPane xmrLogo = GUIUtil.getCurrencyIconWithBorder(Res.getBaseCurrencyCode(), logoSize, logoBorderWidth); + StackPane qrCodePane = new StackPane(qrCodeImageView, xmrLogo); + qrCodePane.setCursor(Cursor.HAND); + qrCodePane.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + + return new Tuple2<>(qrCodePane, qrCodeImageView); + } } diff --git a/desktop/src/main/java/haveno/desktop/util/Layout.java b/desktop/src/main/java/haveno/desktop/util/Layout.java index 975bb40df6..f3c705f947 100644 --- a/desktop/src/main/java/haveno/desktop/util/Layout.java +++ b/desktop/src/main/java/haveno/desktop/util/Layout.java @@ -25,7 +25,7 @@ public class Layout { public static final double FIRST_ROW_DISTANCE = 20d; public static final double COMPACT_FIRST_ROW_DISTANCE = 10d; public static final double TWICE_FIRST_ROW_DISTANCE = 20d * 2; - public static final double FLOATING_LABEL_DISTANCE = 18d; + public static final double FLOATING_LABEL_DISTANCE = 23d; public static final double GROUP_DISTANCE = 40d; public static final double COMPACT_GROUP_DISTANCE = 30d; public static final double GROUP_DISTANCE_WITHOUT_SEPARATOR = 20d; @@ -33,6 +33,7 @@ public class Layout { public static final double COMPACT_FIRST_ROW_AND_GROUP_DISTANCE = COMPACT_GROUP_DISTANCE + FIRST_ROW_DISTANCE; public static final double COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE = COMPACT_GROUP_DISTANCE + COMPACT_FIRST_ROW_DISTANCE; public static final double COMPACT_FIRST_ROW_AND_GROUP_DISTANCE_WITHOUT_SEPARATOR = GROUP_DISTANCE_WITHOUT_SEPARATOR + COMPACT_FIRST_ROW_DISTANCE; + public static final double TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE = COMPACT_GROUP_DISTANCE + TWICE_FIRST_ROW_DISTANCE; public static final double TWICE_FIRST_ROW_AND_GROUP_DISTANCE = GROUP_DISTANCE + TWICE_FIRST_ROW_DISTANCE; public static final double PADDING_WINDOW = 20d; public static double PADDING = 10d; @@ -40,4 +41,8 @@ public class Layout { public static final double SPACING_V_BOX = 5d; public static final double GRID_GAP = 5d; public static final double LIST_ROW_HEIGHT = 34; + public static final double ROUNDED_ARC = 20; + public static final double FLOATING_ICON_Y = 9; // adjust when .jfx-text-field padding is changed for right icons + public static final double DETAILS_WINDOW_WIDTH = 950; + public static final double DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT = 150; } diff --git a/desktop/src/main/java/haveno/desktop/util/Transitions.java b/desktop/src/main/java/haveno/desktop/util/Transitions.java index 300524ccce..691d9d6243 100644 --- a/desktop/src/main/java/haveno/desktop/util/Transitions.java +++ b/desktop/src/main/java/haveno/desktop/util/Transitions.java @@ -37,7 +37,7 @@ import javafx.util.Duration; @Singleton public class Transitions { - public final static int DEFAULT_DURATION = 600; + public final static int DEFAULT_DURATION = 400; private final Preferences preferences; private Timeline removeEffectTimeLine; @@ -96,7 +96,7 @@ public class Transitions { // Blur public void blur(Node node) { - blur(node, DEFAULT_DURATION, -0.1, false, 15); + blur(node, DEFAULT_DURATION, -0.1, false, 45); } public void blur(Node node, int duration, double brightness, boolean removeNode, double blurRadius) { @@ -111,7 +111,7 @@ public class Transitions { ColorAdjust darken = new ColorAdjust(); darken.setBrightness(0.0); blur.setInput(darken); - KeyValue kv2 = new KeyValue(darken.brightnessProperty(), brightness); + KeyValue kv2 = new KeyValue(darken.brightnessProperty(), CssTheme.isDarkTheme() ? brightness * -0.13 : brightness); KeyFrame kf2 = new KeyFrame(Duration.millis(getDuration(duration)), kv2); timeline.getKeyFrames().addAll(kf1, kf2); node.setEffect(blur); diff --git a/desktop/src/main/resources/images/account.png b/desktop/src/main/resources/images/account.png new file mode 100644 index 0000000000000000000000000000000000000000..53b40584653823d4d648a6e2e08adfe8290be793 GIT binary patch literal 1014 zcmV-}~OKq?H~Nfr#s1gNRS2Eh1iqzweB> zWjBF{cjbpY8~!d@Jb_S_D3OS&mfnP1eV0`*FjkxaK-`vdBHHIvC#-r*Yg!mQM`MVB^i74wn9Kregru5YdJEB>keU+jtXl+;UwPy^TkD&-wQ#{vN2w zYXJBzhHi)`b&2pIh&Wec^W?=#krn5o=JV{EmFM3Wsh=OeTlA}l6#&R|k@Yu|oMR;H z{38)fGY=0o!FEbSk9ywosIcfEM-0yq9_Am|K0(f1f8>a3>~iEu#%;B84^-@Dz!ed# z#N4GIbMc`3&~jN)M-;w=E&tKk_D zJ>=Hqd^;qmwKvbWAfin*F(M8DVCyYbK{Fzvo#uG&C^?XNe$A0NU>Dn3048k2d;nP2ypN(&Cz?(xL?x@u~+;vc@8g2V!1F62c}qXwQ#BZa&Ke zfC!*H5hX=RF7jd;ToKWgnp?&T!1MJ|{gKVFxr@QuiqNfmQN!^nI#c6ci74s6O5MZJ z#j5ONzDU&T`!A9f)1p{&E26dI8wi6)>Uu(kyi2i>Joc#Zm2O%|nJlcikz6rA#_W$) zOIJ(2-~%34nE%IVP+hLofa}XK2k4>8=73$hh;%8s*bZ$IS| zSW8YK8WoluP$gRJOf`8Wx7>`XldCQ0XTkkG`ABHCre$G&r47gZS*v! zzDJKlT0N9Tt$WPG|MQyNr~&iHDt%V7u1Yi)da}*hT0L5$;A_#v0taDLLD`z8Cu(Q* zT)h_iM#XWjE+_N*pNC%8I3k9J-WpRo?%;lju@8J9SuPd6JyyAdM4xMX$o0mgvF6Y? z)FA8@#o%ie&0Wvj*{vPd9Y496O9b$XhOG0E>kp4->UCb#qKz5>`1ogidK-g3W)J;Y zG)cNE%KEEzugE^3c%7lTujtlJ kkG+6;Yy53``U3#|0Lz!Kp5vt10{{R307*qoM6N<$g5vPx?*IS* literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/alert_round.png b/desktop/src/main/resources/images/alert_round.png index c5c182ed94f0f197bb3fdd88d4147ea02fdbd943..4ba0578457abd6197009d0e8fa8363d568f12710 100644 GIT binary patch literal 4964 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 iGz)!ATD`#r;Qs(RPbZ09nW$?30000gtZ# z_=a$=mE)3;31Hlh&?%yBIA_79>ak?7niOI^zkAIfZQ z4su8X*Fpf}5XB-~D(1)%w8OuE9cX_tJet198L}*|=l$Tr()7$f*+1?mn(Kf^A`n7g z&0@@d4d&<12%R_q=jgBZ)03GFmMi4eYm(ZNyLmIIOiY4TtcP%2if2KQHR^XmcgD&J zqStRATf6xC+~h{dI(T*lF2jE{p44?|Q{fe}2RDt^Nsk>j}5F!&Eg`RuPut!lq&tjm&n|s()v> z9&}kq`GmOQfTkh;DSJV#NnsjCZx*4FXasi{sL-&lveD|*U%Poy{$yiv93(u1w$=S(FCN-5xJ%ZzOH#avgG&D32E)`z{7yzQt Vzwv0Rr8ocp002ovPDHLkV1k6tu37*9 diff --git a/desktop/src/main/resources/images/bch_logo.png b/desktop/src/main/resources/images/bch_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3c30cfb9fb4c23e01afac293ff11825aa831adc0 GIT binary patch literal 30750 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 literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/blue_circle.png b/desktop/src/main/resources/images/blue_circle_solid.png similarity index 100% rename from desktop/src/main/resources/images/blue_circle.png rename to desktop/src/main/resources/images/blue_circle_solid.png diff --git a/desktop/src/main/resources/images/blue_circle@2x.png b/desktop/src/main/resources/images/blue_circle_solid@2x.png similarity index 100% rename from desktop/src/main/resources/images/blue_circle@2x.png rename to desktop/src/main/resources/images/blue_circle_solid@2x.png diff --git a/desktop/src/main/resources/images/btc_logo.png b/desktop/src/main/resources/images/btc_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d3c3e7a7c813088a510c8346525267c338ad1fae GIT binary patch literal 21730 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 z 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 literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/connection/tor.png b/desktop/src/main/resources/images/connection/tor.png index a88b4310cb864faf5920985fa9156c883349c831..6553e06dac70d02ed4a9697acaeaafe5c36177d4 100644 GIT binary patch literal 3381 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 LNkvXXu0mjfOz(h} literal 3583 zcmV4Tx07!|Imj_f+SsKUhd+Cjq5ITe!iXfc;(rf6_!A3|Zp~Mgf3SvVB6&MRD zBA^I53Zmes6tN+qU>O7p*au`#QP;tO4K@3KCG6~;-97uAbI<*~@7?#m-@EVLbN&wi zat}u+OoQbBNEc*@LVesB(J`?M+*3dTihv4ifh~udDRc`72>?jU_J56!0|1)pZ<|hO z{rmm@8uGk^OfCRW2*UBaOl~^Dr;v7>D->k`fZ`!}-4$8FDc*!IO@tJPaMcti&G7Ll zj-TPK=`rD<9tht908i#{L`eXU{y_4KY;F>wLq;QPF5vM62wNlU%uVJX^N^8yU|xFq z@+sbruwnc^=_LI{GhV96;Ur0U=3S-(@5#>;rg3uq&*k{vk)9@=okIs{F|pHAGA6;U#!_N%MjT>3Ct5yHrz{7BW|VnvXP}nH?s5HVfHcvz)^5 z3z^dik**KmgFwIm+^mF^Q~L^dEEnd8_({oG3^(MIBrwrMU$QfT=S#^GV;+ zr_T~Zzc!cK&J8{ z0b0RXa1mSq*Fhh+3m$?YFbZCQaqtmLLJ)+5C=eA=fwUlf$ON*4SdcU11qDE%Pz)3Y zB|!p64CO%^pkk;Ls(@;sdgxc^G;|TV3iU(xp%!5XjuJRfGkZm>Tb z4ljX|;0!nyE`Ybd-#K-ZyL&|T=;=wb9Y z`Ui%BQOB5I958H5G$skN0#k@7!yLjiV=iOvVn#6^u~;k}%f#AYeX%jv6znQ&33e~G z3EP3~$Btkpa5$U_&IIR#3&wG9S-3*n9^6sf1>7y%DDE?!gxA7b;=S>)cmX~iUxq(| zKaanKe~zCd$Px4j4uoI=kB~#yN;pVpC)^~A5+;fAL?+RR7*0$ft|yig8;O^RkBIL{ zB$5uvp0tp}C#@rula7;mNW-KrWCgM@*@L{8EGBOuA0~H@?~~tCC=>&VD~Qa4Z! zQZG|SX;>PQ#-^pvifPAay|i&f1x0JcNX3Lo%Dl^&4TxJ9Fks-y<#W2IL z*6@xI)`(@4YP8p=&longF-|tFG`?v9o7kH0P4=4f&%?~KpC_1CJ8#gGZ0cs3ZQ5Wu zY(_T=Fv~Yjdk4*7t0vHi0%9Z8~kf+uGTRY#VLI?DXv7?5gYrSu|D%tAy2KkFj^R zUu%EX{)>aHLzcq{hd&)n9a9~TIKEiGT#&Hf;DQk+9Vd=cjnj}b!#U1*zw?lbwhPCl z*5#?It}D;=kn3|dBexW{qi%29&D}-r&F-H(96VNgbaevlxjH5df5gLec!4q=95gq&H3T^O>ka^bU3i_pB#?l5ZDvalmzAH&_lw}wBA zFpLmKbVkZXE{?2^{1oLGRT?!EJwJMN^z|6k7=BD!EG{-O_E7A^BCkce7L6{pSzNey zU%ra4eZ_O-?c|Lm zI3<)OJWq5=EKPio9hzH)iL&_q}$9LjLW*p)FZ@)sS-gff?8wq?-}b8|y%DE>)2ob8-lnf-Z1 zl(cWuGi(RDuS z>edt2r>_4k-#ouG|HFo;4ebRQ1?vh%e)9RLp-`q!R5-ZNabxu+^d|nM-py8<_iX-A z#4Ea1Y+hVe{H=sra_wh}pUZ!iY)RbGyVZ8+33 zN7RpOZGaoZ4P!?aAMHEld91C`w6Ug%-c)=X92Xyd`D@&-11J1Xbe?3LY&@lZs;ZgV zT+{-!pCbtf?#kBRG_B-9#?%3XZ#_Y`Dv)X4X&nceUdY*W`;JoBQ?uCgCVaMx> zNf$>tmvuh86n*JV*TSye%l?;pu6SR$)a}-Np~tD`>{W-Wr?0WDwO+Tq-g3j{M)Pkr zzcu&T_O{%#yV=%f-`C#1p#S_Wms=Nad)~fshkfVz-JrYu0}%uF1{V(w-Q(SRaX;<; z+XvYXCLgYQgn3l-SnhGz6ZI#xzZ?DDG-Nw;ZrF49#?!E;4@bBoul~sVLc{ptGWjq&L5(YKg)5#72j~*%D)?ZZ~x){Wy2*A6;08nB8p!OmDL1xAiPX{#P+B&_rz#YFeYl7~A-j5cBnV-m?=XMG0MXVCLPK_s)6eo_pVD=pU|N zFsSS4=}~QLY%m$7k%MNV+wHbhDwX@SS}i>~I%-&3TPt#mwi$3}Xvoyr+38+cS$URB zCV!|@su#e#X@;xM4-O9cIyyQ&xLmF;R;%?LS5lvB7EDe~YHT)J$Y3y30sai|^=265 zyuQ1;`-V=ZOKP>+6cpT2PRj9>9{BzK0CvBd$z&X@t*x_rdwZ)gNI9yNFUq)7Dt(`t zntD1jGsD+Nxa*};uDg-|j@Jr>LIru7j*pN102!4)IlhcQ1<*~e*ZZo!zu(^8-p<$h z>~{Neq-2$WSB?6z{HYVf1z`*Od zxj85=u*1Vc##=U#NU(*41qLHI3Dama8T7@Yze&`?!^5+oP^ff%e$M#r>+9TvA9?)5@U>z!Ug)zGoR1jgC{mYpx5g?=rqdZ zasnKUMyaKxh2Xj%L@ET?4FuI{)eb`?@l~x>m-$Xcqmj^ggubV?wl+cqgnTrWAa7v6 z3(g1=3a}uG#v!;**=&|@d<6h!UwE*)yG!YGx&aT>`#U>3`+@-APxJHhU%BLig9GAT zK%!8@N3KftL}tMu3!j~x{Stp9C@8|&6n+*Dkd_X$r>?FpIyyR{lamvQL?X1bv_vpY z;355+)9DOiK3D&xL3#qa$50+046|_gXeL@*T%>R~OxxSr0*s2SqDqgUI?g|+#8<_` z6y*1d0?d*5Amy{t9KWaj&K{r2AfBoPLBmzf#(7ytlZzM`dJ z1bzyDha}xG@ZAJa{~Z7ra9!`!kjsMcmuM+PoH0*6zt2=Xs%sO>Sh0H4 z$7nn&%MA#QSg~FppH>9`*DtH-Mr(E5hl57PGp6W)$0C3h_cXl+@R$s=u!oqOqpc4G zY>HJSVvpE3Qw3kRs-u0ZK8GqeL9**I5S)POvIOd%Hfd6h*lYt@NM-9dD%JfXz|he< zO!5FTEpVG7O^aHUKn=hyp0lP?jX6@^pOmz#27KWM3bppnr#`In&Jb1F-D47dG=3Be z4b@A<>}PeehyGEAmhuyO5mFDSg5xeQMjQh;0Hb&%6`j^6{U@S1#W|uJsa59@8|oA1 z%pcFBDmJJEst(c2I5zLpXFrBy9>&d#3Dl$b0rV|WirS|!vCJzK3`p*d_ zhzJ>XZN+LgVO>%*Y!qUbY#h6fYUyY@JUaZiEe-}BZJE%2P4N(8mn+zD9+nh9c=Q_mb>BbwSCYE%E;KrV>1{(VpnbQ)04)^*tuJ~PU+ZKCu!{H zz>l2H0ha!*fw8>NoPBfJOuKX`W5Pl?!&?(I)(T1z|1MzEiq?_HO_fw^vK zVOg&E#xn2HfWjL}#zZDbH&*9OvhQG+1KHWwrq4#oroEWC+!8ZD@Gfl9@_}LOtO1S_Gp%AF4r5-!|GKfEz3(K2_+56 zd7*;5+&!Z!ktC+7L{dh@wW_fxM5W=#=Ezl7Z&{V{Dc8)A+KQ@*_R&YA!#OlLddau zbAH2li!!;gT-RSG1TfOEh|tf{_tQOO%wzKBus`*by`K%qv3ROlu0n|B3iMsqHZ;{N)#Jl%-|Ky6`%bhmCG5t%_IJZVJ@V z#(hxCTW(%hL5S#m5_|Pn+z*L27GdI7Ee9$`?0tII)3_VN=C2uDb12xu#g66_o1VFL zKF<~Q5q8}WxnsPl&)g*1R6J4TtT?b}_27os_2H5(_t7X%741-AGwmhq_N#Soj_1{_ zm+W_QtBA?G#?OA9Ep{I~_wHP4hQjk2*}>TCcH3$>fb$P5zHQy>DG7tNd?EFAt1ck|+!Oy(xau z4bsrk>C!1bdOyZ3<>BLX1(f5t6(+S;Us~*c%7&8;ai8Y zt#aYg&15>I-p|XuWBwnvCPR!hGjsHuQ^vf*JThh{e$Ov`GpGGioO9P;Vi4irO=WX><`ijR zv&^b|Y^!!Iz|qxvT+U1C>vYqN>KkVG?ut>b^UGZ|RV6E1(0PwZRl5*lYVVW+H; z_*nRs1LPN`dc@{(iw2v5j1O;Yo_rwNeE5BL$Sze|**Pem@^1g*zVx?+jiRID_s2zRZv^5G*|*5if|t`CuVefHpdJ8p^=ZAm zfZ!CG`q`r|vL0@~2Y}#*+iL(>sU>K7t@Z=}+ywZjAbh}{0uV(6o>71-K$Q!4{{xtx z%{q0MRZYAc?hZ?20uC+GybD-WU1)bCj72`&7Ude>rn1?lLU}a%xdXr*K2@P;3wV1$ z!KfC)p-~X+ zzjT4$lmwlKL^l)+=H=xj;Uy)3_i%(s%FD~c;0PE30R<6If{!Z^?G1G$2>mHR14FR$ zz`7B!cvr|ziD+BACs9cdwDb=baBhEzbtU`?6Bsa4cux;I%%$6)nLDyu?{|e2bBh)GXDex zNn?*DqW@2@y&cK{?}0;uE@N?MM-0r()lm@gS0GX9co)0}s2H?P>L0IP0iWhP@D5lP z@W!vFy5Eh|)kR%#B@ofBc9<&~N`hceB(PX}lqA{?4Y!5MK{1lD7$_1gi-4k~5He7- zti7#_lmprpDfNew;Cce`P)aef!@V zFNbiDLdzqhppv#oI24JolYz=%q~)M+8GA`vJEWbwjGV)t&i|e9(nu5%ME^nM|IYYd zp|x{DyEJ5`THfXlm9&Z zYlMJb{u)9US1?i@;E0L+5YY>cF#HvbizepxmYzAgm^k`22d(&ZB@D|Gikf8$8-|`e z=(M%h2)=LroUu1h>)vl~pTm~4qXkRT=|`_s8B&Ce?kC!(TVX$i%KY}SFne~njOHj; zkEV-&DOa&fNQ;aduippRRg7W^`U62JKCp1+eK7N4=6>-Z%&{xUY=xrG?UOPSkYXkm z)wptf-HRE3Aa!r-hOmV;qHnK^bv}UAYI%9dDyMb_Fv+#&dwf>9T9+IVL2s1J70zFAwFZ0D=s9xl2&a8Pm-MTf=dOp>DFyFGBO7ua+phu-kGud4R_hpiaiOE2O z3(lmTkdiWhNYF_$5V1)-mmBbdTOgE8Ly)};KduKV`{w2?eJ$UrrfsS{-iI71D;KyY z2dmpdG&VLy4-5=+O;1mMBchb#RaEpVZw8*F8+@vq`iMkAf%fVW2vSC4E2wx0OTk!e z_wTcYprIutC8y*)Li6+UKeV>Co=M2?xiq-h9TpbGw>(nWXOlP$G4N*7I0^{RfO5*p zWKaqU^fc6f|3;_m_pL2b>eS8}*&cD?Wv@t@fmSI_p|G&<#RuCWFB$xpf8)YtAUFwD zGb1!6EZU3phRT#G5~WRRBiT->YDb`X-%9!7Z5~y|6w+28Wkv&60 zT4@HW8GUojDZcaB*H2=nxe5U)6+FP4+sZuYpuIgF zc1T2{)6@qy>yWw|v+MW0*+RuG8D{0?zRJ#yFv`}@*JoWAcw+}1JO0YbKW2p{*5t0H zy%G6s{UJ6{ftHrG&f3xvr>CQ{2V}(?9aNZz?q3yVI&P1Qs|D0?iJO4U zD9KYU>Mojh(nB7owt~fBve&%#&PS15U|b9-CZd?<85i~g52KB;zZY!Re!WF%U%JQU zU1Rm`8{91aW}1m;ng~M%*4Bcj3@4QSer21Jj%JmzpPX%S%Mp?AeW*-VB0lELn>UBc zo4(^TZI4L8BeZE)J}D`wtep8kieK~VJ}hXtkdP3qP9Iln(=BIb6-g#?0pq=QIeI$= zbQgrvjp$Rm!(WQz3dbjUR4(FhIN|tqC5zp@h0U!6(m{+pB0(a)y^guDO_?3k?XBpo zm{N7jIWY`3tP-gq*q4~GFP^LyLnJTPttl%kbW?iXX*(>VM(5QP`(!kSEsAV2 zEc4kVE;>4z0`XTqlb|!*Pky9wU}$DG$eNLvsV<8jdz+h+(^Xzxe%QUUBiCxbo`3Jm zF`)f+FGFw!w*S*7_pkL-;_Yg5Avrlu+c9lZ2#|KC`S}m#_qWH#`5!gQyHAAfZVlsu z_~~f$;@hW!Dh4^kc1T(X3UEq13qpwfiretSlyEhi+=SM|Fv6Q)v z-U{6OWVZJ08(J-m&2M|zlu$Y1Rq}bU;ikll>!;XwU#{%fo*Vfdl^tv3W~__L*#sQ~ z3e}cxS>=SspFhPCqjk35U-=Q5x1+Puv?HHEU@s0cbp|_yc_P-!AV7EQE)6vmQ=@7h zQZSm1lauo%iI`oJlXJ^2AVA4Tn`lW3iEqb=#kZ^EnWu4BJmR>J>)W}$N<2kdFnAS_ zuxEwGuLRYuw~0A}-Qxkq;_chFCSfPyF#B)Fa^rPg`QhoyrUIf-r8H~@KNboOK_B-Q z-;|Hd`+mE3d~#&uR2+sqLc{yVT&9?v^XXIpZ}p_9J0*SqlaPTGOrQMPa5pRGwy)r+mB$t8CIKGN0_zS4pp_Ic;j%I^{7- z(sOoR@NlM6Ju(2mzLf1-t5R-3ii^d-XNW><-?{78uk$fC$`yeD+CQ(KVn1FRu>5tn z98dW6?kJeiUMDWyKJ#+d(=jnIL@zI6V`D<9VF+Y36gPZ^T35`%O8Y`DlDS~;!BeB> zsi}uEN`705`>Tzt79|eMK;_M-lobB1B>BcKrOrMR5$6p|x!a53Oh+2wLsecd5NZL{ z^$UhZk*nwBqGqg|=^iz!o0@X>Iz0uW>ZqK$l1!NCGPJa0u3uafKMu2cUxrKZTb7rF zaXq*MHomOLII7*d`7E*(=(*eIS7}1fyFzIy%O| zmi_*y)>#h!y|w24!NHBajqWM{kaZnVHRQ_3$e;^v)Ocl>n3y<3@?6jc|H)uGnnKl& zIk=0|joIDx;fQ!20?vxkZe@x%0G4BkV|OuOMvqigwUP+sc_5IkVaWC2!-v&f|2&J5 zFmRyVUK}do3#Fm1XL+%fPYb#fK-r3JpQe}4i=41(V}1f`z#hU0~LfB7O=wX$R8q+va;jStCtunac&6~)BgM%h2BO^=805gss zizVGz1qJ817^1sp#7>?Drxw_L!!ObagkA`Bqis`5tKVE0Y_&(!9ZbgPYfpKcNbSCy zZjvircoU?x%a^zGAp3N6nF9g>KzeCjzC21=qbd$wP#w4jMij(C*yNPpa5&+|cPuzy z76uC(ZEWh7+QdY89)Ns$l_RP~N3=Z8^FUHT0qi~11!~q83H4W+-AAgSxZw}Q;_JJ6 z4|8&wt%CMF!5zo!;^OOqf=3#H4NJr2b;)Sg{Nir$+Bi&fr$c1`MJBeA|K%aK+IOuB zqa-8hcg5o9#BkgGeMk);VWrYC2Nw)0dEtfOc@u94fPvtOC~Sx!iONpK_|*UrV3(d@oDm;}KS4 zi;=*SI(`1fc7$G7XrC}KHH|f57%sbqv1n7Oxkuk|LZ0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU;rAb6VRCwCFSb20*`9pH z3rWZZfsl|A5J47^;(`c>R;+qz7mvrHJ+{ZH{R7+nqo-EU)(x~pj>-`QjetuKkR>1x zK(dgXER%hb%)Y<(dcQY=A`nTe`kiwb^4>T1yWhRPd++ZKhcVXrXMkH*@vu8Y1bBeM zRh9%y(f~P=IMnxZ2nh6uo@ru`n5>sf>Y&up;h^UVKxCVvi^q?+I1YJX50(b{rCwi{ zt`NhqXBQp+BN-4qU?It0-CFTvkBay71bHqDB+*O##okm-MB{gEiSAL*l|+$S%87X1 zBnj&c3a-hkCQpQLQdE(am_w{8?TghdIjmI$n)=! zjvGT^>2)P9J*0}^{IW7>90ssG+87BU0Y{R)6KL=X+zKfyo|QQ6RXzqj1t(R9IsP3% zK(WOjZB8|c5Bo##_l|N9!uVE7jcEYS5Nn_iD5P%;pf8335xJm*c4ct_W{MJ*D{|ai zUXu0*;2#SMC^D<1cgoYmhfO+oMe?SRE_BMuAtXxRxvLm(@{k0#7YGI;?qD`Zq@;8% zkd;S91S3Wev={RNH&THy!amd`hQ*aemDCo#Bz!yih8L~V zeGxnG=NHdA(?JiMafV?B_<^b&A|@A5 zSq-9CrQovV5JlV^(&@#&=;BcQb%INmrBJQ?hdtBkfFo7FbC1$xyb*;HS!z0Iix5`Di3*{me35itIK)lk zD6p}ys7Xl>?>Fk;)@$jOy356ECfra4{hbc)IzN$8{LS+7Ja^`S-`?{tNinj#x9#8* zQ>u*ukCDDU{Ce*W*gQ_TFkP{(s0nzDr-83({=Z(6eeZp1ZIa}cn%j3spB!B$t$!l@ zs3jAt=-p)e6e}UYeBgZhK`9vSmaHxH;-8jp6xB#zm;B8{b$zdP?D6>e>*`twcL4;# zLr71ua7MLG8eiZ10SusXEF#rb1r>g`*U7njcErUduv{qrDGaF4>vglIP0gdm5`w4< z!x^$+M(SMlZpB26n5Y3v`3sLc@+*CAULl-zJEfKhE>$0L6Q-do?b;N2|13$yL<-Az zmY(GIBrmUDe{cR%8y=&^&nU{TLp$gCp3@s+f*)-%Prs)Aq1!))YUc9?G11^T*%1Gg&~Q@&h1sm zPb@)3Tn_Rq#WB8+XU2fbdNb*WAvq4YfsW6BOp-UuCc{I|Z`@#b>Z!+(nwCu9cgqxL zXmFSeXvELTr=#0C0JBMt;_1aWQF{d4buJ{vB}1)@MNv{I%$fwF)zQ8gfg^*TEXTmk!ar27cT&{zf(lDxfW;p~#P-+C3Z%Bj^0 z#2R=;T?}YxZpW@&yICc~!iDqUatsr((BRmy)5yumL`rH3Y8%eLTIWV;d@50|Ai`n@ z3A$wDTV|l&)txfx9b%aJwn; z+36o8Qil)NtBVczTS-ab9oye~4Jj!E0l_98J~HaS3mZ2P2lQjj+B+~~MloxOGC>^Z zw_$YDPVt|P&-NY!g$)W-3=USEgh5D#hPb*U$t*2ugb7K;44fM{l^+s<{S25F45-kk zRnKkS^bbvG>Fns?T6CQ6mJgpZw;ZchuOin|IS{-uv2zn+Wp%*ebP*QP2`4m^*0o?p z>2&P7U}89ID4jvvcZH=-_R9%qevbxYT{VEU}rC6|E!xdDL%2H zqJ&{Wt{LyYzYEi*6;KwGk`hnUFg-kOAC4YAMs;8a<+ICV@D&#q;P9d2a5)_W!YSzQ zvmr*UfKPCe`$gIHmr^5+C{%WQ5&Nrv88AC9Z?e&3ijzmtmX;O_5WiY-{es8{LAlHh zZ)|MF2Rn8Vu+*5hV6Ob>cDay~n}cb^g*bic4D^OXglYEj`n(W%>Xg*7IgY#Jd^Tx` zgoR|p#^_R+0V&DJ7PV5Tkt=26xn|_$=fGk~iZDn5_~^qum~+isl$Mu4tyaq3 z;;5Or-7YL$IuEUFofHOKtn1VnklRAG43iT2hG$^bplZ5_J1I+2t#2~LMwPCID~ zg2Ymj=M;!hsFcirfJi=Jp7PNM%iTU4kdX28(Mup=bUtuz{C)P-(&1nBMSMu^7T#Y+ee z(r9G*pfvP){n+)pyX9#06&HSJRJUEcmhrv+wrc|+Bfv&u~YP4112RVQW5H;wD;3Z129stLA0i` zRvZrcaK6KeRV!~mh^AA6!GQMmZo-d11<50OArJ^a#cPPPDlWspg|zIpx3enQ$_zL} zpg8sB)*Ujki;D7PlF`=ADxDrZy@QdSCCR^9U~tGL^FXa34naD|K(pA8%|_VqAruNz zYxdJ8WdpAzpz)U#Bj)}<*o(%&uh^eQ_$W(xxALPZ|Gv*ZLtK15@(XfNbMhF~cnfN3 zYQOo>h(f{QYmmJXtxpOC)dLUwUI+aCAYmp%UJ_wc$0PESg!HW%z=%ro&{+Zgvz}P`ZY(^yV5F4*_8&=~Q&GYTRMV$Z z*Q-8q90ol{EqV@49RHHQXNR?C0O!d=pC713Lv0)8XRknc)&hj+_I^V9sh-2AtlJ`V z3^yJo-M@^|^KbniI@Z#WJg8JC?kk&BT$q)S%4bbZ#}K7qVPP)PiKcIVZx6-XOCjol zwYwi>>GQFC>MtAR%2MDJ>x#f`D{`v?z6H zsVn`>IT!!mTkoy6*4eZ7?Afzt$8YBHMoHlz(FN)Y5Cjp)%19|g5PmQG-#I++C0^j0 zJNVyudl@Z92qGYZ|HFae6RE(*D^AjyPAYb$POe4{&mdPy0uj)3oGD#4tkJ0P7!3U6jf{*qARcpq)V ztkg7ccg;2?U(@`W&G@9tqw4C9l#uYR!5ZFIDU}7+*WfY<>3*=!La z&dJBRd%pA1BPA|N_R{CnvOIC@LR*}a zuy0Qf9odO&`BopCZ04QV_53}VIN~lnx$}%vbYol00Mj#e#O->rGWqU4#<=e&)=7+S z^4$}RvGZnY-qB|(U&88Z9k2Ih+t*r$HDZD0r~l>~1rS@;TXQD(tX|*!GOX3lZoH|- zN?FNy^rrZnn93Yl_sW2)hV*8g3gvjcdh>?(GDcC4L2OLOW1n&`F)c02r}LWNj$`kU z?%`5dPbxj9v)!o`}Hb@IlYz!Z>>D1>?d2+#PAkv zmMOiOUaR9%uY0gLD>6AEcd)*`REMf6%viI^IT%~2YdL%r`M*V$t`QZ}6t0PK3dbr= zX|>K4778y_R}=O$vAd-36&P2n52kh2FEJ!KI~~rB=8vBq=-Dr&RD+64`4L zA`#=c-RSCMP(Atk@aR$W{{(L*^5SQ)YX!}WHQ!muHvVgsHRXF$q9%8;KKzvhHErRI z9?bIB8|EUe8{YrDyL@2GRNpdQxU*~JGgn-D#M^tMt=YbBXI^~V>=O|7KdCM0H(b{$ zY7Pc%w9@&V-_@~gIk^IFiF(|`;8k;UP*@tRm)*FfExcokInYQhR=Vj~^{~x#{kcJ< zIlIRQ&*%z8{kY8kj7+2HDyZMhWT)ePZM#je`#6{jMWW8R_q|PV#N(Av-tX%1lZt~f z4T>3D*efws#Rp}h$MH358^_BA^R9RP=P2l_EG1K{%Qf}+odefCZO&d(=k&HCZz?Q3 zer)x#|I&kpv`vleg_FXE0c#@;TCaSSDB4IywEEY5@(*Yg|ECR^p5sBM^yKf9>egHh z>54K>9w$uT;#%KQ7TdV4aN{?bXay0LIerf#S5DGi!Q`Qlrf8-xa@?5cy&xx}w$4~$pWX|CD$idB}h4^*zTU7lRO`>^Dg z+$SsQpNac>2lWi|bt4LeEk%-aDu1#xKGadH+QuFJO2!(q47^V5=E>}A+RVY54(I+SGnHxNM5C=v@1}tQKyX#n!G^p^3{lp;5(TohNLOcCMVw)F>eMe`=<4W>o7!xRK^;`RTlDmLX>xS=p_x&4cEA_RNW(%+D2L9(R zL`I7BX*Hon8koB2wc2{7Ybq<71co73#r6Us`u!&dgzJt&cT6oK*Z;R2vdg^#?+n(b zRF2;&Mu{yk5EzT;1Zj1l_Imx5$Ou2@S@%vWW3I*D0Zgq zXt_&`^N3}d^w#M%^-B%a7S^!zujdDCl&BG6O6!&`o%a)~RVAr+*z|n$_=_fTEazjdXj7POh_7su|LNML-%kuxLws~_L5EZpnMoko0Z zu@cSgsXlL#%_Pl#TH0tnmR^tNQnY0@uE-CXIUXL-U4)A6Cod&C6mOPBGE4{s*5-Fj z44O{o|4-c<%y!z34&lk#O{z&MI#(^+298`~&BbFs4X|}p59`?{c)g?67ynIxerp;3 zN#1{N`|c#uU+t5E&>0mq!VWfLi0XOADyB4GtbR&=4n3_Mc*K8D!2Zv5lhq=-Y|(Vi zo|}!rT{*GB^ATtL&i^sTm_)6eXPt~xkGEbv%ZU4LG-LMs&+S{&8v?scY)X)!^xn4d zbO%H2r?!2{gscBHk~)o;_J@y_>v<)RA;_C*)#Rde!=MHWL4ER>-{5=r07}{ z{|jptui)^J8#^`O?NOk!ufdsUk8=jfoev+L0v6M{^QE?Xmj2%+UMy4HEy|UYzQYtDSztqj4;&a!s zpYD@A);b|#r2Mxs_0MbJE0dlYir$TfTWpMwA&*$Zdovv=> zB?P%WkrtReLZ6b-!wcb4z4jz;(zA1!iq7FY1m(TN?+`v*b`U*mo=;AqjIa1Np`?!c z^_q_$m8y#}Q#3t!>;oARsZHh@E>XGMH61AAee=&Ae5H&Dll`?RPIDp|lLwECHNSM? zLww(FOh($ZgAE_O=Upc&f2u+0{9WLRY8bXHA(XcoX>w^EK7{K15QP4^Hi?Dr zuMAyo5D)9tY;@B>p>JH_gG`4a~8mMEtaucqG$r+ z%5LIAZu*K~lH4tmFP^m_vdX9HZ}|Y9iphh{>a+IbO;0-PAwO>QvaP|dYqe-a(}8J| z#(yG7E#Ydg>a1P)V*b-&6EqQ26tph?Zsu4Br3VMA$gITTEud_5jzLcjpoHiDCBZQ~kQTw-GhJJ?2q%O%QtP&NTr>Gq66D%W zm14}yS=?!ltpg_{IJy~vy!_b*IS*6R!hNdSw6J6PHNRAdd3(oYRWDPCK4T^Xp=RF; zQ~EJuZ#IO{@>Gu-dd`Lixyw{8YMc|(+Fu|%=;u}8A4e}bZCiLO9%BVjzad_)KYsQs zc|y5)+mO8Jd9XeNsgJ%{BQbaJRdR&4PVVQaUZ@=SU=2(EeZhhPXZ?=W(ix~obZnGc zaba2bD0foMk}C$gm!(E(TCr?!*l#nAE{7oNqNe;>;Os`q{|2y${H~p^x05A+8aON$ zh5~YglXNCh>ZblXjnWosR<%91o;pOQV5J`vO!x6u7>IX^AzWaf@8?WYA zA?U`MIc-xN@LM<^WO|<(fL+_$b)ENF@>;=Dw;vJYkl&y8$4@{aiQct4P-lWpNA=`@ z_>|%$NN5yyBE5k^vU($l^dh1-&iiNY^=am;XDBrIcCP)W3FJXK)7m-q z{2+U)<=@eX5LCoScW289)0gM_nk*2z2|TaBCg|@;$q{C}W(4su7*_ezL<+Y~{RnT^ z`&S0&O*A;Y`Zq5@5Z|r}+L_d6>3h;WHdp0vtfb-3W?o}8#o7Nsent^@M2wNEKFn4a@K!!KHib1&)*;VuH%>(Ql+OP4p(4dbQRUw~|C6Iw2DiLQw zf=ROU_B_{GvQX?4PAFc?N=;FBxllOm{<6MQHhIr8_Z{K|nrc=)*as~(C z3Ax2QO!RB5#vVkAUz@QmVDSHR=!0`p{?_(F4pnkdDJ}$|8h$&6uY6ixt8IC6nuV5? z$q(!bk;QhtBnIXfZmk~SBvPjjr}ij`=k=;uK8xypKAUl8zqnJ^)0r#miP zS?Bld{Y3dQ@K3NNwKd#J+&b*Pa6d6No-1(^Ro!6zWb=)D?j!&=dzQKz#p z)K<&H0>B{`*X(*8^7DgYCQBNt-1$$S8ko5ITKihbb+G=w+@epwmUVe^F9_bpmZc#U zT_X3`SE9wA5vF{;2*$WQNde{;_|RRFE+iQre&>vwil7Ow#5n z7*VuTr`}p*$Glws^}H%}gpAHu{i;qbh!LS$<~ZQQRT@u9?@O%IkG}agg2lL^Kxa&q zQ~22nSjL)yDyN8M^DG{dDt668Ic)u_Offu0qm2MGGQ2&ZVJD! z3K^_>#(!7v`6Yaak_Nz2Lxqg!F9vLeLKCtCZ)dWg@b5%}X3`B~CZ*Cvnwz&$DVjVaM*8Ewx*Neb)^P(9~n~mGQI#g=6D+U1S;}8yFbM z>l~O=BYb$r2_9qYzeZwr4L}?niQkdP0s82b>b3r;;4KDWix{Z=ze zrdZzuzhg344R+8#PVAI!%f=OgD?k~PmZ!^+Qxa1NT=!Y%BP!=xI!{fjlh2gjF#;o~ zI7#k+);O{j;TBrJ)B5`*M~0h#63`y#c-X_9EQ;S&yrOi*>w0|g-V?6vM* zq&Q7OX78sOT1gVEhxY=G4Pbgd?oBsPQzdx1=$kiT#{_Cn&ft$V9p9j!10w8q+o?qL zDBCdOJlI)Ys(n7$EtV0rZUgL4ZdT|_or9=*LYF_H<&EA(FNbGWnrv26kC{Q^2AqbB zX^Cn#3cr!nngJO?-B^W5EDylwtEXS#LY_Ns;FRC?f&$BS5lvMzd zAJHFsU@gAO$o|?G!kw>L^5Mf^7-6P*iAh_S8R?)ZDEW+32^O?E6;}H>Ip^wUMHavS z4yVv^pBeMv`bIoSSU@^&9@q+9I1iE%Yk|ZJHC$wEjC3Zfj<EmG=Zy{N`-$H%9*0n5P3I8taZH9;MIlVHEmJPn(AoU0OEZpd* zx(gV_0V+gxU2%c3WLC#o3enHwL64iRr^O3|l%emQcd*J$IU0uS4rAR{ugz{DRKOiV zSnfv=-8r;E!X?D6QtGrnj+cPy6#oHQL&_r(Hjr@31d%D_r4+>qYRe3nVX49amUExi zMu!Qkm5~{Z#}SL1J7%YVSKJWHZ2Z>=K$D}{$fPYfT+6kagTKdO?!w%D+{)=%a2!EN z!}j3m=XF#`SdKk3@%$n3tAPZJNc3+45b1&Kh~aMrST*X(_Ptq~)tOx!d@n#YBtNnx zkEF5IM<&1V#L<2~0@!a1ca<~11Ev#Q1&v%jKxdwk-9Y%qa)kGicP}2C#fR?~3~9YS zbq~~A-oO$Ig14UTt2q-MT%7>g`|ZTg<-sKd3Mtk~Q(^6L879W))ZYo^fd~fCEXvR} z6@a4QsWBoz8G{ zESW*}CdpAv&d!!3-oW7}d|Dp-Sm+S;%94Kvn+LtZ)*X!2C=2Ll#6g&8q~q^)GHHw* zB3q-7n;AEz3i^m$CHKLv;0gtzy665kQb^I%1{+n_e@M7AC5-vc9uW@!YQukl&xSoS zLXgvh)`D%L=)F|q1X@2(S$?0fW$G6R??GWB0WUzaR`35wBkqJb(^-ONn;vuOTB#u0 zMy3sSh)6Unp8WuOa;!5hIkPT61sl!FpW$%#I>EFlHjWtU zm$>d9My3dn7&u;sCwJ3t1qJnhi4J?w6U6SEd5MZs>dmua_aK-*@rzh?kbVy~$F`)4 zd5^dY%=Z!iihWDy0S}VTL+<5Q>Rm3a(ge;V32}a`8Dcfj?vtg-{A7x5jS7#O$S7QAyFLC`OgN4Mvg##8##Rc|v z7&B@JAB=u#%s1MZjMe+rg-y2svB5sq_=oFa!)}LlDKaflPOy_Fd1Q}}ADk}UzGWv2 z+Mwmrfd!X7;w|sdRRuM~Zi91UDyW$t!`VBoikGd46%3vZAnAWWDQPDXM4PKTNF7xv0mR^fbt45 z)Xli26Evfpzya&-@nT6h&W7Yk04)P7yhUHFwW#9(!dI6H*B3S2LBuOXU0xZAl{2di4@4m2t`eXq6I_F(@l*r|K8uk{xDJaU6rRps(d^w`Ex#k z&T;Az`@y8dXyGI=P=yz1!FNzR>Tt#9xwwamF9uV)<@O)K&_EL+b=C?r@M@&CHYE3= zn&6BJnW5c4I+W+DZf=3wo_-m=@3u8npqRuLhRN!Zv`uz{S|>fDHL%LmpW16M1@QhC z!RNqG4HQ>VOw-1=aK~e@2%EdmGf~|$R&WMjV6fY2!8ipB`|~*f{s>fY^PRou-HlB@ z+^ewstFG z*4Ztx?=%9?F64LKxdznHrnZ(PA*o;hPbz#{Vc{tdNGtQ|baS5tqI1~9;6x9EV5yc2aADcA=ja)P((ElAE}G{Nl#B%IaE#VBSP3CFaM=fKS} z05-RZV>n8G4kqglPMNN6@9_l#sr|9e;C{A`_^^Jd7j`ZvtMKg%e@7nX5zPZwvfO5! zBm7x_$si{WriJ5*FiVD`G$J5 z>|0LdT?m`hDVue6Epo6Q!J7JSVKt?~YkSxZ>;D+}%CZnyUe>g?a8&`G$nZ8O9QNOI zWq{dByNBHWrnaBT{~`LOPL%@vZrl)F&2rf3YGi(lgL-=U0o*War-hmxv5tK`&<}uX z9Eb|X{cjn3*EL~^kmq3E3!S0I$5NR|NahF1f7rwFa#iC3)~)eZn~0G^#t{(;?lVOA zk^re+5VSMHE)Ak^oH4m|D#vza}CQ|zkJ5dkq=MM4}8f@ZzpCDSN@#i4ny zx1@iG@ZppKQMEqa-q3-4wER2_@am_3JtXq2qx~qha2*kCyc!NLxH3|hn4HC;==#Nf zi!=ZZ84#5(z^?!rTi69>t8Jnv&b@=xUa!Wl2qR;=inE;W7z%FrEs=QB-o!2h=NnK@ zuts2bJ+pesIf2;zZ!U1|8Jqb_hYts5a1pXTb2L)lIS;dvjVSk3XVM0Wp^@_g3Fp6^ z;f4zee-^OqZ$5QL_}n8DTM~yEL5bQ*xi$MB0Y(u*PUhU@e2ZxPtO5|)&%IlZpAm{# zronul^FYjx0+%neJ~2cvSOtJ%%>vN5jo%$B0#>x}V(VESbvL4IIv_ek$Z;&je+?sd z*uPl7Dt|AxAK6x04fCwOe+?!J1U%lJ7KUT{bA{LyZ2Tb^2VY5c2uAbJ!stJkdMR*j;vBw|eVH)aWQy8VU z3VAkOL|i2=bq?P0@36$%R>h-t)#QAqe!_($lotbTs$krouM`-NI=N%ZdqhurZpQBg zfa)OV2Xv4Cz{DSeFl!Sla{cDh8HTW0$1-?1an!HQ@qGcTV?`8CPr6p^_XTlF=(z!~5+=zbTc3DIfibkaECXz`a0q6oA{n0Xi3@7VcFp zUjUXdTvIILTSF`Uj;0aIwF`n_N>LwdwK2Wq1+=X{Kg`3y5Dlt=l2Q13&M>GCb7Jd8 z8sK~az5QTd3s(!78bHMbA9tUgZh~ClS(G(QG<-EV2?r7{5EPt-k=9p@)ThH9%lQJI zVlBR|eIDjCn}9LYRq;JpNdViy=LFjU;K3KSS(ApM-jDCe`1EmLIa3SYYn|lteFZd+ zzMPLE;L`E?MJy?wL0D_y$cC>GrcJ?VR|s_$7s+iLIPDH~Co$G2pv|@!dr$Ce!1#+l zv8gap0mWW7$lcnIqFIbW*VI_q1^48Wg$RnUkNeTNwu(sP4N z-i3SyA7A}5i6LA=L-vp8d_Kc)fM~4s5g3PTHbUECccErH)7{^G}Z`FU6wRC zuO!_L_b#AG5N$eTh8lephqE`pBiq5%r*wijIOU|^<`<*Jsm1jT&aF^h9KeqF$7O&K zh@OMkR75)wdPN@*I6U!(3w5n54qjj*sO4vD4Y^CPObJ=-3`fstz-3Z8eGb4#RzJ8j zBIP)MEu%pLn=W4lR>q;5yfCxwgd0y!4o_u>%ib5fp~5!|>`9{u?_ehU8IoimImzt; z3=thjiiETQF!T89H?ZepInZsP9EuN^%DuJRD$&@k&8f~ zTxa(wZxBt!i4Y(GN&^sT5>i+KnP^=g^F>Y&`oy>a%Vq;!lJ&xXTNf--4aT-9*%DQKj2Ns)%2J!tUfg zMJ%_vp-P2KG%fmwoNcd|1%NStdoX?;M3`JVQ{TXzEx)xSm}R@OV=FR6c-qbo(Ji*& zu~Ui^02GCs-IKd)nS(cQ{25>~HGE^y{Rs9M$XbaO%5A zCyZ_*atx)zP1*Wx$@ZJ*C%10+;L;L&Nd+Y|(CtP{RONWz#~9)Arn z_wUevmm@q=F$-OTnJ#}vtsq3)Pw9y%@9Q0=pj-`EpK~YmY3oH2Cb*n@23y5Kj=d)m zb7dD1C2&LfZ-#BF^#E92OQsOXN&g4zV6C=D%#YH?5$S&_gxMoR@Q{=MbxVVXcjFb{ zb9ezuV-NKTB1Ds1dQ;JQ3amgzy>&Z_2*zcB!>=**HJw=Ff*Y3ytDA@G)U(*Zk&m~9 z4@DB(fa;gZnmVSf@6!CjQf_pp$sOOLaR-#aD0hd)_bL)OY7o;E&BrWCZ%&VOf|*nE zLT3uga66u3%YV=td;)LbOeDcPs2mnX-wayS&?}%C1OI56wE}q7S_LM3gi$CLaV?&7 zti=0u>MgU?*5DqqAEn;PQ|jN=U=LUHPy=2rMM+W9Jb-U)WZ<`x_ripYZX>0pq0H4! zPfu3){sJrN>A^;R2*0~$(s{erIn%V&V2`daZe7YHx9!p zcXdemFEc+5IJl)b#+xJKIKN)eBbA*WXBiA>+WrR)ErB|6wI5BWh|KV|(pW8d%?ZYy zlS_n_nN}M(IFMc);-Jp~PcEzJ!8ARV7d>^x@6ViXg1c~lH_rdLPf^z;4-g`43t-AB z7xBNwlBcw#;GN0ZkqXYRo8U4FJj1ib_gAD2MfhUvN$3DbULgo*5?0_%Bb<+wkaGgD z9-qKz$M)h)+4zc%tLmOJ`nPNV`rDmDR_wP>IwJq_rfj5x zdvcZ|3MsH4H+r;~13iXj`8uW|)m^w%<^gPu{5OWQOf^ zz**L@1*)@E{;eYvQVBqsLyx!p;Rr1M3h=5%F|Bic!^&J@HQu-7Ck4vLJR|jKfe>eg ztg=GU)6O^OW&6VrSl(`eiI5#(D9ThAl-@kDWdzgogF3~k-be$5^Z}42r8B%RfSD6% z!7tKwX?D_3NVbABA4&_9f-6f~7QhxkF(1j=K_zOVgjGEx%*!Qg(yfe!KsJJ}VtzL+ zc+9RFLxc22km2w5oQGNPdx)w^u)%3;Jqe%Woe9_ZW3l=x>3;Mr8lSBG`nPapGPqjr znK#ph-^Zu-gtW&$jg5^3(*^`337QsqM?k3#q=h#q3Z<9d`bFp52rYBF^1D>oGGX`0 z107}7mY`w=)Kk#Scg+ik3D*eCn+Um5p(NrdC#zFLe zP97B)_}wIGp?RhA5xt7zIjjj(b6P6p>L0*-FIBpp=_gy)cwU7s&_Ku89Mo5*U2x*c zLjxuwdyhLEl&ryQ!ha9GK7%0fhZPHbXMH!W@m>Yw---6mrq8!MsBSdEZlqBA&P?WL z&!AIaVBlM0me(s{LMB)~rgk6s0eS`oE(K;vt`vLt)&qk4y2&ZIt_`?>(N~wdw!C%| zO^j^>b+H@|tu!{{_$@`Hq@+T*Br{f6BZ-_FQjs$QRn1U%@Cc}Sbgf&7!*mY+j81m7 z3vmauNWlSp$Q9;ze{U{^7tF|DELTyy{;J}l_Xu)b1VL4bl^64gDBqO@E z9^s!uW?i&A`3Qg96MRDu_f8P0dLeO4aPTOJ)dMh7GQ*sglX|fcA1w?sgWis*^ittS z7+8QXn}6lVp?0 zWX11En71azx-<5t#5>m}Ir^`dD@aMDXQ{B6%nMOr2{)2n%mXbv_@2{mvEgHF@iXg? zgeWL5aC@q2OL=~%jZxF(B6hUTBi`;EGahCJhJJ3r_shOU#W=RE*mXk5-%#RH~aASz(AQLvU$0CqN5ejqu5Hp?|dnv?x^caQ?UQaNx+TzGLc3Jk}Gwf=hoo9 z!N7pN+p{(57)fOH;~(kKo8+Dv4M{-z^tke3Qlho$vjn6qgE-#&dX#!nZ)qtdb(8l> zg~u>K1d(O6Jl36J&tPafcSH<~0i#Ot#Z1~5YdHezZbZk1!L~be=sP_8;1Feen&nO@ zX2$M`O1|!QF9}DOB|7z~lSeUqnRPqDB7T7b9H(|e5B1=oloaE_t~>n!hR+a@dOiQw z(|Lywn9Gf2Z}Dzh3%B(a(mO%Q)&kvTJwq!X=)B5M5A@+5wa2rr!s>$WR{F)T#m>qN zM4bsTha)ES3vuH%UrvdY+;`BP1({&77Tz{I$0&JhkY5zv;t>Jb+k)fhI*P3{{1nvK z9xPaSNlDGds<1xOC#C#&KYLdNitG=>urlj(@8c`WO>fu#~;x1aA^O5G#bp zr$w+HQPmMeeVvq4@YmlpY(ne}@j`~&uJc6T1IUSUT9T49{PQhc-UGq^=ZjxY4G#QH zrOf=mpxgp`3Pl<)z0y*%pTJ>p9lL`Kv-&fhiwq2#=~wpF+>4(Oj{f@$6#j~YSZP50 z{fhNKL5A;*Gv6ab(GLrk)5ay%(#gU7u~-{a}&FS68^s6xDMV2z#jR!LEUnE zs4rdEXuoK*>cHuVnwODE58#FaUnJ3tQ33;&ZELe{ZS&)zm_-t=11Z{nW|RfOxZoJe)Fk!=2fIw+`(?WAHQH%lJnngo zE*}N1zw5<&$Z{d!jJxn@H3fVfk%Wh=HXc}6Vh=_4gW#{hWR%=?Wi!~(Z!4vIgHhls zO~;n*$>YfT_0y)n43wd~)fwmFmH5uKv=7U7HO_&vgk`qd6V(Ek zA1gDPiW&Q34}w7ZrP_-}ui@{k?OW)9$gfjHm8V=~rn+y7rR8wQtW5Oi3MI2 z1_leZ9>*sJTzNEncaZOX=SSWKKT(&w#Sqc>D%Vtu>B^w%IJTgM7$46chb0^Z$Fq=z zzt7MOtTTzk?oRCXrK4Q<>*Z(J@AsNa#7V|KoX#?7pAqZs-||T6=2^jw{6+UR!zm-j zQ!Xw~)FW;mX~$a(T+On0n`(TwuqrS|_eAq(fUu&SY&LBoi5)Ey;J_(l;19;0y>jR> z9UO80Ft7`oRIc;l&N>?q(Am`2A)^VozNw z-YHE11orF)kEKkwY?ImJ_aaO-=!*8RTBoaLsJGWhlz_=zoHrVK-qxUJI)L2;nOYsm zwHho^ z>ZCEzqBVw~uZs^`v-*Xy_SunW>?VEqbBXjy*88nd#W8Fp0tu;#?&q}t4X<*&4?cd( zrJ2izph1vd=uqD5N71gB!0(@Hs*b>hN4%)|nt3MdDS-mPpG`-}9hB?qrI2-kq4nb) zB^_^0h=VwX-}}0%MzUDdNXz4H;;7dpNdPzXQ~1I>tKbohCnq&~TZP%Q-jEwdCT2;V z@>NT%M!<00J&Ai$G^l`asNts72>wIN%x-UYuaH~MV%@|^o5Y> zdg`U}6Z!1I6YK2!y~Raehg|)W-HYskOsbDc60N{opFE-t3@jT!D9jN~5%u17rsE^? zQ>EH7_H@K}WSoVZf=zFgvQ|r48S0p?jb4nuXLl~-ppIpX4{fqts30WaBH+MST?$+=vdV8Z}0KZz&Tem9j19$xDY+oD{Y4a|uq__Zte zUG5O=Tt^^o+ip)g+pgx$pzYrG#gN_hmjj8PK{2*qFqfQ^MoI>1y7r9D z+{mw^Meb1g!>w?gC>-$OK6<6>MK5)Q?=~4{$ZAIK53lB#gq2)2Las$RAYT2GQq@I29wG6)~`fK>>lu)_uKw7 z^_S@w!0b+~t|J}Q62KfJ)7te%n@ZJtl4*A1&*yO=Kjw1Vbk_b@u<~dS-eePS#UBxD zt5NRz--CK5n>}ocn2cL;ykE`vhI_GC(CmYvVWn6(skrt z5JMZ=@lm~F<;*;59k2Ioe6QrN_K-yN%bCdnFqe9-Q<|EG6y3Gy)z=ww(|_Yh0*>Dq zTIj=asb#A3&$Zqk`WJRWe93cIr&f2?!J_yAem&i`iU%ys;|l~fXP^sCkY94&?UTPd z>o=xV-^J8)yMtE($*L}I80DorSe|np5I0>D2Ws?g0W#**d^dS&orP_7K~*ni5Ih9u zd`|C?@Bj>Dz<2cSDV$ByLy5r}__g{fiV)}5mIv$)@1=g3H>TI$);3cHUO03ESEI+{ zg{iz$)ydFm^Wri}6k*^r=a;nldE7LuJk7!M?eX-=+Y695dt8B!CW9yKjm6n<6gnJ46OdvI8}ej zxk71r!<`=}y(ly%QEHQJ++X@eX5^U7AX~bvv0g=2IHd1RBN14N4*Ru+6 z#QsI4#AV-4cnVTN<>EbNk5Ph%!U{D>bz(dqIF&iapxna8(cnZ$R)+0`!UM~M#agc; z6Zf9g-sznvp&wj747kWHHX?Wh#M7vrnkQsT475>2>FbqkkY~e1?AX`k0|73mmM>Nk z&O@*DcZi_|)gPpF?wjo9v!Dl227Mj%6zpG+$D-Q075jPM>t^QQxrVfEr*1(wSP64a zT4dzcAplvzo3tJKK#<2ZpLmDJQKDwCta|`lzo(lD@*k1#I5I@6X_WVXN-2ZNyQdcq zwo&i$w0z;~`6esLqc0~kznaYibZ{Y#0wZ&f|Rn` zfv{&8Jt78r2I!Hj2SoBof$1zmzqVB(ad@r8k!|>yT_0Cx<(!>=BA0zfpiAp2>^f zJ<|^?^h_uLiYh99cdELb?oF0WfE5FElTxAg_1RY2^U--$R`i9 zSP%V~7*w{Gb{FsXQ^5xwQpr)#N?HQrm2?1H zcqW$LM;1Q96#5AuGsxcskd@6BI4Y0$s>17rtUqjD2fgzZIqB*KD_&pY)d{LU-#?b3~p-My{al6~_7zq_NX*23Ch#M}N=WCm<>CUJN zfw76L%=e{C4#9jM z>p4si#d@QrAK4MUZrN4NE)bpuw?pDFZwN*o^LR6G)y=uv?iVuR&WF(xAWJW(kMc2m z3@3CDDdyuqCTG2{ZC(WY?sJQxRLj=xUOAB_vWo4&{Oz%?N+)0IQEq!WBA~8ws#)RZ z@nPU%dlNhw*JgNyPZK+RrC`*9D5xX2-n~(ox@fT|(>EJgcJcX{)WkQ2sS3rOCf%_`eoo7C> zWVaa}CO+caXOg^(tmo~Yoi%*Efc^a*)FX>bv-6N2Pls7u-ucVnH`-zFs1359{s_SV z^5;2&b-Hz;ox5A$@Dug`bJ4!e3`t_G$J}6 zjLGSKFBm)*^gZ^{LdgPkA|lN&5#+zm#unLA8wx!DT(VH^thvv9n?W%l>>gSRME|bd z{}3Ploi|C?rI#ydv(uam0F3^bIs_OxCq2uo(rqSYp?L<`!8b!fMYjg|JI_ZwtzRGp z<%D;X=e#ks1gU#JAqW(YxNhSSK8D~Z#vv=cpNdb%+HdA|3@(kS-x2#%uS>^+krJR= z8X=U$j@lcW(?jy;9f4-YbD-|(@KRH`%^-FFip*^>89b>DKdgc(n&}#t*Ddp`5}+ zaL^RL5WKx_m~EGI4FpUNQ4s0O0m2b48BPZl0?Tu`4d6L~FcuYa2~g0aLE& zEMnd{nNdrHC;K>iZa)Wakl;}16bK?VDD-~aD%`3$196ZRIM&fAn9t$2bML?Q%Y(25ktMgK;X$`lG;S}Hf7W66k11|39b zx4mv>_u!o%CwGZh7u}4oqq=v`I`G{lGz-YHAQ1U{#H}krI$P ze~4U+PTI1TPm^(-5LktTkLKUR9jY@4t#WPifoC7od9^e89n63l+yz8P1XF9bOL}2Z4vPz zOKs3BCAOvYCf*SK5B#cZw9tZ1T1a?K_arys>?B_=1iKvc@MEQwGn|wb7(gO6CnC>G zNq=z}Jv+Ye+C0t(57{XJmV6H+@C0{nc>M>SlU%#oFCOHyQr?r5Vi|_jD0J^&vFxEF zNa0G%-5ZJjLEAL|4yCVF%_8fAy5usb0qAWNLJp;d2V?zZy7lvA zW!EsBKv+~oY^AOvGuKd|E&Fb*fL0i}YQA2M`=}bs?Q=sb(7EXxRuFl4UrgevAR)Bq zv}4J(Ufqsj&2_HLES$W@b09|!R@{TngJy&Od=2!@zGSpLWaSON%Os3Fnc;%x z3Na*;@4zgi@|&3}`K$-0wp|5wA~%Qp7YCBgb9>3ZA-~coiIe0Om2az#$a^SAH*0`DkI$BUOW-i==uG4PD%~30br`38V zzw73bIUVuaW_zaGqQ+{!Hz~d_F1ckhlIWHwMc)kyt4L83x&< zJ`CnbWq9ay;I(f>bP>Ct--nMdlgpAdvsZgOL90Yv0HC#y?C;@|qV=O25K@hB;1oK6 zTm)+UZSXf7OWvPu~HQtUaE-3O0an zXWiFjo5Jk(@l$|j@s;AOku-G$)sioM(5PBhm>Rc|Q0v3PayRx%lKF_-B zA;U|yshCzK7BX7vUly)jgyjZ_Q1ImEcLYnw?9ma zLFmsv2b-pi>UY>5*!lDMxY?M~*0kn38}`5T3Sy{R6W*eNVr7QfKgO~5B?i5n5M?*1 zv%C#&vs$?|oh}+fB91?yok9vg7w*AfZDjNOvQ1Ma|1WfaZwRe5L7_3LnMW62K46P4#hcChWBFy@3`-^*7&qu%k`Wcc+yY9>fCC^tc`ls&Jp4j-DfqvgPu#nH$ z)@_WaTL71Wwcvgd%m23q@C%!653PgZ6Haa}&h;f{HomQYwA(YAmd1x#29m>;#h#?G z)b4cy?j`_NBKn+xEkV@wUbyLq@naqpBP0k{(@51#>PeDz4c zjQV6HZk4X_L8B6L7taagm!=MycuUQUaf4}}l4fF%f1!;peHPPG{OiT2!LNbBWw3#qS|xW&moZXUOw!lI}G1C=QQrxhwu6d zRDfK|U>4Rsa3eLqGP%FMdzL~Ft64)2_ht?ze4VMB*!nr&3tQ$sD||`kA!*aAkp1lD z6a4`ey_l~OC%y_kKW&1q0>1>++s{DTggd@ga2ch2uqLyMjxM?2RIGk?u>QoilMnhS zQ)c;k(UrHLUk{x0INvW7pfyVai8u6}oewua7VQwbWhG=) z0M>PVOD!bVNglq=y0vH2O2K!kWJMHur0#2%(<1oWGyJdG!J`*$$S{w^e|pU`d|s_; zZhRr7Jo|JO#ymNL-DtRZGN<)u(83dvv6{}j$nKsbhzGru2RA|UYC@tge3}t9YV`#7 zgZM*$1<9e7h(O~5(y^y`mLrjqUwD~z?J?~d!z#~$zsQuBvX?@wiJll=fG@sC_UMAIV6A7E z)3FVTSxLu*AdZQgd+XxVWtOR5)>P)np^Z}TF2bJ!0ITtu=uU70w~}zz-!Lh_aEaHa;-+d4 zcPA6{lX?3@BP7ph7#!1WI|?@JzKFL@npUpWC{6E&^oC-DD8F8|5-qjd7ORMrhZ@Mb zS6?q0@D}%TewyFck3O_dpkV*U#?s}$N~`>#?4%obd57E?h^~;7GRXcW3{+r0x~Gr5UUm9Z8nR2K zFT-tLB&fd=^_eb>ub(gLaweqPtr=wEur9s2Wk{lQJGdNGz+1Rqt$LeFjL!pxgJ}M0 zXyNTeFV(==KQ;uyu~$(`Cshh;Sz9sA!DnY#E3TTT>dVn^GB%fqj6DEVnSeyd$#Hu( zkN?(^?-zOg%9s>)VHn({Ba}d0aJ?s{+wM`X1C4-mFofg2pksYEcpb>v3JRXo32aRN zu5@NakI84^qZ79d-fpc5YC5gj-0|RK<#=9$6l8m5zf4lb%V@72Jy6vaRag;~Tm7Am z_{h+~WWlDhs;L&K6~mK_l925uQ=^>c%0K(=6ORNZD)Ga`D7ev41NTlINFA?n&z`u? zBYeZVG9cNH8xlZ*Zq$!o$6jp!#RvdOeZNKKdOgNekVCJh$| z*EW!z=XiDd-ZjMx;V7BG;0U-Z1!K=rXc0@61{eJuZ@(BdvHSU-gNjvW*6`%>LjLCl zPUbR0<1ZZj0^jbTqTBz{4_!JFAD5^B*iGe2EJue6LHDCT`mm-k=Xp9><)Dwn(-mSQ zQ!ip(od z96`NlHy8MPVAilru^E9gtX&){-fS7y0V--&zp+4V6Pr@9RfKZrKKrAp-##Oio(ntX z^sJZ*(lx^wYB;-JsWWYyE8n%awQ$$rzj}f`n}MQ#YVi=Q$=-d}cKGtSQvK3?EvVvq za% z1Y>DR`YXug(vL809mh*+%mp9nyWp>Fwe~GB=Yq;Fk!*7(7dKnQb%DCkJF)YDEbKFj zUVkKp`6jR1hsY{5BfF$Pc`IHPBFOgX&&vY%KU}&c-}|`#3dVM=2+iOcJB_L_QRsg=!fKP4^f=J$rj*kJj&QD8X z{Sz6>?`bO}ZMh=aBn0<{yo0LZgo3TZ8H87$%Au+O;Y#0U7dO=JE!&eCKHFaT_7`i9 zJdg>4n2Goi*z#3All1AD?cd_R#Y;ehMu&bdz$BP=;ymn`qm~Hs7p|@d%BxC-KaZ9U zdJi8Lg_3g*R;oBygG9L#)pFh0|BAk8lS7ZoW02p%euCiS(18qXtsVZRI*W1 zgm4kj{LL~hFsQ;|V*b|^Vo2y(1{~!(#`x@?td8nFSUFlhxC-(bH`gpV^j)zZLn~XC zboC|+@o&*Y(ai~YtH(b-@|0P+e-5~!hWj9=FAq%~#YBHL`%Ho7u_8sqRt`z`R%_~K zxRqHmo;fSCRC-zWM=PcSJTXCLs#olBT~pj%8N2jC$oxEu&6tq~m<<*IZa%!XF>SBH zK-j!n2EuhRxSh6c?kaK|#1NM1G`scifrO7-e~khcfiDL=s_{D6rdz6M=4*Z?=-KXr z&~4wWaf{K@I|*(@I2N-r7xqV3OKk4UT~jlDM{TQao|W#pS8bKuR{=pDVS{E5_g`23 z?zS6nOZg%TA{F%Hi`<2N5Zi`%Zaqd$XFx52viX71FT|=#tMsFm$M>|1tV%LA*e696@OSeCxhrry0QPVxL zYd9l))F9)Rn>2*hSdwoB2PWQje@R=BSelaPdawac3Ea*-1vE@t2qokSYO1wFZKhaX z6N1oYUtL>Q)vwQrkFYn_0Xc#hpk(T{g!b{KHNeXzNenk`py#T@fo7dl5kdCB8_D0} zvtt^&q=6(A0i(Wk`7<5I^-E{&&dTa$4)`IUlD_d{ahTTC^|79+ZIq=yVW;2vg6t(1 zoWNcuw~1X^vg(B@o0K*t$qtL>Sj@a$$g&4z=)zQZj5kaC*;}AeLyU7XgypijIYB`} zs^@;56}tmF*X_gNsvI3grt-LZrY#!yg;3IOy}WV<`G2VRqM^&onN@cY4~fm z#XU*h(Oi!N74ZG7dXU$viv`wdyHJ%n_(MUW0>H{P0y%<^vEo5RpDt*$yAB=m|M=Cd z{9UiQiwqd3S?*eXn(=f^abPxI1gTsJAV>&uzgpyHfBW+wQ|wN{*`ebl*0a^o-`pyQ zC45-pr}#i~gIy+ZAmHD-_z}m5uqpG3VRT^*|p>Z0DSug81#}uJ+g~f#q!Aww@x3nxuM(Ts^!xX z-SGgwQj&P%d(PyjoWY$*uy)(U7z+U|eF=Wa~JHw!*& z(~RGPw@aFKO$Ggmp<|KYD<^+_Z0xS-w6;5+kujQ>mS_vmHUdsF=LSTQ)IO|ry7BG( zOv{F3yHhMypC%T!1*#YH>o%o&C4quf6w$X;iw`Tb8}7ME);oewF;A3-bwRXWBX7^+ zZwzbGjA*&BIid3618(%thhv_*zU#ZrtH`15Y~|-#VKnpHz8@Nmf;BnuY6{)y-2$L6 zSyX|*sXa9;IW6dsS6QJeQ%<}H=b*3&6uig{baO;bxGQG_fcU1Que((P^1?0;k#)@y zQTW&IHUSHUFXSyoeW6)lpC4v4WqLjEUQC5M(Ntb+L5cx?d-Uc%(#AIq%xgr+e0)<2 zCx~z}$>|JH)qy;ptG2Tn_ul6cs0N#NLXTW)mP;6aq>VB_d-V_t_+jp)4$j8b89DiVCiCaI*o?TYBBx#=j(Hvpd)X= z+~B^J4Cki-<8$M+!&w%fRr&WWI5jl;6C>I&ZS8PQ>q2(2j!W>@cwVe}C659TWsu(G zlINy!klFVIxk9)UROChx1zJ zE`9NE0g%+e^o#RS@dMU=K>>Ot)sfOG$}BkmN64*J^rp976SH5WYzm$swj@1sdKRz+ zZjFCKdDkQV!47&8K7KaS=9%;4EJE^D~r3%+T%n`i^E!-El!&|8I7BPfqXj@6hEkJl<{csmCXJX zeB|3LuL&P2djpRT=|;Y0zMkWUoSpy|^1Ul`4H*58UgDml$ihWv*hsBSK{0zW?YnvW zbzrV}0CxELmR;II?VE>O1AxFhlVvnmc+*B+Md+7;%rNF*vocBq1XAXe(4!64ch8=C z;MrDQp=&|Q|(u4ia5X{~JpYEx8>LT!L zPv!HQ6H`wkn!nIvS+vNDsRSH%l3^dgsz37GT!f z-lv3L6;K5jP>1#Ia#>)4m1uuoQh0~ zz^`r8l}LPWbeclQdvgw9TX(tDIZb<}bLe{(ZJKLET!t^z71gEBlLGGpo}ow$ORGn8 zqX%AJz*Wj9mr+?U3-_y_*d(E(m=tw%fI<*UaZ2K<5?P)^eLR7?tvdeU*jAb>%6jEE z+e#+$gV7fFh+b)-=#rB(&Zk4Fd-pkKXz%!Gsa_u`x^jj#p?*pbWjcP+o9cao5zt{l zI1xMHQ%Awi#;MSnVHz>ut2VZQwe`f!7Gcuv_HtI62TqnV##?tlCkjC^E`He9H;4v8 zcC#5^t`;l;xQ>yxJA}2~RBCE`ehgH!K_#i+Fv4~ZJ`@~E?-E&SVh<)>X4eXLsy9LS z-Kb0S@s_i~@4irJca5uRn_|qKyw;OhjJfeNCLgR7ch#ThYG_DOn~fV< zB){ygV(!QI2M;B9HWB6~^ED8-q?ziWjU&duFKz0Yyvze3q!1ud+qWq;`b?qa_~!VB zrxaa}_wlH_1+YQ;6#5Z$8Ka*ROqf%c7MW!#XiJ>P_H!QU`jE{qU2fTCHwe|Hg8lAIHR?Ty*e2s!ZySn zX2+LEY23K6DYTBT9{9%Cdw2B|UtTS5np00oH9c|6tfHx66?Xmu=Jn5=ty1%f2aEd? z6IV!3tzA!N*nG&$ls)5=uzWEhF@$q>G`S^ zO*Eah@Hn=|RQW@b{XLrj>vj~eBMKszRvvrOaB5^ugt?ZxeQ3Y<3e~ufp?fxoB`DjH zR{Nbl*Iszy%XmA#4Jt6js2Wu#k7VIzi}xEJW>ImqrYL2|BH<+_n$|r#W(YZE1Wx(! z1k~9OrMo6B|0BSxA|QA4T4vWTiS;nh1N}{18MY?B^VBscUyKi~AUhI%Gw$eI^+0ZM zT!&e4jP$X-Q2Aex5aGWNZbOvex8zroDTdXCNw-(@^+5~bbJ?W!`_OC%bAQRgUimF6 z>V59s`r$YCe_E$$J1M>a8}O(m0DR7JG_=}gN5^&Iao2ic5F8awm zrcxe)O-~ivNJU}@9M4+2`Ahe_X`n(J$_CW)?DDeMW2#sxm+ z-p6iW1?gRiiC0;Nx4h1!a=0Y%Wn8E$txR_{WF#`=DX-9#g9)Nh*iZu|X~ac?w!s3c zYrZxcZ07d_tb^jWuT?(x;ld(yp3$Zk?Dg13vmN?-dT^ZsPi8Xxa`8^3bwS6+P>^GC z(zm_&7{g)LW;gdO0k5M3R`lRM5S~#%-~D~B*7&{xvh2JJr2frTr0mGBYx`)Y=aBX{ z7RIoKpaZ7e66i*egnfzg%vz4OwLdiD9yvt$&|mS?2j_Oc!NiI!+sk6^{Mk`g^{iJI|8z{JSk~j2mCLAEbrTq-+X6 zf{ybIB6=PfJt;@;Q>AFhZ)AC}3j^Z})*z+eZskH4PD{naLDl z-({8*q-UKnU5Ih_&xBdxzf=8&=V!1QyN5?HD&!UV1R2NZK`LG|&|h}|MV+!(8od16)GNs3 zxS$oqnkERzrLMD&H2Wm6RJ+(EmZZbt>G@Dx0&0K?FiupcDI5-(mw?na6K1RZ?)ipP zUW~oa{|B*RF%J*=T_SBxX>Bs8N4ex7&>?-QA5O1#`!hW2_dR9&(?2knsgg_4qk$Hg z=QBE>;_f3|DXDOOaEX8K({Pt>Bd}860@cP`LlzE_6w?@*|qK##odk(P3bLGgU^pi%t(LwkudI`qKoU%_r}A zd{Oul#!s@F+AGh$qD{iu>5u(U7G>QxP)etY-NIp0$VEG?AdAD{7g?Ljv=E&#L`0MNZy$K!UY4oRoarf3~yiP8J63QFOn;N)Iz zg4fR^m&5qCiGV`kVmba0nH>Xsi`zZ-L@M5C=S$kQqIPtree#v81cYPbX_5vb0VZJ< z{p9jP(*_wjbzU`~Ac~tsYtc-eXgs$g<1{H?-?rKpcxi6znjK^cJ?SEY)K+9zJ!~w* zlIQqfqp>q>UjmO~_0_#!7+lyjcWb&{-dju$zP`zF{13EZNSXS>@jlyR-{14YX1>~3 zxUKf4`TQnG(pWE`_+oG`1uteyE58^k<>A3n{^=_VF+>d{VQJBMVE?=rBnkeM%&SlM z>1;!}^N1koM(CD*)?#bYN zyuVj*taC;-W54*zrwu&niwb1VaTQ4OzNSD0E)lh~%HQ?DKS0td;QiFadKJ7N$D$qQ za}y&OynOaLm%I)fP84AY)S;_v{P-(muBrr~Yrjiv`@ATmQ`phNsK}xOq9bRkB@zW# zpz`5BTjC004Sed83gkXId|}ghpvW77qE~D{@xsnMH43ppBuTv{yW*lAt~kH-T1tKq z)cf{NzKs~6#x{fvqIoILgZQ@YK6*A;U*+nYh!)uSeL2u0d@i@Kq^@<=X<8!c@yMh7 zHwPOp)T1uBA*xazh)}ao`3~IvTkDeg*r4rGQpDZzgt9CG-=quA^T#TBi&cf$UdQYuH zn%IK7;41iW4;d@&cyA=R^e{djN!TSVGF9A8n=-ee7|pm zB*&1-cdC&AJCi{&@#3Q1ufbmK6>sTun(lAmc0c(Il~i;_R?J&m-&TP71v%nF|}T z<4-=bMH>M!5^n;F3-#HZKJjVij`k^9Uot^dr5|c{qZp%+Ax1)4D{^~eZ#`g~|5W47 zya=R2wNMn87z_86rcmgQxmhH6)i!|mP2C{l=_gdcOI;pl?l5eZ=`?PCJ>QTJ!4=BA z_yIKvp1(@mI!&M{QWjyhII4s>H@?^_A+(RBrM(*7<#kDNF9gSp@6FnGo`&49`g)pq zVilN*j%wbOgInV4O|5-tzvf|}=mo!A`YMR;-mL*EDLln;&!|&6^RNpW-?g0=^3+V+ zFPo%_fdlS%2LMODn1I7pWe^clJov;}27Y)t&1}1D^sf`u>%}eRkJ8paLMK<}fAP77 zj;!gGWW${sxFHs+p!wfVDaG;a(2FSD?1V~8a1^%yi_IMP@K4Hl@)$lZ_#+aq29gfu z(*2OD*zeauq&#oNgwzP_VX^zu^M{~}s+s%2x&+Z*$DygO!X$%l6noU$Q7$S71HV?U zB&3l40T7$m^ULLFJ^jRQ@+JoA)c`42z66NXGeh`SwkCfv>Z!MWeE%u`Aio?_gc|P# z;z0G}sm`m~PmBJvP<`ZQ}MVveT z35ezG1>~-gG9x<;p(&SuCm`Id2uRk8%rx;i$+D#6gqP2Pf8DQsezr)ZC=!qdCuTQ{ zfbhCr3hiWV9I><0b)SQ(Q%0>@VI=5BOWG5OMnqJ~C~hT3hRRYkx4k`MTK5xxKzP$k>m=bR1yut62p? zpL4&Q=W%vFbtT4Spt~L^$2`%ME&m{V$)`y}vAVHOTlP-A34=)2i$h=DA3FME|Eo$#bm8Y(2cVk1?T zKr}Lc!G7K5bKT`V3-OSpd@dqt|>R_usw~{Vee+*sk8y=&TNL zH)inkVPbUd3x{N@f+vC za|M39*@EM{BMFi}e?Kq#-LlPkYC`}&b|MB}@j8l|>L9fZI#LO2jaHd7m}xNiO0dTT zyl;w5;Pww(JW3N(((bm@_p~of)jjG*-+pv>$W2@q?=de9S#2hGdTMFl!m9t{TLYPj z)(@5M2v-R>WC|xk3(wS|X6`WH%*XU@dLbl>;Jml1p9O4Pr)6eM}Ny z&IEj$dZi*?Qxv2pZtdr~Cvd3t=;8#Tq|pl1&GajriJy)7J&M918yS;g5f!LX{wz|e0dJh}K|DCIq{ zxWb~FQZZ*HY@DSL7U}1VcV5Uff-8f*wXeQudtpN; ziqLVlVyR6tOSW z=fixvTzE?4a3DWO+C&)A(uBr$dwu_6^=J9R9#Q)OJBMHZQj}&f!&rcnASH>N-()>~ zNNf@QyhPRW=t=6!fE={gH`lQjz7<1H%uOWd5)#*Kp^o&jK?&s<1c1UG&F(7tRDKSZ zaDNPrEFP{Qh3@c44B;Yt##T(|vrwG{2~^%j(#{M(1ByT$BqpT}_WxXwbr?rpSKE?yBJO zfuG%C1&Wm=MDTP)Wy%cJ`u;5+t#!b&!)uS;VJk>yCyKbNGS&?k$5FA;gH1lH2hnK0 z*cPm`eWlI-qI?~1+x|@}PXVG%WN0?f1CRClfoyK);c8_*ZU7BqT;+vT*vO-usg8?D z6K8(83GNzLh`_hw)c&IuI2a93@HP{0Q4{_ZixnblB_2Qp@8qUdYOiMF_d1AOY49nFDmgJR*x#i%m@J&@v?llw9sMab+=FJ3{M8MkZj6E;Jo19 z26ac7M20I3WN`zGlv+MEMd=kG(!BU&NQ0U*y2`tuJuuPAi6~BNN!e`FrzB@%|hn$!?wbRSsEDH%^?Iit=7XzQKuH zsOVh=&>CjEKf=!Yt3C(z-+h;j2d^uE%C6m%n5a0L9`zlKjr8mMHpJwB;zPXul(}Ui z@qi3TJM{{%ReO~EW47FmglMkS>#CC{Fzf$gZ}3Q{*zLVC@En4oo7l+D=-ia%Z7bM= zn|$+?t@Y=hxRt_(0%){k*Xta3qpaxPlRL;jAH`LDT>pgpNI3{YB!j>rMB~DFwggP) zq{usbjb%A2_fd-I_x4RkZ#c9A3-A=I4sCGeekUHzCT;&o+)%*1waH&(u+X zZsiD$-|McrI_nTM9?=aV*0Q13{(N#sUN6;;h>E$|s#&Y$6+ zQ6WY9EdWP`m|Woaxn+k?{-j$Epl0e4C;rVFXR7@g!3DC`weST8L#MqDsM-8xJmTTl zGNX0xisgw4v*j(K2p5gCLAexpImKZ&hj8;hR8B_5Pf_Q`vUkY=KLMX+AY!L;_cAMc z?{fP~4jR9Y<=E4Ft4!JZH72;BrXtRtFU-G|MS}K}gXjkKu4hm;rFl?JAB9QPS`lm5 z@+ui{-Jj;0ye%&MFcY~^X&ZZ6g`(NJT>hyTs+!V&>|eWW@!Rv0_YGsEhiLZa53=Q0 zqK{F%HUo(9bbEJ~O{7)6ESK0*%497Wr>tVgUeP&~etH4Cup>;V{QgQC0?pT@y}8TM&1o6i%;)x!<@}3Npo;^`3-6z;WO+qt?i~)ZW0)|T==y` z-fDYFP}?>xfA@#A+-&+wFXNq;B!JbN=I3KeStGormoI98*WW#z6F9p9K6-nlLAyaO zf&)CJ{l`filflc<8q~@&Z{0bh*{(qdd$P^*40oPe%KWYfUXCp~DE&^%@|$f%4v~e9 z9FGk65cuBV6MWHlzPtj?g_kk=HV}v|)f}uE7h|sVN|1{2=BgZ;HPzF|y6J}FBj$5H zp0R^Aei)qK=tCUrA6Ihzvj+G(0k-m#Zv@C9f+Bf1p~e#df5B5-@)ve->|+j>6KZ(L zsf-F#(RmIQ@>3nDDbo9JF*EI#c8PMRY*!PdQS&OO0=BnVaAMNN4H;{J>Y#_r8%{h$ zT>(N8gb2I&UHm+3$(hv;4W8J78)DR7ieS4CFW&ToXo z3HUp8@CYZN}xY))f_LNGq_62ujSUaZh2zz((EzPyoo+PaFfSxTU z9>I1WnLS_{rB_XKHCsW+kC088UT9=KvEYAj=x+)fCx8s-kJ1JuH#jdQN+0wp;xFQa3g}Mk9WE8tPWA)djKcs z&g4q-xBr&u_qg+ySAV}fr>BgYl=#X>twkIGhhOn;++&C{1wm)8hBsr!0(}}$&Lma7 z(}adkTF<5^Ppt&66WxET6|39?4t-~OS)wD;yG}%KKYAihFiRf&PX_j?u>~`OhbG>0 zEghB|ymcK>z6l)p>yYUNLJa2fAw3vE)d`e``(P{sp7IYt1$2kcJwV5|?1QNz-ImMCRhBtxw2f+Q&Kct(m#T_wgo zZ3~orIbQmu7oT@F?8$F!6=|u@HrL?nQ>A>-wI|}q?*WHJ?zR&PNGoLC!pC2V?x0f9 z;yAt_Pe`{73q?TE``o-uizl5mAb)zaTSgKsWls?(vE#xFsJHqX!X#U#Ky_CIdTyro z%R=n$fySg?ybfUviJ)UR?jehFI2#3;!iC`Dc-ZHfs-DcOs)nPIBg3=4zr*L(fXn+8 zKS>)SvQ%*h0SRR;Wv3LcC#2UUs1yO+V+|RP<~|AsiY0|$C`+lGPtlBEf?hU(z6m zPpx1q%9WS=rwA`|?B1yqxS;SuL1}+`!>0w%qn);*abw9uewnX~0acxo( zY6KD*vC?PSgCl0aA=#uda@@Iw)?oC5k9^jqPy-^}8ciJj4)%es!(D43cLFw;PU{jN z)wWGu`|x8Cn5pECUzFTNBXHSzCFloJBV_!zbQ{W>V44I;ZRKr^SWrO$!wH~*0tvnH zV48Xg&#CJ2SplqP6cI=iL?-zn?Yp67*zF<|$gsmJo5F25fP{jMYU zEew9iN%00XABb-+sAGcdO>k}kYi0lNI#>gs)ipg&ViNMbm#~L1TbwH!>4{^|{ z002%!4j3I1Ftn7;OtU}0YKFz$9cg-RFW>MOm_S_~Hx(rAmm?5hix1Pl1)MlDa5JI7 z_=iFsqXZB!vOU5$P~YC&_Ose9UKT6DLL`c%lXqJ1!=-^*u+Ph)qRPqPI8)5L2?x;G zo=x#dFECs|-*td$enbozVZ80#dr3p*1y*{ukeu^ElMKvI@#N4l9+ffL=m|-l{mb;g z5n8e-a zB)J~HNBfivf#M2uOyX5({B&w!PLzvPc3y+OS8uGLeJV@Ttq8UO%@l2g)2s94IkwSJ zRHi>_5BDFq1#hx3ZA?O~HgMibl$(Vke5V!7IA1u@6z&_sIu5uXClrD-U;U3?NrG5q z``9PV>0A9{NT@|grcC4X-FI2$Y+iO&EN#md?&qeanRNiqLE9|fkQ6UYH)$%^#In~D z3xhKsg&U0TpSD<%1KT`B-Atxm&fGMh2flj&E_}Ew5&NcCMOAyZ9?phl45VK`^U7Rj zUwL&aYu)kFk({f+X-$Nd$Y0=S)0DKqWCUPmO7NfP(X!4p0SzYL+$5l`xIvGcK~JaS zmjoZYBXsN+4;Rxpx!zFB~z^5aF; z(IK2fW88}u!>Q&T^vE&bDJI^XNJKt6|4;0jPx}pgVXeIZpEK_pe^Ii9`KOQvMuZm7qxqKS)P2Z1%Qgu&d*#_Eq?gq!Iq9TKm^rf z49bt30nIFU`DYxM`n$rRv8g1KIto~@jKw8b9oIK4d5orQ*7?%|A&4{yU_LJ-Ui~y3yo5k^~wRU?~Vq zS?j}cF`E;_VuHUeeOka)fZik@0JZ@cl-!tzTcger2vLmz>eo6f$-TW?jSjIXzE58P zr`s_u&QA(ZVoQ*Re4z=PiM(@f;ShWshq3EEhW5TeMB!#9h(H4-PB1kU>oG{DR&u zEnw?3_Vou0<*%uyb}yScP`%s#Y;Kmkd{$z-;pe;WoB%!C3v7?ImXuuqoQ)n%tOyRh zx}`uvIxnA1d|KIH;Ig|S>Mo=A--XNjAM|Zjj^L!$l|R`UyZc+a!Ue`Y zX$iX$@A^HpJ3chC*9pR;QI%y!B4g>gEFE`)Oi*lRUxvUdOU%H|3Uu9oe13v?I62{= zknR6-*fSRa1UL1W%U^F|xfh5?ku4(N{2ODI{SAmFU@~H^+_Q5{Wea(IH0>FzKQN&J zpIg*TJq&Po1Ww0a$>d!Y!zD+60NhXkPI3?5p}?zKE8{J|rxbNB3o@_r6CkuZt+(&3 z{Qulx2j{9c0y-VO0hz#5h}qu*;~^X-%0+E@4*-?}5nzOHT;>v|A4`8%%Xxj!l$Qya zYXg^qsZ!HWMXbVMP^DqtW2ZWc+LsQ}cNOH2t%?jOF8A3`#*L9VxQ&)6#-i7Qqahr z1ckx;Nw!E%{3>g=hkaH`Wi4#lJrH|Fhy!wVJPaNrS2@~+PmNA0OqrFgJ`4#ItM?~93jRVM%%ulgb529cTX zHdp^0ZB@lRPSI(AN-Fb_tva124!pns?8p7|s^th~>h(gzX%Ni5tVP1y+F(?b)HL16 zo@@gW-Wxb8ZV!xbIi@>O9%M(faX3bAN4@>0{}Y%V6Q1d?F<5^UIUc26b!0VCoT4=W+D)kn}LiUFpfui6;cY`p`71W29iOhFi)vIHc9 zYDkLPvL)D#-bBz{*?GWiF6#URAR7E*Fpdf!fZ=}_t-@AxqyeW)Wjah6ds|F&K%*KrIX9?8!f8Fvr%zk^u+67RC$oi{M-R;8FuxP!PXMzH=#tn4R@;GEv5AX&eeEj`N`{-lIo8aNg0k6^v|RW#`f z-k-aCN>>cdAD6Sj=}(Feue)A!>pth^D)Jf$J(+08;`V=Uxe(9)FO~8g5tAC~`ea=) zr_{;|ePbTKH9(R{@#8K(qxth+?r7VE4KDWlFJN@+10HO)xHA;51FF~_zx$Q{WrqF9 z!g-6gJ%t1K$VM9Gz%Y6R$sY}q>AX~$r3j1b&-t{n-mXvi?`4XA07TQT29|8}XoBRT z;vK+KHGN0WGcl@QIs!{~?w-dt8I5J!z_i0Vy5Y=b+=8`mW?^9uJx&23B{^_WhZ@1i zf-TbxM_l9{lAG5vm>;|iWZ6eVt26=)yWwK)43_>yj6}JL!inhp#sr zU@Fm!M_Js|uTxtwy|*BUh9t?5RkuVT=n;b`@b8W`rI?=g%REg2;B8Fv1$PDod~ze` zTN%*{lsxFJn{%lz!Vu`O zCzp4~s|^cncI_+GRHT0fFXd z>GwoUn3lX%MvF)Vwj|vx4<>s^t^oR%JPzes5rz=|(HY|QL;*s;(iSY6ko}i z1BTgMI4MDjCT(6u-SjX(SKw>fxxGI`Wt`zZbilt52%X5~ON#krrajRZx!AfI3O}o4 zXf`$}JfUbM3H7#*WD^IrqMu=esB;Sw3XAJL%?rqg22CA^mI9cC9=Rbopf2(hGtmrCYTlcfaiJ z;Df|Pw4)`hD7EnN<2i10+L`)AwRy@K3cgqq>a9M~KT^47+(<)fA-~92)~m2b+5(v? zkQ2}(*m#4;pVL{fK3)T}9)gZUU0^zysJi9YNNRPqj_>E!6yBYo_GfT!x!Rvsgt8&S z)EhJn1J7}@oIJ!I@DH4*0B}Cz=z$+~sV_j8_mM`3o6g>Xln>IGIr#(6bdd+WnS#@< zLD}4JT^D_)K&G@~_8hkRSaaC?8q!ZwhOV*@TAzzrl7IDY`Pj+Da{2(?5@F+@G(;>; zG5H|I_)&}aTXe^cIqLdWIOJkeZtCfH{Rr0kZGFDC4{p-IxV^nxOne8|`f`_B-UYZH z{9c*_oUe=>Z0#;|$VCtSj94VL%i|_BeSROelw2vGe|^?5rH{9|d4j+8P~`^`G;*uY z$w_9r$IX@Qz!uq#xs!@wIzBw_UMEYq{5aMuth{ceCV$z$VaOkEdGmF^?~X|>V>ZzM+p5BP88g$>ylpgR#! zAL@xDs}Y#Btd0YgJi4zi%;E}+^ZTm-(kS0*Qs`+bRu|w%<@{$w9XFmAB6^-cKFO$W z><3-Cr?25JAdW2xe-R=@f9&LV=<`n*O4-IKHNx@GqWj_m!#lHKrK~o@qu{yY55el! zGXE<**`I|-1Ack#tcJ!GdLMt!h@avkKUmNY#0frvL(GJTRV$4habIqFpHD!~+RHsr zwIMb-_J5^X`NJ!fCDrcDW`Ts+b6ZA7il+}xR+8WR>k26#No~n}0QU{#iETI{iPgYC zm+W=ZY6OpT?rh#G4A>t~X0@Z9Ul?u8gSYGMo96c4&a@I+yV7DYl_s<$4FhD?LnrTp z?6%oSS!6|d&zFQJe1eHE!egPQzX$>?VV?Z!HvuYqlKSBX7m?sW#pVlEJ60^_$fA7L zQgNZl>`~xHq$;SswqF z*efpb!r;!WX8m4^2ZIlISU_A*FEVJr0&>9*7L`>sjoErfMm}w7+seQCuK@HLfKuEP(? zZxIB6iF~FcAv=2Mx-!R1fz!H}nJ>osabIee9!0EXH)eO9S&P{-r$K;~#4$SXh__RW z*{ou+A7{(o0{&C+?|%e@J<@(bL794PWlsPc`H)jQ{IG}lRZEOx65=wZLboKCm&@O- zm~#-~$=Uj5%n20eF%#W6hcXIGy*diG{Vkg?4Ylk!g)Pk>c3S%dUnN0mmasa(R?d^x z)^##k<5+7Y!#uD5!IQxs9Mpjbg5~YOx$s8wcGrR8xJ~bm71VN0kEBj~ZTvx^M_LWt zVvkKcI!`qST>B*0O;%il8WU?HjNt=d8t!` zQ)-KAC|@~8WWqNOkeJDDu@x?wOrj+xnisK32 zG4}Iho3fa4@-$W&4nMAQ=p$CmfgwmX{e4DEl!Nw{&(@|SPGfs$GC?t%13qcsb`9^* znoV(65&KU}jQzxAacaLflB6ABP}%p9LpIw?M3KSQUH2W%NhFSOam+of&TzcsT6j2T z1N{@{_|(>O_={*frj3VVq)OZkH&nUX1)R})#@>%PXutE3&tVb}&k$5rWgnBHCsl-` z_5tuylK#aJ0&Lsea9;q={AtclO7$JXx797);TQb3ScUD3PZ)2e#dQ7V82?fw>})on zt|!zh?_=ZVo@i)Cf*VuJ(vht{UUSQEJ*`ndV_oH!S~l^vkIvRF%F?EIsbzkgNJ3s) z>ns%6>*B)t(rm`)^@pw8IgtH0vbs3LgTAR8(;lL%$=*8TrMp3{O*%!za`+ZomZ~@e zok)c(kR16x&@U`@2EKCxG&M0ZI7afS(62-ouOn03@iKjB9BA%tu1$@hDB;2e_?T@y zl?9_8ITme4V5tpJ_xm$j=9G2VG;t5MT2>n7B-#vef**m?v>+t7Jhi$x<_&GDmaK>% zGn1r;@*~;8-x7X&@zPMQ@Pq+otdC>UBZu!G7egxe$ZtkSLlnlqH4TtoEjQ@FfG46^ zpw>)BMMDh8!)AzgCE^2wscvZ<9CSVde8&g)WjQzw1X~H*KIYZb#X~+u27l|!#&Y1K z(jj@gIXds`%$E=F3mPo&gJ_wD*4hpTUGVX>-*Oz-*CB$O$Sqx#4|`NDW=kO0Ty)jB zH8sxNt#7{Kx{56y&sQQ=>O;#HOnW0pSP}3B2q|0T+5wWHr!>JO<0I(UvXwtb9!iOs znT(@ht>ZNCoKF~YQ{5TJA)OTksbu!w;ULSZ&z?7Dh|tZh4Rd77WWU2W7P9Si*8A^Fm+te_YNUMkoN?$ej+WB6snW3hUB5>Z`7 ztYG`k3qh5svW-U`_$y#YRjbHSL&||iC&l)Td}c-5Tm_@a@41}zP%lsSiVg8Pk%Lbm zKkQz?uDYP5Ju&+lJny`}L_@3bRLadX8-Y1m|x+Z307 z#+0}QAI`)Lm#14J_jgj{U2BP&t_x2KnU_iw>XRVl`FKK9&`V&H8T}m;xia_ji(l*7 z=kj3uw@uIzou~Z3dSnnH8}`SW{Q5hUeY&MRIB)1Rx`d0`LdqyUAX#qHb;)&I)mQ`` z6Y=0Ln(d9FsL}n3kK`i%@ZKN?Usvqjf(_nr+to9y@h^bFc$nFwUj<2A@saf6l@JLK z528LCfZ54&tob`5a^9up-bo2-(&ElX%F#z0fDPZavbkXPk5ww2l#zO45_6|j ziYU}r+9Z@ScU+$~?P5R+A3d7>CAVWXe7==h3(~S8{r*>#Len$pI>&iTuU+NyUG!(cH~Ks`jZAc`E1A1ySMc5I$SRP%#{GC0;4qPssy#C$opfz< z-qEdO`laLh>AI|sXD68xS1X2JGMQ06x^yCDq^!qXD((hIBySHkrXay)y4u`Z_@x^J zsU-S;Vx^~`M<7e*pMEE+Up8Z6KyMM&?=LY4ZKV$o9%uwiRq1)7$fH-wbqi!F?eRoM z;%rfej25b(6$xGhnd$bicFfUMQ*R4&!kZCCV#?j{kteg2%+=E|>nnnPck0t;gbn&T zOzsRSF*n7JHu;Z$0g>qanYsCMg@g}%2Fae#Bc+TN>I;bSr_6!`9>{5-qHdo|J$e$RQUB0)L@h-6$mNCa7>zr=-?DRnen?T>ZWY=Qs=q6zx_(J-HcP%d4m~oL zjVAr%20c2_u;2_zgw%~1@Bw=ZiDv;j9=GG)y9rLeYcmJW5=^2OQ#-qcPkdbHnA1hs zSCt^P)Eq8SVWitMZMXYTw@B+&g^q*`D%nh*{4d?YC$MufP}6<_uw%6~{iNHU=a`-T zh%MjyJiN@571AxjgU2!vXf5?zS^=N1_<%z)_}e>4P=VLjALZU#IaLpo%LFr8d^-(NJ2zifwgZ0&;>OIp!370y`JXv*0*yok4ycfW zo3CjIL8j{nc?*yDM9c%Xcw%bdmv{D`v%C(H-E5g=*d3kfsa02I>65! z9g#;=UIAsHDoIDw^>Told1WY}(lH4_n}GYrvyaRFy{TqTBX~b6>9*cebJ|&$XX-BN z#VosmPH#E8`}m?SdPE@z#ZLmkaX3Gzw9cS%au9sc!Pu1Oyl@LHA_cXU&C;4=@6$8h z@I5n^ho%O2EJ@(cDhX%x3;BF$ zfwjBs3amUu?Z(tP;#JHQp(!wrh|zkBDA&^QdYS<5{C`9}cRbbK`)~P_L{y40>uYCb zBwJLvMx+ojN_I%d9({x))RmDfD=TGYUS(zP5!rFCadBOn%kRAJy*|J9i+}EU&U4Or z&U2o52CUCSO9u$&(5D&)a)R9FCI?PHdwsv2?XT1yTf>GPU}C&bAzxSyk0yRiF6->{ zb3TuiNKiR{&yH{gOhr=n=tdeQ<~f2L1T0#^;A0SQynd*21Wli~O#73fUqpJ$kiy^U zPn5&0D{2MNgNfOOA`VZ?NY8Z<&FkOXU|^-){TU z=PHNIdIBaavQI3cqUx(aJON zy>nm>Fx5Dwpu{98@)}{k(@uCO zteLhMU(pqIh*fNNW1C=sKH1n`K`H?Dv}JqDfht#8=VoQG)cf^zGBCAr;X0bO zv1C`^=m0IWh{ocdyBG-PbAI`Y`pa{8+T5VK0vq3PUgP$3PK2R2^i?qm!=b~{Wf!$~ z#o_RP^1G0-481A^IP!VryuRCDkajMuffV$Wqkj@pl6?|GbRm|demkX+=hwdv<+DF= z8BN3U;#lPjNB#w92(=NNL1Xek9S``=(e*z-iCokD{jsr%Y40Cfj>%GPuZEE~2TnqZ z<6-5+5o3<4Mh+6dZh>Qk?+l|-fha92I?5pHH%eFrLwcKPDC{k?4lKa4V@sBYBeLm# z=oRFAl%dx?(7iuCj|Dof@~{~tO#sd(-M*w03z>!RF}88$^dM)C&qi>7}R)G zMojKN-`U`N_4uNv6zd^q{6NsTj+Lwt@iT)#1g)rf@`|>3;EwH9bWZ&|h-wAWHj~3``jF1)p(59=0u!aG^aU?`&7NE3$D_;u#;v7R3t+0@t>R&$AHFzE zX{tj_-|x@wm(A5a?2dFL4Z(&0TaCClOIL2jo|ndds1Q>b1=OR%3>|bDY73=)rM@7! z<(02CiT&+E=`;7QsT4Iix#Q1eBmcFn97Z@cbQ-*=27`L1^=EXeI~%a~KX-`;IovA2 zHTBkICfLhsHr$yq(redy6a_barEI290ajER|B%OFHX&qiwt|yhR$R2atTSE~yjFC4 zfePYDF$}Ls+V*FUp>Y(yaM=9y!3xx3RqbD}mZwJs=)n#?eoymR%AMP|u7k14^JzT) zka=44s}*OmgO!@%?jAyMmlfjqQZ)T1P^9@nrQ$UEsIBs0&OCt6WGAjG-@!%CY}GM& zd_&XjIK-3pEa3TmQRnxVPX%$O$h54zXe+&Km`)L3tZt3_~dX~ zX}|RUZYQDr^AQslnRq&OxO{%VuP_Kan{rDn@Mg=lmMScA-0sfvz#bdC@&XFA9gurgC71M!zGOaAEWLC~0L;(z zF3)xn-g;%PApRfdpZ54W&R4+;Kgo_KN*W3Z0YWj;{p=eYx^2ngwoyNmV_6Rem7rKF ze;@=~EmzHK(bKd z)n&=+hm%^ajan@DX3mZ$a9;ia19%-<*z&}HEzB<$;E93}S`LEquNW~`}jV9Y2-+x2B5ZHanYF-1 z7r=Bjj22C=|EuW&JX6cYd8ZkUk!4z}ZYcf9MC|4@N>SI*7a)N_XGaZXW>!?56!^=q zEse>r&Zed$cd;#VfKq+&;A8Xv$ngOvhY&G^N#Ol;zdEG2B{t7_G;5LYF-cDnqy;0R~7zrCRKuEDG0L6=)Lf zAn9~S^9vv?w>JwfE6{=W|B*wsDv^3C+KG{kc+ zfIg-gb|k01)_UHjzOACkez=M=7sHb_b~Sdfcw-i#dOr&FQiD*@={H+ks(kFb`0CKI z{(lbdqA>+87HF!Ny2V@-Q>McZ2OMU;<1@1T9~&b*t!JxJ6z1EZq0 zb+(BlAzm9ZP#;3SnKeGJjOcu8J*r=v37H-11G%ufKeMiQGT~(YA8O5Gc35j;qm0eV zJ2QV|h#f3gL(sl`;K;d^IjS|3`1?bWBC3D8;415^D=qyx+Y;UncsExZ@xO2?Caw3} ziVfA;-X$J}!(~57Rn!_mH#%&-vC;OrBYammUu=B%i2uhe_3#Gk)FTS*r-2D=$UkhY ze0Yi4G_%PmnRv&-0;|SF1tudSNR+cQu$7U(bkv(?Ou68OqG;-2KXAnZBBfbYjl~=$ z_Qn*o8a~{gSqYV^@+zFdGZB*6hhUU8U*zk2lRox_t5nTv4lqJ(_mxC$E^F6VSwG#q zyQW?pIxP0>5SWl;{AKUid2Hz-`s0;+S1Uu%0cROFGVNzg$tW8}pr@#R3nae4TgpVh6BfkYc0 zidx!G^=|#BB?IR5sjf#n7s%7~#K>n;I-~cio7F8>H~ZtT8a0PRdnu}Q9;CQ*_8f08 z_p8zCaJ<@0rq;zY+}A74T?~6A-x7~}R7({d z%x$>edmXdwA`7iG%glIc9|1FA!Ekt=&Y3fv92K=S@%!Etgt*9jX$TX)hmKmf4kZ3( zGi9=qRy)yz9|*`@EXU2gfNrNfcKP~T{vszxFIC5{z79Eby907g8E)z?mJxxnR9{FL z&3O9X%0Ej8{eKYV&eo0LwdGzcux2YL{S`pIv_N2h^6D*+o{#_Uj$VJ}#E`FVC-Nfz)xP*rw{qk;GE?n5b$?{7_&l2+K~O~f zSut3khh}d-6uxm{J;kq4{x4ll6b=2yujI+?;stR?Y*Rr=Oy=@%#KWfqirNHzxxaWU z^@aZDlXg6UkKLytsm0GngyO~_%mpy*U5Y{{sye{-|E<5D_$+P01+?EOHMVaL@c;vHWCdXZ1z}m^FdPtDGaHr_)P1Md_oH-g#j>2;Y3SH> z{sS9snbfpz?)wLr(fYd#hqs$$p6(n>>K@^Ju^9DnI@znH%M*-D%1~t1Pc`Ju8h8(lHbyHwPGm`h1s*> zT=le=%U#*-G}X_(Q6F9w$M7005Kf(lbsJ$4hZHyWUMOy|LdOvNel@EJab;b2-q=1e z-#jy0rx|Alk)Vxw+FzaWenqa-5M?^Z^{IN?X&!>3W1dna4v)!hcR}SI(BDY^Nn-MExwv54>(`_ z_i%=jxI>k9yIU9iE+iy-0nZoLBK0615_K#$<%&a(V5?*8RFS*O_TybyAm90kD zHfm_Y?}+z_D0c7s<406+WjFPRum3<${0Acv~vnwTFl0eRA+0P>~e zT9cY{&bYG39|zxlF-I8KNsG_FuL(~ReljtUerQ_EU8Y?d zwRdCTEOg9(-!N6s>IvW~Sxry!#OPGVpk_aSKy4w)LbQ|B?oQbqFfj*Wmi&Hsg$eIZ z!#i=tG{+sL4hJg(c2I6t6ag2`-mz85*8YJ*;*r{kk<)KfMD9lyfozZ3ekG3rSz=^+ zs#^n1NT_$+D5lLLk8~Z3v|xqprnGsQ$y>&_Ei$>&ZxVVAq+IMGes_v}t-xiT0;xOC zqyAib4Q*=owLge&7j>mo3LSi*w|8*~ z4X`}!H2jB*oNE9Gy7dV)uLs8-@`6;s2ha%wP+*vQYR~lmi+ns7sYY~xBRiyk?(Mjp zKt~$j*A1h@R^Svw+)@9(m`3$kLMAc++xgwp@)Frye8=Ub@Ovz0xKeBdi1MNMqN59p z5YoKl-vr#<{IlU@|6zR95F9j6ePGk6_CJ@a1=O%JEd+7hhEv1<_O$O-iz!Ak=rq{K zON@>CU)MveV1W&xu&a9pB&VLej^VHYZ&MV$eFZY<_uqv*%s-Q8ex2M4(a+w6i8mVX zvtMB6bwC*6>fDSnH}5=up@YNWKOpNVi-MC_RpAmOSuRsV${POTAoC86l&uy`JLG$&MT{~wE? z*sh^Mmmi^?k(Tv8iG2X?5Lk}Mlr?c3MZW?LlQE@|=i5bxPc2`seC+&wQ2QTHi`Sgp z9qLi{NCJ0um#3M5qX|=_lLOgDA5nm<14I%-4EKn$=-`|s&z;@<+tZHdEFSZBDc7XF znhDGT?w!v^!IlDH01rWLcL%nQ`mqnkJA94dGRArDRs-uq#*P*0DRmP_mRHyaXEYdd3CfpkGbvXjJS*Kf(cr2)G`a66ExXCF!8Kdm~OuKV$fQG`c8E`<4cR zmX;>pgS&2OYrJOYgE~6aHzLySlvP}*&mFT)q*P+oT^-l;51aFCtTwv>e%pFJcn?Ub6h+QQa1M0l;Bv} ziRF8~VHYl>e#bC*McQTs9wp-!0=jMwTyo5|d!r{;_vCu(xMMqb!v}jD&O!2_8fkqS zXh_lUi=07B3hU(?1Fs2}<_5wy6L#~wz?o6-@S-DMP2*b-*!0Y3I7IzYex;v- ze)N4XmIi^k!zuA)C)Sl7x*#g%{fh%YKLz6~IeLuTk2_!=Lf{6&;0QhPCWX%3OpdcKlPFJs*itRE)alL&pvhnW|uB0**d{C$10rW1)-{f+Y z`kbVt#j7Hw>J+KwQbTL<`se?;^|h;l5D)Uk7WB6pr?$t9K!tNr)~!p8*=x*2S3xqk zqT#SRe}iAEq&kgeDVJ4rk44Zn;!$`juw~qK*=XzEpLT8YpPA_Q^T(dp89gJTr~Wg( z|Cf^(BJ+Ntl~s-w3Ey&tAh8TKk&m14Ckn1Cq)Gj5VPqx65|p0ziFi1IeF8sBA{HC* zc3LFzA+8Z>k#cO-z{1bCW;imbOPg?44d*gF*`JO6=xWg49|PD4>tBNc!A1XsVo zDaY99GFxF!D-Z(H#e~aM#dN+RwAA+KiRGM)X!jCLQj#nCMB7k z5*<9-Zb}P+JiAvH_dhNlh0pKE$G;Dg3)8+2?~ir$d}L2TU7rkx^62-93}*CFi3agg$+{;z^W-S#S{ z3jT}pPixsy-T-RMqW8smfrGYWAYj@1)h69%2F|~R4V^{JmV8DYax-UtZWj&)%~lk_ ztK5W^hq*JkO1!=`V%6$6wDJM$R!*qK9Bs+JA`0B^ETaVSOh@`m?>(JlC~Z^<^TbVz&uey+Hc*omn5fA$#ht8V+k)R-Ke$RbDlhjjqFR!%$V2X7!$yh8V99_X_%D5+F58SxpuYaUuA4XPEMiLY)_ZdL-ssvb z1Y(8y&)EsN8htonoS}W{8rp*=d59ck0j^BZLBzhUFxX> zjoIhI%bg4B`>lhmC@~b;ts}-cyg@H<(+Gn|*rGgI>mLqlMUlGg8Q#FzYMb+pgQMkU zGSk}u`mdg1ZlkH86|R1+ntGjFI&j_VMECw}3egmEz{u`}!%}0XXj` z0DLGEbCo7v#!E>n&23kO+#$?}7AjAUN5bLY;dvI1uIdvTgAW8GmbPTAXrQn9{l#m- zph|@{yY~xR#JZE;SLl4#6|hWvBoj=Xo?HKM^*x_UU#ay&!+KDK(0G9L2FFt7deIR< z_-J;^RNbZ+UYyn5k0WMEO+ehQRk=8Q9$)#^<@tAZimv-b#J?BB^v2KG>L<^?1@#QL z_di(Zjb2G9c(7JD#bMveAq*XJp~ZlG8otAOtC7mn9R;tx?VWbxjh(|)RS5Of?2mLX zLlaFle`kKg7+Utrb9yoxW9XS6N||FWP>L0;5cN8X7JWWeYP(pv4Y{#0`>w#Z2!9NU z{kI48;7Px(Zsb-;gW{NzUln;+qj@j-mxE{JV}icz+}Y%cy~%Lkq*k+f9u1yfKh7!T7VUq_)4zQkFw^RHpcLdrX^rCI%j2R!eZf122CamO5ap3FrQ^-s= zd#`4vJ#`yso4(v&OyEH2`XleWnIuYlUbVUTFA=XaJT(+Mo7FkpD^xHa@*;-HRC_P&%TC=f z(#nW+Z8c?@AN1iWF8y+H8VeFcF0TjZ2hRB=tIDQ9{&Ow2f<7@Bn)ItAD~k{!6`zvH zV({&;&F69VouuxnsPaywU>k>h+h0A`t*vWBW7%z^=B0~W4ULA8-&0Shca+!fWTmgzQ1l!$0X$8 zn^ez9cZ57p6~#xszeBugkUsc^)M~tNfb<%EIT0Q4YFKyq#nmHFk-huiDrb@vLU%k` zUoF}76uC(@u~<`I9USY!tJ^HI*h#NZ8h-@C0flDGtWXC6GTX?=&@$5QAJo-v6cOG( z|D5UwiCaXQ%(ruDDhsI#KOdM6$pekBoaku4Ryd<7Ek)s9{w|D8XkUc! z{*Z#kh6eRA9y{V!aG%PiKf!QrVS~>{3?cy=w%rR8?%@WA5)@^F2?V~OCe$~wV zxGm<*bD<`Bdv_&RX@P{Ci=`k8Xl-xOD{cA~dN(YnVCNN&hso{>OK?0m2joN&>rc7q zm;47~9@uYFtRBhY=b%{ic0-Q|E1|3&6nLB@3^ip2)%xFQqQ$W6NBA8;E=>;r*;0Mm zIW&FMic4JW&$IkK1IV6)u3ht6-orTtd#}eQe1mEq$~HZ|ET6iw+E5_4FFH&|1?cWl z@9#S@z6H8tb#g`^g1W?YnL6yG)hk$h4{(;f0wTQA-pmr<8IyRTqMlF*(AW(|Zs{`K zn^U~6`AMDa6g;TcQgnm?P6#jgp}znw0Uf zR)Mnw0bZ1tN1no>G;JGUInjzfa9GN(^EWNj7K7_f4J`QfLCtGdXf3_Ct%O}F zvz?wF&Exc6^1U3b|Lp0few+I<^}CPqcXSbb~P`Vx>h-}K(Sicf%iRw zR@2YQ@*cDFL>0V{Gg5f^?yxw*R?BGKWL+710qdJ#=hePQp+t|trb1(&gbr5 zd|Sqnl%*7Gr4fZRVi%=Ww zZb3UG94Eqzq623eMPAL$sw*$-jvvQ1InbM@jVJFVnsp*lau3YAcbeT2Fnko}KE{G!9ctc_GyfY-3(t+^%;o zn*U9;7P!$NtKz$DB+mr-cep1;1FPq)+P(d=o>d(`h!eokjq)ljDES9-b#Ce%Fik$2 z9&|Try(?p{`XA8D957GW`yVK|Y~M{{fRwVn|NP%&tV&u`j}8?T=!?wMJsH`nfsOge zC$=pd*Q6-A5Xn5KRXdxgGn5al4^F; zG&kd28;;z}?tZ#cFkcQY-z;_WmVy;o`)-gjK8dJ;C#$;txHBq^+y2@3sEwNI#`RT2+vY?HnU3lS(Q=8>%4!##kE#0s;8KHm@E%UkW$94Fd7`Wt7{sUG$*j8R+ zN0`WYS0LB1AOoe~6?~F}Jre=Y((9!91vW0Q5GWuh^RqF1m~6+ICwTb2E%|Rw{dlpz z+OBvCeFJJql%f{C^;Wc@;ORl*NOd$NB(qi4Nfl8FAPk}#2d`-ZP61n|nSkV{~!>`#2$lOUSecIv;N-0kosnM6aTVRx%6@Npgnjc-FwFyD7Pv8_O;hZxAbMrNNxKN zvhlOlV70`U6i`6MX>s-Ag)VAE+WD*^PQQS}1shR!biJC~RD2r_Rt!%5aFmTF`6uiTuRU4uUqCUZWClYQvxG)5y4;) ztnSc4m|N2ZVq3Y4cWgEk18aW1qy)+nYyx)$rf1+@Aw%_5N~mJJ7k#cZQ-TV}WN%HJ zs?_WRi6(35mXg5W-kp*}G%tujDMlr#tvC^bs~OTcag9u}?xdkrZy2HT^X*iCVlauI zfJTMFI?JrpoFB-CnCkA~N?4VMRPs0J8uPIFZ0wUW_y}CHXgv@k;+t{vUfjaCvetCL zHGdMW(WiMe;u1^O#DN7NMJVxZ_!MIxma)Ba*7J; zwaRwjisdv?CR9YY=L5JLTUU#{$26G|oim6%Y4A1)FxjCLbIDv-&=H9bS~QN@qHWFS zqX?tDDI&Pf=LX(7Au?HF1{>@UdG}MBm{LI14(@IqD@UW%v(0N-WiY3Z2~NFV`@m|b`4z^f`xM_V*3gdhj+LrN%j z@1{VUvtYDNu&(BGFPk=rwjSLrD$^Yz!A~1>3Nj%78lC)lKiljNGGCwlu=5iGHZgc9 z23|(~;HHOCg}*VVemg!Wg+u7`u z?U;bKb6~oeHI}dtnYKV~V1XO*=i2vaP47DiOyy*6Z5&l)bLvszjK+Q&mmc@M_u1Iv z%^(7T0_8%M=@z2dpMLyMF>?rQBqy&*VF;ToY zT{=+`$eJ(O`O^)OdaMzB{k!R`K;Cd zQ~y*!d6EeAb+Ev$z<~xR9*>itV{OA}Gg~o&?{o(JPRQ@0=p;yeyBCCSd}i&7<{)%P zL$&wQaWR4sI`k6W-AFW(U!N@NQ_@5s0Dq53{dMc0fHH+Ud$+6rRDt)2YMq_$;2|j@ z4-J#4B@1qqSQ|m(vIN6*=+AW+c|O23RX0#j5y`r9&#Ni3dAE8{8bIy!Vu>Wba!V!pRyGq58c%<`G+-W|PoX4= zSs&2^sUQ{SX(r{;Jy(+va699;PpbD;Dgahx(VQNX#Sj;Q5-*HThq4F!= zN^_LmBB6A&wny>z4eWmz6onvhD>-#0p+Z45!Ycoo#QvKA5=NJ@jp^}^RYhLdBarB{ z>8Nk&&Aani6h1TW6^-|BR&+|lY3K^9+y2s?>Pt|qwgka&wi3gNtr^Cf*~3&wTyWjQQ!T{!QeQ@?>p%;zVyP*E5FIc{G_1D1%-6<@cG6~`D#vEXA0vk`z zc+vF)RGasPebj*6{R{Wq`QBBo9sd9_&iQ9G?azSEo~7nzef`7E_pc6fKi~%o>&WF@ z&rH1Jh`c+a>R}AV;0DIvcFk(y6wRl;gMNsHida-Oy|u0Ax;%uA|b+85s1@XxJAcg1reXR$9!|DVLYL9B6PvglxK+JY)b_3 z2QbW!$&}z>0EHVFcmj(NyTPN?!5=my*R~HLrN&+UOKMheJUmphBcB5JE(s|O=L_MJvhd#>2xEa;Mp?}>Y)Om z4`w?!0FYXmtadGE{KT>HJQ~{SmWYFsJG+F+zCMU z%Exie1QwJYR~sJGO7;6eae_FEq*=RRw^2e}fsNuY*ro38usgqzzP1^ygxLMFa#KAi zIRj@-rvp(|NZqD@CKP=&y_y$}LjFr@zrM_<%5|RMvx!Sqju=B>UTBTfdAc1bA-Q}f4U~`VT*GT6{PZ}`F zBb*Ig`Sx9dMTvcx{p;55HV%TZ?IMcJ@@(L?9riAP(D0JKh`8i}=UVC(R98sr1V98Q zuT;)*Z-TT&=IR{2z;K1DecE4P1vw=;j8!@Y%Yxds)_N)nKLmgcqeu@if zKA({vv_vB$4QLL0TJe>^Z4wcJ)~Lu$XvGg6H!=J{P?-}oi2CbHO$duQ#6f#GHA zvc2(b5aZ1j0R4Al`$Ob;&&kcUB6*Zm&E(s6rW?21oS7zN+fX{E+6 z5$-$1VxOlFhZDOzF4n06yzSI-oM5sTAfFMoLyvvCm+>0H*f{A1$1^fqu+~Qq=WB0| zc?ElIrF(pvMnJSYTvKR`PyZq!X!OhscuKN;M3!$`H8b%Ngq=iKp#d*p6lBRTY&PD# z^|MbrBKufOXB5s7)SSLcvaJw%ZMkaqTimo2hz~_;z19HKQ>Vzjdlpe!_vqY-47ZD; zhP@woZy#&l&17XbI0G}O9&mald(p|o#fbUpg6BjFQ)Q`=&XZW_9%~y5=K;8rboD97K>@M!SG1d$5vaX;kzo^(3KjyugJ6+F?R8U?33FjnTU z4;84XqouDo<@byPdY;BS%}51=S3&6S7Hn0a4EIW)L8~krRTg%=A*0fV%vcS z=3mp}@Mg|{q@K`G^b~R`$eLbo7Jsk z2OJqRFi_%E6GUk24VwXwO(6Jt`PEUX2c2B&t{}6Je3`C+jh&V_(JDX~tmWsKu(+-pcB>`sp)kBf(!t}k)y(|I)efH!D{;u|qtKL;N)a@m})K+JW@U4zY>T2{OMeF6j6 zml`}9-LJLZx7D8&1RU2_z@^&+4*{DU+@C^yeU0e3k81H5?Fk62a}J<74zosV&^OET zx$^A8mCN+lrlsTg!63FHWXI!YUIpCM=Id2Xc+*&ZY z2iBhp6ovW*s-^mBaN+MjP#UCc%zx>#FN}e}&|0v>jsQ(87j{s;JjP)|3H$^;j3H=>93KJb#~e}G%7V~n<{2R+;wg7pIQ=TFJ>%E}dJO(B4Xn`cnrctc}y`w}}-Yyu;%nkF=xe$k}g#Vf()+sfjvY4p=*oZsR;?p+of{D}ysMpd331?q2|(D7w*h=0}>4 z91-6ufbg{UNaeRFvW347*0H zAGlnvue)^n3D7N%vYU_!#T|;1>I({QBXpQ_^XpM)ietpzMT>H5_Fkxi8q=?n8kxM_ zksEmjY(^!7PE09@oZyisYZ9Ja>@J*CsX3~KtoUxoEt7MtYlNNr5K8aWtA_kSC2|NI z`VSzVEH^}|lF1*YXyb%TXXhsxQ{Tri6r{QQ+UYjjes52upa!z9PPcdKF})n|SAPypp(r7BWZyK_(6=7{ zM&*ElUV&6nUEyfuF;O1PjhGKuT?KCo?`hI2l8UmdXJ)LsK5-#@?gN9Xjq()n=ZR)m z4yq%$)(sy0{+BW|xYm$fnN&d@=mOivD&M#eIvjeiOW=N9Zt>-DV&N%8WCo?kw~GiJ z#R!Z_fFmFK@oDxq#3CgQmd?xF@q((z^rQX29b$h=`WXeAS3CHc-DTB|_2eZgu zB$D4>!9|^q!}oK;ZRA?oJYp|LE<{z1i(FJ#2~$ewCNez>u;2Jb3W@i2ZkFhH#s}U; z77xA&ZYXDuCAF~}+EGf2Kqh^IO@%j9jrQzuqyqZgJACvCBVt^Loo||Z>|mqD5O$Kp z{A(%Q@1LwN2`VGg`O)O{=x_4Zt&y*H!J}=m9!>f}-jwot-=?U^arU>)T}z7?MGzOY_4iQ1`98b%?YMWHgiN z^);VfU}Qt|B*5<8H4rD1drhJ_-RiTFDomE3G^i*dq8iW;@OkbLEfJ%Oza9u$f19|C zoL!E@AlB~E1H%}6W;HXg2NA|)V?y)j2StMlKhBKQ(rovekoKR?IT3N2#0aNc>=N!7 zxo9sEjqu(Kr%A?uMbkiOTOYtoL>jA-me%~KPDm1&9xhPu;m&3D`_t?L z#G+c(^PQ1Bjt17CnvrBk$6753rKM5QEkE;*@3~xEQoUEN8*tb31h9~hmEfw*_=$rh z`PJeUvJXcL-Ljj|yZe*oN4#Kd!WslK44!ZyjPHR!VIfzJ*K2aw=je=3MuW$oL_fMQ z%agoj#d*TU>|cVmaIe`3MN}Z71-`EbpNfP;km+B9VaE@I&iYy4rCo{v% zql;AKp1x;6Ab>Bju-W6L_aecbs4+8m&$aF{neb92$Ht{3m?RWdXuq7CsiQB+yN&d$ znQz%@Xb5o34JJv2mHQb~s==#0RbXQz96`XsE-5AZ#Mm z)aLAkN$ggOYL3L5rGaa-3gE`eNsdL9lnMWbZZFKm#n!M{t~_qsKi9+${}%YwrQubG-2# z)A=8w$M3Vu*#l49*Qezm1PDGQQ)^w=ZGH#DK+J0T+I4U>F8pCALE1k0Gy>_XVW6|a z%S2k17>kSptJCj+fi;10H-` z_m()e=WG2+Ux8*K!nguZ+H$wu$~B0EN?Jqmae^{`j)3k-&kx*pL+_F?H9ut5`vLxxt^HqM)vk$xeX*_t;Vosd+Z+Bpld` zK8ZkDmV${;Fk+-@G{X)IK_}A5yLm_aVGI`^E)n^4#g05 za4#E4-n)d`GABFy06(*z01MpXjOm0jS@ulIEd+8Q@Y0_O0K@llnJnv&3h=yIQ?A6| zFbx7ZTRuATgXt{&Y^AX_aa?95f285Kw~-(hV*Z`&4CWl%btI{(Grwms>}IF~#B6%F zcP0pj?(Ldk#nzSgvNn$Y0A&+6ndW3Y9@z;H)WJ%9@#uUArt8Jc=D z{%n#YmHBP-A~EhXaFOh%m@z%Ml_q0PVv`dW!t^6CiP1+9NYARgGXzH_a8VHWhAM5D zecC<&9gXrnBOh|`<0xq^lfY>$?(4{Oh0Cz#uNNX9e4Plt@=uhz1z)oFJGju7rMnYk z6C^_hD@m565-S*3e#mBr&~aca*vPh0b_xj}9e;6!NaU1GAGfVf+?{2tw~>A583D%X zyzl3qx=ND@6{FbO;55cDB$rL0=NVMacP_apBvF$qnIfi}ZUw8Mtoh$@A+#=ou>NIX znlxe3@>&ZK1PX}p2m-lQ97cDu=?UV#n+*wPlO#yeR9Pq?x#Z0~n5#>^Un@3FB8~%d z*W5#AM<~>v3lXPdaHhKRDDb?1S7P#kH-p~r2p6KKCJbYL~?gAT*fg~<3)fxFg{L0_p4{in~B7K6$VKkgio|GvSJeb5XfRbPS5QXl_HSU zpdm4#@lG?o6}E8Pn@o{h?Om@Gwia#A__f|7>T&<2GsaB9?pQ6)EV$GfmWPoUxO`zF zVqaWBM%;uHr#!|cW4I7SJ-_s+_MM$3hyL~uO%NsCTprwJj=(gNmla&w*27~ZqhV&+ zmx;vEGWX_wz}s4?(?Gvx(}}xMgfG#TzakCk5qmcMO~vIzZv;{zietf~eX3 z>2O=tg_vi{6Y9ux4}KB)TOMa3zJDNA6iwzC&%MJWwAxV(C^~qncWb?2CVlo_qOh%% zYgQG=$Qb(uI?Q%A`oq}6huU$%HpJ1ieY+KXi`O&D1T3Tb9vLn=PIqm}^GG;Vhdm5; z?3cShbK!Y$jgBJ1g66{Zq-urVY%|ubF1LZQ4)$&V-13EPVx`!d(+14R^K2dnWZ$_N z#2N+DHDU!z3%P>&<;^=)U_E|0Egz=1_&=h^uhq!WQ{&jAgMV)+RI~X=L$a#H|0hH! zcN^md27d$wcR~0C5zFErZzu3#mcN5$llZizI?E?WU9HB+?Ncom!$0$b(8v>@ahc?a zRW=aZ`&0E^ZDTfI^wm2lB? z!s+VoD`%OK@x~Cb6?e}Jzbn8(Oy9rI-3l}SFYc)DOJW_lyOJToiSG2jW#^Dw*&M2C zey*2@OUF#d@N%%LI{gM+1wc4`y4vM^=q0#{QgUuRu@$jTF~IHv>H5lx#6`1~F9An# z-P*Vj$bI`8T2tx|&k$SR!A7DpF2Nn^xp!-=jy~}_U%4o$O1Ha-!k+f>JK!S7V_z>x zk)~qDTrNUD!9y6ZQpRcdeSN0eaKrxWQPC}p&In|>4@n+(g_o@gT=GyHC?eVyuF*kIaBYn2g1U1F6tpCKTZwf;v}kfy5w^hL?yg=5xJig1h6O>>ngp zR}!D}1#oC%Ibw~sXz9Q6WL zFB4I6_=k&{Xih8QI!XZ9KxaEN%%RB-e?U*?@L*w;#cS`~T0IsjJ z=gsIFn-zbENHs<%*H)_D5zR`UdB8@*rcfo!yTpZZna~IEz%dt*6U;pv&uZuK{az*)p3h{*NfcR-Sn1RPYfw90_{8V{?O^ zhVC;@2FmRcQC4le7kOntL$^i;<4)Gg|Ddp>L#(();O*?iI98d1PNg3;;P&-%y3Xp1 zAXeJZmR0|>>5=sSu!-Vjkbz}nYKUtk8fRUq%&?+aJ_R~6#}DhLzX-Xh9umrzwvr*? zn;l=d0LvRj3mbmm?oJc#gh!!7Kk_~%?fUX@xLVUynfp5pg8cHBGsLLUT2fB{Yy)rH zfooG4&2Z4WOxhDr4tmxU9ih5x`!D>0q#yqsY%_8YF7rweZF;uB8NjB$j^QUXc}B|OXA zTmaQ2b~!x_WrVe+4#vl-p*h#2KsM9urHT5OKZ&7iO$e16XX-pVy%Z31ETRmwIBnr0 zYFWo*Qc53w>fv{l6-va?e$3Hmz5nV_g^d;M&pRW|V?~A9c6<(^)O43@mM%@ay2e%i z!|AETc@9C2kLC03z0;ZY&nTb=&gxgHE;lTS3%7uQz=ip4uC&R9 zIhJ1v%a1~4qpbYLVlpo8h0wT=>n;rKGKMZBIhxZzwH0>D-zWDfP8^Kh1o9ZJp6<$5 z+AyIUZwNtGJ1%+3mheYyCxp6^>)LG%ZH!|7^ch?&hM=dn`DYbJjK8xxVh=E$Cv0Fm{a31*8h~Pl%hRn48S0FK z9YPMupIijdk61tqpIW`jGNQ@ec zU735|S|^WOB{?p7%R;{(YYJd7kh4KJW8oe0 z<`cPi^Bx4cLF#SUsR#8Fzv}@&!?b2+u3jq{mh)bI11{*_1IMy!49mjU;rNvz@~(iw z1u4drh7Ia3;C+*SR`5vyzKRT?>iUx+#b4)iSA>m*kxs+A9G0ZqMb7%EVj0OL(V^~@ z35hygZ`Z>tW{&^Nnci-*OZUoIfSPEre1?9!qVTv3g!3PpeH!~e+U%24-M=Jv&`yf+ z!PLnkcN5I)J?ve!Q+&v5|L|o;MQS&XS zpGr%&UCDZs;o0)wzlHW@nwA!`-_;Y~FIs6z8-g>-!p7ZU_CFw2M94Z)S>37|m+w(In$gFi^U$c9LTQqx#+S~HJnEd{U<8#pK`c#1t zPL^i>yKRTaVA2bkPh()~aw#0H)6%BVy3OVRwdw4C#M_k*GGaHy8{%-5yY*QI7R%*n zx~l&y*)(RkL6F=pZF|`f1dqL>Mn~|UExE;5r~gGao#xcv;D$Uf4cW@9g)d%g6lru( zu8%q&E4=%D7hI3C#*rT5d;57B6Bq(+O?OV|%B0`=R}YiQjE! zbBSurTcc9ofEe0D+iXE*)7^eN6dCZOXh@63L{8DI1Sf@e%~+@_UG`M{XOZy+XL^Vx zPJJW*XZA$mUey0`nT=t2SN0^r`Vn=)c zbg$|tGKr7F`}#7fcP#k{w1{y5Po^d^NMm4%#{# z&M%_&Q|SmO=I~xCqzGmE*o5VCeG{QI??)M0_k;np3UOk#uLEt zBlUc2#qVb9rw*q?#yNJfQ|kj;mIF=mcLs58D19|g!*uDujOJR-t9eJhhvIeOZb5OQ zZHvT4-ahLkYC%I_COSpZ0%WST&NmZ`#lctNfT9q6b4nckzB*x^q7LZ0YExmsgNjla zEuAY9B692Pk3Ir+3f}foK0vUScLWc)KH_B(P(q=_y#wSYJQw~M-G&ou!GC>ZN0%aS zxEpY5R_|nrp6&C)aY%K${gTT>J9#hPBUlr#=1>Y=bY9d2ldFjU~3JEgM4Yb$W z1d-N=K*>%r7O8S=7hY6cZqe5W`BvsUrHgYQy>Xv!}*nQJ;%x*uWdTNP$rr*3h}b$}+( z+f$SV&M&Y_Wl7FG14{xP{ZF=%)R&^}3ah&_9A}2zf4pYl{wI>2mJ!OnoKYhg4?x9U zRq_KrL^@S+`7(E9)$t(LYmgt643iVBz}J`Ie{8z`M&%^D!{9!Qv;Q+nz^);>kOD@z}B_iG^0&Ukoxu( zZU^IkY&MQ1oiWqA?DrU>Zsvh|q$+Z9eg&f6W|T{q>wJojoX0lZ#VE68KKf2|hq?ng z-=qrHnSb14=Eyd6;*p|LactX}E>3Ud1-exM$LVt>;(78kig>h$x|6jV;fHcW(z__W z_oZVuQ{qCya%0iHJj`T`HWtsPC0=R+2<%B~U&E%9I*jS)rHL$mH{BI>NyjxGh&ihz z$18>-QtRE-{6)J$;>6)+4wiU~<&yv;;LI;TR46;b334zMnI3FLQ%Q+oT@FU8X|&qu zhIrNT275(Jb-|I0$xmO7&REAoE)Vi)X);deK%3qYvE59xoO$C#=xlheIk=-6&ak7= z%txH3{!*m+)pcsi`rzp9vxvBDk@+Qkwf4t*qN`CLpCb4-h@@TkuM-p?_Q6(4HLfO& zy7spL8-ir3S9$!skeNu38gVBMBR_|H_EEyyaJiLeQ~v18VZAP>oyjLXMJ$bgqC}D&l&e*blximwmyAg^coE7DkIIP6bh`;g{-E{xUNNz8 z((noXyo>?$8_3_xkklY>IEn9^rO;}F^srdz6xGMHE11>$b%y5S70cU)Ap!(BC(9^# zaF#c!;B!Q#tt3-TLgaEsVcP}@wkoBnWFcpauaiJ^FJQ`>R_?@Z?_|8S69cY7)h0Ei znj-fv3xoPsj30c*lhjR-XckV+qNlw;R;_z|i zgVMSjk^Ig|e|UoE-WEhmx~4bA_X+46SfV|pjD=M5d*30NLt*Ve4N5-i=5vlfg9)DQ z)zR>F0>_gzU`|^>aIZeU2RlRc=No_hsn6G1cQr-nY z!wf{6elCV!I3!IsYb3x$|4#KK=2LA}q1%86d8f5pm0~=p4N+Ffaez&>e<|L;DRrM% zEMbeXNy0HX*9`y6d#EmV%tWCC!Q^%RZ+1YV5_B9o?gJZ&0=zL|k=kz7roxr0c2d}e z8Bl3eNq-$Md`W#b5RH;+70c6Q9_YFp!KkgD&FdycwVXVP6p-pqg;Q-KPyAlaKTG)w zJ3T40{K=9TZFfx;4IBse#J)s>o+lG0t+W;+#|71&l?)q=?z;`dsy07PE_Zgfk`zYU zOpn8J)1{ZQT*Y8-aJxrWh5P8by%wppEGPlF#Hs7*sINRB-d@`nR=SK*IaYEUy3z1h zJA2Tk+_3XYG6uiF{j*HLsmoW4Kd-b^Bs^WG{c*6eI+|H{(h9vzoI5W0nrQgz+AqT( zO)HI?BQ80FE#@0Jj0u{S2lTt!B zPzB8^@5Z$Bu4BH&JFE9u0eLgaPE&+iO=e7=EZ-{55 zvIXT>Dc~ZJQBz&o=;0prex%0;NmzajZi#taPT%|o>}pvR&c#v!b4$3h>hM}FZr@M$ z-&VVpItZv2uVAq6n!H@s6CtY6Ev8M_+Xc+tr158%N0 zCltk0yx2PxZz3KRrGPk+3Ce0_MPv4afLvq5F=4D$JMg;#Z8pns;~o*is2{jUfyda8 zJQWxQ9-}HHSyAP@c*90zjN?~s6WY9Fz+kw4ic(i!-^wUcw#JOy*_kMj8LJV;fsGCN zBP(li%tTzSI0$=m(r55omwGo!-)WL6{8<7Hmzj#Lo$0XdBckBC;U;wct;@%}ZG<_A zHpUacP!;C_$DE?bIeF>FvtRBZ6)ph{mDbU-@5hz%rDM908lN_9VxYMcqsf$}azRuc zbe7yQmuxEQX}^i)HEwtkfpU&saFVbkuL?8e)zvWL$Z;)8BRe;&|E&IUXYD{9oTah2 zzVbEfNt{bc;V3`VL`la~AuSJcp+sB|xCgCgwEV4)_TgIGp^s{b8KctiB^X1;N1ds> z$lvwF>y-R45oz5!jm0Un<1?$J)WM8>>NLvKo-0XsqSWbrnP{GAzlXCH}(u9&>J$)CZg z;=H{7Axd)SI2RRMQmFS;wd(uPA5Q{Zh+de|N9VROC>y2oc&Z3u;rzuIJKV-tWan6n zI!LLzuEA$s{s{@yG%!BUef86}fS#-9Xd<@!gH{-o-`Gyk%b2V=g5`;O-0JH#P5#BJ zq=7ado*q^`20$noFI(u3nestX)9z(Wyb%ILxXeFuS7wtkIOZ!x5wXCb?0Nc8_rK9y zvE0enK{>va!CjyKdxHYfXT^^)Y2<6vZu=t_MY@-d914f-)Je87T52$S(OEv2ULPiK zaga9!a*)I`C|tWud$QK+<)M~aM6wx=El)dSxTYu5I2kx_GVMe!=9Ecna|-^xC3IQA z+hW~^)U2kAH`reJ3A?brVkNQEiqTt2B_Lwg==2q$X6fYI%PVuW@~4rSUwyDrJw`aT zo*@>6rL$}GOgjs<-PT^p``_hJND3l+==WmGB2fRvGyInrBq^Xx2mYd4yN~FZ6bdyo z&fW-xS3ra@854nP2*{ebQk_=egckx~@+Xm)RjQEm*H0}f&@LIOA`Yp3=HgnJ;_-I* zwjIDw^>b!etSh$N_bmMa zNX>IAh(+^qzqyB1hsa|CmeYMXEBofiVyXBj`%n=DlR^f*aq zBugj{A_h{Gtbv&=guk~xWAwPo856Ck^}Z>1Ny`?b#7dZ_lBP(#*jxPlsuVUwAH%u> z6}w?lW6-K4`wvyq%t615I(I=HDjDcY+>PWX^TSWwWuG%Vh$Lioe}mogiL~?h4SBA; z9xCa&2%OUdB`XeM&RM8W`Py| zqN7{_2?|rf2N3ReoP(8{2Qwz@r5JP%_r&Y51|p^tlsbtKjY~y`YCWC1@_u;^^7pH+ z7?GQf!A56(G?WYhl}8da?b%4py+8F!hsQ!V67Va>@X-|Oz38N(>18p;-1q==w60{5 zY#VL=e1<$B$SmT{J|?TW)$F1XdNi2~hm6T*>65RZT?-#eJ(Tkeq^62fW2iOHP|ar5 zrPKL_BQ3eHO*$UkQk;FlA$HdE@diUs?eg7~sM%YX23f**7b4~?JA?|TG1W4KTFg?%1kgfDdFUg zDb`#Q;_KyU&!dt-PTWMC>CkbTp)Ty|VJt>31YVC(@k(GN68+jIQJE*z`Yo%>WP@)( zE$6sA@)_KKK^$=+LhvgOn$vk_Q9oX@^Nr^j0G199D{2t-gB1E0qr*bkg3E~IHW}_? zxIX4}xU>}1OEGEm;ILN35;dJ>p<+z_^~SOJ1QcE%^F{5UASnZPPjyU(#p8WbV*Zf4IjFao8Ixy^VbjdG8eApuNE!5aaV|^@=jPkz2>RhfhI=1Wr(c zlsj7vjh}tB>&iB-K^>h}=NP86ys;I%v!mWNvDib&oPyffzh=HwuBaW^t61K(z=f9J z3U`TSuU}Bc#ofKhC?=H^?9Goq-^fqH-@h;e{_-#7J?2HBC$N{?NueP|ION)xgut#c@f*AAD3o4nbc5wIaa`G15ZqlNixwr!=%zFWLp74zp_;~&%t7~+I;NN)_ z=3*{J88yL>slO`HE1^VeFjGM+RSI9EGojv(C;oU3$inI%5@0nuV!_>??bQFJ1-mW- z;-k`8=_}i0r=ajDV3&0b_Mk_^b+$;!P6?!=+QIU%{@wGg`L#RBJ0le_!CGI}wvDOM z57c@V4;8}x#Z6zPj(6AvD?_Zdd5C$J+u_BZitqQRBx)Mzy1!Niu<>?tYRsao4J}b; zu4#1S^(5vy`0e`Ac_W>G(K9A@)aG0sMV$315cC$+I{v!I@V0iq#NGX?2yY#O;G~02 zB-=2G4Q8=&Zb9al?m9ibRpTGtDU_3N_IC@Rt(7#13qsdz5u#+#oYBpV+NPgV5a7ddOKSW~d)D3XV(Yo!BJzd+jq4dWR!nlIR~OdS~@AZC&`7-Vwl&?Gv_vb<+bEpgBoty%?>CFUlA7{d2-!|-3LC#+l`*W z{47WU>t)L(n`mwe8Vc_rYtg z>dZcRYYMfn0X9E&kLVR6&0p-M1k-1<{6I{BWMe#j#xEScB?6z9f1`_3Dz3|XGf}2M zo8MvtZ_g#o9i`gaTFjo2fI777D(2KdYLB5~Sx(ohxbIM`UuhUo5J-+uftZqoKo5A2 z*r?p>6Jp?Cy>`1}N!gggEg9IUhq8yhX+_SKHo8xq6urKICyYPoqBa0vfc7@r0%was z+_8w{M_(TbM)!s7c6;Ph-!Tb&a zbQJUxeIkXXcV?+$bI0zmOY}AxwBpO@3xhVi7mhQJ3@OFstz>`f2?rh*LTF#4v?oq< z^H*$nb238R@0J{O3q>fHgbG5HC3@u|$m}87D24oSXv><2lhb0954?Q1>y=4esZNiY zU38D}Vx|FfTpsZ{F|k#pmjOtjxe4#QM+T=8;uQdltzS<@GT zrdh%MyD{&@sTZL6+RR{hAg|Z^6Zi0~uMx>6qaJ`6bOryLdA_E{JKz?=w9sEWTA@yZIMRO0+-LIov?sd|*`*Pj)*rn+3{D9DdAO_oCb z;N8ZkxHD|18^7h?{hduhsBtiDRG@{Ew|WEK+u60#M$V8R92cxep>;IMto^w8$)Glt zQ*c!-m!UytbN7#Us9pt;`c-wFbcTXK?=~~GJ%5aeI@i?x54twV%FCtaBt;LGb3&3; zG~QY#w=9tu!&~c7N|Vgr<FOeG<&|V$AE5WVjyzs|&?%0U%OWGLdN9?%&7zZd?ZkO;hB&aO_SXmgJB1XI;AG6`}s8nP&Im z4RV)1l&%8T81iG0N9J5XXemumcwca?&PQCRYSu#~)lX!tXN;8I*M9S;;Li8#gdZP6 zvk&lIG%O6KuGVeUHFz%U{k#VQ!BFO$5N3TBxHs4FtUWT^y6Ve9GAF9nGoCSY4R(KE z?0(|=LH9c`aOgpKV-X5E(C;4FqbxGlk&Qa@<7=zUxwa}(@$-z_-3K@KS10W^2)E)< zb5iTW>H=HjIerUXV`w;0X(hU$`#w}&Es>p)d*wLOpfK!lhA>~To}quHysJCu`F^df zU*_J`k9fu|p0J@&`>F2y#6he#l#QVh$itS`IgRr0VlqZbGjfS z3S)l2;O>my+Ig1mRx(cU&?4JUcqK&OSxzm#!5?d?V;y;d1~5B2MNz}v2%BWdtfX7?JBevFicf77*@#oNtV@^pq9CcGC% z4hvuBOme?*y%`o*iUdri@SS(X7G^g-Fb>HLW#2Ms6dEWpvZu*~8U*^Q`4L^~>-rRN z{`H&Xomr9Lh4bb*Go|6x0!ObQgEh=LCn2Ht@$A~~a{2m$18Qh;9w0)MF!beC2V6|7 zC?GfNiC6r*IRA7EW?S-3^oi~yieOA(n9gJ^Tj-|rO`W+WT&P{IH+a~O=qjtdvROBG zlJKotYMuLqS<4Ft#MkPwsN&z{42>K0sL_5x!tfs$m<9lLaq)Tn7<7D5`E1gdHO^x9 zv%c9zv(w^Z=xS!O^ijcL*Lm?NKnBjx{^JQKym7&z-X2~ickQQ4&z$*@OSf7x`zPxY zzdPzKczW=gvt^@#`{sfyb3xN&u`e}Rw}bU-zc68jw~i;gKTmfJHx%7B zoaH5iOsEJYNb-j*X2K`9Ye~`#{zbNUe-D5Z@ruG+K%D_-U=f|MK*Esi!f>eA0Bc;3 z|7?CFW2`AVs)9UJ_2b)|*yn6tR(p!uaRU)#!ho=oeA+B2uy jCTQ-|$AkY3ei4e|7@z^d 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 p7%>GVU-e{^Xnzwb-%5K_5{ts9nK7s%M literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/green_circle.png b/desktop/src/main/resources/images/green_circle.png index 1555f43f0adda0fe5a1b556bac67a031e83f54ab..439ae2571b585b74f1eb6c6b4d6ef73e6a8e50d8 100644 GIT binary patch literal 5120 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 diff --git a/desktop/src/main/resources/images/green_circle_solid.png b/desktop/src/main/resources/images/green_circle_solid.png new file mode 100644 index 0000000000000000000000000000000000000000..1555f43f0adda0fe5a1b556bac67a031e83f54ab GIT binary patch literal 3351 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 literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/green_circle@2x.png b/desktop/src/main/resources/images/green_circle_solid@2x.png similarity index 100% rename from desktop/src/main/resources/images/green_circle@2x.png rename to desktop/src/main/resources/images/green_circle_solid@2x.png diff --git a/desktop/src/main/resources/images/light_mode_toggle.png b/desktop/src/main/resources/images/light_mode_toggle.png new file mode 100644 index 0000000000000000000000000000000000000000..870c5c209ec911fc16f702e68f501aaa240800c3 GIT binary patch literal 2013 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 Dtl2{# diff --git a/desktop/src/main/resources/images/lock_circle.png b/desktop/src/main/resources/images/lock_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..f176e08203be30f132cbce567767618993354d9c GIT binary patch literal 629 zcmV-*0*d{KP)R9J=WS&MPPFbtJ?9U$F+Za^m>Y`_Q|fe{#i4PXPhLD>K{ zfDK>+^$bpG6xmij4xh`r<3bYq^(nUmklbDDo7jFcq;;ML0zbd0Z z>fM1VBAU@ZW*Wu6(tqA4r#SM^ODyfLf_G1cc~pB6RBU=PrXf$@1H{k0!zm^E?+gCg&9u~tVA|AQ&=JvZq$&=0xYm%RB44$?CJ;<~l80Y2EB#wxO@U$xqWbOlz`aB(n23wx9huYe>H%q{C(Y}ob-Zu$Py$`u|} z-Zt%6Ke4nGL(?qkK5UDK(Y+^J=Y&1WeZ7jlD}qzEQ{>>7IOn67KQp))Wa8-?EUJhL z6I-rqG;|%qxZadgR@}n|$0GvAZeyUJl P00000NkvXXu0mjfHB|9|iy$Qz%IVgK8BYSUt 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%0A( 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 literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/logo_landscape_light.png b/desktop/src/main/resources/images/logo_landscape_light.png new file mode 100644 index 0000000000000000000000000000000000000000..e7ae4282c72ac1bebb860f70930d099712ed2ef3 GIT binary patch literal 22767 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 O+EpKg)2Yz14gDWBe-zFD literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/logo_splash.png b/desktop/src/main/resources/images/logo_splash.png deleted file mode 100644 index 110edbb85a586f05b76633e99ed43665ea1debf2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21326 zcmeFXby(a_wl~<&xVyW1;}+cAEx0xA4nYG1hmhb-aEG8l8v=x2!6A4cxLY9THkr9I zzuEWheRrPS_rIZ=e)?N=&gWE}I#yL3qoF2`jzWS0007Vx6=bvk02n>!_isoD(9hb2 z-<<#eF;ReyftQwrFO{2ztBt)An99rF4NL|0v$p{N{1$7n?B9`gysLh)!ta6`{p`UB zH@b?kdVc3wuXL>--#%A%r>964r(y$(O+j%iA$<4rbkhE`QC7Nb!cjA?S+&EMB(m{S z_BucRH%0ijle34E+bfp)`|hRd1LfpaoNs#B(UV&U+_UJil`wvw0xH94v-p#+Z?B|64fV?f@?>D6gmc2^sj5OX2b4zYucQ zf!(FurWQM?rI7a}W5X7p_!O@Eo^VUl2jeG|>5k6Y=Cuy;(8EBxqb-3+>8$HHCM|JvVJJ(;(kLLFRpL(vneCvX21U%`B zuN<}3v;*q|-LTIuyY+rM--9|`PJB<7$ahzY?{AI>dtSt2#oV9^<#2vR|CQ&vfW{>s zm#{-55Hs~kv{`UNZ8f>9ddkjup)< zEa%zm&C5zvHO;GX*`im=%5_~=$G5wl{mE2cxcBXflDM{iet6HL-YqI*K8~+%=%1>q zZxockCvf`RKi$N4Y2~P4Cx(&SZ)0X!O?*=Y6(NPqsKH^Z9X*?{e8ua6y>->+j@G9|~B(M22xA}&<1h|w`eKtGSt&T0jC@Vy3#f8YdngRnLPvu#Y9-n0P9?qmp zu4GEqB%t4c1IMG;pf)U@()vCPFRS}|3O(hY6ZG)xq_jWDZ0BfQM~yy+y7qis#D;gx zUmkfotRZ<7bG`{U;PFX}K4^O7IFf!X-!>TIgxGQW>dni=(-PH(EvKc4xQycl1l!UJ z4BFwGJr)!*#)k{fH)p#|K!>*2a~2inl^~@us6)$#^%0v0>{$I(kq$ z_`{1US88mpZ@c)b9(7q`wFQKuf0F-##dlCrcZSnArh5(Nd^npV0Q)i|KrLeh~y=^rDlzAi;*v6^Xv`K)6; zz9I90fQrZNhKZQf-EDP7UvU#K$U{>fX%5jVz)=4b;=GbM{p(B$LE%9pGv|G~BkJYY z&+Ss0@{6FkOgzU+*wQdnOs!WL*#w3p>xy(ioVyx#yj7QYWO# zD7X_gWr*OMRW&q#Sh2Qm=aUdvJ>IO8vCnKckE@Xnanc0g(tVvNU!8gTK?Ok2ZU@q~ zV~XWf%q>HVmkgCJNn|CqLHfKL4dxhk9}GNSv`-um9F!%T^rRL86XINtJ zj+{MB)|>B4O}7~JqTY?w)SgQ&TP_p%z!l6(U{|Wb6Xt2oa{G!DkYZ5qfMWAQxIMz< z`=xz?8y6IlGGgLWHC>Q)ajt;8g|;nSzC(eQW!X5!SH{+}Ull53U!#>f5J`b{=M*fK z0Vu8;QxbUly$Oon)UIS`mjJcN2byCNeM-Gz53bsK&ZIYA@67Q*f(HtV2$n`-mr8Ov z$ZkZrvC^+w;U>|FN=!^~5oSKe{ASSYyc9N+kUX;sX1VEKOfOMm?@*ET(ooc|KzbJ> zAMl@`Mh2J1P;qVX%)I z9*1yw{1FaDy~=bXu3*j&@=BcUW6#NHef!5~3M!}e{i~8|Jr)jE8~Mtmz7$4l8k{If zys<-LcW_U(*(6UdGVri;8-1gIMT7`O_40_1xlZ!s4jGiJsw4w?JytTMaH*@vi$C)*hs|m zj24(Xrc^}ERnU2)W1f7gpQS~Y9zwEbmX|3|kfX7y*a1QRwD`siyF2qh(xFJe^MPuN zYenIeLs^%egm55(6BCTaw1Q@7EE5Sy6$4Tn+ywFfuFxs=Ag?_3gj@2cMCOFrj6rm$ zo$1~Qn0G9(a;6WJy+wl9H|j2VBxa*fG`av&Do#7`Q-P4?g-fnpWztR@dVVDv5!-pn zuxfck$NI2rx`evdt_(_ZJADx+!LKkCH{?Npme2JzAGQ*nA9&Xg6w}RrD$zxK>7f8^wI)+hhy7LaSKi#_-;9=wN|chUoL?kaj2puYxy6lzc|9tc=&i zFb-Fh#Zl%b%G}QJ`KV-1?$5PGW?Js~^>_T9cU#)mo14-tQM|(S#)8BjM9w7#$OdCd z7nu<=NdnkX`{t#zQGHe#*pw8hF8L+C#8P;? z=RuLaKbm%n1`ah#RuhV7O2BW0dWE$UN`D>sJZhc$OYB&ifwG`w;7}{1IqDVr@db^s z3UF!aE{r7tG5B|lGu)7DbUCM#)@azPkrtKKqxpK1D`jLPuIRTUYvUifV z!@qWiFvRE`LAO}5u=8CdU%8j1vZcWWVzntu1@Zp$Zz)&8bgFle0 z$Pt}>;j><90$!eRo_M^1=R`K9qb$(Fx>S>&D|w?VslzSiu@q`hip%Q(m}wreL|8cM z-^W1LWGBW{pTSlusBR8U?tt^U$3jbKYP5j|VLyZq0l0n0Q@X|94^@azeZVK?rd1tM zl4L5VKIZJF;RcezJX!$bQD|Tb7v84(n>*Yfc z@P3uWGTS>|g6)-yKiv9tn65ePzB*AidOt8oQz0lVTE zu}O0txSA#Pxt`J;yo(A71S(rS%q!iPrPbZ(KRm7t&p%WJKkaP2zzS-{%)on6 z$fXW_KbI8mQsE$XCd(|sM#L=klb2R@qFOE~e9Be*RqPyN*1J!=vt%+x_0vhT?C{8n z>O|_t6Ll!gpo%d=6+gh}yhxZS^h~^}sp=ky>%%M!0DDu`u@Jcy>zD7DXhp}ZEKgHXWS3=JaWl38 zx@_c3&N{_r5U|YH5H$iN8kAWiqOPjyO0pWg7|XYY#Z#N|A)9l_ErNSJg^=6QlL>`L zRI~oOm$c3xbw1f>G5AwG<10!$UEM_7VpO<|0Tb&l%q*2w-Z(hxI7CRGTs95sC=Xa1 z?Kqv>JkVT$p=u(|JdfBMy3Vh*6}Z$JcE1BR#aJ_knxS3zAANuw8Q;kWIcM^!&z|6Yxe!vjM`f@NNcQRtp^~b8b9!^ zZktcL#U$%#B75^~&~W8@v(G>(23j1!@VIsIRsfDor&ttHU=s-TX0pG@mEx?Xe38>I z(Xg32R<*jtGd;=vQdE8^RA@vr&T>@4VM6ZfF823oQ37*W?BV1$c@HVM0CFzWP1TY6 zDq1pS4B}w7M>({*$ zE96?tDg^YZ^_6L_?W4i;%RTn~ueEDR4#RqhnJEW5?Wm&7@B}O`g_KLN#$mDVrCBB< z%SEUN__Kb<*f8>R6x&gAAAOcR;?jx$rp8zC%P)~DfVGNcrfTYxFtY{%9~X%H(NXLh zu->NC0}ZsD;c2$O4Mnpt=Vk{$Bpr#zX>Q?ZBL{1p(Lssp z(&QIG{jO=-Aw8Ju!L8V*NxxtSY#513OI0!q9`3*hCs+fB>$Z^}o0J~|RlaV7PBfjL zrg78i1-%Z-$tvFTc=IDlRuqnzT**_SW&v$jwohRU0#N=gb}P?Ljv3kfMbr+b3$Yuq zhGDNL-Vflk7z}uU#^>lq4bPY4v|(IBoW3X{ho6t!cKUMhm3>pRuaCTnvu1#*;~Q0R zZ0SrN%ypt2q=spn({w}Jutok#!qF+JQw6=QG(mu*_YN|?L9zsTci8VJ=cb&8c(;>W z;gT#{Go-H!3wQx)szmBph2-VgrCxS#?S$B2OLAeTSBxA@W-=;DPKJ*MaI;guS)-eAt{dv+s*dLoW^- zhOyvazbS8L`grY9yVe)k|Iip2QD4kS%=C3Y-%UykR@d;A2sQe#lEU&%ss@vEv{DGj=?F!t5pchlV(<;o-Z} zcpdZCEbDvabz-B?-sUo=qNja%-H6QSk4XyPN+>&Ydm2_y!_HhtPKn3#KGbw+J^|^C zi0jR)zRSFue!WOJxAfS(e$`BJ@|YWO(@_1()yJVne|CBPW%G!pg5mrx_za_qMmf^q zPfWmJkSBt(q8PRk?6J}8z}Id*QW&kO`1<=ANk=ZLLoLL7d1wFa$29rD$w6`-MCw>G1B2LfY9_hktL6 zPRZKa?k~Squmm!Os83H4myN&@iwaX8zL^%p%e^in_^f*?{nHqr1B+J$v~_IGv>eqs zQ^3K)NIf~*LiInZR{D4}DL7$6ZwM``UR`0;nV}(x$=l*$P5906eurfyzYVWuV`>WB z!d!$AyCi4#M4udEaL&h{iXAUy|1b)BP9y~UELzs675f5_W6+7qX5y`j!h|WZ?p8B( z#@#}L1d};Ih#V!|*HgJe)+UjKU<oOR;M3={0+4j}pC9W?L3(f~z1#U|^ zm>hqUcdwdAcC92(_(GG~+p7Rfj?^d>@=JxPr=lb{aFY2Kh^kbx8nb9`L6N8j!DyEV zUB#k)8#x*1f{RPZlARffxY|aJdyAoPzQc?%3LROjb(7jb*YBfG?Jy147>b>Ft9yJp zSGW>zHC44`b5ZC-ua|$emg+U#PzdSKSC7mhX)2$_k^o|_hxrP;nHP6oY=t4q06{fV z@alzh#PV{UTEn&$ezrwG+N>X}LqwZg(j@%9D&W-(-n;WBUtznQX91y zZOSH3JN6Q$#JltyaE;hZ&6N-D;p&Qbdk!cr_zzFF`WMOpPr*E9jslfY*{G7F+7_xh#JA2eIq3Ceb`u&M$2H-2H8u6X&#n-t0=c= zV`3^}tq{Rz?$#-+_ip#=m#Nj8LD`Lvc;+cDslbtpRlqQW@Z>6}d&*gZvnp_vUj@(M_H z)u;;10uSE>5l`vjs$#ST>|)Hd+b@k3_H|2touR&_r+DP6p@kpnL7PBRw~dys$}`{_ zxLeVaytnJv9&9KE`%P@nbN<3`mwSV;X2+O|vTtfIod}*kjtQ{qEwuuBHNsDSky0<~ zz(%sop~2`>U@f=ENpa~eZ->$FD6w<3;PyR~awN=MkzI*)JNg_u<^C3?a7nmf5NA`e z0KP5=cJFf{E44MeSq3#Q_Yx6)46xiwVK|fbN8*QZv0(&irmm_If?X`N+jAGz z2e8G)wb701%f5$>$hb9-3L6m`?$kkteDPKIZxSzT92#jeZ;TZjxtCktU}IW1_7Lc7 z*S9Gp=4Wrcr;aDFv@AXry>dKuM>5Y5|8*y%W{HJ2A4ujT@vdQ>f>+V!jIT2<{!dQmB z-BMmzB%&c*k{1Yh#i4c9qIAh#Tja#|Nm#s)h+&)vY7UNO zVG8@eXZ@)@op^Yn3QLIOrxg#XLFTTBnm{EUz#sLy|01hPE6au<(Gbb*QD#P=DbD8W zFNR!4oN~^u?9RfG!xw1CL*VBO&zkUPb4+%Z+P7p|sSUJUU1Ed_cBtOJ9VdlNVTdm4 zzqkd}LbAXsdF6L|IsHkIg?Bp&Q9j$=pb5>rJ_dAWN?FI~Fb;gOw}>{DeUolE?i$gH z8%5-NHM@@N?g;~$u8p#pd*|=Z`*_K+D?6A>G@43>itjkwdso6Y=otiuRN&INaNlpf z4#r74=Q|NKqYrd34-MdaGd1Fs<^m=WE1ySN?2{ptZ(^Y zSV(H!tzEGNFom!R@1@h$)4xorNb(?+FV@M}z}V7p@b;JN`p}_CBQ|rf4kKC1#sslt zxd1y|9JpM1h1yfG+Q%{*rWn@+QqT)$`DJAYnVXEUblC@DZL3`m3|s3o`Ilt-@!)ww zJ>IMFtbCBNC9uZU=*8Fyz{KsR=nczzUB~1*h?DK%_cNuhIL<4Y*>{;R=uK^y!pF*^ zmY(xy!!fIO;^)zK%a+$;beDs_zNwu)1YN7jGAW;T?j8C~#7jloVx?*Qw4nOV2{hbv z=wZw3Ot@P_n4PX9=8S79FIA}eg>;R+nmlGPX@oLqE~x@e-Y5U}N2`&EL}@U&0K%&t$* zt~dr8JHI^$-KKPY1>{SJjCg&fS*6NbZSoqw>&Mo6hqvLm;U3PRa6jbzeM?|mHtIoy z$I^Q0q95C`aG}=&vGH9Y&X<^?*h?j{&9_l6%BKuzfnK?JA*S1c;(;r)1*;J zg~pQ%mqVoD^%C%K?HASkZlcJ06gV=C!JHm!1t|FMHW8n~*H*G3D-;Eoa{#YFEe~8M z5k6ryD=j@q&K>@+aCu>@HAi*)p_%Ga2hQvPaF?z?!g*)aRUn6NXd$BsH=R7n#3dmL zH<8-u6~K45-%)$mR*2*5p~yN@6!2sap;(sg`xoHxGuC_24=f~Z03k?HiM;t~MB zjI)Q{SsAFR2wS;2b68lrT7o(JoZX;zS^$8ExSyMal_S`T$`WjA?;=Wb($PahWp6D? zqtB{ z{*2|1UWH{`t-POwD$0n`Jdan{+SSV5TKLab9!^04D_%hhb_;H9L3Un2A!~L)Ft-i6 zC8rIqCBKk>0GN;aFHnjuo?aF%R^VqSC^&~b6i0wlfRl^QLWtdp&w>{UVa3I6$qTk% z=LT~L2=ZD8^708-`~^bY!yf7h7EXU1)iad!GZc>%mym!BCp#Y>uMIn|HNOSBr8So| zyAU@QzmO$2FTb@Q_z%?cItWW@D2mc>b8!B%MZ?L$%f{8iS(HY_-o@MRpB*~(&R}gX zi)W>A@pE(X^7BAH1^9Wn1bF{RqzCr!gc|uXCKo3M_uuAiZ6z!RMYMn#o4vDzEg0nH zV*7{Xd0B*^lYuJM;@LN#$bZ_Qa}k#I09$yudg!>iI*HOekCo~f@{dbViTvGJgjHOv z{xJR_23tS-+uuD-%EA`(=TQXoKN0^gNZNL;zApdoaQ;d94-`ocFJD&=M|BT%O9!x( z*Z++3-x2=p75_Jr`oGbM{LNeypuDag{(q6L4R-(A)87J;ll>o6QBnN~ z0>Tzne^bAwg%8;J&k{iO__r=AI|~Lv$w~9K<)sn;f(He4+biYee2z=-wN_QdwRG@bvsD=qh^;Z9#TZ zcwLX>@#yX%J?+JQt*7g&dwuZ`xKCSpuv#g6y}TAQ z?wJPx#6Vrkt*hbAP(y#kOs)Wo^fzpd?HmJ3(^>Jh13CF zJ&quTAH5s58xbc~_fa5N$c(JrrYa2^HvnD!jl?c*Q8qslxgYX6l{vX#K3WcJH9Wy= zMpQ@z@Z<#~Bfj7nNV{g*sniT{^*h8UYzKDFZI#cE9UVv-MmfZUvVnpXQ6}q+e(JuS z1S2rWTnkiF-Z=*ot4gxzSy-S}evcQ%zF~W7{{!aHW1ViDkh@X}L~X$?)loA8XV^o2 zqu7??3f4`5ee8TY*2ewuwLx{C6#ncMBr41^OeS0H6^R>Kee=M{_8S5>!uGNHtiw%b@yGD3H^+JMB&9>q_L!S3nSi{ojXXRT4KFgzXB448O4q0bCM2+WVB zsVz*s6<#lbX!49+1DJpvp`@YTiamcjdu+jD>lHf3=LO_UnGPVN0YJS@WX^;A*$km~ zVb{PUAbi2=v!cfr3=2K)Q=|nx_weD&PpTfIV%*b8v0kZjb}!nr0sW+f)7Ll}C9y2< z4UGpemkRO;QTABF#9;||JUi|2Po*8zt@_Gw8ZUK6AdF!lYCGp8Bzg|8g5)=Z>!;D4 z#z3LSVG84sZBilthm>6x9_4kork4Fs+qAK zy+DMdbBe9J8^wa^AljnFhQI>$OntYo&Sb#Ju1g?yBnkgxCf%t`Jf)8+>2@>XJzg6U zc;)@-M^zd`IOvoW9cO=@J_(%)1%NKczsOJpPHF8gsdpT_|Hznfj=@Is(obKhA%{cUd8nXKtUE zN1?+JF2ET_VQ{cn^8mSD!8I5}8qV$aLfA8I#*h1bG~@KmDW6cPX_Mq$5=d4k)L*sU z__{{MNVQq!^NFaeybh77NWM%Mfttzym<#xh}UC|?s% zHM80D^(m!)^!b(1Y3(CM$`{+agChhnbmOe?EzF^K92o<6l<%X35Z6%Yc6TL1P%wT_ zh(Gl&>ZtLi`+Q6B0G;4hs9VY)xuoN~BhWrj0C*x-vD6=cUrHl32X9-0VFXzU(=`#I zP8y!@eO$As6RPemhZV$#!BuDYJx?g#7MWGw24<9w#MauuKXSZKh75S!L?bI7^i5Ic{#ZZ>f}envG2 zTMr8O!=Q>7@h01CZ!waGE2eq>?aSkeuVYque$I^Uf*CVlMs=Tf1b|ucZt8n zNYcGKJIH_Xo4_G|UIZ*Ph|bi>Y8$vms@}F0dqJ>o{|=8bsHw=zea3fT>uPps+)z5* z1q}si-$pgFKj2P%Goiurx6B%@wx&;sUV8BxT1dpM& zNUF%gLp?5IZTEfHe3R^5ljV$hebM5#U*}Yq5p_|l;fIyKxF`e@(uTdyT-s5dejH7P zSYVy@s|su$?FIfyMaU~JE#DM*=8dgKxtKQ`yM%c{g8n|cyrT>6I-T&$ ztt4O6wC^wjksmi~to)>YTKU}E|E+im;hA4^zU4ZW%#}#|QIXexz{G%EiMsqrPk!#& zor{xzluqTMr7fTCh?Y2ZjY(z@Dk*Yn)>E?Ai z?>@p#`J~GFq((R})G2PR-SfmMW71ruc{U)B_<@?XA-|56pZ3&$oMf&T!oaVA8%{nB z1?!o8Zq9hyV#56(V-Wcp>L#!!3^pf>=!X~YM1GW5vY!BFv?+$O}S5 zg-&|X)qi@^C_Qq6r4RZVmF9;5C9;x0b<|P$b$8@9No@qOvy5J{Oko3_K_PsVCY(e; z*+P@RDrDEWmCNt`j4fy{=91kkaV7nO78B>=g!Ez8ZyP1wC(L%k+(sAcQwx{(~) zOEMJz2r-yd+wBL9+OOaCH5>@RF^VYBqhjJ;6O}62KP=|Gtkg(EZGoR{Sv{0z>QkVuO%YNRCngdpTsQiSR1W@Ene~rdQ)^Slk5X18+BT6!Ik)QS zu&z=s1&YGy>R}dGj+zKDaPT?5O@y`LZ5k!8Rd(wgmY2%t(0-g(SHbKNM!FFb7DV@B zbYnj)lH$LZs<#QJBB+|+;Q&l~Y{9N;XDnYS99I8o75M)=gD;XZT9zFjnWWA1*bjrKtH0 zC(+0}idj`ztOo9*ujdE|UCF9Q5e=Q&ZI{};hs3l{;;La=+nT&Yh($&(tvzB#|kw8^l%!ux>uaY(-5&<{S>N4!2a z|7i`A)2Bd+tDea*TjIitKIFk`--)M3DiYU_S#UdE-)$e>_4CIK|Bk%n_Ed4>OR>|= z(xjIJhBd0II$~}Dry$6;sZaM2qZ_}&Z(ePq8rC6<)4o~jBojQmwxmOS$?wA*v(=w1 zXG$~RQO#aRHDxOhxU2x$mUK+W&0sFixcFm_>!E4}5=l4xnv4-_wR-tvk9u{jJ zLSRp_!t38*PNrk1gphi8V?cGH=Y6pFq-de8XnY-h$Ak6E2VUfLymgZGbLg^k_LW|F za6!F*gHwMV*7nP%yu6l$9c71COB5d6%0kiLUQB>1Fj#RtpTK7tA4(_mQO{XrBsJAN zQ5>rc$8^@EYF2rWy`;SF;6}ActgBwL9Xq-n#S>g3hAH@-h%}i*aJ{2J1~MPJ64thB zYsu@jVOaAX4aQ9BpjEMo5L?gUSB);Bc7K3<&Zej{7lBNV>la{p@`e`0skTCn#!}6k zA^}W#s906KdoCL@RQ}5*y?Gc{Dw~C6nRPjbwz4o$Q_5C#HgI}6mZq~^bBmMHFCgRV z8;1b&mm^oz%~^`(u?AmUY(*&+RO5;&{AR!84)CU8EbqMk3WM*wX^)C|m9lH3v{|6Y zlrLl8QBPPsCmI|(JLkuGMnK|+1|H#2fyPSuL`42CqJk1!+X5lKuYDABsunHt^Q}eP zm=Fk>hb`2DE306q(!RVk>D67y#Wp-8PkX_yxb>w+rJg5fOb=Vn9UiICyrz^hQqNKV zsTz(zcV&4?e4Vkaig(6G^QvjXqAowz^j!mFux{E)BC0O7#cxXeRKq*;A=tNrHn3)a zkSp66!6Hb-rW5L;FUT-DKc?cQG)<>4`7dtdO=)P?yxgev49^(ewI5Ie4Y<^G2u%FC zvBPaiJhcM zd$BI5(H&m^osZEq%VBiQuxfuD!ROO5Q;1$b0P23N6^+}6b{!S8DzIXCDwK+q34fx` z#a*}RjetK0-)2uArgG#x!tkQQNAqhQio6%o^UKtKLM(zsU9o%&G4X`TePxl?VGDz* zQI_4UA=2f;&Yn zrYtHiby?J8t?lpyxRC+6#kazKztX_Z+@`7SJJjm4-X>f0cO2O{?T49bv%&+W1H+Vi zecg@F8pw!9eimP>Dqv+?=rqY{E=^c>90YI8SGzR_jya_4C$iT>pcps_%nWrw)}Y%9 z_sm5o;_?q{0Y!;DgEP36DYQj=gd-Hhj<+7M?4OVb>I}wN#{-Yz37n=Gyt~AG!|GYg zBaH^_J0;6FlL4=x3m4T|jVU}>+rUh+g3)Ui3G6b!L|baA)7 zqWs>u|I$SFeM5CJJePfp1urREOI5YgYUK&gRDYq#bg}+9tjGq8@FyTsB~`yerte$a z+j_$37qy}JRJBiBwO5?`XNVT0V(Qos>g`6qU$0*e2_QeWa@s?8^Df~scp;>CkTJ&5C^WPW@#6j&f` z0^lcdJZaw{l~1254*dyvSF|^+X=n@<)^qSn*Ilx&Ao^ItTeUou)66$%xcO?e(KK&I zK3G*;e*N1jun5l}zFFX&7pAiv8ZnpiMPrMbesN0cuhpV{AGIDzCEp=?&`Z^Ii6Kwz zeyOgrb~Ryjf4OPa56VV@M?b{GiQlsm2TS~BYhY1dibqKlb|&?3SkOHB ztO@gQ6xU+#dTyfv8>-7_c|&Zy&;cLfHQmGNQX5@k(Qj5eCQB8GX?eG;j9qxI?qx18 z5w<*|Ur73A&30}0NAYCi!&e!e88qiN#{iEEU{nBR$~$uX>a?{0i$uSSj9j=2xQWT0ffouJ(vCnJ#Q5Gay64smYTAio?02SFl4}k{q#5U%7n3LsW!| ztBlkz+$UACDp=DF9_=MX)r#dP zw<+>5niTcy(uC+Sym!V=Dmkewkn!JsRLMSYA<#u#QSy{Vzb9H?(SEi-N0r_Ka&jR) zPHTNZHm=Dt{)LOM;1NdRHE|P!ZmPLTq^b${;?{ULh_n}Hn;oR`<089KMX&IJQsWeR zv!$jP^1Ys%7W8C+f}-?u!tR?x6xNa!3DuF{Op`)TH)#Rf^X{^WI}2J{`64@hu_{gz z0uy}qI8nDvK5~C+98X~<=pev9Pga@D~HMU zTi7bW>lb^=(FRq}5ws?cZ?P6|z>dQm7k%E6C&M3oX)|Yxx^LMroE+nh;jySyv@X0( zkl72RosT_;E_1qOS=WW=OFNcZvH7r=kXNs%ago;t>UGd3DkU~BaH*Mv8zNv=&nk~e zYi77AC>@Ac?(Q$~ohndg`!l)$-I(i(ar09(#DYvZV`d$gQ^ZPyL^SD$@!V78nxCjW z$lczLLXEmmUt?JMz=P2Q78>KIK&AM}l_7+Hr8vtJ=sk~XyfXArA)IPtBtIUsj* z?srmu!#K#DT)rY?fDT0jsjEVQfJ7gjE>(T_T;U}J@|Kk^!=XKY&Fu5x)rV0XsG{fl zy+@+U*j}NYW=#n18_cqj+cllEUhXewsUWW3otcHL9e;UeP=$)bTql&oY|SGLxeG-N z?QRK?Si{-`i6k>NlODK*Tmo=?6D1~A6Bv!`6Ak$ko+d#6eAn0mQFWaAABkxlK(z)v zb@iQd{o(58BoNis4j~Oyhjm;GdOU;t=sh>rXP?j)1nae%G;ci8*DEdqpr?u3agJ2! zbswrJ0ohy@V$OV++AO=3x7CWQ=9$yF)E{)$J!zi9eIV{U`;wP%Iwg!LW?&dU!9a`7 zgp%zPzxwu9sbv%V=>S%@gi$GIHS}m$_bLoE>>|wMNU#hxO)$0gl$qVVl5M}1W?5H- z4w5k{2Biv)bA$&6;ro%hkvcEj9k+_yvWKXJ=tG@virHtWw08wUkm(H-+vkQ3w`cs= z3bBW@N{>{Q(BDXUF7(doW=Su{_~7Na2t2_%NZI5dRn^y5&wmN)fmk~nIc^z4L!@kr z03xnI<0AXJEUP$w1d;VpFPHxGOol!dHW7FBq8WO~9>m1m1XvpG=&nZ#hgaXn?wf7q zt)&x^NC(GeK$ocN7c0f@33vL$S@^PHU*UP*tap^1+BQe;lR6s{`Snjsx;NF zIc1HboF}rmOpxZ}av8hzM2v~e*>+XVno>{%MTFF(yc6!Us&$~p7JBFW%uW)XZELRCgLwKns$IYDaTyH|)~4%EjYw zVCM@3aH!=(o~V|xJ%(r8O8v`{FvbC;=besT|7@=edSFg;q%q6l#-x(?CVM+P8*ddZ zecFPjDPjSVF-#yHcqJWrA~}MF4Q24#Xm`(*Li`K?LzF+XA&5Ai$uuWDbdx$ht-@mX zD6@HPHxxQeJQ@YQ~x)CY&-#i6-H6iGS*Y6H5wUUKDE-Ez+=V~kK z0+Rrgz;0-S#2jLKrdEq~gcU@*L8=$Ya(q7HvhF$A5Wrdx8?j9_vV>&uY`US{pqwhE zTt&0pJ)a9t*TSHZcf#GU)pHCRPZdHaPEF*3(%-e@Igpf3(A7XQcWRSKW))7T?m8i4 zpuk0SM^5C^#&g-0J$JD0mKlhE^+`T%u)pip;xQ#*aL;%e5?Y2DbWmYtF}y~J@CbjC zj8{eg9oQ$ldAw7#^>B%HqaKD*(XlS=wBM;32AB~~MhFzTEgi1V5DfkfdQQGaBt+0X zc50#H`YuPdQ4-7vAoab2_VdbU$GeE>+4ls%mA)Neuq5!XrzO#Lx+P2T&UO+*em)53 zKtSMPHy;P4!74)Xw(79Wvlt&`h0}Y?YGa?vbHaHE67BviLFn1Oj5++oyoKW&Fc@H* zF}{_bQbi4weK8YD-XeKzrDd4}{|Kjj-Mlg|Hv*xDMA1c=L3cF-Ssmu0r;Y_Z31#H) ztC+@l(9Fa=+-Lk_2n)%xe+7b6dMsfKtD>3082W_J9i}y3(mFKW1C`7+)cj6Y5^)~D zC}SY>yYTHXzy;9-?h)4yZl=vz0T~)e`pMspb&_c_34fPRpoDjUcY$@`>Z^>UDTZD$ zygqeu0a5_+#AOv2nN4~u2nd72bt72u)! zNhvg6iJr`;N;)YP=snN>*LUA*IbkFuDNL;)j28vk5Rdf`7}hW_M9f^=jj}Y`S@XM# zH*^7;r_2RFB!}g6IyQ|A=Ky+v(ZjRoJ6aq%uh~J% z0#Qq8=jZ~{W%wKdP7NV7?l}nO(2$k5YnA>Ixf7-SAm>gm#hX%C5Lxg;q;g5vtJJM! zVXrYgbk27B@To9gDJHe%#NQ`g93igpYrkOE+eof65snph&VQGFYjw|!#CQ|RR!{85 zpuAg>XUqI$u?@8a2+<^p)ZIqR1Tur3Yb~AKxXVLlT@Q*c#^}*&|7jIrf z)vhj1WI?YHlqY-rAwoZ-Tj#nQ)jze)N7Si8UQOQ0L)oU8uA=rZ)eE}}xZ#}1l{*Gm z_XVqjcRm)?^2W=KWrubn?3kc*T0r4!bIiW05#KzfkouO+yF*ZEABG|7u(of~+-fc! zaTezKi3ILGN~t>kT2y^(QIF7jsy6*1<+A`yn`M^53y+?5FJEwNZx5gT@WVAUBjR_* zgZeH3TJvlnsRSPvrDWhE0+eJY%edcpBt|t)Z?y}1g7{FmM-a3lld2AT0z_C03_yflD_KxyzhrNX zLaM@gKv0DZQ92qc+@lMdC*;Qvw{nRP`&0)&+$ZZGSs?0-ecTk*^8y5-gA=?b-Acno znodN^KF^QBJReOa!1 z2znzXeb8cq2M;k_kG)KD*6fQ29Ch)lNq2?opbx)LSpO)+-`Ivr=;$?#0N+V*6BYeo zKdKhDZoJa6)G-%+RUn%qF)~yJe$>{{d$T@ruFUE_UsDjzkMH!eZHD8gz>>FPfkK_c zj*+J1s3MrS+dPdmBsUcZdQAx3aau{TH{%qi*z3m#CwE;RtMx2}@lWL;h|qhV4(J_X zbMCwf;;Pd6=Zu6wIi0Y(FvrhltHff}i3pFFgCQ$J3Fa_6;o~p@!zQ$SEWDk{S6ZXB zuXJ$7;7!ejFrMggNzY0jj^1c=zHuab0qCrGOCGE*tj|-tBV%ro`waxM7DMDo3=n`i za<+|ywX@3Zd`sV;_nM09_qnu*l2geW{X)*Uu8)v#2x1tk=00TRv|@6MH04)U5)cF{ z7JHUGZy#N zvb*h>qge~GH6>`43tg?7`xTioVwwvF4C6XN)guHvLlUBn^LHV4u-}G>w3EWSNd#Np zNC-oOpl%#nVc%(SyK67em5{#%G}_>p{EVnHHmSOEZkkRF=scrM>JL(=fsWKK?!S6**l=|F z7u33*);fln4H#uFCgI$qYl;W?IkzpgnYp~go-=j-o5x9m8KtyHfg(j#{&V`dSY(iWu}NrKHptv!6{nmB3q*X zL#q8#E}|J-pp)~-5BoF+!6nqWF!X_pX}%%vo5aBY4;x4sl3w1$Y{}oyWD2VeHtZ6@ zJ22F{#MIcIz}58gs>zM=tAg!et_Ge&B>2XJpqoL$buPAF#Jr>j~bgMhM+2&ETVys~0N# zI-*aT*pMAd2Fy!MaW)R5nlZidDM5b;=xwHMhNJ*yX|SO^^39}nL;7$fU|!>eFa%x= zbDFIzCSg(iVZ{bf@Wp7qaO!Qgo&Z1AjSxLqohvY1TYTma9}>t{;(+cCR_UA zio$_Q;bDwR^&tfewT(MGP+t{3zeVNnyk!LQcg4aNRQg}YCG5N@emgY~bN#+AwG3i6 zjs2b?)H<$u1hcmP3#)3P=eMK^^XGu;8EerMrt9>iGp>1~Qp%MLbw7-&gENGgwR_ww z8U8w(C>A&72^GoYuf}I8UIG%OATQvIGJY3D$uRu;I?7eG$1cc8IXQVf(t=34{e$hB z+D}&Pr>Q8)o*yS)o}K8r^;ulKvbbaaFcJFb3x1kXcR&r&rx~kk7r?6R>iV7LKa!yH1OH1e3CQ|Hzk!f2;0#X zx0VOWf4D-Nc=Y1(B{8sYoeT>Rn!XR*GhT&eqI6&xSEHFe45%;|zv`r=Fjf~|J7hhr zU7dZz<&$CB$K3xbJO#u0`?T*}0DOnqA#E$dhfQ#5O!(VxqCa+m z<(N>ndRFwk4OSn52y6Gb0n*R5_GYjwhBZ36RTbu$pGEzm8y03~zLuq2k7L4{dM4re zFW^p0G(yyK?*+aUGsJ2-6A@!nV=;8~R$a&r=RkiHQ`Nswo0P_u@Rn^sA)C1^e9+W7 zZUFdTA)7fPoy(6#SjjlR-%`7;*W?`i6-rg*c5NN;vCbRm)=NScLL-4xHC~NKF+-i! z(1IHWz8o%Rqdstc4C;|y8}2X>;1b|RwC)$5p2`2Fwtuc2Ovbb>o5_O1)E$W0Y_AM_BX+jZoQrFIA{4A`?HOUU8~Bd~ zLOoeo>kEZ!W=&z^x)~peqnnRag=}UJ^gjasPUF&%BSiBF3*rix--Mw(=Mp`uC@KeI ze1JcgK^2|^{UhK4nlDp$GwltrN;F}~dF`yBn$G2)C}cC|0Y9U~;QI8I5kK@-zZuim zvEF@q7H|?e_);O8xdyWVIB9LJ)_(O>7@zY?ns7;Yz-N`^NR0roScNGZM&oqOc7%Gl zz*}Z2bP`&fU!}Rohsm4yTgpkaE5ynLzK+Q%tM__eVdJ{?&U>Y91>Q|tikGOnfXk>K z(>l3=I0922;WwD9th+E9P1Ur-;bG!D~QE953N-bMsp^nA5$%MV``{2 zW2qg1@zvH6pFhHc8QnL--|of~Y{@q9IrjlS9lGkacHPXA9Y`CA$g=jiM4z%WgF-g* zZA?VQUlU=|YK8|?P{?LJ9*W{CXs)wv64Tx~4vzru4&54p2&-VEA{fMU+^lDX{S5d! zl;t;ZzJtQ$W32+&i-1!{H*NUzbPFe)%a;q;%;zyhlk2%90^o`+YZTb3OQ2fFW|S1fEC-i_8*VzP%+ z4GZZk9eAI|REwIe@Dix(U;65}{Y!0NG&&vuRZ?O>rv`+MI+%P=4INZSZ_*b$(S=0h8t$ z)LD*S)r%okP5C2i_dei!+K_Xf&gJh4-7;6vxV1{N1egebH=?YcN_dqBYfs;A9r6P# z$8=MzcN2Y2S$5Q_NH+ujQ|Ip~;EY1{*)gr5_47k&=e?JKFVE1Va9%KU_3awewU{P; zE&s-_Tfmu^Xp0Al9xFWcfmXnDjd)Eum;bd`s^SXhb~GkzXipt6sYW>0-PCSHT;=Js z!(%Wz!S~d7?-=mCp(ZlErdgdObZ|NKQ!% z>Gw1Ir286-cOjY>j`)Ra<};Yhv}5Xgmxr2p=B9J`iNdD!mlOC`V*Kf60WSQ8<(}ktMZ`CNvi(}n29>ZsUmqv_kTCa5ACoz8YV=)!0 zj>J?no1A|KLhJT{aM^zcN{z{~UxZZR#)K(o|Q##vsFhgM-6XQk2z(gF~oKcNz0bVpNZqC*q2OtgD#|20O^afeO!FjJ#Wq{mXSEPjf;eAhr z;Ll63Bq{qjFWhWHB>h)|)$+BxmA2_%$oJw(eXx zE;YM5e^Y%Cr~SP@HX87;zhvx;Ua+uQ?&nC-wk^EY@tSI@7 zd^-R9Yn7@dz2FUpvLE`!j#ZZeD@SF!Z+wrdycQL5>En=uQnVDv{bRV9h_hqFZm+fx z-K$$?#wse?R}SneK_BCRtol_(UIGp$t+Oaho@)P!pHU885ED8bqs`atWn z@-kA_RA!{CXx&lQghSi%SR+Ac^|yZJX6@F`FF$$emyPzlLiaWGYT**R{ISw!%d?L29$gp2exRr&eRncT*0<&IL#i(>yF zM%nY1r)5JI5rAOD{+kkq3H}!8PLs*00$dMfuuboU?;uKDnXl(U zOl;$f_45|lh#@39iJVZTsKAkLu|Dh4l|$Bfsu5?c00I_^h;Il5&1!y~!}oWFhE^N0 zwjqT{zKs3(I$9;69nAVFf}fjAg@F|>Fq3Pw zaF=8v{^OT*;QX|DERHRx$^X+UqrN}1R5?C*|od^mV%#MwPNGpm(CH~v0V;n?!MZ3r`F<_iX%2@tnm4k z55hW~aoDKWdQ`P_SKsC)kNA;DNBqv1`f!`f^Gy?Whv?^>ivS?0S90Aj!qy5&C9V*n z5#w5&7b@BbkG|hH8=i#OYsq;h_u}?n!ljAN_epCmIQVO23UTxslkZTY0bwd5ws&xRhBsiw|um= zi9?w%{kpKb{2{QRM%q8s$~OCEP?! zHnCCt8vectI!>D}+u{$!zk4ZBXT=8PuT?$1vQLnZT*k;8&bY66^Ygfp9Zw>K7d` zDd`+9tkObdkV$3beo$^o!2N!vjuM>wx*lIZGNVuUw(Sj^*ULKnU8URlN~CrHjo7OU z+;eRSYRO)$wu8xiEppbF88l(5Vow_XI%9{uC2*9)@r#Qk+Xj|sMM6qiQ~VSm1_87W z?JX+P1ySipmghfF$ZYZu5zH!f5qMfS9_Up+uBwe&C^f#{{jf%fJ87y+k`+feM-Ip$ zLt`)nwZA4GK0zNMDpj^#c&AAvY!MY+SRaa7ZC(*x-dM(52K<136)ODeY9v^ND8Re_IPxKAsO@OFxNnkw-F5iOO0K%23Wvyh2@^B%_@LL%Fu}#5OQ$Ae6}3*T zhJYA8$RC3rMqa}!mdO27y=!oby*^4K29F)FJen(fEa;UVTdPv>g@5#>QO2n7QI5TH z?&CWs)&3Sm_FWWvN7y5I*5ywTI1S!7omC-YEjeYj`uW}=oiGqAwKd+xjVX!In-(Qj z7|B`ep&F#*Z3~_sD$%5 z0Cu9kEZK&3CvsqE*VJgTy$`4q65)0)t9WTG-F357by6YV5IKA2Kq`4RTVUo)M7l1; zjp3EB8BGSz-Z< zr&AWPhuBFZ9dR}Kv)qq>^%yBb>g53*t*=+IXHQS6C;Hnf;;`x1`4s~!&vT0 zF_2*;y7moE7IR&UM?RGNQMK}=n9vU`D!Sjq1s@}`sxdF$8d4QWzsfftOGRjlV<~8k z9%W4!D>1~iCMSF0@a0%e1cH@3Ss0SwsYKAx|5k|0>W_7hc30+^vGA*YqDqs&pgW1Q z;cS+r@J1QSs}~|8Hqo~WGF2It&s#W%te4cSjV$0Y-g_lf$Bdk&jePiVWKETXz-S;E zQETYuxR;;#LfodpPWXV-2kUMX9_0g&ZEG31=DmN7FeC0%o%uytGOD+zB54_^5?>?d zf%Gw9gBDhtF^HENUSH+zLJ0xSE}->`*i2sm-L8-V4@D`%2D7048w8gAMnGi02Ol0i zH@)zIOC@)hcRF5Y6keW6#i<2z`@7y=xs_G=#@aywaipx@M>`{gJ4!QVnOJ*5X_%}u z1xLXrL7y=UU#;@|$q`>+SJ@9Mtow$*u*9tUQq;KgTcKRo>jPpul&d#>__k-Fzc`CH zFVFP&I{uXI)c;XT-f)n9==}EWcjwm6Q$exS-DpY7){XCFTj(f@GZ1_P?q)K4x;zlJ zPU*K!M>g|<5}u+h2BQ#=D3Xt^()NHHfbyHx+via-({v;TsSo`v`g0`j86-10ygrys z?k4A~bdcJoV;NA{-WCKI3?BWsdp#tx#?cv&Ee$p{G?RsdL3 z>6edeghcVJG7i4BOATR!pSp1A`dG`LD|R!ao~%B=k*dJd9wb|-lhj`mg13oy5mjfu z1Uy2vi(Zi@1Gx`VTZHXmtz5zL8|SVf=}L!B;h%J%91~Tw}7i3lYI8QbNT^) zmKa@gh6ABU96xb%j-iobOGd%Y_7_$LvX&4P?R%wk_D@?bKUH_#{0TzEhd^4yXjbMV zG;6=42}?)Er+M_yRwB7JXc47YD3jFa_lfmFwUi2uV-eIkFck^y9p5RBWafwwp46oI ze!(k?U0CFX+lhMT(=Kjzj&SW3Czan|F!$WkQU_cM-w%0V?v&%$@mhs@uuTl6r#329@BbMNMp4S&Rlr@7$xkDE@i?~*!t@iEj*#`sG zvfO%Rj!t8KIV)X#GrZQz4v=ZcRk~sw>>l>`*?~FDj6!jT&`%@uG9>?V;7h?$)zAU- zx69atvS|cIo7NH3?o6>~#8gGWUTB5JnOWepUqYHpX-HLZ?=>B@Ihg3Rc<03&_JjIf z<2IAunjSn`$vH9(9>z;T*AfY~ROKl95$p7x}*J(r|fu5S*$1-e)IYRpA+iS*sL}SVz6NTTpz0qHs4rMOzM(;mFD6S=o>2YdQXdIO7zB@P3cl) zD8SS~(zMkVi9miePYm%TcYrsicd^&^_|#9fg8HD~F~Q9$?y-{R%ayBScghOvW%<}s z`%_A4=U&s%ky>3guWV7>XKDaOq)cV+(0Zwm=(g|?T`JsH{8I<8uBhWa1aW_4;=-Wp z9aBn4iGzq`k31x>t2~U5pr<`6o3_LwTjLv?ji#}y;YOqU9GOdB#D_q`_uh_FJ3}Ot zU{Lz24(r!ER7Tv7#(YZz-+roC+N^cEh@F|0sElxEphb#h1UKbf;pdHiz*-W6!EbjIC^w_Yd!i)qV)pK*b)s<4&1&_z8WRpTO}) zo#kEg0NMOo-_q|dc$gB6M%Aj1JTwmgr`8s&?XM*urN<#93U4NkjUBHhG6 zK0^vMpX)~=#FJ43bX{>FvT!6F@|Er+%wB9#nurWIR{3Hsb5*=On#o*c?4;(lWH*@s zqVzVN5i*3l@3o;}w!Dt-?@|PY-~gH!vHkVAYZ&;5JIV{4vAkS>8f@Kw3F)r3b;!~zl3>d&??btse??o>zRHwx4pN*R(XXjAPp2?MJ zm6I5h>YL!WLB!eh2KbgNXe^{r?7s2+TK$f+u>-&9toZ=;o%jPD3a?LG!e)G+Dvd!S z8hmfneV(s#aWYulFwtIg*&L@Xz^`h7ay-_FN?CnX9=n3m-@Zg94CelDa$6d zYL;fsBGOs&NVnc+DqE*D#QXfhW1$Wx+_!g@~jn7PkkJT7@~SS zQI*MGQ^rrX>XMPI%*zt);i;`wIU;u!Z)4QG>u=P+%CU@LMonPaLmI`^rC+e#Af6KY zy2C*i%^HoUWYhBQXo;0{J~SIdaGPGX9s`%pp(f6XMyAhX7D>ajGKK4=B?cvJdoc6ZR+G_N1cROTwSUNoPa zw|Xe5<2$Kd*5k%Te{7cNOobNvpya9EJ(rSVG8PrWpEI9ce;5>q-;vHclSNjiC%zqD zWLAHzNN?*XzH2mZ8=w*gm!VhJNf5T0SoglHBF^Q+O@<1DEwW}s^E~xb#bnpsyZ-m{Z$d&vmetK-hgf1Rz>SY+OOc1XgYXc3 z6$!9YCfoQho-rEZ+sHmveBjz$S{S}W*;q>GLPtgHAm%im?o&d;s;bz6!ta?NNsAG| zs=}kBt9*y~^81A)V^+M@@`^>m4uK^-#E1bb+;r;}^fP84l+;GycDI_G1U$!IEpYkv zBWN+slUn_&Wa(O{460!J2iaoQ56_A^!SMiwcW}H2W%5zF9ZuWb>bnMa3GcZXMPPrf9=%tpBJKqK4&Pvn;t6RAqtw5V*z%PIG+-9d= z@OiDe?e*gy8I!Sfw6kyCT{dC`m*`Q#iF-^K=6y<)2WhZJliZ@@n!UTJ62D}g#(a+` z3J(R+HNnhF{tF#`S3!dZe0>R^o`nB!vIt-(_dU7RfOUcW7U}h`_r_n(Ngd1c`r4-! z_i=E*vRHiY;BcyZX7fF;v8Dd}!0Gj{U>{T{f|EwV%H}{!``Jvs2A9+-|H2_W?3*7^ zRkoe&7(*{L!kb{<{y*x}jT)hMU21+JI$oOa58=AZ@Z3iYz6rzd7+wdk7 z?8!%Z#s)jjeHCOBfNjCQs<+D$GPp34klB1Q$ zkz2D7Bp4-h4n;_~iZmK#>vYOjl6oF*EY!Z{IVFAngU0zzM02zhd3AAAq`mq8!c+hG zg?~InMcn0HHO-o)rmMKB;c)GU==U(`rS^?rG8AT{PwlDbgp`d6(GpZv1t_Z}HR^)f zl<4Z3%7cx%aKd(()rVLC9MwTrdGEs28+ys>-*pM+_XVxAO$V^f;4Z}94A~TDGVNpg zWs&EWrZdPI_ovbdh){_zEop zs{ZQFbj~QuuFrm6att+ePCW|Wee4{C%as%x2lvyh)8wo-f+z0>@by1D`|keTHH1U) zVZ>wkhv1}K#G@Fmh1JaY0KP^3Qm-3o!-sr=;i!W5@z~piiY=11Z9sRNBV0Zmm6Ci2GNHt)IT8`YZ+l> zN`lN;aPK&qAGt9?J%g>+ntS3MJA4q3a)Mc_j%x&h(lurd9oc=6E}RjG7ai5s5jnhq z@|jF|7!)wRTo7YG?n(W1a7bE_eHT~Pjqip0;dtis4p4-IkcQrW>^k{*k~6vfTYUE5 z6jhVnE!|;euBHyQ-J+c8vA6|r=7n%gc+yi*1h=KeQ&Fv$8}2Ku-4z~1~X zia^EvlmEX_{|8=Ax;)`3BI|7F0TZeuD^3T~SH#NM5@aRv^e=#$TTsxNSD4*`kIRak z&yvTI-9o_1g5Ap68VKa&wh{pH^ZbpKl9M|a;A9Dev4S$^073ciSPOCq30YgQ3v*lZ zu=7FZu?txU@vsXBbMx^CKpAig^8by6h8qYP2>^$`YXxIv1!ZN$0|W?j@e8mE0=c=N z+F0_l1FQg+?A!nWZXiFem9?;i^%EUGm8jxF;HLuu-Jfdej10aMMTC82mm{~={h?*h||GzrGc@03Mm?~ ze*}w&s2ebtL*L?mn;(v0|hUPkVu$zz4|7KDDH#)Ju+*J`e*V)bIZ}N43u75rKm5>}j zPf*d&JS71UfaPECy8}FdR!=5?;`rAqOIv`G4G@}b{}F2cF%J4q+!$aX#LW%lvtqXv z7P4T6dI*3Wz%5|K4z%F06yg!EwC3fu{CDo|&emWrfE!TC2I>P)J3~Y3$<8z{pS+Lx z->JQ9flwb|<>C?H;-dS@yfk8*u!Q;_%M*j895pqOzXKo!OGzS1FwfKRaB*<}0p0!` znE!B;{|CCi&Hsl}{_m#$o$RmKGR`hO(4e&iYj`>Rm*W2u!oN7Ef-Hef?#};}>VGHs z3zom_J5Zhf8iVd<(Cv})pZnuK+yWLk{|A5m;cov2B|usKkC6WszW)Q)|G@R%Lg2qe z{6F6HKXCoG5cqEq|BrY5e+C!EzdCH76SUCtg7(i~F-ZYv?~P)iq96M)OyFQEGwi66`T@SWXGbE|5aq`(_*ieVjH zEBKhCGd|CaZ5Ch4D8(`rcv`ibzQ12^aDQLmn>YycvTpAvkH0?uxrB;20oL;`Z$B{I zxLdf!r2GHTk3ZAPZ4ESxTM6wKva&M(T?G8ZkiPwwwHQLrg>Y|@hBjn269ekN73BVOR%UwlEhdtPX3* zGKe2y>0s!Ga&}|1zG)T|yNAeu0&&FDsw(N| zMqQwb-Tz+OPH}^B=7r9AHw{U<3W|(GsE8c-)^{MXnF?uR&Kk!$lR8F*YNOsxN4NG& z*Bx>gEC}DvT&;|4xJ#_EG5-c1AH>;I`(9BnY`zJy8{}0;p%2dk87GG~mX%#mEI^7> z+(n)DC?RnGvz!ra0>G2A`6c-A`@SVtA0id+OT+YJMMNu~4~aQkU~EP}(82l!#af%6 zS5)Zej*DJZ(#`Y&_$6e;*7tJ>CElIN-UsVESb*FkkRP$)bgpZbZ9N#GU@9y6^&QX$ zO%dSI&X2O4IZ<5KtD{Fq(f(oE6oxnlX9mSWjTjNfg0E#xgb$KiGWL2O5}SkgmdxI`Yd|u63Mnw6JizsJ zRn3!NaYAkJO>T%0WGjd;7?(uwJZ!qcxv}9P26^C7?@#0SlnVc*V?f=-dZ^tWhfN-u zjqmZlh0dUQWGvuw&|yGvjD_b%je#2`Y&j68K8rHB#=%N9a1PB;d9)~d8#-lz_?A+r zL19v9UPv|rx2d3r-!L$Pww#kb;NcHK9wj01TyPJ6oY(ChzaZ!%JPgJ}4z#jE4Sv~g z^uZ-?8VKsB3zOdsdJtb@%f{k?4YqPomf+doU%_Q9SiY!$cfO7;x^Day{%tC_1s)f4 zE+7k$8yXJ)UX+tRGnSJLhB^UT9@GhHU;SiLC&rg3H&a_}nakc94#WL*9oZ@;@U;L~ zDe&%x0@V1$$Wk6eu4eN#iK}d3*Kit8*5sE}c=*Wr`1=WY84@@`)I#JWi0ayU8J@^4 zqi<#GEz@cwP}il!*d1q_P$$o$i_}(OYE(K=^}J zd0HzUhPDLPIqAK;u_wp&ge*h<0bL$ar8Yl-E72JN)R#QY0!YpwZ?HP?4QG1~dw6l? zcBmh|d#gb;g1J~1fS;=R;1O~Nex*1U2+w<_jF;sKF~xUK1xnGj;IE@dp3Qjp9_W zd&rImh7-udoP9eUB>;n>!j9pjCyMEfH`SSSdqbEXldE6^?&Oa!9()*Gx{A3!@0pVx z9lu|YoM`|@GlVm!XGlo!mfR_>(d5Z)#zguW2-S%4df1@gE(lX^=Nf06fgTP%=moYG z-Ap{dV=WD{6U8vkPBEGdfdlzIDK9}n=LcS*zBXosq6Er&BJx8eBXgmA|Us+&5!V_I9l7f6M_q}Z&_^L8&6>eR4itfC%VMOfU=caGNUDugc zi{@jmSDD#8wnHs>R(A&S_4Cb3lJgBa$0uNgRn0%=s)`{^*OqfjOx7`RpOve)Y6y`g zjZQuy_KAF(q}2U=>2|PjuiumGjB&&05@p7AyfDoG6Yl0>XzKO8S6%`sC(mv|#_+}Y zQ7Z3QUr!$vR^#URfo^w4@B!0$YJb7O<1Z zlRBh3RQg>duabf#0&rR#eWk`oc;bE8+6;P7KXtzq3E(>$%u5JV(IlD0PKMd^q98AU z8fhK1wS0&|Bf(z7dJ|X^eB1e&fAP!R64R#mZTh3}fz&3Y`3C8;U-TlsU~&roh!G2E z0n<7rcyl7QI908TtX(2kb02Sy!bj{rbe+ccT0~NvG5tRMImIx7{06sENg*flp;uV@ z1>U}?6s?4+m{QQ2<_yb1_c))I{nYmajDl%UbDf|i3Af9~a_)GD@6l*lZ!b9In6pn9a3qCTF^Ffh zL~OlGY+Ys?F(M7_@m`tieuI+sUaYu+Dnxyxe1cDy$>9f2L=a)np$Efz=-#V{+W>In zK5MbH3c3xe=f~}ji79S=d!1HLCPBlXuD~&?75J#G8qjugflwEZ4<<-g5ca{Mf~I-kW(Iph2EhW zi2Gjb#i2nHj@Nv6*>W)lH!eu^E$tI4R%kfsZu*qVo*$iLYgQFD zKRlW=2koCkp;aXIuQk!qiJl${p%_*dXu-0Z?~BT6^79~-Amnm+OoYQ7lgO)**)cM` z3RUN|8A6XN4DsgqUGbb_jDJ|VEEK4Llv5+!_>yn^*8bC4hqjTY$Hpt`p<%z`6u-lt zNT}xt@KiPbU=A5TwjtTv!HXox%#yKPLf_dy9rujwjHmbo0plf&0nYy?>lGgD;HMuAEcpP zeNe}&cwTB0G0N3{!lm-U&4x$^Ym+4Ubb-Xf4oiqOM+r3Y@X~5$tcLsIeGf8clH@d- zN%y1USf=x7TP?=V0fhSlqvw9^zUhwe38+qUEvAJ%Whz061`YA3IqRU_?dk)rOcu-~ zZE`E?6u_nA7wjs4hWI$;JU~ECzO_Q6Z=EZ1OjYC3zDNDhfSk}s|90JP`RcMJxT(vh z5aH_~qVDcG|K@A|sV9r8AcxgaoJolk+G`B9uj)}+y8}a`xMbEciVZ5QDH)~X+2?7U zvPpp^jtOqt_>4`X$)sbazLe9Gl*iaZv$GC&BN1hnnj7;nP9^*f71)zSIzjL`dCncM zya{H%$J(5mTSQoTL%n@9h9;E;GWj$6h0Jr_ef16HPTUyU*Tay{II0pI$C<0+7k=xq zjTPeg{8{PWt^(WMZMGl!*0Vwok8ycb!`%-o>R4WG^Uv49s%CIzUcwZ9d`E_wyEw}N zPzMMv3jMxWAib8zs2{vtUy!o3kgg?Qpa9JXWyURz0O9t_R8_M(w?4m9&_4bB$^cS} z|2FmVZEVf8oZ;gm_9WzWyLTnR^L-OeShuvcOA0XpRYQJr_3wcgd?Rl;n#(2(|EKflJ?%fu^C&RpR zAnx#Y>f@p*u?E)W9{i$qP2ZTRkAQ@4ml<1jnRCyRffNiW5uD(ta)y3ghYFvR;aD`W z222?{bWNV2Gx1#1MkJc}@TLD4M`L12Z#(2D4sd7X^;(zGEn_L9>O*gP+|HtJt`CPG z^cb>KSkOHx5u!$dQ&ncMD!!H@VpIgWe0AnZ$C}M_wz<1f`>A5IFvO`vx#wjb({~Wk zTJmb7Zh4fxDTic}88Tm>VT-hopVBbt9Q2~$2wK1$A2%0nlE~NlX z3DiUKrmNUcrZQn=2MRx{s&0va$w7jCDaiiMTNx)P+2k}8cbEF0dZ6Q_| zQW0c;_y^~K>*e+o^tr9PRMB~w_f;Ye@v~n{@DemslH%OFiQ~m%k#|+f7Q_Yf%b>R{ zBAL`Z!L1V4+>phf#QVn{5f|jMDQL50ucr^IgjO6Z9zjF$m)8i+s%~xg;i4?`s&aOv zAGWR32G9`+8@jk0QBmOjNd5jp_5J7jjbmaJo$_j!bsLp5M{*G!ut=1_ZGe=H3K_CK zWdbj7@dQE5%vgLUkvH*~8nlDc4U>aa`N{dNYiZF<1_&A&Zf*Je56wKvrUeL!7${i@ zTlGoa#YHX99#DQ0o8(*vRuU^mLoGk z7HVBSE4wLFLHe7)=?yHI+^Xw$_Mb+t1azy@{mmBI$`Q>NPBz-LY)GFP4aUl9iFv#* zGx*PZoU~&pCYxPIs8QTEP-=H5@1(@;X0l?rLtOvOMo&mt z?n7(U_|KRhp{q#Mpm70XbIvo+rnt~aVc=)^%K-d+7qS}pLPEK3Iv;Q<;O_ezEG(8^*3LYnK#>cAS)->?j%gR0*Pg z1tkSjhdic2Y4zrbVbRfVJMrZ_fPiYK+=-I$Q;cU2_~Y>EF5fL#Hd z5QOARBGzd4sa_A{Usn&1Nbpve@y~w~lQIbm%5HjXTvdrG_6gs3m~m3V^wAim*j;iL zl40_N_Td76XU1h>=Gz92xy{|0ehU0XJ-|`b?}^zdG}~Zo$`x{9*?tx&>vRAHm&=PM z3#XxpmkV5~EEsRDK#>tO^m<>N3u~eLf6#3vg4n#BG1lPi7QT7$zG~T%<4g}J_gKn& zW43T_g7FrUylU~yQ?-c+#p#*1J=u6Oq?>D*qrtDAVo{A9=yzW8#g{ZWM2LW;K8W5wI4Wt;E9ADf?$zOUkTzYB6&Du=W(KrHBRH zH@To8TOy?(=z-nLIg7a(8D<<;R7{r$zI7YOm;388{UW*Y1%TI**6?gilY#7t274tP zGxX?kD_CX5IAD#;dPPhGqWkgN&M)a=`W7bN3i-7AcFolu$EmY>Mz>+ZEZLQ+`&4+( zMK?ANn={pergcnJ`b_SQqn zwmSIT7&|$al+UliNuPHRb%O8fPvGrV$YBQV+T+nCBi)n~vR^#XhvsQDQ$B*ALA+2Z zsgFej!tB&`tbKU0q>P`igbC}uRMmgc3wh0|OBVs{KR$sE%bx4QR9Yu|_6u48rtgS+ zq;FyGH=3{NXXnLCKlo59VoB*#5u>S*7d$DD;^pigBo0U^=p+r#YVXEV$=k5n&AXkH$h^li4kQov?P9>s*3OvN!F z`x5@r$+1^$MmEjZzy;IcvSzt-?Io<8(4oS}%Dqzl`1K3rX3h$iNkxX8M>8E(M?ahB zL5OU|&x*2vR?dlHy?(T%;w~E4410BkA5KE};mXk+h#%=wHzs3VdVlU;=jy-8FgDKQ zCiT^&W6G`}beN{XgEf5oEk#`jiTt`ILd1Ty*gY9WOEXhPf`rhf44@yD#)Nr<;SO5m zkb@krAU+{%O`LFmN%r$ht?GW|v=X>m zc`+wTJ4Tb6KD{*&=+Zw8FS9GX3SaY3GZQt-=T~LeP^P>Y{9xS%s|EIjiOdx;ST&u# z@s)WQM6`P{R;cpZ)e|z8UGHOUw_H zNtXtHF`eSBHb8DJX?jrJHaEeGWUEz^;cnU`(L&ECU(4Z|4uFu+O2YVE$Gq`RdU4X9 zp}bJBvTz~TMACeSoG*|S!!F+fuT~}({$8!p?2=LMHH_F`O-+nK{X5HoqY?#1U(B6x zbFe*Ij+VLOm34X6M0Ai}FzFMaY;aQ%v77Nipa;uEBk@;A|Mv{VFxy{3H4KD?UeMG6 z%P#ef`$+mSRdV7C^C(MHGU}zTD%rU#O=b=5(XJ5m^h}=Q9V^kF8+~0NtuDNj&&qiJ z1%dyN;V1G}nX!h2DA<1g^=5F!v~aG^INo^nz{MoFw^e`7X+-oS3$2apa!qGSNk4J)KUcR?qf~X6G3Ik z`HO{3H9wixlJ2>$?{eonzvKDmk7`@RI#3W~KX`h5z~q%&`X2mS+>(@hbyzY!hTJab zM|%1|i%pryapbGUt^wZWHTP9{+Z0w<$iGUsq1dj<;M@F-k}Xn#eGDvBSadThu_b%a^QPuS9yTE#a{V#lIJx%#sy;_J%0>gFHs ztiK9>@59;jg5c_c61PFxE{WTEQ6-*Y)v53LES$p!*RiD10QMef(5ypx*x32Uf#7TX zY9=&&lsY;NA*wH(3@`YDzMzx3^;xs}k-KO;7`P*%`VOqO^!pZWHN*C2ie9YR1_QxK zFMBqMRfR#3j|Ess`NP#*U$Ji-)t~PZk08zE> zG5SNUI}f6dR&v(GMDyua(M7ox8893kKc}%QoE-m9YaBrQZnK8bZ2}|?{!b}IE9FtVxNvB&+LQ?^BWKm!HS|K(*#u;Sxm>kLsJHJ%r&xT6VaxVpHj3n z0mQHsNQ=p;TYS#RnXV9-@q-@a74XhhIaN?2J`|_CqQ##R~Pu?KQ>NGz0Vikq`%Tb`tItmXjQT!JkwVxMj)jBm%G~sxQfwzt25-s zF1>gs#bo8^*Utjs*#tjDt?cI2K=^B7h* zCI+-HkA`83dSK6UniA_9085M#nM*v5s&O%HwT{uBC_H68V3LCme19sphug5(*Xyvk zRe;uQv(YN&)}OWr8$ENDvM^HffvU2DnV+vMg#|&(p)X{AIxL2z zuZ`uLoj(7XvC{Ci4>+1}6vYLqB{e&wONs@^PvsRc^a@8_tW!^!32uBk9mJD;@7a72 zh1Q>PKr=siLUx9>>3z(exD7i@K5&AcRJH#`N?6Az`Wbo@C2fCKvM14c)4sZC4w|(&NJq{P^|_ zI!EHtm$8;Y`>Vqj7Xj(-#hT~BjWV+f5uI~S;HzhtRbZx}UYkW{h1OdH)>vQdLi`Dh zW8H5Qx8bS@C90i>iJ?oz$U&q42rC$Gys4A8fSwwRjH$LI>5n6945W2{#|V^i?rPr` zo-U{xB@Vs`v#g&bIZnDk0@>QLW7YOmaP934wxF(%!;YYm^vkf`LN>UnaW-In>A!!a zZiFwhI@8W5TEuM(pIYkZ&rHe2+C?hAR^AOCfoI%*;@h00zh3~m{=JHU8LMXnB{aKH zy1*&cvOxz+2w-+fvS=8-qKSw|@k8&Ke!RKa5_j+=)h5PY1#Eym%9Y28GPbZs+_-vO z-W0U~r-#i*v&#}E?s&PX5y0d5!2)=U%V48V$jde!X6~1@o-|Xkm7{8PJiH+?fu8s- zvA`k&C#A(8c*1%S-p`Rbvsw}=lz4_E!Pj@>PWl^qFave#+rNG$dcgoNI`~{GAi6e_ zGUNm;tt&sPav#jO?BcJQvSDVERq)Pk-*sjVzLwjM$9}-?F`OBJqhp?})u-FfES51b zJarzZa_uCnFA^EBnhPw6i-|H!XVz)r_%Qw zhRdCO14{$=!`X}&*I*S}Q4Yb;t|lqZ7kr4iOx%)_q1&qoVX?1D^$zijWvYJewL4Kt zLkn!Eb{SXA%h9^+m-hm39ZG&ug@_!=CP&G!nzYb$b)C;t`%DGQ7h$FQK#I;#v>FDQhObslR&nv>;6DL0>1NZSM=-$8&t(HGk&~&I@*FUqKrbj|`3nwxzl1uTPZyfsF1TlHU-tu*T z71?yx(2pBTH!`-;BlVVfgcbaai$+gFO!_sP8EaX6zum)1ce-OYg%)QhJkX>Kx_|?H z9~y>k*Gl$3{em>?IANZkh>NNp89^t)s54|6I3~t+$pY#gxJHa2U3>yO5p|Il@nd|u zUP{rTGNb7jbk|lpZLNb89dxk3_(yn|a5_%>QkA1e9jU|?qGg?RO#jnp#`dW1Ck?BE zaGdQq9bxz+Wn$vtWK zTBgE`%^&Z^a&xz<(GdlDRWO&@G?gY#y#qbNh3-ivYzJPFki%dcxc-xdl8xx8j1U`)4j~#s*JEaJOGXz7eZ%qUNBk z&ai%Mny4scvd68D%@a9T5E9YZxmo>v&xp7O(Z4hcx{bQZP`p0VMM#{q?mTL7?DA!I zwLfeKel6%cg;282>`zjpg!6oO!1<9ATyf)seH{SbB;%|~x5*yh+F36C>Nu-1p+<2!BXn$12Y{TQyFj`mEQQl9A-1HrMtK+nu zPXPf~OwhQ2x1=TJh3I+&9$3@o43}tg}ib%wH-sY$Vvv zieh2FzBR3j-7K|EUZ5%{C6E2MBJDSZZ5)_j4L*MPTu)(vDB!#D8?iU5Cz>Sc z-ptN-cqSiA3&y!BejfSuFkYY1#ohW{$kM^${mI(z0$a8LZFO_|SFCYO==T*k%UVu3 zA&4LRwVD0_UF5r(51M_!>E>Pgx-(*8jebxGic9*DeMQxa&;87@OHUx1w?w9e>7m~hZ}oumWK6cYxe)bIpeP>e zs8w_7D1PmCOAkFCa55J1G5M_LlR0}id`RwcIB{d*RBR@cwJJPvj87{No;yhtI%P_7 zD*WF{!Tkp~a7ltAiY?j(pR|L`MKd}V`-nQhsQ!(YtbJdLBtVtsS-YrK$!BX4>SwbZ zjUAATu+7x49{vO=O5O!fb1UL>ec(-)Z|mLsKX35=?BND@_1l|mCkEPVC0m3{I;dTc8edT+`efvwxq}4@2xElajr*h409iSKY7A{8lv5!7 zz)MaC-_DjF{#J;}1Dd2S8lihC@He6E?i#KV_~ImWChgagPPSGBaBGw|EO&_ zfZ#B+o4S#EOHj$FA+kG?;a{%2nyw{BvA75E$`Pd|(Q$RAQTS|gwWi|Ug9wp!fKwd# zuPn{{+kQXFFz|D!;cEsLLo4W?dpS_WhWvQJm#k5P$_wPV3}2NyC6X$ieB(}N$*O!K zhZzEQx^cj??Ob%VB&-EiyjtqvMeF|1s>pZcw*G=P{YV}C=(w}oJU-$JU+5nDzB$!D z*Nb=CKe-bSur%CPe{1$xJnZsymw%Ip+)@-Lx$i^kf!;+_2$X1CdDvb2i5LG=(DkTB zWa;aFY%9_!HG9r?Z7oDZv%Es1(9CE_ExeeyoD~Ju$M^0nT@oKb%4jk z8jBKpd&eX`KG`^|rh8rk zi1zz~2gS^DBq%|SAgndJCu1dSKv+fKP2v1HuMG3FwlLi@^k#rkO+?WqMF=G(=Mq)*vo@s)$Wq}<0+82bD_L-XOT+Qi#^iXKBr-z`bC=L|n-;x59C+UX&_S2viebs4_6 zsrg?-xch9)o2j#z@e#`;PCCEiB}LiOacTcXr-av3)USP@4%DpZfOC7HLOKJi0s9D5 znN$u5S$B%!AKBe4nI1~MVC=A+B+%dN|BFsbzkXEFbA?3CuN#HuH1aO9qWM1E&aGzj zTq8c5sw>V2(@Rg+XdRnsZ04;@y*g9Vx8o#>x1tWY(8)iHaWo|E>+2j0X=RsI6nCq{ z{#z`)zPt4>9?SYjpFFHGaf<~qF$_c%4@yIq7N1I{bJ?;88j?n z*DhF_Tx3+aHMA=YXRU*=7^v##&D1~k&H<^70R!9DS+|p&S^A<%P0`=XDJO;4v^unk zw~l(}CEOIIbGLrRc%KoZR>taCQg@do*V!`Om5Ih>!pl06Ou@~)Pn~jQ0Mk#4$o%NM z=?Ogff9bQB`8s^c3Tf;~2?gMXzGan+>2J}A7w|XJKUZQ&dDbdYXc=Sp;%dx7EqdOcs9X4j8OcinKwQc!_Zca&mGDz1(R_S_I z*KgcbF~)DlIKWC#+Y)F~x7ZWeD;$_jZxD?yL7FD})EI~TJF%htvr(~|Rib)2FZ1Ze z^osDc0tX=&uyc*zps-hh3PDL5Y8u&!NAX(BPIPC|fWeZzy&IR&LO(g#Jj_r^ITYeY zR@VpOTcO6)0PbcsOdulgZoM{>qUbHw4{@merj_d=h$?)Q{M>Oqy4nu;5Y>xhIc^`0 zYazKxFPNB(cJD$i3B^SVs;a=1H{cV+^O*~lQ30-6{Ux2#W-AZ;Eexs=sDW#v5LeT3 zBrM57`662tR-5=K>tB_S@Xjt@eWP22cV?bjFZc<$H%Z5<;iST=5oYNOCejzovceg@ zd6-l5R-rO{qGrvblDlk=37jSiDmlcIOIthj&vCa9i$1zhCMC6(S29YU+f^AjepHV{ z3f$CY=?{WMJV)K6#|6$O=XevS=7__aIaC5XVdW^cEg`fUI73z0+N&dX!ku`E5ZU%F z3BY4JFbJ5XXaB+pSVRu@FrG>Y0s~b0LZe55Hx=G#Ezy%?Z_zog)AnI&rv9tBHZvOF zBXwZ`WW;ys*RMUZApu`x?c=d9E{;?UJNQItdFDiCp}(Z9|5s;QX|(L_!1c*%O|ZT? zQ^vB@d(>F(`j?3IA$1nn+Nx4{b4#s~%fmk3FsL1!d=0-VDYQ83ITz)wR>tp|i*kv$)W$>X(P*qKwsl%i=A#dw#&Sg8IVZCuKSwj!IjCJ~DW1KD?>dvL7KUYC&YSP<0~ z`m)Q1af|K5;$E=}8qCbSG75&ZSn_`fTR_Z)OFW)&O`NU{@`#)kmm20s_9_<_K`1`R zols*}cID7=wIP#F5d`J*-uIk!76+4!o8&WbO#uCUTIF?Rli3sB*8mI`axM3SI^S`5 z@TxWmdeP3Ea3SNXA$<~`M-eJlwhJWq$m^3pJuv}M%@#8eErDE;Sx96x-@7`1^Q>!4 zjX3Bv)y=X!UqH?XsB zcfuHr3#_h@WNlv$&9{&cb$kSv*ap7O^e3piKwRYvhL%+tMbo4<*J!xJ_|j_PH>1Ba zokax9Zfi(gq1O^KIN;?>Ma{@TKMrRCbk12k2i$2pJZGNqGak4^aHMyQ2Dk$ckJ~lw zB>M{`Ku-zG{)kJyz~8zAO$uL1*6JPm+J5Dr!jY{wQ7}AcUT}B;^c0y1!@v+$O*4Cc zwI>XveM0jn%YydylhZuANCg zo&Ez+g+`^WVCHcxr~Pibla5P}c_U(lqh&urso|#KEp5-FT019E5 z6s6lAI=l~WlDxN+Dd6&rE|;jLza~ovWZA;^mL@>d8$)TM+8s!XLUwG>EP)zoU8Z2z zpoZeBbikmLIj271D}KWxJ+B_40m|nKiJ<=NjDGu;@ZiS6_^!2ICwGG5FnDkq9b02! zJQ~fw0=}$ws$!OQbbm)tBU@Qwye;;nINzfm#wNhbG77b`W|wbV^(Fj>$NHh2@^dl?xxZ(ejHjg|xw z5DXB(AW7A+K*gOqE!Z#Wh}`l4Q`-znasaW7XMTDj0KRgDMAlib@`i5JDe5U`ljGwylX&Y0f2u2H^YMzPH3u`b-OO32e$M*Q5VTD&QT3i(K?D zp@dV7HVM)78$^c|%v%&GzV|s`7a-0Yto;-VC2E4&`=0Q+t93=2ZJZ(xA+y6I3*u-o z(CgIILHS>TyR|@9YfTjz(q~)k2If9Gbx{uPY=|zbpH$gH{nUiELKdoOv-6A{Ir#Ul zST?6~yHQjs7g14om?@9n!&%{919s@n$h%k6!N;5SA;_1tE(8H0otdo^lVeMKs-OM2 zO-t@FQ&!Tk007r4dc-_BgbD#WWF8$nCbjUpF=Mtpwc| zK{@ADjpPjNlgk}&4*5)zoSJW8J6VIPDBc;}zU#KNeqc^+o4ppZar z!HSvZD1PLjVCByCWYIyt@;JH{RZTE@Vj7uxEL-g06CDYZ5*r_a#vrPlzYalO1IS|9 z=!OLb>M<60Qz;1x6ErpKZsw72#IA8f1s*)A?XR}*Bxs=I-@avbi!c!yKzR0gQEYy7 zh(;z%*26p3lr!GLkqY2S+ZWRC^ER-U zT{(xMym$wq*a#o{>_^tfMo+bV`(>(P&MbMYUEm>w1!b78vWsOU&}s(FP!iRA+QN5l>E&$U zi1*$pZi?@54}@kQAoM}wTW^CNYd_%q7pk(Rik#4v;qJEkf-M%@SsvU5%J6_7UTzS! zjZ2Q6fFTtY#kXRq9Zz0Q{44S3j77Oj@TuBi=hr)Uz8G^*;*)#)YnBUGaGZ=Lj1E-dGj5# zfE9GVcfxTTM^0$qdE-uAPJCXQC?tUOpa3p(S8mi91zlr+uak5flFf*!ws#N3h7Ex= zDgoh+7V_@)C4_SNN?uHCoJlHB@4wk!RiG2kw7Gk*P$jYFM9czfp_ zOFT6qO_S^-vV$1OyG1`tyiVpK*lQ4JMNhMB&CZ;JBawBx%z>IhmBU#Jzhs0yg{6!J z&A>Jp|HFIgG0HI1&?9x=igk6p;c{Ypd2@6fQb$d4U+=ch(Jp_m;}w z0~sqX|5O}Qpa!i^iNIc~0>LU(U0oPo+Vhzck5f*>fr!8a6A;Qe-Ctk5ix=!y$-Pn+ zSw6dovJ87^Yj*&c<-iHtHC6wRfc}oE&Xwb6kK6_&<#YG0r|kn zb9=FJimJIvU!S#NW0JJ#A9vSTGcL9R2L;LX9;0!;HE5QmE>32%uro`#w$O~IqgClM zchSEk9E1J+W2ss*ygiGuAIFNg`#*}X4xvnqymo}MMVyi7Wf?llwm(X&z7TxD)##aIp6=oPa z&cn-EFU*imoE2kds~gu$6+a;dN6zb#2$6-zV$1mVqBrNtgA1&NIjWknEtfeGb*#hQ z2sbS#2=I)sdHnlLY@Jf8q<<}qTwPs>99O_4YDQ#5DWJ)sUXO7g<9uNK@ygL@vBw3P z4}*A#Ib46eoPOxf%&;QcGBMUL`g_u;rlI0vw0mR41W)A@1Lt%Boc(!At!n&WXU)uZ zlVzIj^N)gPd8G;>h9JSJ=g^|iY>MMKPt>(uYscxO!Cs!mLkF()0xmoG^c}j%l93q# z{;Y{^-!Z+AWCx7HHhq~Hhr9fJVQ#_d-N5b1KS#KgGZuDBzSCOUex9}B2%#v=4Tkg; zPN$oUsIi~axW%j=$DxxOMj5TP&Z~I8>CLD;j~`5aTZcf1ov`l$s1GU!+;`vn!Kr#r z_aP7a4ue>-Rb=gy#uLLY1di7oN6Q~<%UCWq(RS8TB_&9?JwqBQ%o`>Cj{M!_pH7_e zdfLI!wrEH8D_c-%2)CVOI1DiIF62xLkh~Yd7L#u^E9)Wa61{;QmP@{kIfRaV?K7T{ z^PjI{X1ZI`BaEkh2_riNb~Nxxmfaq+cnbwBtKhVlZpZ%pTTU|kE3mfcLLcY1FC9+t zKK-yO-V%IXMT(y4skGu4%ov=;pk;9| z>0_dX%%{Cux#jKn3CjLW8T;jgnNPWU7cJH&q5$ot6XW=gbKP2O4)>xuIG}Z8$Jf$( z$=5JPtWW>_6zvNOza?LPp<^+YM#hv4)E|0R9VvSvc8_ES*l7#OAqJz(S2D9o{43PA zOZK0Q0CwT`Ye9E=D?TqhPsV=q?rax|3Ikx?T zC0vN*ORncgMRAByN6WSEo2aU#%91Co_SA+oYS9I8+8db25>PP<$VPG!f-77r zh&SG=XN*+o-@+wlJJ-C;WA;BfztA^PT#8am&NG+7rtMswsU_oZVVavR{y$ za|Ous3EmINj~b04hrS4E?T(sonRUaBwY7ny{xG4rpK@3m(I|`!L{N81%F@_Bk9`H@ zukej~t?)}r+Hv=cw(iHyxd?0_T5&Ano&K~zsV;IwaEXub;*B#E54B#Ll-tW^>$>@C zy$_@3W9zbhF(e#(Z+WEFC{u9{06DHZ(fcj-L*NNmbxlJBl`h|M!^#7?p48biWm_v9 zu@RO>g8IEaQGW~(hwQ;rKO}7Y@hkDBYb;&4l@Q~fms(}^JbD)oI}y7&QwGs<^H9>N z^=HI3wh`cOcX#6_^(gCKR_XlnQ&E9^buEEraQq>!Wq2Eo?0jfaMEXc{he0!EP63u; z-Fx_t(TvSAzQlm{j?A6YPy0?uCkHjr)A*1HsV4scsiKXDt(!`7ONF7+!Uxcn_vg6< zYFWDPzoUr0@nhb^vfo`6K&ypMh%qieQDXO_8_AC< z+HpN)(HwoHJFd$2f3B}8-2D0~OCkuu`)uIhbuwI)3gT>16^XT9jAGLtr5IMsP2bRl z&xbsFCCQ`IZA=c1K}7F2-tVu)43U0LR#XF4s_PH>#R*+5yABonQ+5m}IO9{@TP1UI z&pd2NU4xv>mwXT5S-pO<2J1bxH7^M7-Dt-_`%DM66_l77#!H z(;{AnxP2$esh^HgV=#j3_ggqNU}g_`g{&K;>QoYMr~?&(F^$!a##nxG>5!4G# zaV|5Ag|*h3DO@|F3Z7E-q20vlOH++GliUwo16_V>CqAGpK7exJ)pUOn!7auHf(D9A zh+j*pXA2n*2mMeI$CgKs^PMd^N)y+ZnM+4vVZf@5XDX> z)ZLEO^H@53;fyc3F}0`vs{}9xR+9Qc++fAPMWirmwfr){_0)HjX18bpdr9(GElY17XJENqs1pY=R)yk!m7nfrw_wXf4oUhIQ%J+#4O zK<0W@m5>%l{TK|B6;ttv3Eahx#M-+0R-0SKV8d((B8E89|Qy5!re ziI82VJ)Uzxg(nd@o%5|vr=o)B<29sv+Bh`g%siG5q)ZML-IAS_cbr0_bF%#TJ_zOg zHAZoFb8CLbLmwzCcPjlGdDJGzd<1hg`*tEouF3j3I%4`URjmDMl_lTkB!>KypAH!; zPV90U(FISu7Z~5$Gfwk%MH5-Pv1(&n2eoUGb53~{5z;+P&HPA6ELq*X^Y^Utr?}R+)w<*T@Q^!_ zy4SsmBmFS({b!l89L|Nif%f~)uVV89e67FVwQOn3tz(B9STv*k%}D)IzOVbYfs~o+B);rutqfy+d-^|MY@Lc9woX5-~>UC#JB<+1Q5rIZ^QN zE!LirJrXRT_h?@lNTZQ$@ggrLvhKH5e=|-MgE_x;#6Pv!Q&!Uoqj7TF0Q8Lx`ElmB zI{qV<>30=RN}lsGp(5C(UXOp<(@uceKjr~37o(gSF;lR%?>$rVTLk@pE`5OGQ@2CL z{O1^5^e4e<6Vffh%#-+&MNiJ{qWmzf=-TeO&CO-V%AI5pX+mX1m~l9Xk~1!UD(Nt!lGvnivb!Mrx7iNs*rD{xWY4S{|UIIs`z> z$cHr{sWnNS)B{3I_EGQ)FS}KCnC^`N)B6u0Dw^dSz#-ToSzfdT9;uquTQOMl z7UXQ={We7PMQ{x((>u;ax@itc)V@o0vvB5ubK#A9RSLUvp;s#W^{3q>fM0LHCx0P#K zeQ|12I)5NJFzk|;J6CfVc^E(@78o*f(?}cHGH3SMPZFJEBW-aqWtnGvi=ErPUIHg& zHhH&-vcV9qX5wE0|T`$H$z^3*xVxn3p?8!`pR#P6TFrRx zK3QPAMMJCuw^*yC?WI1}>pkbKV*@W$?0MMrk5W@{Q_6ic)*SErDfMmtv* zyCI5GiA$5=r7Xf?mGLL|8*EnFNNLJPPFmiKVv`Qpy$TLdt(2K(=^h+>cpyk2!;(Cv zgV!pjXNJLXy%4^p3Inf-SR`a=4ez4L0{v%hHcgX!k9OCO$#@bB&$hXMHQBhh46OCO zCU?H`XD~^;HM!Lgrd0lSgb=+?7$C75I1hny{?a4au8FH<-nKKdSI9IEM~2LP${7~n zH{{_*j*%Knz!epb0x&RL^|?MP{NuQv8|xi^+PV&9;4~T+N^1&c$^+-btgYL;q4+Qh zp43Xu_H6H7*RM9Wt!;m1p#)KWxCO(MB0faY($YIL$8(4zzIJBz!yPUZ>HSn&eCoTx zMcmLqSb%EAlTdJih)sUq|t86J)Z8DQ0xM#NH;Q ksE_9V$&*|kV=iG9L=pbM-7!Jnl`@F@a~0{*r)F>d4~HHpA^-pY literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/logo_splash_testnet.png b/desktop/src/main/resources/images/logo_splash_testnet.png deleted file mode 100644 index 00e44cc5b1ce8ef316367d1b2a6e9801e35f94f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15693 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ʓe{JL3z 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 F{{hY4yx{-< literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/logo_splash_testnet@2x.png b/desktop/src/main/resources/images/logo_splash_testnet_light.png similarity index 100% rename from desktop/src/main/resources/images/logo_splash_testnet@2x.png rename to desktop/src/main/resources/images/logo_splash_testnet_light.png diff --git a/desktop/src/main/resources/images/ltc_logo.png b/desktop/src/main/resources/images/ltc_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0c7de040771baf2148fa1982446cca9b0ffb1ccb GIT binary patch literal 77610 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 literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/red_circle_solid.png b/desktop/src/main/resources/images/red_circle_solid.png new file mode 100644 index 0000000000000000000000000000000000000000..c5c182ed94f0f197bb3fdd88d4147ea02fdbd943 GIT binary patch literal 895 zcmV-_1AzRAP)gtZ# z_=a$=mE)3;31Hlh&?%yBIA_79>ak?7niOI^zkAIfZQ z4su8X*Fpf}5XB-~D(1)%w8OuE9cX_tJet198L}*|=l$Tr()7$f*+1?mn(Kf^A`n7g z&0@@d4d&<12%R_q=jgBZ)03GFmMi4eYm(ZNyLmIIOiY4TtcP%2if2KQHR^XmcgD&J zqStRATf6xC+~h{dI(T*lF2jE{p44?|Q{fe}2RDt^Nsk>j}5F!&Eg`RuPut!lq&tjm&n|s()v> z9&}kq`GmOQfTkh;DSJV#NnsjCZx*4FXasi{sL-&lveD|*U%Poy{$yiv93(u1w$=S(FCN-5xJ%ZzOH#avgG&D32E)`z{7yzQt Vzwv0Rr8ocp002ovPDHLkV1k6tu37*9 literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/alert_round@2x.png b/desktop/src/main/resources/images/red_circle_solid@2x.png similarity index 100% rename from desktop/src/main/resources/images/alert_round@2x.png rename to desktop/src/main/resources/images/red_circle_solid@2x.png diff --git a/desktop/src/main/resources/images/settings.png b/desktop/src/main/resources/images/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..8a534387f017b183f2e8bf63434b43de2747a118 GIT binary patch literal 1193 zcmV;a1XlZrP)e6SB{%yP@7+IjlLkaV;dNk(Q?^5iFk=-CE-UqF;%q6UJKqtJvK>csJIlY|yJgvM`E3||3pAe0 z&`@-p?7Jy@Nv|dj$~x#eRn^Cu?oq81k_~TE^@Ju|qrD2Q(^>HPqpF_zMoh4C{MnC2 z>>RY7MWj*H;|K-Vm4Dw=^?PAV4iA%j`c<_t23kZMb959J=2Ugz+i2VvhS^?sW*4v) zC4ypth5P%7$Cz6th6VnHbdQP-khR@1uh|A8zVx$}1Yy9t=NUF;En*5P;qjJV zfof6ew=1OC;>%J#P>==-+W{XH_-)|?$%p3xUigH( zI!609RrR^JSMX|Qwi>gAICo9ED^}1ILDMUc$&IRB?F;I)f{e?V;n(XEgU0HwO9KaZ zZ-JY6M5zYvIkhXSx91P}mF+4QJs+LkKmPTgVBG4%IRV~a%Z-m?OMU*PTaiz$tHtN( zF$JF)Y>^^VzYA^%=TXSR;veV`*4_=S-DS8zh-`UXiyr(GvJ+Pw?*=6dmxz|0E&(M@ zqUf>TljdI{o*rjd>EA|=b53ld%HWl|0f$mZ7#sX1!M&t65n+)&UboI;di>{ffA#?u zRDrmGFX0sd&@%$ufWxC9nG?J9!!@_~`jX4eVR;J)c0Eom1l*_b75+cxt&Y9!*L!Iy9^00000NkvXX Hu0mjfBIGvH literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/support.png b/desktop/src/main/resources/images/support.png new file mode 100644 index 0000000000000000000000000000000000000000..b40bdd28e1b50a4294638785a7f4293b65fb2bf8 GIT binary patch literal 728 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

      Example:

      ```shell -% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb" "ABAF11C65A2970B130ABE3C479BE3E4300411886" +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/1.1.2/haveno-v1.1.2-linux-x86_64-installer.deb" "ABAF11C65A2970B130ABE3C479BE3E4300411886" ``` #### *TemplateVM Using Precompiled Package From `git` Repository (CLI)* @@ -195,9 +195,9 @@ $ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno ```shell # export https_proxy=http://127.0.0.1:8082 -# curl -sSLo /tmp/haveno.deb https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb -# curl -sSLo /tmp/haveno.deb.sig https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb.sig -# curl -sSLo /tmp/haveno-jar.SHA-256 https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-SNAPSHOT-all.jar.SHA-256 +# curl -sSLo /tmp/haveno.deb https://github.com/havenoexample/haveno-example/releases/download/1.1.2/haveno-v1.1.2-linux-x86_64-installer.deb +# curl -sSLo /tmp/haveno.deb.sig https://github.com/havenoexample/haveno-example/releases/download/1.1.2/haveno-v1.1.2-linux-x86_64-installer.deb.sig +# curl -sSLo /tmp/haveno-jar.SHA-256 https://github.com/havenoexample/haveno-example/releases/download/1.1.2/haveno-v1.1.2-linux-x86_64-SNAPSHOT-all.jar.SHA-256 ```

      Note:

      @@ -206,9 +206,9 @@ $ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno

      For Whonix On Anything Other Than Qubes OS:

      ```shell -# curl -sSLo /tmp/haveno.deb https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb -# curl -sSLo /tmp/haveno.deb.sig https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb.sig -# curl -sSLo /tmp/haveno-jar.SHA-256 https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-SNAPSHOT-all.jar.SHA-256 +# curl -sSLo /tmp/haveno.deb https://github.com/havenoexample/haveno-example/releases/download/1.1.2/haveno-v1.1.2-linux-x86_64-installer.deb +# curl -sSLo /tmp/haveno.deb.sig https://github.com/havenoexample/haveno-example/releases/download/1.1.2/haveno-v1.1.2-linux-x86_64-installer.deb.sig +# curl -sSLo /tmp/haveno-jar.SHA-256 https://github.com/havenoexample/haveno-example/releases/download/1.1.2/haveno-v1.1.2-linux-x86_64-SNAPSHOT-all.jar.SHA-256 ```

      Note:

      diff --git a/scripts/install_whonix_qubes/README.md b/scripts/install_whonix_qubes/README.md index 21b6beb468..4ca3cef867 100644 --- a/scripts/install_whonix_qubes/README.md +++ b/scripts/install_whonix_qubes/README.md @@ -35,7 +35,7 @@ $ bash 0.0-dom0.sh && bash 0.1-dom0.sh && bash 0.2-dom0.sh

      Example:

      ```shell -% sudo bash 1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/1.1.1/haveno-v1.1.1-linux-x86_64-installer.deb" "ABAF11C65A2970B130ABE3C479BE3E4300411886" +% sudo bash 1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/1.1.2/haveno-v1.1.2-linux-x86_64-installer.deb" "ABAF11C65A2970B130ABE3C479BE3E4300411886" ``` ### *Via Source* diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index b846dff4c9..c92a3d61d8 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.1.1"; + private static final String VERSION = "1.1.2"; private SeedNode seedNode; private Timer checkConnectionLossTime; From ee49324fbb86456446c122a41f215f520fd311b8 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 8 Jun 2025 10:14:27 -0400 Subject: [PATCH 307/371] fix divide by zero error opening trade summary with no history --- .../main/java/haveno/core/trade/ClosedTradableFormatter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/ClosedTradableFormatter.java b/core/src/main/java/haveno/core/trade/ClosedTradableFormatter.java index db42547d45..cb5b0f1d6e 100644 --- a/core/src/main/java/haveno/core/trade/ClosedTradableFormatter.java +++ b/core/src/main/java/haveno/core/trade/ClosedTradableFormatter.java @@ -76,7 +76,7 @@ public class ClosedTradableFormatter { } public String getTotalTxFeeAsString(BigInteger totalTradeAmount, BigInteger totalTxFee) { - double percentage = HavenoUtils.divide(totalTxFee, totalTradeAmount); + double percentage = totalTradeAmount.equals(BigInteger.ZERO) ? 0 : HavenoUtils.divide(totalTxFee, totalTradeAmount); return Res.get(I18N_KEY_TOTAL_TX_FEE, HavenoUtils.formatXmr(totalTxFee, true), formatToPercentWithSymbol(percentage)); @@ -104,7 +104,7 @@ public class ClosedTradableFormatter { } public String getTotalTradeFeeAsString(BigInteger totalTradeAmount, BigInteger totalTradeFee) { - double percentage = HavenoUtils.divide(totalTradeFee, totalTradeAmount); + double percentage = totalTradeAmount.equals(BigInteger.ZERO) ? 0 : HavenoUtils.divide(totalTradeFee, totalTradeAmount); return Res.get(I18N_KEY_TOTAL_TRADE_FEE_BTC, HavenoUtils.formatXmr(totalTradeFee, true), formatToPercentWithSymbol(percentage)); From 183782982c1b4e6e7c67c5a7e7ab9ea73c3a0404 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 9 Jun 2025 06:51:59 -0400 Subject: [PATCH 308/371] fix display name of non-fiat traditional currencies --- core/src/main/java/haveno/core/locale/CurrencyUtil.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java index 911bb65d96..c94d55c70b 100644 --- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java +++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java @@ -406,6 +406,13 @@ public class CurrencyUtil { removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na"); return getCryptoCurrency(currencyCode).map(TradeCurrency::getName).orElse(xmrOrRemovedAsset); } + if (isTraditionalNonFiatCurrency(currencyCode)) { + return getTraditionalNonFiatCurrencies().stream() + .filter(currency -> currency.getCode().equals(currencyCode)) + .findAny() + .map(TradeCurrency::getName) + .orElse(currencyCode); + } try { return Currency.getInstance(currencyCode).getDisplayName(); } catch (Throwable t) { From 62d5eb4bc35121018dfa3e502adf808f0b466ac4 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 9 Jun 2025 07:05:46 -0400 Subject: [PATCH 309/371] fix distorted confirm payment sent checkbox in windows dark mode --- desktop/src/main/java/haveno/desktop/haveno.css | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/src/main/java/haveno/desktop/haveno.css b/desktop/src/main/java/haveno/desktop/haveno.css index 7c24526aaf..7fc6948fda 100644 --- a/desktop/src/main/java/haveno/desktop/haveno.css +++ b/desktop/src/main/java/haveno/desktop/haveno.css @@ -2283,6 +2283,7 @@ textfield */ -fx-background-insets: 44; -fx-background-radius: 15; -fx-border-radius: 15; + -fx-effect: dropshadow(gaussian, -bs-text-color-dropshadow-light-mode, 44, 0, 0, 0); } .notification-popup-bg, .peer-info-popup-bg { From 53e2c5cc243c8f8b0d0914af7ea109e2869c34c9 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 9 Jun 2025 07:06:00 -0400 Subject: [PATCH 310/371] center the top nav buttons when window is small --- desktop/src/main/java/haveno/desktop/main/MainView.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index ee7434324c..2a5b994cdb 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -305,8 +305,12 @@ public class MainView extends InitializableView { } }); + // add spacer to center the nav buttons when window is small + Region rightSpacer = new Region(); + HBox.setHgrow(rightSpacer, Priority.ALWAYS); + HBox primaryNav = new HBox(getLogoPane(), marketButton, getNavigationSpacer(), buyButton, getNavigationSpacer(), - sellButton, getNavigationSpacer(), portfolioButtonWithBadge, getNavigationSpacer(), fundsButton); + sellButton, getNavigationSpacer(), portfolioButtonWithBadge, getNavigationSpacer(), fundsButton, rightSpacer); primaryNav.setAlignment(Pos.CENTER_LEFT); primaryNav.getStyleClass().add("nav-primary"); From e4e118f70cfdd6d84728d340b2940a8b09484c76 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 9 Jun 2025 07:06:43 -0400 Subject: [PATCH 311/371] rename logos with dark_mode and light_mode postfix --- .../main/java/haveno/desktop/app/HavenoAppMain.java | 2 +- desktop/src/main/java/haveno/desktop/theme-dark.css | 6 +++--- .../src/main/java/haveno/desktop/theme-light.css | 6 +++--- ...dscape_dark.png => logo_landscape_dark_mode.png} | Bin ...cape_light.png => logo_landscape_light_mode.png} | Bin ...go_splash_dark.png => logo_splash_dark_mode.png} | Bin ..._splash_light.png => logo_splash_light_mode.png} | Bin ...t_dark.png => logo_splash_testnet_dark_mode.png} | Bin ...light.png => logo_splash_testnet_light_mode.png} | Bin 9 files changed, 7 insertions(+), 7 deletions(-) rename desktop/src/main/resources/images/{logo_landscape_dark.png => logo_landscape_dark_mode.png} (100%) rename desktop/src/main/resources/images/{logo_landscape_light.png => logo_landscape_light_mode.png} (100%) rename desktop/src/main/resources/images/{logo_splash_dark.png => logo_splash_dark_mode.png} (100%) rename desktop/src/main/resources/images/{logo_splash_light.png => logo_splash_light_mode.png} (100%) rename desktop/src/main/resources/images/{logo_splash_testnet_dark.png => logo_splash_testnet_dark_mode.png} (100%) rename desktop/src/main/resources/images/{logo_splash_testnet_light.png => logo_splash_testnet_light_mode.png} (100%) diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java index 73e78ab6d7..c6217cfe09 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java @@ -216,7 +216,7 @@ public class HavenoAppMain extends HavenoExecutable { // Set the dialog content VBox vbox = new VBox(10); - ImageView logoImageView = new ImageView(ImageUtil.getImageByPath("logo_splash_light.png")); + ImageView logoImageView = new ImageView(ImageUtil.getImageByPath("logo_splash_light_mode.png")); logoImageView.setFitWidth(342); logoImageView.setPreserveRatio(true); vbox.getChildren().addAll(logoImageView, passwordField, errorMessageField, versionField); diff --git a/desktop/src/main/java/haveno/desktop/theme-dark.css b/desktop/src/main/java/haveno/desktop/theme-dark.css index 1e56814261..5316735b69 100644 --- a/desktop/src/main/java/haveno/desktop/theme-dark.css +++ b/desktop/src/main/java/haveno/desktop/theme-dark.css @@ -578,15 +578,15 @@ } #image-logo-splash { - -fx-image: url("../../images/logo_splash_dark.png"); + -fx-image: url("../../images/logo_splash_dark_mode.png"); } #image-logo-splash-testnet { - -fx-image: url("../../images/logo_splash_testnet_dark.png"); + -fx-image: url("../../images/logo_splash_testnet_dark_mode.png"); } #image-logo-landscape { - -fx-image: url("../../images/logo_landscape_dark.png"); + -fx-image: url("../../images/logo_landscape_dark_mode.png"); } .table-view .placeholder { diff --git a/desktop/src/main/java/haveno/desktop/theme-light.css b/desktop/src/main/java/haveno/desktop/theme-light.css index 160a21e3c3..99bc71a49c 100644 --- a/desktop/src/main/java/haveno/desktop/theme-light.css +++ b/desktop/src/main/java/haveno/desktop/theme-light.css @@ -144,15 +144,15 @@ } #image-logo-splash { - -fx-image: url("../../images/logo_splash_light.png"); + -fx-image: url("../../images/logo_splash_light_mode.png"); } #image-logo-splash-testnet { - -fx-image: url("../../images/logo_splash_testnet_light.png"); + -fx-image: url("../../images/logo_splash_testnet_light_mode.png"); } #image-logo-landscape { - -fx-image: url("../../images/logo_landscape_light.png"); + -fx-image: url("../../images/logo_landscape_light_mode.png"); } #charts .default-color0.chart-series-area-fill { diff --git a/desktop/src/main/resources/images/logo_landscape_dark.png b/desktop/src/main/resources/images/logo_landscape_dark_mode.png similarity index 100% rename from desktop/src/main/resources/images/logo_landscape_dark.png rename to desktop/src/main/resources/images/logo_landscape_dark_mode.png diff --git a/desktop/src/main/resources/images/logo_landscape_light.png b/desktop/src/main/resources/images/logo_landscape_light_mode.png similarity index 100% rename from desktop/src/main/resources/images/logo_landscape_light.png rename to desktop/src/main/resources/images/logo_landscape_light_mode.png diff --git a/desktop/src/main/resources/images/logo_splash_dark.png b/desktop/src/main/resources/images/logo_splash_dark_mode.png similarity index 100% rename from desktop/src/main/resources/images/logo_splash_dark.png rename to desktop/src/main/resources/images/logo_splash_dark_mode.png diff --git a/desktop/src/main/resources/images/logo_splash_light.png b/desktop/src/main/resources/images/logo_splash_light_mode.png similarity index 100% rename from desktop/src/main/resources/images/logo_splash_light.png rename to desktop/src/main/resources/images/logo_splash_light_mode.png diff --git a/desktop/src/main/resources/images/logo_splash_testnet_dark.png b/desktop/src/main/resources/images/logo_splash_testnet_dark_mode.png similarity index 100% rename from desktop/src/main/resources/images/logo_splash_testnet_dark.png rename to desktop/src/main/resources/images/logo_splash_testnet_dark_mode.png diff --git a/desktop/src/main/resources/images/logo_splash_testnet_light.png b/desktop/src/main/resources/images/logo_splash_testnet_light_mode.png similarity index 100% rename from desktop/src/main/resources/images/logo_splash_testnet_light.png rename to desktop/src/main/resources/images/logo_splash_testnet_light_mode.png From 4f9e39410d9071626761a49791680fd6efbb7543 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 9 Jun 2025 07:22:23 -0400 Subject: [PATCH 312/371] make account creation buttons green --- .../main/account/content/cryptoaccounts/CryptoAccountsView.java | 1 + .../content/traditionalaccounts/TraditionalAccountsView.java | 1 + 2 files changed, 2 insertions(+) diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java index bdadfffbda..7a4f20cacf 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java @@ -181,6 +181,7 @@ public class CryptoAccountsView extends PaymentAccountsView tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), Res.get("shared.ExportAccounts"), Res.get("shared.importAccounts")); addAccountButton = tuple3.first; + addAccountButton.setId("buy-button-big"); exportButton = tuple3.second; importButton = tuple3.third; } diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java index 79243691bc..d3555d197b 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java @@ -462,6 +462,7 @@ public class TraditionalAccountsView extends PaymentAccountsView tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), Res.get("shared.ExportAccounts"), Res.get("shared.importAccounts")); addAccountButton = tuple3.first; + addAccountButton.setId("buy-button-big"); exportButton = tuple3.second; importButton = tuple3.third; } From 145f0c9cf680dd127f812d7dd31863bc852ea5b0 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 10 Jun 2025 09:03:22 -0400 Subject: [PATCH 313/371] make select backup location button green --- .../haveno/desktop/main/account/content/backup/BackupView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/backup/BackupView.java b/desktop/src/main/java/haveno/desktop/main/account/content/backup/BackupView.java index d8d3eea770..98d1e80af3 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/backup/BackupView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/backup/BackupView.java @@ -91,6 +91,7 @@ public class BackupView extends ActivatableView { Tuple2 tuple2 = add2ButtonsAfterGroup(root, ++gridRow, Res.get("account.backup.selectLocation"), Res.get("account.backup.backupNow")); selectBackupDir = tuple2.first; + selectBackupDir.setId("buy-button-big"); backupNow = tuple2.second; updateButtons(); From ab5f6c81919a09d2eb724681c3563baa37482cf0 Mon Sep 17 00:00:00 2001 From: atsamd21 Date: Thu, 12 Jun 2025 14:16:59 +0200 Subject: [PATCH 314/371] Add disableRateLimits option to daemon (#1803) --- .../java/haveno/common/config/Config.java | 10 ++++ .../java/haveno/daemon/grpc/GrpcServer.java | 51 +++++++++++++------ 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/common/src/main/java/haveno/common/config/Config.java b/common/src/main/java/haveno/common/config/Config.java index b162e211b4..03a61cbd40 100644 --- a/common/src/main/java/haveno/common/config/Config.java +++ b/common/src/main/java/haveno/common/config/Config.java @@ -119,6 +119,7 @@ public class Config { public static final String PASSWORD_REQUIRED = "passwordRequired"; public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries"; public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath"; + public static final String DISABLE_RATE_LIMITS = "disableRateLimits"; // Default values for certain options public static final int UNSPECIFIED_PORT = -1; @@ -208,6 +209,7 @@ public class Config { public final boolean passwordRequired; public final boolean updateXmrBinaries; public final String xmrBlockchainPath; + public final boolean disableRateLimits; // Properties derived from options but not exposed as options themselves public final File torDir; @@ -639,6 +641,13 @@ public class Config { .ofType(String.class) .defaultsTo(""); + ArgumentAcceptingOptionSpec disableRateLimits = + parser.accepts(DISABLE_RATE_LIMITS, + "Disables all API rate limits") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + try { CompositeOptionSet options = new CompositeOptionSet(); @@ -753,6 +762,7 @@ public class Config { this.passwordRequired = options.valueOf(passwordRequiredOpt); this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt); this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt); + this.disableRateLimits = options.valueOf(disableRateLimits); } catch (OptionException ex) { throw new ConfigException("problem parsing option '%s': %s", ex.options().get(0), diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java index 1de4580038..d2a5fc49b5 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java @@ -22,11 +22,15 @@ import com.google.inject.Singleton; import haveno.common.config.Config; import haveno.core.api.CoreContext; import haveno.daemon.grpc.interceptor.PasswordAuthInterceptor; -import io.grpc.Server; -import io.grpc.ServerBuilder; import static io.grpc.ServerInterceptors.interceptForward; import java.io.IOException; import java.io.UncheckedIOException; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Server; +import io.grpc.ServerBuilder; import lombok.extern.slf4j.Slf4j; @Singleton @@ -55,26 +59,41 @@ public class GrpcServer { GrpcXmrConnectionService moneroConnectionsService, GrpcXmrNodeService moneroNodeService) { this.server = ServerBuilder.forPort(config.apiPort) - .addService(interceptForward(accountService, accountService.interceptors())) - .addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors())) - .addService(interceptForward(disputesService, disputesService.interceptors())) - .addService(interceptForward(helpService, helpService.interceptors())) - .addService(interceptForward(offersService, offersService.interceptors())) - .addService(interceptForward(paymentAccountsService, paymentAccountsService.interceptors())) - .addService(interceptForward(priceService, priceService.interceptors())) .addService(shutdownService) - .addService(interceptForward(tradeStatisticsService, tradeStatisticsService.interceptors())) - .addService(interceptForward(tradesService, tradesService.interceptors())) - .addService(interceptForward(versionService, versionService.interceptors())) - .addService(interceptForward(walletsService, walletsService.interceptors())) - .addService(interceptForward(notificationsService, notificationsService.interceptors())) - .addService(interceptForward(moneroConnectionsService, moneroConnectionsService.interceptors())) - .addService(interceptForward(moneroNodeService, moneroNodeService.interceptors())) .intercept(passwordAuthInterceptor) + .addService(interceptForward(accountService, config.disableRateLimits ? interceptors() : accountService.interceptors())) + .addService(interceptForward(disputeAgentsService, config.disableRateLimits ? interceptors() : disputeAgentsService.interceptors())) + .addService(interceptForward(disputesService, config.disableRateLimits ? interceptors() : disputesService.interceptors())) + .addService(interceptForward(helpService, config.disableRateLimits ? interceptors() : helpService.interceptors())) + .addService(interceptForward(offersService, config.disableRateLimits ? interceptors() : offersService.interceptors())) + .addService(interceptForward(paymentAccountsService, config.disableRateLimits ? interceptors() : paymentAccountsService.interceptors())) + .addService(interceptForward(priceService, config.disableRateLimits ? interceptors() : priceService.interceptors())) + .addService(interceptForward(tradeStatisticsService, config.disableRateLimits ? interceptors() : tradeStatisticsService.interceptors())) + .addService(interceptForward(tradesService, config.disableRateLimits ? interceptors() : tradesService.interceptors())) + .addService(interceptForward(versionService, config.disableRateLimits ? interceptors() : versionService.interceptors())) + .addService(interceptForward(walletsService, config.disableRateLimits ? interceptors() : walletsService.interceptors())) + .addService(interceptForward(notificationsService, config.disableRateLimits ? interceptors() : notificationsService.interceptors())) + .addService(interceptForward(moneroConnectionsService, config.disableRateLimits ? interceptors() : moneroConnectionsService.interceptors())) + .addService(interceptForward(moneroNodeService, config.disableRateLimits ? interceptors() : moneroNodeService.interceptors())) .build(); + coreContext.setApiUser(true); } + private ServerInterceptor[] interceptors() { + return new ServerInterceptor[]{callLoggingInterceptor()}; + } + + private ServerInterceptor callLoggingInterceptor() { + return new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + log.debug("GRPC endpoint called: " + call.getMethodDescriptor().getFullMethodName()); + return next.startCall(call, headers); + } + }; + } + public void start() { try { server.start(); From 2f18a74478d8e36c81124ebdd87ab3ab86ce2482 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 13 Jun 2025 07:57:02 -0400 Subject: [PATCH 315/371] support sweeping funds from grpc api, relay multiple txs --- .../main/java/haveno/core/api/CoreApi.java | 8 +++-- .../haveno/core/api/CoreWalletsService.java | 16 +++++++-- .../core/xmr/wallet/XmrWalletService.java | 34 +++++++++++++----- .../daemon/grpc/GrpcWalletsService.java | 36 ++++++++++++++----- proto/src/main/proto/grpc.proto | 20 ++++++++--- 5 files changed, 89 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index e8e83978eb..5162bfdb33 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -299,8 +299,12 @@ public class CoreApi { return walletsService.createXmrTx(destinations); } - public String relayXmrTx(String metadata) { - return walletsService.relayXmrTx(metadata); + public List createXmrSweepTxs(String address) { + return walletsService.createXmrSweepTxs(address); + } + + public List relayXmrTxs(List metadatas) { + return walletsService.relayXmrTxs(metadatas); } public long getAddressBalance(String addressString) { diff --git a/core/src/main/java/haveno/core/api/CoreWalletsService.java b/core/src/main/java/haveno/core/api/CoreWalletsService.java index 68ec8c13ea..0433a8e994 100644 --- a/core/src/main/java/haveno/core/api/CoreWalletsService.java +++ b/core/src/main/java/haveno/core/api/CoreWalletsService.java @@ -173,12 +173,24 @@ class CoreWalletsService { } } - String relayXmrTx(String metadata) { + List createXmrSweepTxs(String address) { accountService.checkAccountOpen(); verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); try { - return xmrWalletService.relayTx(metadata); + return xmrWalletService.createSweepTxs(address); + } catch (Exception ex) { + log.error("", ex); + throw new IllegalStateException(ex); + } + } + + List relayXmrTxs(List metadatas) { + accountService.checkAccountOpen(); + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + try { + return xmrWalletService.relayTxs(metadatas); } catch (Exception ex) { log.error("", ex); throw new IllegalStateException(ex); diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 65cd28d3d8..b2c4ce90e2 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -441,6 +441,12 @@ public class XmrWalletService extends XmrWalletBase { if (name.contains(File.separator)) throw new IllegalArgumentException("Path not expected: " + name); } + public MoneroTxWallet createTx(List destinations) { + MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false)); + //printTxs("XmrWalletService.createTx", tx); + return tx; + } + public MoneroTxWallet createTx(MoneroTxConfig txConfig) { synchronized (walletLock) { synchronized (HavenoUtils.getWalletFunctionLock()) { @@ -455,18 +461,30 @@ public class XmrWalletService extends XmrWalletBase { } } - public String relayTx(String metadata) { + public List createSweepTxs(String address) { + return createSweepTxs(new MoneroTxConfig().setAccountIndex(0).setAddress(address).setRelay(false)); + } + + public List createSweepTxs(MoneroTxConfig txConfig) { synchronized (walletLock) { - String txId = wallet.relayTx(metadata); - requestSaveWallet(); - return txId; + synchronized (HavenoUtils.getWalletFunctionLock()) { + List txs = wallet.sweepUnlocked(txConfig); + if (Boolean.TRUE.equals(txConfig.getRelay())) { + for (MoneroTxWallet tx : txs) cachedTxs.addFirst(tx); + cacheWalletInfo(); + requestSaveWallet(); + } + return txs; + } } } - public MoneroTxWallet createTx(List destinations) { - MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false)); - //printTxs("XmrWalletService.createTx", tx); - return tx; + public List relayTxs(List metadatas) { + synchronized (walletLock) { + List txIds = wallet.relayTxs(metadatas); + requestSaveWallet(); + return txIds; + } } /** diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcWalletsService.java index 7c3ca22e3b..9fbfa02089 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcWalletsService.java @@ -43,6 +43,8 @@ import static haveno.core.api.model.XmrTx.toXmrTx; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import haveno.proto.grpc.CreateXmrSweepTxsReply; +import haveno.proto.grpc.CreateXmrSweepTxsRequest; import haveno.proto.grpc.CreateXmrTxReply; import haveno.proto.grpc.CreateXmrTxRequest; import haveno.proto.grpc.GetAddressBalanceReply; @@ -61,8 +63,8 @@ import haveno.proto.grpc.GetXmrTxsReply; import haveno.proto.grpc.GetXmrTxsRequest; import haveno.proto.grpc.LockWalletReply; import haveno.proto.grpc.LockWalletRequest; -import haveno.proto.grpc.RelayXmrTxReply; -import haveno.proto.grpc.RelayXmrTxRequest; +import haveno.proto.grpc.RelayXmrTxsReply; +import haveno.proto.grpc.RelayXmrTxsRequest; import haveno.proto.grpc.RemoveWalletPasswordReply; import haveno.proto.grpc.RemoveWalletPasswordRequest; import haveno.proto.grpc.SetWalletPasswordReply; @@ -185,7 +187,7 @@ class GrpcWalletsService extends WalletsImplBase { .stream() .map(s -> new MoneroDestination(s.getAddress(), new BigInteger(s.getAmount()))) .collect(Collectors.toList())); - log.info("Successfully created XMR tx: hash {}", tx.getHash()); + log.info("Successfully created XMR tx, hash: {}", tx.getHash()); var reply = CreateXmrTxReply.newBuilder() .setTx(toXmrTx(tx).toProtoMessage()) .build(); @@ -197,12 +199,30 @@ class GrpcWalletsService extends WalletsImplBase { } @Override - public void relayXmrTx(RelayXmrTxRequest req, - StreamObserver responseObserver) { + public void createXmrSweepTxs(CreateXmrSweepTxsRequest req, + StreamObserver responseObserver) { try { - String txHash = coreApi.relayXmrTx(req.getMetadata()); - var reply = RelayXmrTxReply.newBuilder() - .setHash(txHash) + List xmrTxs = coreApi.createXmrSweepTxs(req.getAddress()); + log.info("Successfully created XMR sweep txs, hashes: {}", xmrTxs.stream().map(MoneroTxWallet::getHash).collect(Collectors.toList())); + var reply = CreateXmrSweepTxsReply.newBuilder() + .addAllTxs(xmrTxs.stream() + .map(s -> toXmrTx(s).toProtoMessage()) + .collect(Collectors.toList())) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void relayXmrTxs(RelayXmrTxsRequest req, + StreamObserver responseObserver) { + try { + List txHashes = coreApi.relayXmrTxs(req.getMetadatasList()); + var reply = RelayXmrTxsReply.newBuilder() + .addAllHashes(txHashes) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index b9615b5bcb..b2af139042 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -944,7 +944,9 @@ service Wallets { } rpc CreateXmrTx (CreateXmrTxRequest) returns (CreateXmrTxReply) { } - rpc relayXmrTx (RelayXmrTxRequest) returns (RelayXmrTxReply) { + rpc CreateXmrSweepTxs (CreateXmrSweepTxsRequest) returns (CreateXmrSweepTxsReply) { + } + rpc RelayXmrTxs (RelayXmrTxsRequest) returns (RelayXmrTxsReply) { } rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) { } @@ -1036,12 +1038,20 @@ message CreateXmrTxReply { XmrTx tx = 1; } -message RelayXmrTxRequest { - string metadata = 1; +message CreateXmrSweepTxsRequest { + string address = 1; } -message RelayXmrTxReply { - string hash = 1; +message CreateXmrSweepTxsReply { + repeated XmrTx txs = 1; +} + +message RelayXmrTxsRequest { + repeated string metadatas = 1; +} + +message RelayXmrTxsReply { + repeated string hashes = 2; } message GetAddressBalanceRequest { From bc8f473b469c6f3c2aa662bbf0e202d10c1ee0d3 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 16 Jun 2025 09:24:05 -0400 Subject: [PATCH 316/371] adjust spacing when creating new accounts --- .../desktop/components/paymentmethods/AssetsForm.java | 2 +- .../content/cryptoaccounts/CryptoAccountsView.java | 7 ++++--- .../traditionalaccounts/TraditionalAccountsView.java | 11 ++++++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java index 7baa76c564..0548c5d4b6 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java @@ -192,7 +192,7 @@ public class AssetsForm extends PaymentMethodForm { @Override protected void addTradeCurrencyComboBox() { currencyComboBox = FormBuilder.addLabelAutocompleteComboBox(gridPane, ++gridRow, Res.get("payment.crypto"), - Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + Layout.GROUP_DISTANCE).second; currencyComboBox.setPromptText(Res.get("payment.select.crypto")); currencyComboBox.setButtonCell(getComboBoxButtonCell(Res.get("payment.select.crypto"), currencyComboBox)); diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java index 7a4f20cacf..ba62725aab 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java @@ -175,7 +175,8 @@ public class CryptoAccountsView extends PaymentAccountsView, VBox> tuple = addTopLabelListView(root, gridRow, Res.get("account.crypto.yourCryptoAccounts"), Layout.FIRST_ROW_DISTANCE); paymentAccountsListView = tuple.second; int prefNumRows = Math.min(4, Math.max(2, model.dataModel.getNumPaymentAccounts())); - paymentAccountsListView.setMinHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 28); + paymentAccountsListView.setMinHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 34); + paymentAccountsListView.setMaxHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 34); setPaymentAccountsCellFactory(); Tuple3 tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), @@ -191,7 +192,7 @@ public class CryptoAccountsView extends PaymentAccountsView, VBox> tuple = addTopLabelListView(root, gridRow, Res.get("account.traditional.yourTraditionalAccounts"), Layout.FIRST_ROW_DISTANCE); paymentAccountsListView = tuple.second; int prefNumRows = Math.min(4, Math.max(2, model.dataModel.getNumPaymentAccounts())); - paymentAccountsListView.setMinHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 28); + paymentAccountsListView.setMinHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 34); + paymentAccountsListView.setMaxHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 34); setPaymentAccountsCellFactory(); Tuple3 tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), @@ -473,10 +474,10 @@ public class TraditionalAccountsView extends PaymentAccountsView Date: Tue, 17 Jun 2025 09:56:16 -0400 Subject: [PATCH 317/371] increase contrast of nav balance subtext --- desktop/src/main/java/haveno/desktop/theme-light.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/theme-light.css b/desktop/src/main/java/haveno/desktop/theme-light.css index 99bc71a49c..44e703ef69 100644 --- a/desktop/src/main/java/haveno/desktop/theme-light.css +++ b/desktop/src/main/java/haveno/desktop/theme-light.css @@ -69,7 +69,7 @@ -bs-rd-font-light: #8d8d8d; -bs-rd-font-lighter: #a7a7a7; -bs-rd-font-confirmation-label: #504f52; - -bs-rd-font-balance-label: #bbbbbb; + -bs-rd-font-balance-label: rgb(215, 215, 215, 1); -bs-text-color-dropshadow: rgba(0, 0, 0, 0.54); -bs-text-color-dropshadow-light-mode: rgba(0, 0, 0, 0.54); -bs-text-color-transparent: rgba(0, 0, 0, 0.2); From 81f7bac4527036903b240629794c91606203f71a Mon Sep 17 00:00:00 2001 From: Olexandr88 Date: Wed, 18 Jun 2025 15:13:22 +0300 Subject: [PATCH 318/371] added link to build badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c8078a97aa..9469a16e2f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
      Haveno logo - ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master) + [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master)](https://github.com/haveno-dex/haveno/actions) [![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) [![Twitter Follow](https://img.shields.io/twitter/follow/HavenoDEX?style=social)](https://twitter.com/havenodex) [![Matrix rooms](https://img.shields.io/badge/Matrix%20room-%23haveno-blue)](https://matrix.to/#/#haveno:monero.social) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md) From b82d6c3004b417f14bf3a5ddda42f51f31fb6511 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 19 Jun 2025 08:09:43 -0400 Subject: [PATCH 319/371] widen withdraw amount field --- .../haveno/desktop/main/funds/withdrawal/WithdrawalView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java index 6217bb1e04..04a960f486 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java @@ -144,7 +144,7 @@ public class WithdrawalView extends ActivatableView { amountLabel = feeTuple3.first; amountTextField = feeTuple3.second; - amountTextField.setMinWidth(200); + amountTextField.setMinWidth(225); HyperlinkWithIcon sendMaxLink = feeTuple3.third; withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex, From 32148e744010e88180578024098a8c8c7642fba0 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 19 Jun 2025 09:03:51 -0400 Subject: [PATCH 320/371] fix translations by automatically escaping single quotes, remove escapes --- .../i18n/displayStrings-assets.properties | 5 - .../src/main/java/haveno/core/locale/Res.java | 6 +- .../resources/i18n/displayStrings.properties | 95 ++++--- .../i18n/displayStrings_cs.properties | 19 +- .../i18n/displayStrings_de.properties | 15 +- .../i18n/displayStrings_es.properties | 11 +- .../i18n/displayStrings_fa.properties | 55 ++-- .../i18n/displayStrings_fr.properties | 239 +++++++++--------- .../i18n/displayStrings_it.properties | 31 +-- .../i18n/displayStrings_ja.properties | 7 +- .../i18n/displayStrings_pt-br.properties | 31 +-- .../i18n/displayStrings_pt.properties | 33 +-- .../i18n/displayStrings_ru.properties | 51 ++-- .../i18n/displayStrings_th.properties | 55 ++-- .../i18n/displayStrings_tr.properties | 17 +- .../i18n/displayStrings_vi.properties | 53 ++-- .../i18n/displayStrings_zh-hans.properties | 15 +- .../i18n/displayStrings_zh-hant.properties | 15 +- 18 files changed, 336 insertions(+), 417 deletions(-) diff --git a/assets/src/main/resources/i18n/displayStrings-assets.properties b/assets/src/main/resources/i18n/displayStrings-assets.properties index ae23634d1c..5d67b53eab 100644 --- a/assets/src/main/resources/i18n/displayStrings-assets.properties +++ b/assets/src/main/resources/i18n/displayStrings-assets.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break diff --git a/core/src/main/java/haveno/core/locale/Res.java b/core/src/main/java/haveno/core/locale/Res.java index e44092561f..3ea4d1c5ac 100644 --- a/core/src/main/java/haveno/core/locale/Res.java +++ b/core/src/main/java/haveno/core/locale/Res.java @@ -103,7 +103,11 @@ public class Res { } public static String get(String key, Object... arguments) { - return MessageFormat.format(Res.get(key), arguments); + return MessageFormat.format(escapeQuotes(get(key)), arguments); + } + + private static String escapeQuotes(String s) { + return s.replace("'", "''"); } public static String get(String key) { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index a0fd2962b1..1ebbbb2829 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -115,7 +110,7 @@ shared.belowInPercent=Below % from market price shared.aboveInPercent=Above % from market price shared.enterPercentageValue=Enter % value shared.OR=OR -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Waiting for funds... shared.yourDepositTransactionId=Your deposit transaction ID shared.peerDepositTransactionId=Peer's deposit transaction ID @@ -350,9 +345,9 @@ offerbook.takeOffer=Take offer offerbook.takeOffer.createAccount=Create account and take offer offerbook.takeOffer.enterChallenge=Enter the offer passphrase offerbook.trader=Trader -offerbook.offerersBankId=Maker''s bank ID (BIC/SWIFT): {0} -offerbook.offerersBankName=Maker''s bank name: {0} -offerbook.offerersBankSeat=Maker''s seat of bank country: {0} +offerbook.offerersBankId=Maker's bank ID (BIC/SWIFT): {0} +offerbook.offerersBankName=Maker's bank name: {0} +offerbook.offerersBankSeat=Maker's seat of bank country: {0} offerbook.offerersAcceptedBankSeatsEuro=Accepted seat of bank countries (taker): All Euro countries offerbook.offerersAcceptedBankSeats=Accepted seat of bank countries (taker):\n {0} offerbook.availableOffersToBuy=Buy {0} with {1} @@ -398,7 +393,7 @@ offerbook.clonedOffer.info=Cloning an offer creates a copy without reserving add For more information about cloning offers see: [HYPERLINK:https://docs.haveno.exchange/haveno-ui/cloning_an_offer/] offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ - {0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. + {0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} days offerbook.timeSinceSigning.notSigned.noNeed=N/A @@ -436,8 +431,8 @@ offerbook.warning.newVersionAnnouncement=With this version of the software, trad For more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits/#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\ - - The buyer''s account has not been signed by an arbitrator or a peer\n\ - - The time since signing of the buyer''s account is not at least 30 days\n\ + - The buyer's account has not been signed by an arbitrator or a peer\n\ + - The time since signing of the buyer's account is not at least 30 days\n\ - The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\ - Your account has not been signed by an arbitrator or a peer\n\ @@ -546,7 +541,7 @@ createOffer.tac=With publishing this offer I agree to trade with any trader who createOffer.setDeposit=Set buyer's security deposit (%) createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) -createOffer.securityDepositInfo=Your buyer''s security deposit will be {0} +createOffer.securityDepositInfo=Your buyer's security deposit will be {0} createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} createOffer.minSecurityDepositUsed=Minimum security deposit is used createOffer.buyerAsTakerWithoutDeposit=No deposit required from buyer (passphrase protected) @@ -730,11 +725,11 @@ portfolio.pending.step2_buyer.cash.extra=IMPORTANT REQUIREMENT:\nAfter you have # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Please pay {0} to the XMR seller by using MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the Authorisation number and a photo of the receipt by email to the XMR seller.\n\ - The receipt must clearly show the seller''s full name, country, state and the amount. The seller''s email is: {0}. + The receipt must clearly show the seller's full name, country, state and the amount. The seller's email is: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Please pay {0} to the XMR seller by using Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller.\n\ - The receipt must clearly show the seller''s full name, city, country and the amount. The seller''s email is: {0}. + The receipt must clearly show the seller's full name, city, country and the amount. The seller's email is: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Please send {0} by \"US Postal Money Order\" to the XMR seller.\n\n @@ -743,13 +738,13 @@ portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. \ See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Please contact the XMR 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.sellersAddress=Seller's {0} address portfolio.pending.step2_buyer.buyerAccount=Your payment account to be used portfolio.pending.step2_buyer.paymentSent=Payment sent portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. @@ -761,15 +756,15 @@ portfolio.pending.step2_buyer.paperReceipt.msg=Remember:\n\ Then tear it in 2 parts, make a photo and send it to the XMR seller's email address. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Send Authorisation number and receipt portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=You need to send the Authorisation number and a photo of the receipt by email to the XMR seller.\n\ - The receipt must clearly show the seller''s full name, country, state and the amount. The seller''s email is: {0}.\n\n\ + The receipt must clearly show the seller's full name, country, state and the amount. The seller's email is: {0}.\n\n\ Did you send the Authorisation number and contract to the seller? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Send MTCN and receipt portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=You need to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller.\n\ - The receipt must clearly show the seller''s full name, city, country and the amount. The seller''s email is: {0}.\n\n\ + The receipt must clearly show the seller's full name, city, country and the amount. The seller's email is: {0}.\n\n\ Did you send the MTCN and contract to the seller? portfolio.pending.step2_buyer.halCashInfo.headline=Send HalCash code portfolio.pending.step2_buyer.halCashInfo.msg=You need to send a text message with the HalCash code as well as the \ - trade ID ({0}) to the XMR seller.\nThe seller''s mobile nr. is {1}.\n\n\ + trade ID ({0}) to the XMR seller.\nThe seller's mobile nr. is {1}.\n\n\ Did you send the code to the seller? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. \ Faster Payments accounts created in old Haveno clients do not provide the receiver's name, \ @@ -789,8 +784,8 @@ portfolio.pending.step2_seller.f2fInfo.headline=Buyer's contact information portfolio.pending.step2_seller.waitPayment.msg=The deposit transaction is unlocked.\nYou need to wait until the XMR buyer starts the {0} payment. portfolio.pending.step2_seller.warn=The XMR buyer still has not done the {0} payment.\nYou need to wait until they have started the payment.\nIf the trade has not been completed on {1} the arbitrator will investigate. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the arbitrator for assistance. -disputeChat.chatWindowTitle=Dispute chat window for trade with ID ''{0}'' -tradeChat.chatWindowTitle=Trader Chat window for trade with ID ''{0}'' +disputeChat.chatWindowTitle=Dispute chat window for trade with ID '{0}' +tradeChat.chatWindowTitle=Trader Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\n\ It is not mandatory to reply in the chat.\n\ @@ -818,7 +813,7 @@ message.state.ACKNOWLEDGED=Peer confirmed message receipt message.state.FAILED=Sending message failed portfolio.pending.step3_buyer.wait.headline=Wait for XMR seller's payment confirmation -portfolio.pending.step3_buyer.wait.info=Waiting for the XMR seller''s confirmation for the receipt of the {0} payment. +portfolio.pending.step3_buyer.wait.info=Waiting for the XMR seller's confirmation for the receipt of the {0} payment. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Payment started message status portfolio.pending.step3_buyer.warn.part1a=on the {0} blockchain portfolio.pending.step3_buyer.warn.part1b=at your payment provider (e.g. bank) @@ -857,7 +852,7 @@ portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon e message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted \ confirm the payment receipt. -portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\n\ +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\n\ If the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n @@ -882,7 +877,7 @@ portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Have you received the {0} payment from your trading partner?\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirm that you have received the payment @@ -963,7 +958,7 @@ portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediat portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} -portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\n\ You receive: {0}\n\ Your trading peer receives: {1}\n\n\ @@ -972,12 +967,12 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\n\ If one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a \ second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\ - The arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. \ - Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for \ + The arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. \ + Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for \ exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion \ (or if the other peer is unresponsive).\n\n\ More details about the new arbitration model: [HYPERLINK:https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \ +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout \ but it seems that your trading peer has not accepted it.\n\n\ Once the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will \ investigate the case again and do a payout based on their findings.\n\n\ @@ -1271,7 +1266,7 @@ support.initialInfo=Please enter a description of your problem in the text field \t Sometimes the data directory gets corrupted and leads to strange bugs. \n\ \t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\n\ Please make yourself familiar with the basic rules for the dispute process:\n\ -\t● You need to respond to the {0}''s requests within 2 days.\n\ +\t● You need to respond to the {0}'s requests within 2 days.\n\ \t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\ \t● The maximum period for a dispute is 14 days.\n\ \t● You need to cooperate with the {1} and provide the information they request to make your case.\n\ @@ -1284,9 +1279,9 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno v support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorReceivedLogs=System message: Mediator has received logs: {0} -support.mediatorsAddress=Mediator''s node address: {0} +support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. \ It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. \ Please inform the developers about that incident and do not close that case before the situation is resolved!\n\n\ @@ -1428,19 +1423,19 @@ setting.about.subsystems.label=Versions of subsystems setting.about.subsystems.val=Network version: {0}; P2P message version: {1}; Local DB version: {2}; Trade protocol version: {3} setting.about.shortcuts=Short cuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} @@ -1518,8 +1513,8 @@ account.arbitratorRegistration.registerFailed=Could not complete registration.{0 account.crypto.yourCryptoAccounts=Your cryptocurrency accounts account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as \ -described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or \ -(b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is \ +described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or \ +(b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is \ not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=I understand and confirm that I know which wallet I need to use. # suppress inspection "UnusedProperty" @@ -1824,8 +1819,8 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Offer type account.notifications.marketAlert.message.title=Offer alert account.notifications.marketAlert.message.msg.below=below account.notifications.marketAlert.message.msg.above=above -account.notifications.marketAlert.message.msg=A new ''{0} {1}'' offer with price {2} ({3} {4} market price) and \ - payment method ''{5}'' was published to the Haveno offerbook.\n\ +account.notifications.marketAlert.message.msg=A new '{0} {1}' offer with price {2} ({3} {4} market price) and \ + payment method '{5}' was published to the Haveno offerbook.\n\ Offer ID: {6}. account.notifications.priceAlert.message.title=Price alert for {0} account.notifications.priceAlert.message.msg=Your price alert got triggered. The current {0} price is {1} {2} @@ -2158,7 +2153,7 @@ error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade wit popup.warning.walletNotInitialized=The wallet is not initialized yet popup.warning.wrongVersion=You probably have the wrong Haveno version for this computer.\n\ -Your computer''s architecture is: {0}.\n\ +Your computer's architecture is: {0}.\n\ The Haveno binary you installed is: {1}.\n\ Please shut down and re-install the correct version ({2}). popup.warning.incompatibleDB=We detected incompatible data base files!\n\n\ @@ -2208,7 +2203,7 @@ popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version Please check out the Haveno Forum for more information. popup.warning.noFilter=We did not receive a filter object from the seed nodes. Please inform the network administrators to register a filter object. popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. \ - Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. + Please wait until the mining fees are low again or until you've accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\n\ Transaction ID={1}.\n\ @@ -2230,7 +2225,7 @@ popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer For further help please contact the Haveno support channel at the Haveno Keybase team. popup.info.cashDepositInfo=Please be sure that you have a bank branch in your area to be able to make the cash deposit.\n\ - The bank ID (BIC/SWIFT) of the seller''s bank is: {0}. + The bank ID (BIC/SWIFT) of the seller's bank is: {0}. popup.info.cashDepositInfo.confirm=I confirm that I can make the deposit popup.info.shutDownWithOpenOffers=Haveno is being shut down, but there are open offers. \n\n\ These offers won't be available on the P2P network while Haveno is shut down, but \ @@ -2298,8 +2293,8 @@ popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\n\ For further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} -popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts \ and the initial limit for one of your accounts has been lifted.\n\n{0} @@ -2681,13 +2676,13 @@ payment.zelle.info=Zelle is a money transfer service that works best *through* a 3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n\ 4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\n\ If you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\n\ - Because of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer \ + Because of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer \ really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster \ +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster \ Payments transfers. Your current Faster Payments account does not specify a full name.\n\n\ Please consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\n\ When you recreate the account, make sure to copy the precise sort code, account number and account age verification \ - salt values from your old account to your new account. This will ensure your existing account''s age and signing \ + salt values from your old account to your new account. This will ensure your existing account's age and signing \ status are preserved. payment.fasterPayments.ukSortCode="UK sort code" payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. \ @@ -2736,8 +2731,8 @@ payment.cashDeposit.info=Please confirm your bank allows you to send cash deposi payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. payment.account.revolut.addUserNameInfo={0}\n\ - Your existing Revolut account ({1}) does not have a ''Username''.\n\ - Please enter your Revolut ''Username'' to update your account data.\n\ + Your existing Revolut account ({1}) does not have a 'Username'.\n\ + Please enter your Revolut 'Username' to update your account data.\n\ This will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account @@ -3103,7 +3098,7 @@ payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\n\ Three important notes:\n\ - try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n\ - - try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat \ + - try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat \ to tell your trading peer the reference text you picked so they can verify your payment)\n\ - Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=For your protection, we strongly discourage using Paysafecard PINs for payment.\n\n\ diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 53658b4038..14cd7b0812 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -754,8 +749,8 @@ portfolio.pending.step2_seller.f2fInfo.headline=Kontaktní informace kupujícíh portfolio.pending.step2_seller.waitPayment.msg=Vkladová transakce má alespoň jedno potvrzení na blockchainu.\nMusíte počkat, než kupující XMR zahájí platbu {0}. portfolio.pending.step2_seller.warn=Kupující XMR dosud neprovedl platbu {0}.\nMusíte počkat, než zahájí platbu.\nPokud obchod nebyl dokončen dne {1}, bude rozhodce vyšetřovat. portfolio.pending.step2_seller.openForDispute=Kupující XMR ještě nezačal s platbou!\nMax. povolené období pro obchod vypršelo.\nMůžete počkat déle a dát obchodnímu partnerovi více času nebo požádat o pomoc mediátora. -disputeChat.chatWindowTitle=Okno chatu sporu pro obchod s ID ''{0}'' -tradeChat.chatWindowTitle=Okno chatu pro obchod s ID ''{0}'' +disputeChat.chatWindowTitle=Okno chatu sporu pro obchod s ID '{0}' +tradeChat.chatWindowTitle=Okno chatu pro obchod s ID '{0}' tradeChat.openChat=Otevřít chatovací okno tradeChat.rules=Můžete komunikovat se svým obchodním partnerem a vyřešit případné problémy s tímto obchodem.\n\ Odpovídat v chatu není povinné.\n\ @@ -1391,19 +1386,19 @@ setting.about.subsystems.label=Verze subsystémů setting.about.subsystems.val=Verze sítě: {0}; Verze zpráv P2P: {1}; Verze lokální DB: {2}; Verze obchodního protokolu: {3} setting.about.shortcuts=Zkratky -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' nebo ''alt + {0}'' nebo ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' nebo 'alt + {0}' nebo 'cmd + {0}' setting.about.shortcuts.menuNav=Procházet hlavní nabídku setting.about.shortcuts.menuNav.value=Pro pohyb v hlavním menu stiskněte: 'Ctrl' nebo 'alt' nebo 'cmd' s numerickou klávesou mezi '1-9' setting.about.shortcuts.close=Zavřít Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' nebo ''cmd + {0}'' nebo ''Ctrl + {1}'' nebo ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' nebo 'cmd + {0}' nebo 'Ctrl + {1}' nebo 'cmd + {1}' setting.about.shortcuts.closePopup=Zavřete vyskakovací nebo dialogové okno setting.about.shortcuts.closePopup.value=Klávesa 'ESCAPE' setting.about.shortcuts.chatSendMsg=Odeslat obchodní soukromou zprávu -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' nebo ''alt + ENTER'' nebo ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' nebo 'alt + ENTER' nebo 'cmd + ENTER' setting.about.shortcuts.openDispute=Otevřít spor setting.about.shortcuts.openDispute.value=Vyberte nevyřízený obchod a klikněte na: {0} @@ -1787,8 +1782,8 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Typ nabídky account.notifications.marketAlert.message.title=Upozornění na nabídku account.notifications.marketAlert.message.msg.below=pod account.notifications.marketAlert.message.msg.above=nad -account.notifications.marketAlert.message.msg=Do Haveno byla zveřejněna nová nabídka ''{0} {1}'' s cenou {2} ({3} {4} tržní cena) a \ - způsob platby ''{5}''.\n\ +account.notifications.marketAlert.message.msg=Do Haveno byla zveřejněna nová nabídka '{0} {1}' s cenou {2} ({3} {4} tržní cena) a \ + způsob platby '{5}'.\n\ ID nabídky: {6}. account.notifications.priceAlert.message.title=Upozornění na cenu pro {0} account.notifications.priceAlert.message.msg=Vaše upozornění na cenu bylo aktivováno. Aktuální {0} cena je {1} {2} diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 57fd93df67..d04889babd 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -676,7 +671,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=Kontaktinformation des Käufers portfolio.pending.step2_seller.waitPayment.msg=Die Kautionstransaktion hat mindestens eine Blockchain-Bestätigung.\nSie müssen warten bis der XMR-Käufer die {0}-Zahlung beginnt. portfolio.pending.step2_seller.warn=Der XMR-Käufer hat die {0}-Zahlung noch nicht getätigt.\nSie müssen warten bis die Zahlung begonnen wurde.\nWenn der Handel nicht bis {1} abgeschlossen wurde, wird der Vermittler diesen untersuchen. portfolio.pending.step2_seller.openForDispute=Der XMR-Käufer hat seine Zahlung nicht begonnen!\nDie maximal zulässige Frist für den Handel ist abgelaufen.\nSie können länger warten und dem Handelspartner mehr Zeit geben oder den Vermittler um Hilfe bitten. -tradeChat.chatWindowTitle=Chat-Fenster für Trade mit ID ''{0}'' +tradeChat.chatWindowTitle=Chat-Fenster für Trade mit ID '{0}' tradeChat.openChat=Chat-Fenster öffnen tradeChat.rules=Sie können mit Ihrem Trade-Partner kommunizieren, um mögliche Probleme mit diesem Trade zu lösen.\nEs ist nicht zwingend erforderlich, im Chat zu antworten.\nWenn ein Trader gegen eine der folgenden Regeln verstößt, eröffnen Sie einen Streitfall und melden Sie ihn dem Mediator oder Vermittler.\n\nChat-Regeln:\n\t● Senden Sie keine Links (Risiko von Malware). Sie können die Transaktions-ID und den Namen eines Block-Explorers senden.\n\t● Senden Sie keine Seed-Wörter, Private Keys, Passwörter oder andere sensible Informationen!\n\t● Traden Sie nicht außerhalb von Haveno (keine Sicherheit).\n\t● Beteiligen Sie sich nicht an Betrugsversuchen in Form von Social Engineering.\n\t● Wenn ein Partner nicht antwortet und es vorzieht, nicht über den Chat zu kommunizieren, respektieren Sie seine Entscheidung.\n\t● Beschränken Sie Ihre Kommunikation auf das Traden. Dieser Chat ist kein Messenger-Ersatz oder eine Trollbox.\n\t● Bleiben Sie im Gespräch freundlich und respektvoll. @@ -980,7 +975,7 @@ support.buyerTaker=XMR-Käufer/Abnehmer support.sellerTaker=XMR-Verkäufer/Abnehmer support.backgroundInfo=Haveno ist kein Unternehmen, daher behandelt es Konflikte unterschiedlich.\n\nTrader können innerhalb der Anwendung über einen sicheren Chat auf dem Bildschirm für offene Trades kommunizieren, um zu versuchen, Konflikte selbst zu lösen. Wenn das nicht ausreicht, kann ein Mediator einschreiten und helfen. Der Mediator wird die Situation bewerten und eine Auszahlung von Trade Funds vorschlagen. -support.initialInfo=Bitte geben Sie eine Beschreibung Ihres Problems in das untenstehende Textfeld ein. Fügen Sie so viele Informationen wie möglich hinzu, um die Zeit für die Konfliktlösung zu verkürzen.\n\nHier ist eine Checkliste für Informationen, die Sie angeben sollten:\n\t● Wenn Sie der XMR-Käufer sind: Haben Sie die Traditional- oder Crypto-Überweisung gemacht? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung gestartet" geklickt?\n\t● Wenn Sie der XMR-Verkäufer sind: Haben Sie die Traditional- oder Crypto-Zahlung erhalten? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung erhalten" geklickt?\n\t● Welche Version von Haveno verwenden Sie?\n\t● Welches Betriebssystem verwenden Sie?\n\t● Wenn Sie ein Problem mit fehlgeschlagenen Transaktionen hatten, überlegen Sie bitte, in ein neues Datenverzeichnis zu wechseln.\n\t Manchmal wird das Datenverzeichnis beschädigt und führt zu seltsamen Fehlern. \n\t Siehe: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nBitte machen Sie sich mit den Grundregeln für den Konfliktprozess vertraut:\n\t● Sie müssen auf die Anfragen der {0}'' innerhalb von 2 Tagen antworten.\n\t● Mediatoren antworten innerhalb von 2 Tagen. Die Vermittler antworten innerhalb von 5 Werktagen.\n\t● Die maximale Frist für einen Konflikt beträgt 14 Tage.\n\t● Sie müssen mit den {1} zusammenarbeiten und die Informationen zur Verfügung stellen, die sie anfordern, um Ihren Fall zu bearbeiten.\n\t● Mit dem ersten Start der Anwendung haben Sie die Regeln des Konfliktdokuments in der Nutzervereinbarung akzeptiert.\n\nSie können mehr über den Konfliktprozess erfahren unter: {2} +support.initialInfo=Bitte geben Sie eine Beschreibung Ihres Problems in das untenstehende Textfeld ein. Fügen Sie so viele Informationen wie möglich hinzu, um die Zeit für die Konfliktlösung zu verkürzen.\n\nHier ist eine Checkliste für Informationen, die Sie angeben sollten:\n\t● Wenn Sie der XMR-Käufer sind: Haben Sie die Traditional- oder Crypto-Überweisung gemacht? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung gestartet" geklickt?\n\t● Wenn Sie der XMR-Verkäufer sind: Haben Sie die Traditional- oder Crypto-Zahlung erhalten? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung erhalten" geklickt?\n\t● Welche Version von Haveno verwenden Sie?\n\t● Welches Betriebssystem verwenden Sie?\n\t● Wenn Sie ein Problem mit fehlgeschlagenen Transaktionen hatten, überlegen Sie bitte, in ein neues Datenverzeichnis zu wechseln.\n\t Manchmal wird das Datenverzeichnis beschädigt und führt zu seltsamen Fehlern. \n\t Siehe: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nBitte machen Sie sich mit den Grundregeln für den Konfliktprozess vertraut:\n\t● Sie müssen auf die Anfragen der {0}' innerhalb von 2 Tagen antworten.\n\t● Mediatoren antworten innerhalb von 2 Tagen. Die Vermittler antworten innerhalb von 5 Werktagen.\n\t● Die maximale Frist für einen Konflikt beträgt 14 Tage.\n\t● Sie müssen mit den {1} zusammenarbeiten und die Informationen zur Verfügung stellen, die sie anfordern, um Ihren Fall zu bearbeiten.\n\t● Mit dem ersten Start der Anwendung haben Sie die Regeln des Konfliktdokuments in der Nutzervereinbarung akzeptiert.\n\nSie können mehr über den Konfliktprozess erfahren unter: {2} support.systemMsg=Systemnachricht: {0} support.youOpenedTicket=Sie haben eine Anfrage auf Support geöffnet.\n\n{0}\n\nHaveno-Version: {1} support.youOpenedDispute=Sie haben eine Anfrage für einen Konflikt geöffnet.\n\n{0}\n\nHaveno-version: {1} @@ -1115,19 +1110,19 @@ setting.about.subsystems.label=Version des Teilsystems setting.about.subsystems.val=Netzwerkversion: {0}; P2P-Nachrichtenversion: {1}; Lokale DB-Version: {2}; Version des Handelsprotokolls: {3} setting.about.shortcuts=Shortcuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Strg + {0}'' oder ''Alt + {0}'' oder ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Strg + {0}' oder 'Alt + {0}' oder 'cmd + {0}' setting.about.shortcuts.menuNav=Hauptmenü navigieren setting.about.shortcuts.menuNav.value=Um durch das Hauptmenü zu navigieren, drücken Sie: 'Strg' oder 'Alt' oder 'cmd' mit einer numerischen Taste zwischen '1-9' setting.about.shortcuts.close=Haveno beenden -setting.about.shortcuts.close.value=''Strg + {0}'' oder ''cmd + {0}'' bzw. ''Strg + {1}'' oder ''cmd + {1}'' +setting.about.shortcuts.close.value='Strg + {0}' oder 'cmd + {0}' bzw. 'Strg + {1}' oder 'cmd + {1}' setting.about.shortcuts.closePopup=Popup- oder Dialogfenster schließen setting.about.shortcuts.closePopup.value='ESCAPE' Taste setting.about.shortcuts.chatSendMsg=Trader eine Chat-Nachricht senden -setting.about.shortcuts.chatSendMsg.value=''Strg + ENTER'' oder ''Alt + ENTER'' oder ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Strg + ENTER' oder 'Alt + ENTER' oder 'cmd + ENTER' setting.about.shortcuts.openDispute=Streitfall eröffnen setting.about.shortcuts.openDispute.value=Wählen Sie den ausstehenden Trade und klicken Sie auf: {0} diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index b5754592dd..a1bb56e29d 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -1116,19 +1111,19 @@ setting.about.subsystems.label=Versión de subsistemas: setting.about.subsystems.val=Versión de red: {0}; Versión de mensajes P2P: {1}; Versión de Base de Datos local: {2}; Versión de protocolo de intercambio {3} setting.about.shortcuts=Atajos -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' o ''alt + {0}'' o ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' o 'alt + {0}' o 'cmd + {0}' setting.about.shortcuts.menuNav=Navegar menú principal setting.about.shortcuts.menuNav.value=Para navegar por el menú principal pulse: 'Ctrl' or 'alt' or 'cmd' con una tecla numérica entre el '1-9' setting.about.shortcuts.close=Cerrar Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' o ''cmd + {0}'' o ''Ctrl + {1}'' o ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' o 'cmd + {0}' o 'Ctrl + {1}' o 'cmd + {1}' setting.about.shortcuts.closePopup=Cerrar la ventana emergente o ventana de diálogo setting.about.shortcuts.closePopup.value=Tecla 'ESCAPE' setting.about.shortcuts.chatSendMsg=Enviar mensaje en chat de intercambio -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' o ''alt + ENTER'' o ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' o 'alt + ENTER' o 'cmd + ENTER' setting.about.shortcuts.openDispute=Abrir disputa setting.about.shortcuts.openDispute.value=Seleccionar intercambios pendientes y pulsar: {0} diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 44693d7e01..b6eb0cc2f4 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent= ٪ زیر قیمت بازار shared.aboveInPercent= ٪ بالای قیمت بازار shared.enterPercentageValue=ارزش ٪ را وارد کنید shared.OR=یا -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=در انتظار دریافت وجه... shared.TheXMRBuyer=خریدار بیتکوین shared.You=شما @@ -359,7 +354,7 @@ offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=با XMR خرید کنید: offerbook.sellXmrFor=فروش XMR برای: -offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} روز offerbook.timeSinceSigning.notSigned.noNeed=بدون پاسخ @@ -399,7 +394,7 @@ offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due t offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=این روش پرداخت موقتاً تا {1} به {0} محدود شده است زیرا همه خریداران حساب‌های جدیدی دارند.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=پیشنهاد شما تنها مختص خریدارانی خواهد بود که حساب‌هایی با امضا و سنین پیر دارند زیرا این مبلغ {0} را بیشتر می‌کند.\n\n{1} @@ -643,7 +638,7 @@ portfolio.pending.step2_buyer.postal=لطفاً {0} را توسط \"US Postal Mo # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=لطفا با استفاده از راه‌های ارتباطی ارائه شده توسط فروشنده با وی تماس بگیرید و قرار ملاقاتی را برای پرداخت {0} تنظیم کنید.\n portfolio.pending.step2_buyer.startPaymentUsing=آغاز پرداخت با استفاده از {0} @@ -675,7 +670,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=اطلاعات تماس خرید portfolio.pending.step2_seller.waitPayment.msg=تراکنش سپرده، حداقل یک تأییدیه بلاکچین دارد.شما\nباید تا آغاز پرداخت {0} از جانب خریدار بیتکوین، صبر نمایید. portfolio.pending.step2_seller.warn=خریدار بیت‌کوین هنوز پرداخت {0} را انجام نداده است.\nشما باید تا آغاز پرداخت از جانب او، صبر نمایید.\nاگر معامله تا {1} تکمیل نشد، داور بررسی خواهد کرد. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. -tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. @@ -715,7 +710,7 @@ portfolio.pending.step3_seller.westernUnion=خریدار باید MTCN (شمار portfolio.pending.step3_seller.halCash=خریدار باید کد HalCash را برای شما با پیامک بفرستد. علاوه‌ برآن شما از HalCash پیامی را محتوی اطلاعات موردنیاز برای برداشت EUR از خودپردازهای پشتیبان HalCash دریافت خواهید کرد.\n\nپس از اینکه پول را از دستگاه خودپرداز دریافت کردید، لطفا در اینجا رسید پرداخت را تایید کنید. portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. -portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=تأیید رسید پرداخت @@ -737,7 +732,7 @@ portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=آیا وجه {0} را از شریک معاملاتی خود دریافت کرده‌اید؟\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=تأیید کنید که وجه را دریافت کرده‌اید @@ -811,9 +806,9 @@ portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediat portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} -portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted @@ -977,7 +972,7 @@ support.sellerMaker=فروشنده/سفارش گذار بیتکوین support.buyerTaker=خریدار/پذیرنده‌ی بیتکوین support.sellerTaker=فروشنده/پذیرنده‌ی بیتکوین -support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=پیغام سیستم: {0} support.youOpenedTicket=شما یک درخواست برای پشتیبانی باز کردید.\n\n{0}\n\nنسخه Haveno شما: {1} support.youOpenedDispute=شما یک درخواست برای یک اختلاف باز کردید.\n\n{0}\n\nنسخه Haveno شما: {1} @@ -985,8 +980,8 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno v support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} -support.mediatorsAddress=Mediator''s node address: {0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} +support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. @@ -1112,19 +1107,19 @@ setting.about.subsystems.label=نسخه‌های زیرسیستم‌ها setting.about.subsystems.val=نسخه ی شبکه: {0}; نسخه ی پیام همتا به همتا: {1}; نسخه ی Local DB: {2}; نسخه پروتکل معامله: {3} setting.about.shortcuts=Short cuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} @@ -1199,7 +1194,7 @@ account.arbitratorRegistration.registerSuccess=You have successfully registered account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=حساب‌های آلت‌کوین شما -account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=من می فهمم و تأیید می کنم که می دانم از کدام کیف پول باید استفاده کنم. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. @@ -1314,7 +1309,7 @@ account.notifications.marketAlert.manageAlerts.header.offerType=نوع پیشن account.notifications.marketAlert.message.title=هشدار پیشنهاد account.notifications.marketAlert.message.msg.below=پایین account.notifications.marketAlert.message.msg.above=بالای -account.notifications.marketAlert.message.msg=پیشنهاد جدید ''{0} {1}'' با قیمت {2} ({3} {4} قیمت بازار) و روش پرداخت ''{5}'' در دفتر پیشنهادات Haveno منتشر شده است.\nشناسه پیشنهاد: {6}. +account.notifications.marketAlert.message.msg=پیشنهاد جدید '{0} {1}' با قیمت {2} ({3} {4} قیمت بازار) و روش پرداخت '{5}' در دفتر پیشنهادات Haveno منتشر شده است.\nشناسه پیشنهاد: {6}. account.notifications.priceAlert.message.title=هشدار قیمت برای {0} account.notifications.priceAlert.message.msg=هشدار قیمت شما فعال شده است. قیمت {0} فعلی {1} {2} است account.notifications.noWebCamFound.warning=دوبین پیدا نشد.\n\nلطفا از گزینه ایمیل برای ارسال توکن و کلید رمزنگاری از تلفن همراهتان به برنامه Haveno استفاده کنید. @@ -1630,7 +1625,7 @@ popup.warning.priceRelay=رله قیمت popup.warning.seed=دانه popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. popup.warning.noFilter=ما شیء فیلتر را از گره‌های اولیه دریافت نکردیم. لطفاً به مدیران شبکه اطلاع دهید که یک شیء فیلتر ثبت کنند. -popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. +popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you've accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. @@ -1679,8 +1674,8 @@ popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} -popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} @@ -1975,8 +1970,8 @@ payment.accountType=نوع حساب payment.checking=بررسی payment.savings=اندوخته ها payment.personalId=شناسه شخصی -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=زمانی که از HalCash استفاده می‌کنید، خریدار باید کد HalCash را از طریق پیام کوتاه موبایل به فروشنده XMR ارسال کند.\n\nلطفا مطمئن شوید که از حداکثر میزانی که بانک شما برای انتقال از طریق HalCash مجاز می‌داند تجاوز نکرده‌اید. حداقل مقداردر هر برداشت معادل 10 یورو و حداکثر مقدار 600 یورو می‌باشد. این محدودیت برای برداشت‌های تکراری برای هر گیرنده در روز 3000 یورو و در ماه 6000 یورو می‌باشد. لطفا این محدودیت‌ها را با بانک خود مطابقت دهید و مطمئن شوید که آنها هم همین محدودی‌ها را دارند.\n\nمقدار برداشت باید شریبی از 10 یورو باشد چرا که مقادیر غیر از این را نمی‌توانید از طریق ATM برداشت کنید. رابط کاربری در صفحه ساخت پینشهاد و پذیرش پیشنهاد مقدار XMR را به گونه‌ای تنظیم می‌کنند که مقدار EUR درست باشد. شما نمی‌توانید از قیمت بر مبنای بازار استفاده کنید چون مقدار یورو با تغییر قیمت‌ها عوض خواهد شد.\n\nدر صورت بروز اختلاف خریدار XMR باید شواهد مربوط به ارسال یورو را ارائه دهد. @@ -1988,7 +1983,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade payment.cashDeposit.info=لطفا مطمئن شوید که بانک شما اجازه پرداخت سپرده نفد به حساب دیگر افراد را می‌دهد. برای مثال، Bank of America و Wells Fargo دیگر اجازه چنین پرداخت‌هایی را نمی‌دهند. payment.revolut.info=Revolut requires the 'Username' 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 have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=لطفاً توجه داشته باشید که Cash App ریسک بازپرداخت بالاتری نسبت به بیشتر انتقالات بانکی دارد. @@ -2022,7 +2017,7 @@ payment.japan.recipient=نام payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=برای حفاظت از شما، به شدت از استفاده از پین‌های Paysafecard برای پرداخت جلوگیری می‌کنیم.\n\n\ تراکنش‌های انجام شده از طریق پین‌ها نمی‌توانند به طور مستقل برای حل اختلاف تأیید شوند. اگر مشکلی پیش آید، بازیابی وجوه ممکن است غیرممکن باشد.\n\n\ برای اطمینان از امنیت تراکنش و حل اختلاف، همیشه از روش‌های پرداختی استفاده کنید که سوابق قابل تاییدی ارائه می‌دهند. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 478ab4c6c3..5d11a8a6fb 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -146,7 +141,7 @@ shared.createNewAccountDescription=Les détails de votre compte sont stockés lo shared.saveNewAccount=Sauvegarder un nouveau compte shared.selectedAccount=Sélectionner un compte shared.deleteAccount=Supprimer le compte -shared.errorMessageInline=\nMessage d''erreur: {0} +shared.errorMessageInline=\nMessage d'erreur: {0} shared.errorMessage=Message d'erreur shared.information=Information shared.name=Nom @@ -171,14 +166,14 @@ shared.enterPrivKey=Entrer la clé privée pour déverrouiller shared.payoutTxId=ID du versement de la transaction shared.contractAsJson=Contrat au format JSON shared.viewContractAsJson=Voir le contrat en format JSON -shared.contract.title=Contrat pour la transaction avec l''ID : {0} +shared.contract.title=Contrat pour la transaction avec l'ID : {0} shared.paymentDetails=XMR {0} détails du paiement shared.securityDeposit=Dépôt de garantie shared.yourSecurityDeposit=Votre dépôt de garantie shared.contract=Contrat shared.messageArrived=Message reçu. shared.messageStoredInMailbox=Message stocké dans la boîte de réception. -shared.messageSendingFailed=Échec de l''envoi du message. Erreur: {0} +shared.messageSendingFailed=Échec de l'envoi du message. Erreur: {0} shared.unlock=Déverrouiller shared.toReceive=à recevoir shared.toSpend=à dépenser @@ -277,7 +272,7 @@ mainView.p2pNetworkWarnMsg.noNodesAvailable=Il n'y a pas de noeud de seed ou de mainView.p2pNetworkWarnMsg.connectionToP2PFailed=La connexion au réseau Haveno a échoué (erreur signalé: {0}).\nVeuillez vérifier votre connexion internet ou essayez de redémarrer l'application. mainView.walletServiceErrorMsg.timeout=La connexion au réseau Monero a échoué car le délai d'attente a expiré. -mainView.walletServiceErrorMsg.connectionError=La connexion au réseau Monero a échoué à cause d''une erreur: {0} +mainView.walletServiceErrorMsg.connectionError=La connexion au réseau Monero a échoué à cause d'une erreur: {0} mainView.walletServiceErrorMsg.rejectedTxException=Le réseau a rejeté une transaction.\n\n{0} @@ -331,8 +326,8 @@ market.trades.showVolumeInUSD=Afficher le volume en USD offerbook.createOffer=Créer un ordre offerbook.takeOffer=Accepter un ordre -offerbook.takeOfferToBuy=Accepter l''ordre d''achat {0} -offerbook.takeOfferToSell=Accepter l''ordre de vente {0} +offerbook.takeOfferToBuy=Accepter l'ordre d'achat {0} +offerbook.takeOfferToSell=Accepter l'ordre de vente {0} offerbook.takeOffer.enterChallenge=Entrez la phrase secrète de l'offre offerbook.trader=Échanger offerbook.offerersBankId=ID de la banque du maker (BIC/SWIFT): {0} @@ -359,7 +354,7 @@ offerbook.xmrAutoConf=Est-ce-que la confirmation automatique est activée offerbook.buyXmrWith=Acheter XMR avec : offerbook.sellXmrFor=Vendre XMR pour : -offerbook.timeSinceSigning.help=Lorsque vous effectuez avec succès une transaction avec un pair disposant d''un compte de paiement signé, votre compte de paiement est signé.\n{0} Jours plus tard, la limite initiale de {1} est levée et votre compte peut signer les comptes de paiement d''un autre pair. +offerbook.timeSinceSigning.help=Lorsque vous effectuez avec succès une transaction avec un pair disposant d'un compte de paiement signé, votre compte de paiement est signé.\n{0} Jours plus tard, la limite initiale de {1} est levée et votre compte peut signer les comptes de paiement d'un autre pair. offerbook.timeSinceSigning.notSigned=Pas encore signé offerbook.timeSinceSigning.notSigned.ageDays={0} jours offerbook.timeSinceSigning.notSigned.noNeed=N/A @@ -368,27 +363,27 @@ shared.notSigned.noNeed=Ce type de compte ne nécessite pas de signature shared.notSigned.noNeedDays=Ce type de compte ne nécessite pas de signature et a été créée il y'a {0} jours shared.notSigned.noNeedAlts=Les comptes pour crypto ne supportent pas la signature ou le vieillissement -offerbook.nrOffers=Nombre d''ordres: {0} +offerbook.nrOffers=Nombre d'ordres: {0} offerbook.volume={0} (min - max) offerbook.deposit=Déposer XMR (%) offerbook.deposit.help=Les deux parties à la transaction ont payé un dépôt pour assurer que la transaction se déroule normalement. Ce montant sera remboursé une fois la transaction terminée. offerbook.createNewOffer=Créer une offre à {0} {1} -offerbook.createOfferToBuy=Créer un nouvel ordre d''achat pour {0} +offerbook.createOfferToBuy=Créer un nouvel ordre d'achat pour {0} offerbook.createOfferToSell=Créer un nouvel ordre de vente pour {0} -offerbook.createOfferToBuy.withTraditional=Créer un nouvel ordre d''achat pour {0} avec {1} +offerbook.createOfferToBuy.withTraditional=Créer un nouvel ordre d'achat pour {0} avec {1} offerbook.createOfferToSell.forTraditional=Créer un nouvel ordre de vente pour {0} for {1} offerbook.createOfferToBuy.withCrypto=Créer un nouvel ordre de vente pour {0} (achat{1}) -offerbook.createOfferToSell.forCrypto=Créer un nouvel ordre d''achat pour {0} (vente{1}) +offerbook.createOfferToSell.forCrypto=Créer un nouvel ordre d'achat pour {0} (vente{1}) offerbook.takeOfferButton.tooltip=Accepter un ordre pour {0} offerbook.yesCreateOffer=Oui, créer un ordre offerbook.setupNewAccount=Configurer un nouveau compte de change offerbook.removeOffer.success=L'ordre a bien été retiré. -offerbook.removeOffer.failed=Le retrait de l''ordre a échoué:\n{0} -offerbook.deactivateOffer.failed=La désactivation de l''ordre a échoué:\n{0} -offerbook.activateOffer.failed=La publication de l''ordre a échoué:\n{0} -offerbook.withdrawFundsHint=Vous pouvez retirer les fonds investis depuis l''écran {0}. +offerbook.removeOffer.failed=Le retrait de l'ordre a échoué:\n{0} +offerbook.deactivateOffer.failed=La désactivation de l'ordre a échoué:\n{0} +offerbook.activateOffer.failed=La publication de l'ordre a échoué:\n{0} +offerbook.withdrawFundsHint=Vous pouvez retirer les fonds investis depuis l'écran {0}. offerbook.warning.noTradingAccountForCurrency.headline=Aucun compte de paiement pour la devise sélectionnée offerbook.warning.noTradingAccountForCurrency.msg=Vous n'avez pas de compte de paiement mis en place pour la devise sélectionnée.\n\nVoudriez-vous créer une offre pour une autre devise à la place? @@ -399,8 +394,8 @@ offerbook.warning.counterpartyTradeRestrictions=Cette offre ne peut être accept offerbook.warning.newVersionAnnouncement=Grâce à cette version du logiciel, les partenaires commerciaux peuvent confirmer et vérifier les comptes de paiement de chacun pour créer un réseau de comptes de paiement de confiance.\n\nUne fois la transaction réussie, votre compte de paiement sera vérifié et les restrictions de transaction seront levées après une certaine période de temps (cette durée est basée sur la méthode de vérification).\n\nPour plus d'informations sur la vérification de votre compte, veuillez consulter le document sur https://docs.haveno.exchange/payment-methods#account-signing -popup.warning.tradeLimitDueAccountAgeRestriction.seller=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Le compte de l''acheteur n''a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature du compte de l''acheteur est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} -popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Votre compte n''a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature de votre compte est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Le compte de l'acheteur n'a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature du compte de l'acheteur est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Votre compte n'a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature de votre compte est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Ce mode de paiement est temporairement limité à {0} jusqu'à {1} car tous les acheteurs ont de nouveaux comptes.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Votre offre sera limitée aux acheteurs avec des comptes signés et anciens car elle dépasse {0}.\n\n{1} @@ -421,7 +416,7 @@ offerbook.info.sellAboveMarketPrice=Vous obtiendrez {0} de plus que le prix actu offerbook.info.buyBelowMarketPrice=Vous paierez {0} de moins que le prix actuel du marché (mis à jour chaque minute). offerbook.info.buyAtFixedPrice=Vous achèterez à ce prix déterminé. offerbook.info.sellAtFixedPrice=Vous vendrez à ce prix déterminé. -offerbook.info.noArbitrationInUserLanguage=En cas de litige, veuillez noter que l''arbitrage de cet ordre sera traité par {0}. La langue est actuellement définie sur {1}. +offerbook.info.noArbitrationInUserLanguage=En cas de litige, veuillez noter que l'arbitrage de cet ordre sera traité par {0}. La langue est actuellement définie sur {1}. offerbook.info.roundedFiatVolume=Le montant a été arrondi pour accroître la confidentialité de votre transaction. #################################################################### @@ -440,7 +435,7 @@ createOffer.fundsBox.title=Financer votre ordre createOffer.fundsBox.offerFee=Frais de transaction createOffer.fundsBox.networkFee=Frais de minage createOffer.fundsBox.placeOfferSpinnerInfo=Publication de l'ordre en cours ... -createOffer.fundsBox.paymentLabel=Transaction Haveno avec l''ID {0} +createOffer.fundsBox.paymentLabel=Transaction Haveno avec l'ID {0} createOffer.fundsBox.fundsStructure=({0} dépôt de garantie, {1} frais de transaction, {2} frais de minage) createOffer.success.headline=Votre offre a été créée createOffer.success.info=Vous pouvez gérer vos ordres en cours dans \"Portfolio/Mes ordres\". @@ -472,7 +467,7 @@ createOffer.createOfferFundWalletInfo.msg=Vous devez déposer {0} à cette offre - Frais de transaction : {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) -createOffer.amountPriceBox.error.message=Une erreur s''est produite lors du placement de cet ordre:\n\n{0}\n\nAucun fonds n''a été prélevé sur votre portefeuille pour le moment.\nVeuillez redémarrer l''application et vérifier votre connexion réseau. +createOffer.amountPriceBox.error.message=Une erreur s'est produite lors du placement de cet ordre:\n\n{0}\n\nAucun fonds n'a été prélevé sur votre portefeuille pour le moment.\nVeuillez redémarrer l'application et vérifier votre connexion réseau. createOffer.setAmountPrice=Définir le montant et le prix createOffer.warnCancelOffer=Vous avez déjà financé cet ordre.\nSi vous annulez maintenant, vos fonds seront envoyés dans votre portefeuille haveno local et seront disponible pour retrait dans l'onglet \"Fonds/Envoyer des fonds\".\nÊtes-vous certain de vouloir annuler ? createOffer.timeoutAtPublishing=Un timeout est survenu au moment de la publication de l'ordre. @@ -482,7 +477,7 @@ createOffer.tooLowSecDeposit.makerIsSeller=Ceci vous donne moins de protection d createOffer.tooLowSecDeposit.makerIsBuyer=cela offre moins de protection pour le pair que de suivre le protocole de trading car vous avez moins de dépôt à risque. D'autres utilisateurs préféreront peut-être accepter d'autres ordres que le vôtre. createOffer.resetToDefault=Non, revenir à la valeur par défaut createOffer.useLowerValue=Oui, utiliser ma valeur la plus basse -createOffer.priceOutSideOfDeviation=Le prix que vous avez fixé est en dehors de l''écart max. du prix du marché autorisé\nL''écart maximum autorisé est {0} et peut être ajusté dans les préférences. +createOffer.priceOutSideOfDeviation=Le prix que vous avez fixé est en dehors de l'écart max. du prix du marché autorisé\nL'écart maximum autorisé est {0} et peut être ajusté dans les préférences. createOffer.changePrice=Modifier le prix createOffer.tac=En plaçant cet ordre vous acceptez d'effectuer des transactions avec n'importe quel trader remplissant les conditions affichées à l'écran. createOffer.currencyForFee=Frais de transaction @@ -490,7 +485,7 @@ createOffer.setDeposit=Etablir le dépôt de garantie de l'acheteur (%) createOffer.setDepositAsBuyer=Définir mon dépôt de garantie en tant qu'acheteur (%) createOffer.setDepositForBothTraders=Établissez le dépôt de sécurité des deux traders (%) createOffer.securityDepositInfo=Le dépôt de garantie de votre acheteur sera de {0} -createOffer.securityDepositInfoAsBuyer=Votre dépôt de garantie en tant qu''acheteur sera de {0} +createOffer.securityDepositInfoAsBuyer=Votre dépôt de garantie en tant qu'acheteur sera de {0} createOffer.minSecurityDepositUsed=Le dépôt de sécurité minimum est utilisé createOffer.buyerAsTakerWithoutDeposit=Aucun dépôt requis de la part de l'acheteur (protégé par un mot de passe) createOffer.myDeposit=Mon dépôt de garantie (%) @@ -516,16 +511,16 @@ takeOffer.fundsBox.tradeAmount=Montant à vendre takeOffer.fundsBox.offerFee=Frais de transaction du trade takeOffer.fundsBox.networkFee=Total des frais de minage takeOffer.fundsBox.takeOfferSpinnerInfo=Acceptation de l'offre : {0} -takeOffer.fundsBox.paymentLabel=Transaction Haveno avec l''ID {0} +takeOffer.fundsBox.paymentLabel=Transaction Haveno avec l'ID {0} takeOffer.fundsBox.fundsStructure=({0} dépôt de garantie, {1} frais de transaction, {2} frais de minage) takeOffer.fundsBox.noFundingRequiredTitle=Aucun financement requis takeOffer.fundsBox.noFundingRequiredDescription=Obtenez la phrase secrète de l'offre auprès du vendeur en dehors de Haveno pour accepter cette offre. takeOffer.success.headline=Vous avez accepté un ordre avec succès. takeOffer.success.info=Vous pouvez voir vos transactions dans \"Portfolio/Échanges en cours\". -takeOffer.error.message=Une erreur s''est produite pendant l’'acceptation de l''ordre.\n\n{0} +takeOffer.error.message=Une erreur s'est produite pendant l’'acceptation de l'ordre.\n\n{0} # new entries -takeOffer.takeOfferButton=Vérifier: Accepter l''ordre de {0} Monero +takeOffer.takeOfferButton=Vérifier: Accepter l'ordre de {0} Monero takeOffer.noPriceFeedAvailable=Vous ne pouvez pas accepter cet ordre, car celui-ci utilise un prix en pourcentage basé sur le prix du marché, mais il n'y a pas de prix de référence de disponible. takeOffer.takeOfferFundWalletInfo.headline=Provisionner votre trade # suppress inspection "TrailingSpacesInProperty" @@ -561,7 +556,7 @@ openOffer.triggered=Cette offre a été désactivée car le prix du marché a at editOffer.setPrice=Définir le prix editOffer.confirmEdit=Confirmation: Modification de l'ordre editOffer.publishOffer=Publication de votre ordre. -editOffer.failed=Échec de la modification de l''ordre:\n{0} +editOffer.failed=Échec de la modification de l'ordre:\n{0} editOffer.success=Votre ordre a été modifié avec succès. editOffer.invalidDeposit=Le dépôt de garantie de l'acheteur ne respecte pas le cadre des contraintes définies par Haveno DAO et ne peut plus être modifié. @@ -633,20 +628,20 @@ portfolio.pending.step2_buyer.crypto=Veuillez transférer à partir de votre por portfolio.pending.step2_buyer.cash=Veuillez vous rendre dans une banque et payer {0} au vendeur de XMR.\n portfolio.pending.step2_buyer.cash.extra=CONDITIONS REQUISES: \nAprès avoir effectué le paiement veuillez écrire sur le reçu papier : PAS DE REMBOURSEMENT.\nPuis déchirer le en 2, prenez en une photo et envoyer le à l'adresse email du vendeur de XMR. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.moneyGram=Veuillez s''il vous plaît payer {0} au vendeur de XMR en utilisant MoneyGram.\n\n -portfolio.pending.step2_buyer.moneyGram.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le numéro d''autorisation et une photo du reçu par e-mail au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l''état et le montant. Le mail du vendeur est: {0}. +portfolio.pending.step2_buyer.moneyGram=Veuillez s'il vous plaît payer {0} au vendeur de XMR en utilisant MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le numéro d'autorisation et une photo du reçu par e-mail au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l'état et le montant. Le mail du vendeur est: {0}. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.westernUnion=Veuillez s''il vous plaît payer {0} au vendeur de XMR en utilisant Western Union.\n\n -portfolio.pending.step2_buyer.westernUnion.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le MTCN (numéro de suivi) et une photo du reçu par e-mail au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l''état et le montant. Le mail du vendeur est: {0}. +portfolio.pending.step2_buyer.westernUnion=Veuillez s'il vous plaît payer {0} au vendeur de XMR en utilisant Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le MTCN (numéro de suivi) et une photo du reçu par e-mail au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l'état et le montant. Le mail du vendeur est: {0}. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.postal=Merci d''envoyer {0} par \"US Postal Money Order\" au vendeur de XMR.\n\n +portfolio.pending.step2_buyer.postal=Merci d'envoyer {0} par \"US Postal Money Order\" au vendeur de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Veuillez envoyer {0} en utlisant \"Pay by Mail\" au vendeur de XMR. Les instructions spécifiques sont dans le contrat de trade, ou si ce n'est pas clair, vous pouvez poser des questions via le chat des trader. Pour plus de détails sur Pay by Mail, allez sur le wiki Haveno \n[LIEN:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail]\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Veuillez payer {0} via la méthode de paiement spécifiée par le vendeur de XMR. Vous trouverez les informations du compte du vendeur à l'écran suivant.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.f2f=Veuillez s''il vous plaît contacter le vendeur de XMR via le contact fourni, et planifiez un rendez-vous pour effectuer le paiement {0}.\n\n +portfolio.pending.step2_buyer.f2f=Veuillez s'il vous plaît contacter le vendeur de XMR via le contact fourni, et planifiez un rendez-vous pour effectuer le paiement {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Initier le paiement en utilisant {0} portfolio.pending.step2_buyer.recipientsAccountData=Destinataires {0} portfolio.pending.step2_buyer.amountToTransfer=Montant à transférer @@ -654,16 +649,16 @@ portfolio.pending.step2_buyer.sellersAddress=Adresse {0} du vendeur portfolio.pending.step2_buyer.buyerAccount=Votre compte de paiement à utiliser portfolio.pending.step2_buyer.paymentSent=Paiement initié portfolio.pending.step2_buyer.fillInBsqWallet=Payer depuis le portefeuille BSQ -portfolio.pending.step2_buyer.warn=Vous n''avez toujours pas effectué votre {0} paiement !\nVeuillez noter que l''échange doit être achevé avant {1}. +portfolio.pending.step2_buyer.warn=Vous n'avez toujours pas effectué votre {0} paiement !\nVeuillez noter que l'échange doit être achevé avant {1}. portfolio.pending.step2_buyer.openForDispute=Vous n'avez pas effectué votre paiement !\nLe délai maximal alloué pour l'échange est écoulé, veuillez contacter le médiateur pour obtenir de l'aide. portfolio.pending.step2_buyer.paperReceipt.headline=Avez-vous envoyé le reçu papier au vendeur de XMR? portfolio.pending.step2_buyer.paperReceipt.msg=Rappelez-vous: \nVous devez écrire sur le reçu papier: PAS DE REMBOURSEMENT.\nEnsuite, veuillez le déchirer en 2, faire une photo et l'envoyer à l'adresse email du vendeur. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Envoyer le numéro d'autorisation ainsi que le reçu -portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Vous devez envoyez le numéro d''autorisation et une photo du reçu par email au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l''état, et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le numéro d''autorisation et le contrat au vendeur ? +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Vous devez envoyez le numéro d'autorisation et une photo du reçu par email au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l'état, et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le numéro d'autorisation et le contrat au vendeur ? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Envoyer le MTCN et le reçu -portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Vous devez envoyez le MTCN (numéro de suivi) et une photo du reçu par email au vendeur de XMR.\nLe reçu doit clairement faire figurer le nom complet du vendeur, son pays, l''état et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le MTCN et le contrat au vendeur ? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Vous devez envoyez le MTCN (numéro de suivi) et une photo du reçu par email au vendeur de XMR.\nLe reçu doit clairement faire figurer le nom complet du vendeur, son pays, l'état et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le MTCN et le contrat au vendeur ? portfolio.pending.step2_buyer.halCashInfo.headline=Envoyer le code HalCash -portfolio.pending.step2_buyer.halCashInfo.msg=Vous devez envoyez un message au format texte SMS avec le code HalCash ainsi que l''ID de la transaction ({0}) au vendeur de XMR.\nLe numéro de mobile du vendeur est {1}.\n\nAvez-vous envoyé le code au vendeur ? +portfolio.pending.step2_buyer.halCashInfo.msg=Vous devez envoyez un message au format texte SMS avec le code HalCash ainsi que l'ID de la transaction ({0}) au vendeur de XMR.\nLe numéro de mobile du vendeur est {1}.\n\nAvez-vous envoyé le code au vendeur ? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Certaines banques pourraient vérifier le nom du receveur. Des comptes de paiement plus rapides créés dans des clients Haveno plus anciens ne fournissent pas le nom du receveur, veuillez donc utiliser le chat de trade pour l'obtenir (si nécessaire). portfolio.pending.step2_buyer.confirmStart.headline=Confirmez que vous avez initié le paiement portfolio.pending.step2_buyer.confirmStart.msg=Avez-vous initié le {0} paiement auprès de votre partenaire de trading? @@ -674,10 +669,10 @@ portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=La sasie n'est pas portfolio.pending.step2_buyer.confirmStart.warningButton=Ignorer et continuer tout de même portfolio.pending.step2_seller.waitPayment.headline=En attende du paiement portfolio.pending.step2_seller.f2fInfo.headline=Coordonnées de l'acheteur -portfolio.pending.step2_seller.waitPayment.msg=La transaction de dépôt a été vérifiée au moins une fois sur la blockchain\nVous devez attendre que l''acheteur de XMR lance le {0} payment. -portfolio.pending.step2_seller.warn=L''acheteur de XMR n''a toujours pas effectué le paiement {0}.\nVeuillez attendre qu''il effectue celui-ci.\nSi la transaction n''est pas effectuée le {1}, un arbitre enquêtera. +portfolio.pending.step2_seller.waitPayment.msg=La transaction de dépôt a été vérifiée au moins une fois sur la blockchain\nVous devez attendre que l'acheteur de XMR lance le {0} payment. +portfolio.pending.step2_seller.warn=L'acheteur de XMR n'a toujours pas effectué le paiement {0}.\nVeuillez attendre qu'il effectue celui-ci.\nSi la transaction n'est pas effectuée le {1}, un arbitre enquêtera. portfolio.pending.step2_seller.openForDispute=L'acheteur de XMR n'a pas initié son paiement !\nLa période maximale autorisée pour ce trade est écoulée.\nVous pouvez attendre plus longtemps et accorder plus de temps à votre pair de trading ou contacter le médiateur pour obtenir de l'aide. -tradeChat.chatWindowTitle=Fenêtre de discussion pour la transaction avec l''ID ''{0}'' +tradeChat.chatWindowTitle=Fenêtre de discussion pour la transaction avec l'ID '{0}' tradeChat.openChat=Ouvrir une fenêtre de discussion tradeChat.rules=Vous pouvez communiquer avec votre pair de trading pour résoudre les problèmes potentiels liés à cet échange.\nIl n'est pas obligatoire de répondre sur le chat.\nSi un trader enfreint l'une des règles ci-dessous, ouvrez un litige et signalez-le au médiateur ou à l'arbitre.\n\nRègles sur le chat:\n\t● N'envoyez pas de liens (risque de malware). Vous pouvez envoyer l'ID de transaction et le nom d'un explorateur de blocs.\n\t● N'envoyez pas les mots de votre seed, clés privées, mots de passe ou autre information sensible !\n\t● N'encouragez pas le trading en dehors de Haveno (non sécurisé).\n\t● Ne vous engagez dans aucune forme d'escroquerie d'ingénierie sociale.\n\t● Si un pair ne répond pas et préfère ne pas communiquer par chat, respectez sa décision.\n\t● Limitez la portée de la conversation à l'échange en cours. Ce chat n'est pas une alternative à messenger ou une troll-box.\n\t● Entretenez une conversation amicale et respectueuse. @@ -699,25 +694,25 @@ portfolio.pending.step3_buyer.wait.info=En attente de la confirmation du vendeur portfolio.pending.step3_buyer.wait.msgStateInfo.label=État du message de lancement du paiement portfolio.pending.step3_buyer.warn.part1a=sur la {0} blockchain portfolio.pending.step3_buyer.warn.part1b=chez votre prestataire de paiement (par ex. banque) -portfolio.pending.step3_buyer.warn.part2=Le vendeur de XMR n''a toujours pas confirmé votre paiement. . Veuillez vérifier {0} si l''envoi du paiement a bien fonctionné. +portfolio.pending.step3_buyer.warn.part2=Le vendeur de XMR n'a toujours pas confirmé votre paiement. . Veuillez vérifier {0} si l'envoi du paiement a bien fonctionné. portfolio.pending.step3_buyer.openForDispute=Le vendeur de XMR n'a pas confirmé votre paiement ! Le délai maximal alloué pour ce trade est écoulé. Vous pouvez attendre plus longtemps et accorder plus de temps à votre pair de trading ou contacter le médiateur pour obtenir de l'aide. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.part=Votre partenaire de trading a confirmé qu''il a initié le paiement {0}.\n +portfolio.pending.step3_seller.part=Votre partenaire de trading a confirmé qu'il a initié le paiement {0}.\n portfolio.pending.step3_seller.crypto.explorer=Sur votre explorateur blockchain {0} favori portfolio.pending.step3_seller.crypto.wallet=Dans votre portefeuille {0} -portfolio.pending.step3_seller.crypto={0}Veuillez s''il vous plaît vérifier {1} que la transaction vers votre adresse de réception\n{2}\ndispose de suffisamment de confirmations sur la blockchain.\nLe montant du paiement doit être {3}\n\nVous pouvez copier & coller votre adresse {4} à partir de l''écran principal après avoir fermé ce popup. +portfolio.pending.step3_seller.crypto={0}Veuillez s'il vous plaît vérifier {1} que la transaction vers votre adresse de réception\n{2}\ndispose de suffisamment de confirmations sur la blockchain.\nLe montant du paiement doit être {3}\n\nVous pouvez copier & coller votre adresse {4} à partir de l'écran principal après avoir fermé ce popup. portfolio.pending.step3_seller.postal={0}Veuillez vérifier si vous avez reçu {1} avec \"US Postal Money Order\" de la part de l'acheteur de XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Veuillez vérifier si vous avez reçu {1} avec \"Pay by Mail\" de la part de l'acheteur de XMR # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Votre partenaire de trading a confirmé qu'il a initié le {0} paiement.\n\nVeuillez vous rendre sur votre banque en ligne et vérifier si vous avez reçu {1} de la part de l'acheteur de XMR. -portfolio.pending.step3_seller.cash=Du fait que le paiement est réalisé via Cash Deposit l''acheteur de XMR doit inscrire \"NO REFUND\" sur le reçu papier, le déchirer en 2 et vous envoyer une photo par email.\n\nPour éviter un risque de rétrofacturation, ne confirmez que si vous recevez le mail et que vous êtes sûr que le reçu papier est valide.\nSi vous n''êtes pas sûr, {0} +portfolio.pending.step3_seller.cash=Du fait que le paiement est réalisé via Cash Deposit l'acheteur de XMR doit inscrire \"NO REFUND\" sur le reçu papier, le déchirer en 2 et vous envoyer une photo par email.\n\nPour éviter un risque de rétrofacturation, ne confirmez que si vous recevez le mail et que vous êtes sûr que le reçu papier est valide.\nSi vous n'êtes pas sûr, {0} portfolio.pending.step3_seller.moneyGram=L'acheteur doit vous envoyer le numéro d'autorisation et une photo du reçu par e-mail .\nLe reçu doit faire clairement figurer votre nom complet, votre pays, l'état et le montant. Veuillez s'il vous plaît vérifier que vous avez bien reçu par e-mail le numéro d'autorisation.\n\nAprès avoir fermé ce popup vous verrez le nom de l'acheteur de XMR et l'adresse où retirer l'argent depuis MoneyGram.\n\nN'accusez réception qu'après avoir retiré l'argent avec succès! portfolio.pending.step3_seller.westernUnion=L'acheteur doit vous envoyer le MTCN (numéro de suivi) et une photo du reçu par e-mail .\nLe reçu doit faire clairement figurer votre nom complet, votre pays, l'état et le montant. Veuillez s'il vous plaît vérifier si vous avez reçu par e-mail le MTCN.\n\nAprès avoir fermé ce popup vous verrez le nom de l'acheteur de XMR et l'adresse où retirer l'argent depuis Western Union.\n\nN'accusez réception qu'après avoir retiré l'argent avec succès! portfolio.pending.step3_seller.halCash=L'acheteur doit vous envoyer le code HalCash par message texte SMS. Par ailleurs, vous recevrez un message de la part d'HalCash avec les informations nécessaires pour retirer les EUR depuis un DAB Bancaire supportant HalCash.\n\nAprès avoir retiré l'argent au DAB, veuillez confirmer ici la réception du paiement ! portfolio.pending.step3_seller.amazonGiftCard=L'acheteur vous a envoyé une e-carte cadeau Amazon via email ou SMS vers votre téléphone. Veuillez récupérer maintenant la carte cadeau sur votre compte Amazon, et une fois activée, confirmez le reçu de paiement. -portfolio.pending.step3_seller.bankCheck=\n\nVeuillez également vérifier que le nom de l''expéditeur indiqué sur le contrat de l''échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l''expéditeur, associé au contrat de l''échange: {0}\n\nSi les noms ne sont pas exactement identiques, {1} +portfolio.pending.step3_seller.bankCheck=\n\nVeuillez également vérifier que le nom de l'expéditeur indiqué sur le contrat de l'échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l'expéditeur, associé au contrat de l'échange: {0}\n\nSi les noms ne sont pas exactement identiques, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=ne confirmez pas la réception du paiement. Au lieu de cela, ouvrez un litige en appuyant sur \"alt + o\" ou \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Confirmer la réception du paiement @@ -729,17 +724,17 @@ portfolio.pending.step3_seller.xmrTxHash=ID de la transaction portfolio.pending.step3_seller.xmrTxKey=Clé de Transaction portfolio.pending.step3_seller.buyersAccount=Données du compte de l'acheteur portfolio.pending.step3_seller.confirmReceipt=Confirmer la réception du paiement -portfolio.pending.step3_seller.buyerStartedPayment=L''acheteur XMR a commencé le {0} paiement.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment=L'acheteur XMR a commencé le {0} paiement.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Vérifiez la présence de confirmations par la blockchain dans votre portefeuille crypto ou sur un explorateur de blocs et confirmez le paiement lorsque vous aurez suffisamment de confirmations sur la blockchain. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Vérifiez sur votre compte de trading (par ex. compte bancaire) et confirmez quand vous avez reçu le paiement. portfolio.pending.step3_seller.warn.part1a=sur la {0} blockchain portfolio.pending.step3_seller.warn.part1b=Auprès de votre prestataire de paiement (par ex. banque) -portfolio.pending.step3_seller.warn.part2=Vous n''avez toujours pas confirmé la réception du paiement. Veuillez vérifier {0} si vous avez reçu le paiement. +portfolio.pending.step3_seller.warn.part2=Vous n'avez toujours pas confirmé la réception du paiement. Veuillez vérifier {0} si vous avez reçu le paiement. portfolio.pending.step3_seller.openForDispute=Vous n'avez pas confirmé la réception du paiement !\nLe délai maximal alloué pour ce trade est écoulé.\nVeuillez confirmer ou demander l'aide du médiateur. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Avez-vous reçu le paiement {0} de votre partenaire de trading?\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Veuillez également vérifier que le nom de l''expéditeur indiqué sur le contrat de l''échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l''expéditeur, avec le contrat de l''échange: {0}\n\nSi les noms ne sont pas exactement identiques, ne confirmez pas la réception du paiement. Au lieu de cela, ouvrez un litige en appuyant sur \"alt + o\" ou \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Veuillez également vérifier que le nom de l'expéditeur indiqué sur le contrat de l'échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l'expéditeur, avec le contrat de l'échange: {0}\n\nSi les noms ne sont pas exactement identiques, ne confirmez pas la réception du paiement. Au lieu de cela, ouvrez un litige en appuyant sur \"alt + o\" ou \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Veuillez noter que dès que vous aurez confirmé la réception, le montant verrouillé pour l'échange sera remis à l'acheteur de XMR et le dépôt de garantie vous sera remboursé.\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirmez que vous avez bien reçu le paiement @@ -778,14 +773,14 @@ portfolio.pending.remainingTime=Temps restant portfolio.pending.remainingTimeDetail={0} (jusqu'’à {1}) portfolio.pending.tradePeriodInfo=Après la première confirmation de la blockchain, la période de trade commence. En fonction de la méthode de paiement utilisée, une période maximale allouée pour la transaction sera appliquée. portfolio.pending.tradePeriodWarning=Si le délai est dépassé, l'es deux participants du trade peuvent ouvrir un litige. -portfolio.pending.tradeNotCompleted=Trade inachevé dans le temps imparti (jusqu''à {0}) +portfolio.pending.tradeNotCompleted=Trade inachevé dans le temps imparti (jusqu'à {0}) portfolio.pending.tradeProcess=Processus de transaction portfolio.pending.openAgainDispute.msg=Si vous n'êtes pas certain que le message addressé au médiateur ou à l'arbitre soit arrivé (par exemple si vous n'avez pas reçu de réponse dans un délai de 1 jour), n'hésitez pas à réouvrir un litige avec Cmd/ctrl+O. Vous pouvez aussi demander de l'aide en complément sur le forum haveno à [LIEN:https://haveno.community]. portfolio.pending.openAgainDispute.button=Ouvrir à nouveau le litige portfolio.pending.openSupportTicket.headline=Ouvrir un ticket d'assistance portfolio.pending.openSupportTicket.msg=S'il vous plaît n'utilisez seulement cette fonction qu'en cas d'urgence si vous ne pouvez pas voir le bouton \"Open support\" ou \"Ouvrir un litige\.\n\nLorsque vous ouvrez un ticket de support, l'échange sera interrompu et pris en charge par le médiateur ou par l'arbitre. -portfolio.pending.timeLockNotOver=Vous devez patienter jusqu''au ≈{0} ({1} blocs de plus) avant de pouvoir ouvrir ouvrir un arbitrage pour le litige. +portfolio.pending.timeLockNotOver=Vous devez patienter jusqu'au ≈{0} ({1} blocs de plus) avant de pouvoir ouvrir ouvrir un arbitrage pour le litige. portfolio.pending.error.depositTxNull=La transaction de dépôt est nulle. Vous ne pouvez pas ouvrir un litige sans une transaction de dépôt valide. Allez dans \"Paramètres/Info sur le réseau\" et faites une resynchronisation SPV.\n\nPour obtenir de l'aide, le canal support de l'équipe Haveno est disponible sur Keybase. portfolio.pending.mediationResult.error.depositTxNull=La transaction de dépôt est nulle. Vous pouvez déplacer le trade vers les trades n'ayant pas réussi. portfolio.pending.mediationResult.error.delayedPayoutTxNull=Le paiement de la transaction différée est nul. Vous pouvez déplacer le trade vers les trades échoués. @@ -812,7 +807,7 @@ portfolio.pending.mediationResult.info.noneAccepted=Terminez la transaction en a portfolio.pending.mediationResult.info.selfAccepted=Vous avez accepté la suggestion du médiateur. En attente que le pair l'accepte également. portfolio.pending.mediationResult.info.peerAccepted=Votre pair de trading a accepté la suggestion du médiateur. L'acceptez-vous également ? portfolio.pending.mediationResult.button=Voir la résolution proposée -portfolio.pending.mediationResult.popup.headline=Résultat de la médiation pour la transaction avec l''ID: {0} +portfolio.pending.mediationResult.popup.headline=Résultat de la médiation pour la transaction avec l'ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Votre pair de trading a accepté la suggestion du médiateur pour la transaction {0} portfolio.pending.mediationResult.popup.info=Les frais recommandés par le médiateur sont les suivants: \nVous paierez: {0} \nVotre partenaire commercial paiera: {1} \n\nVous pouvez accepter ou refuser ces frais de médiation. \n\nEn acceptant, vous avez vérifié l'opération de paiement du contrat. Si votre partenaire commercial accepte et vérifie également, le paiement sera effectué et la transaction sera clôturée. \n\nSi l'un de vous ou les deux refusent la proposition, vous devrez attendre le {2} (bloc {3}) pour commencer le deuxième tour de discussion sur le différend avec l'arbitre, et ce dernier étudiera à nouveau le cas. Le paiement sera fait en fonction de ses résultats. \n\nL'arbitre peut facturer une somme modique (la limite supérieure des honoraires: la marge de la transaction) en compensation de son travail. Les deux commerçants conviennent que la suggestion du médiateur est une voie agréable. La demande d'arbitrage concerne des circonstances particulières, par exemple si un professionnel est convaincu que le médiateur n'a pas fait une recommandation de d'indemnisation équitable (ou si l'autre partenaire n'a pas répondu). \n\nPlus de détails sur le nouveau modèle d'arbitrage: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Vous avez accepté la proposition de paiement du médiateur, mais il semble que votre contrepartie ne l'ait pas acceptée. \n\nUne fois que le temps de verrouillage atteint {0} (bloc {1}), vous pouvez ouvrir le second tour de litige pour que l'arbitre réétudie le cas et prend une nouvelle décision de dépenses. \n\nVous pouvez trouver plus d'informations sur le modèle d'arbitrage sur:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] @@ -892,10 +887,10 @@ funds.withdrawal.warn.noSourceAddressSelected=Vous devez sélectionner une adres funds.withdrawal.warn.amountExceeds=Vous ne disposez pas de fonds suffisants provenant de l'adresse sélectionnée.\nEnvisagez de sélectionner plusieurs adresses dans le champ ci-dessus ou changez les frais pour inclure les frais du mineur. funds.reserved.noFunds=Aucun fonds n'est réservé pour les ordres en cours -funds.reserved.reserved=Réversé dans votre portefeuille local pour l''ordre avec l''ID: {0} +funds.reserved.reserved=Réversé dans votre portefeuille local pour l'ordre avec l'ID: {0} funds.locked.noFunds=Aucun fonds n'est verrouillé dans les trades -funds.locked.locked=Vérouillé en multisig pour le trade avec l''ID: {0} +funds.locked.locked=Vérouillé en multisig pour le trade avec l'ID: {0} funds.tx.direction.sentTo=Envoyer à: funds.tx.direction.receivedWith=Reçu depuis: @@ -908,7 +903,7 @@ funds.tx.disputePayout=Versement du litige: {0} funds.tx.disputeLost=Cas de litige perdu: {0} funds.tx.collateralForRefund=Remboursement du dépôt de garantie: {0} funds.tx.timeLockedPayoutTx=Tx de paiement verrouillée dans le temps: {0} -funds.tx.refund=Remboursement venant de l''arbitrage: {0} +funds.tx.refund=Remboursement venant de l'arbitrage: {0} funds.tx.unknown=Raison inconnue: {0} funds.tx.noFundsFromDispute=Aucun remboursement en cas de litige funds.tx.receivedFunds=Fonds reçus @@ -948,9 +943,9 @@ support.fullReportButton.label=Tous les litiges 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. -support.sendMessageError=Échec de l''envoi du message. Erreur: {0} +support.sendMessageError=Échec de l'envoi du message. Erreur: {0} support.receiverNotKnown=Destinataire inconnu -support.wrongVersion=L''ordre relatif au litige en question a été créé avec une ancienne version de Haveno.\nVous ne pouvez pas clore ce litige avec votre version de l''application.\n\nVeuillez utiliser une version plus ancienne avec la version du protocole {0} +support.wrongVersion=L'ordre relatif au litige en question a été créé avec une ancienne version de Haveno.\nVous ne pouvez pas clore ce litige avec votre version de l'application.\n\nVeuillez utiliser une version plus ancienne avec la version du protocole {0} support.openFile=Ouvrir le fichier à joindre (taille max. du fichier : {0} kb) support.attachmentTooLarge=La taille totale de vos pièces jointes est de {0} ko ce qui dépasse la taille maximale autorisée de {1} ko pour les messages. support.maxSize=La taille maximale autorisée pour le fichier est {0} kB. @@ -966,7 +961,7 @@ support.attachments=Pièces jointes: support.savedInMailbox=Message sauvegardé dans la boîte mail du destinataire support.arrived=Message reçu par le destinataire support.acknowledged=Réception du message confirmée par le destinataire -support.error=Le destinataire n''a pas pu traiter le message. Erreur : {0} +support.error=Le destinataire n'a pas pu traiter le message. Erreur : {0} support.buyerAddress=Adresse de l'acheteur XMR support.sellerAddress=Adresse du vendeur XMR support.role=Rôle @@ -982,7 +977,7 @@ support.buyerTaker=Acheteur XMR/Taker support.sellerTaker=Vendeur XMR/Taker support.backgroundInfo=Haveno n'est pas une entreprise, donc il gère les litiges différemment.\n\nLes traders peuvent communiquer au sein de l'application via une discussion sécurisée sur l'écran des transactions ouvertes pour tenter de résoudre les litiges eux-mêmes. Si cela n'est pas suffisant, un médiateur évaluera la situation et décidera d'un paiement des fonds de transaction. -support.initialInfo=Veuillez entrer une description de votre problème dans le champ texte ci-dessous. Ajoutez autant d''informations que possible pour accélérer le temps de résolution du litige.\n\nVoici une check list des informations que vous devez fournir :\n● Si vous êtes l''acheteur XMR : Avez-vous effectué le paiement Fiat ou Crypto ? Si oui, avez-vous cliqué sur le bouton "paiement commencé" dans l''application ?\n● Si vous êtes le vendeur XMR : Avez-vous reçu le paiement Fiat ou Crypto ? Si oui, avez-vous cliqué sur le bouton "paiement reçu" dans l''application ?\n● Quelle version de Haveno utilisez-vous ?\n● Quel système d''exploitation utilisez-vous ?\n● Si vous avez rencontré un problème avec des transactions qui ont échoué, veuillez envisager de passer à un nouveau répertoire de données.\nParfois, le répertoire de données est corrompu et conduit à des bogues étranges. \nVoir : https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nVeuillez vous familiariser avec les règles de base du processus de règlement des litiges :\n● Vous devez répondre aux demandes des {0} dans les 2 jours.\n● Les médiateurs répondent dans un délai de 2 jours. Les arbitres répondent dans un délai de 5 jours ouvrables.\n● Le délai maximum pour un litige est de 14 jours.\n● Vous devez coopérer avec les {1} et fournir les renseignements qu''ils demandent pour faire valoir votre cause.\n● Vous avez accepté les règles décrites dans le document de litige dans l''accord d''utilisation lorsque vous avez lancé l''application pour la première fois.\n\nVous pouvez en apprendre davantage sur le processus de litige à l''adresse suivante {2} +support.initialInfo=Veuillez entrer une description de votre problème dans le champ texte ci-dessous. Ajoutez autant d'informations que possible pour accélérer le temps de résolution du litige.\n\nVoici une check list des informations que vous devez fournir :\n● Si vous êtes l'acheteur XMR : Avez-vous effectué le paiement Fiat ou Crypto ? Si oui, avez-vous cliqué sur le bouton "paiement commencé" dans l'application ?\n● Si vous êtes le vendeur XMR : Avez-vous reçu le paiement Fiat ou Crypto ? Si oui, avez-vous cliqué sur le bouton "paiement reçu" dans l'application ?\n● Quelle version de Haveno utilisez-vous ?\n● Quel système d'exploitation utilisez-vous ?\n● Si vous avez rencontré un problème avec des transactions qui ont échoué, veuillez envisager de passer à un nouveau répertoire de données.\nParfois, le répertoire de données est corrompu et conduit à des bogues étranges. \nVoir : https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nVeuillez vous familiariser avec les règles de base du processus de règlement des litiges :\n● Vous devez répondre aux demandes des {0} dans les 2 jours.\n● Les médiateurs répondent dans un délai de 2 jours. Les arbitres répondent dans un délai de 5 jours ouvrables.\n● Le délai maximum pour un litige est de 14 jours.\n● Vous devez coopérer avec les {1} et fournir les renseignements qu'ils demandent pour faire valoir votre cause.\n● Vous avez accepté les règles décrites dans le document de litige dans l'accord d'utilisation lorsque vous avez lancé l'application pour la première fois.\n\nVous pouvez en apprendre davantage sur le processus de litige à l'adresse suivante {2} support.systemMsg=Message du système: {0} support.youOpenedTicket=Vous avez ouvert une demande de support.\n\n{0}\n\nHaveno version: {1} support.youOpenedDispute=Vous avez ouvert une demande de litige.\n\n{0}\n\nHaveno version: {1} @@ -1117,22 +1112,22 @@ setting.about.subsystems.label=Versions des sous-systèmes setting.about.subsystems.val=Version du réseau: {0}; version des messages P2P: {1}; Version DB Locale: {2}; Version du protocole de trading: {3} setting.about.shortcuts=Raccourcis -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' ou ''alt + {0}'' ou ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' ou 'alt + {0}' ou 'cmd + {0}' setting.about.shortcuts.menuNav=Naviguer dans le menu principal setting.about.shortcuts.menuNav.value=Pour naviguer dans le menu principal, appuyez sur: 'Ctrl' ou 'alt' ou 'cmd' avec une touche numérique entre '1-9'. setting.about.shortcuts.close=Fermer Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' ou ''cmd + {0}'' ou ''Ctrl + {1}'' ou ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' ou 'cmd + {0}' ou 'Ctrl + {1}' ou 'cmd + {1}' setting.about.shortcuts.closePopup=Fermer le popup ou la fenêtre de dialogue setting.about.shortcuts.closePopup.value=Touche 'ECHAP' setting.about.shortcuts.chatSendMsg=Envoyer un message chat au trader -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTRÉE'' ou ''alt + ENTREE'' ou ''cmd + ENTRÉE'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTRÉE' ou 'alt + ENTREE' ou 'cmd + ENTRÉE' setting.about.shortcuts.openDispute=Ouvrir un litige -setting.about.shortcuts.openDispute.value=Sélectionnez l''échange en cours et cliquez sur: {0} +setting.about.shortcuts.openDispute.value=Sélectionnez l'échange en cours et cliquez sur: {0} setting.about.shortcuts.walletDetails=Ouvrir la fenêtre avec les détails sur le portefeuille @@ -1152,7 +1147,7 @@ setting.about.shortcuts.registerMediator=Inscrire le médiateur (médiateur/arbi setting.about.shortcuts.registerMediator.value=Naviguez jusqu'au compte et appuyez sur: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Ouvrir la fenêtre pour la signature de l'âge du compte (anciens arbitres seulement) -setting.about.shortcuts.openSignPaymentAccountsWindow.value=Naviguer vers l''ancienne vue de l''arbitre et appuyer sur: {0} +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Naviguer vers l'ancienne vue de l'arbitre et appuyer sur: {0} setting.about.shortcuts.sendAlertMsg=Envoyer un message d'alerte ou de mise à jour (activité privilégiée) @@ -1196,15 +1191,15 @@ account.arbitratorRegistration.pubKey=Clé publique account.arbitratorRegistration.register=S'inscrire account.arbitratorRegistration.registration={0} Enregistrement account.arbitratorRegistration.revoke=Révoquer -account.arbitratorRegistration.info.msg=Veuillez noter que vous devez rester disponible pendant 15 jours après la révocation, car il se peut que des échanges vous impliquent comme {0}. Le délai d''échange maximal autorisé est de 8 jours et la procédure de contestation peut prendre jusqu''à 7 jours. +account.arbitratorRegistration.info.msg=Veuillez noter que vous devez rester disponible pendant 15 jours après la révocation, car il se peut que des échanges vous impliquent comme {0}. Le délai d'échange maximal autorisé est de 8 jours et la procédure de contestation peut prendre jusqu'à 7 jours. account.arbitratorRegistration.warn.min1Language=Vous devez définir au moins 1 langue.\nNous avons ajouté la langue par défaut pour vous. account.arbitratorRegistration.removedSuccess=Vous avez supprimé votre inscription au réseau Haveno avec succès. -account.arbitratorRegistration.removedFailed=Impossible de supprimer l''enregistrement.{0} +account.arbitratorRegistration.removedFailed=Impossible de supprimer l'enregistrement.{0} account.arbitratorRegistration.registerSuccess=Vous vous êtes inscrit au réseau Haveno avec succès. -account.arbitratorRegistration.registerFailed=Impossible de terminer l''enregistrement.{0} +account.arbitratorRegistration.registerFailed=Impossible de terminer l'enregistrement.{0} account.crypto.yourCryptoAccounts=Vos comptes crypto -account.crypto.popup.wallet.msg=Veuillez vous assurer que vous respectez les exigences relatives à l''utilisation des {0} portefeuilles, selon les conditions présentées sur la page {1} du site.\nL''utilisation des portefeuilles provenant de plateformes de trading centralisées où (a) vous ne contrôlez pas vos clés ou (b) qui ne disposent pas d''un portefeuille compatible est risquée : cela peut entraîner la perte des fonds échangés!\nLe médiateur et l''arbitre ne sont pas des spécialistes {2} et ne pourront pas intervenir dans ce cas. +account.crypto.popup.wallet.msg=Veuillez vous assurer que vous respectez les exigences relatives à l'utilisation des {0} portefeuilles, selon les conditions présentées sur la page {1} du site.\nL'utilisation des portefeuilles provenant de plateformes de trading centralisées où (a) vous ne contrôlez pas vos clés ou (b) qui ne disposent pas d'un portefeuille compatible est risquée : cela peut entraîner la perte des fonds échangés!\nLe médiateur et l'arbitre ne sont pas des spécialistes {2} et ne pourront pas intervenir dans ce cas. account.crypto.popup.wallet.confirm=Je comprends et confirme que je sais quel portefeuille je dois utiliser. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Pour échanger UPX sur Haveno, vous devez comprendre et respecter les exigences suivantes: \n\nPour envoyer UPX, vous devez utiliser le portefeuille officiel UPXmA GUI ou le portefeuille UPXmA CLI avec le logo store-tx-info activé (valeur par défaut dans la nouvelle version) . Assurez-vous d'avoir accès à la clé tx, car elle est nécessaire dans l'état du litige. monero-wallet-cli (à l'aide de la commande get_Tx_key) monero-wallet-gui: sur la page Avancé> Preuve / Vérification. \n\nCes transactions ne sont pas vérifiables dans le navigateur blockchain ordinaire. \n\nEn cas de litige, vous devez fournir à l'arbitre les informations suivantes: \n\n- Clé privée Tx- hachage de transaction- adresse publique du destinataire \n\nSi vous ne fournissez pas les informations ci-dessus ou si vous utilisez un portefeuille incompatible, vous perdrez le litige. En cas de litige, l'expéditeur UPX est responsable de fournir la vérification du transfert UPX à l'arbitre. \n\nAucun paiement d'identité n'est requis, juste une adresse publique commune. \n\nSi vous n'êtes pas sûr du processus, veuillez visiter le canal UPXmA Discord (https://discord.gg/vhdNSrV) ou le groupe d'échanges Telegram (https://t.me/uplexaOfficial) pour plus d'informations. @@ -1250,8 +1245,8 @@ account.backup.backupNow=Sauvegarder maintenant (la sauvegarde n'est pas crypté account.backup.appDir=Répertoire des données de l'application account.backup.openDirectory=Ouvrir le répertoire account.backup.openLogFile=Ouvrir le fichier de log -account.backup.success=Sauvegarder réussite vers l''emplacement:\n{0} -account.backup.directoryNotAccessible=Le répertoire que vous avez choisi n''est pas accessible. {0} +account.backup.success=Sauvegarder réussite vers l'emplacement:\n{0} +account.backup.directoryNotAccessible=Le répertoire que vous avez choisi n'est pas accessible. {0} account.password.removePw.button=Supprimer le mot de passe account.password.removePw.headline=Supprimer la protection par mot de passe du portefeuille @@ -1295,13 +1290,13 @@ account.notifications.priceAlert.low.label=Me prévenir si le prix du XMR est in account.notifications.priceAlert.setButton=Définir l'alerte de prix account.notifications.priceAlert.removeButton=Retirer l'alerte de prix account.notifications.trade.message.title=L'état du trade a été modifié. -account.notifications.trade.message.msg.conf=La transaction de dépôt pour l''échange avec ID {0} est confirmée. Veuillez ouvrir votre application Haveno et initier le paiement. -account.notifications.trade.message.msg.started=L''acheteur de XMR a initié le paiement pour la transaction avec ID {0}. -account.notifications.trade.message.msg.completed=La transaction avec l''ID {0} est terminée. +account.notifications.trade.message.msg.conf=La transaction de dépôt pour l'échange avec ID {0} est confirmée. Veuillez ouvrir votre application Haveno et initier le paiement. +account.notifications.trade.message.msg.started=L'acheteur de XMR a initié le paiement pour la transaction avec ID {0}. +account.notifications.trade.message.msg.completed=La transaction avec l'ID {0} est terminée. account.notifications.offer.message.title=Votre ordre a été accepté -account.notifications.offer.message.msg=Votre ordre avec l''ID {0} a été accepté +account.notifications.offer.message.msg=Votre ordre avec l'ID {0} a été accepté account.notifications.dispute.message.title=Nouveau message de litige -account.notifications.dispute.message.msg=Vous avez reçu un message de contestation pour le trade avec l''ID {0} +account.notifications.dispute.message.msg=Vous avez reçu un message de contestation pour le trade avec l'ID {0} account.notifications.marketAlert.title=Alertes sur les ordres account.notifications.marketAlert.selectPaymentAccount=Ordres correspondants au compte de paiement @@ -1320,9 +1315,9 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Type d'ordre account.notifications.marketAlert.message.title=Alerte d'ordre account.notifications.marketAlert.message.msg.below=en dessous de account.notifications.marketAlert.message.msg.above=au dessus de -account.notifications.marketAlert.message.msg=Un nouvel ordre ''{0} {1}''' avec le prix {2} ({3} {4} prix de marché) avec le moyen de paiement ''{5}'' a été publiée dans le livre des ordres de Haveno.\nID de l''ordre: {6}. +account.notifications.marketAlert.message.msg=Un nouvel ordre '{0} {1}'' avec le prix {2} ({3} {4} prix de marché) avec le moyen de paiement '{5}' a été publiée dans le livre des ordres de Haveno.\nID de l'ordre: {6}. account.notifications.priceAlert.message.title=Alerte de prix pour {0} -account.notifications.priceAlert.message.msg=Votre alerte de prix a été déclenchée. l''actuel {0} le prix est {1}. {2} +account.notifications.priceAlert.message.msg=Votre alerte de prix a été déclenchée. l'actuel {0} le prix est {1}. {2} account.notifications.noWebCamFound.warning=Aucune webcam n'a été trouvée.\n\nUtilisez l'option mail pour envoyer le jeton et la clé de cryptage depuis votre téléphone portable vers l'application Haveno. account.notifications.priceAlert.warning.highPriceTooLow=Le prix le plus élevé doit être supérieur au prix le plus bas. account.notifications.priceAlert.warning.lowerPriceTooHigh=Le prix le plus bas doit être inférieur au prix le plus élevé. @@ -1418,9 +1413,9 @@ disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nÉtape suivant disputeSummaryWindow.close.closePeer=Vous devez également clore le ticket des pairs de trading ! disputeSummaryWindow.close.txDetails.headline=Publier la transaction de remboursement # suppress inspection "TrailingSpacesInProperty" -disputeSummaryWindow.close.txDetails.buyer=L''acheteur reçoit {0} à l''adresse: {1}\n +disputeSummaryWindow.close.txDetails.buyer=L'acheteur reçoit {0} à l'adresse: {1}\n # suppress inspection "TrailingSpacesInProperty" -disputeSummaryWindow.close.txDetails.seller=Le vendeur reçoit {0} à l''adresse: {1}\n +disputeSummaryWindow.close.txDetails.seller=Le vendeur reçoit {0} à l'adresse: {1}\n disputeSummaryWindow.close.txDetails=Dépenser: {0}\n{1}{2}Frais de transaction: {3}\n\nÊtes-vous sûr de vouloir publier cette transaction ? disputeSummaryWindow.close.noPayout.headline=Fermé sans paiement @@ -1474,7 +1469,7 @@ offerDetailsWindow.commitment=Engagement offerDetailsWindow.agree=J'accepte offerDetailsWindow.tac=Conditions d'utilisation offerDetailsWindow.confirm.maker=Confirmer: Placer un ordre de {0} monero -offerDetailsWindow.confirm.taker=Confirmer: Acceptez l''ordre de {0} monero +offerDetailsWindow.confirm.taker=Confirmer: Acceptez l'ordre de {0} monero offerDetailsWindow.creationDate=Date de création offerDetailsWindow.makersOnion=Adresse onion du maker offerDetailsWindow.challenge=Phrase secrète de l'offre @@ -1592,29 +1587,29 @@ popup.headline.error=Erreur popup.doNotShowAgain=Ne plus montrer popup.reportError.log=Ouvrir le dossier de log popup.reportError.gitHub=Signaler au Tracker de problème GitHub -popup.reportError={0}\n\nAfin de nous aider à améliorer le logiciel, veuillez signaler ce bug en ouvrant un nouveau ticket de support sur https://github.com/haveno-dex/haveno/issues.\nLe message d''erreur ci-dessus sera copié dans le presse-papier lorsque vous cliquerez sur l''un des boutons ci-dessous.\nCela facilitera le dépannage si vous incluez le fichier haveno.log en appuyant sur "ouvrir le fichier de log", en sauvegardant une copie, et en l''attachant à votre rapport de bug. +popup.reportError={0}\n\nAfin de nous aider à améliorer le logiciel, veuillez signaler ce bug en ouvrant un nouveau ticket de support sur https://github.com/haveno-dex/haveno/issues.\nLe message d'erreur ci-dessus sera copié dans le presse-papier lorsque vous cliquerez sur l'un des boutons ci-dessous.\nCela facilitera le dépannage si vous incluez le fichier haveno.log en appuyant sur "ouvrir le fichier de log", en sauvegardant une copie, et en l'attachant à votre rapport de bug. popup.error.tryRestart=Veuillez essayer de redémarrer votre application et vérifier votre connexion réseau pour voir si vous pouvez résoudre ce problème. -popup.error.takeOfferRequestFailed=Une erreur est survenue pendant que quelqu''un essayait d''accepter l''un de vos ordres:\n{0} +popup.error.takeOfferRequestFailed=Une erreur est survenue pendant que quelqu'un essayait d'accepter l'un de vos ordres:\n{0} -error.spvFileCorrupted=Une erreur est survenue pendant la lecture du fichier de la chaîne SPV.\nIl se peut que le fichier de la chaîne SPV soit corrompu.\n\nMessage d''erreur: {0}\n\nVoulez-vous l''effacer et lancer une resynchronisation? +error.spvFileCorrupted=Une erreur est survenue pendant la lecture du fichier de la chaîne SPV.\nIl se peut que le fichier de la chaîne SPV soit corrompu.\n\nMessage d'erreur: {0}\n\nVoulez-vous l'effacer et lancer une resynchronisation? error.deleteAddressEntryListFailed=Impossible de supprimer le dossier AddressEntryList.\nErreur: {0}. -error.closedTradeWithUnconfirmedDepositTx=La transaction de dépôt de l''échange fermé avec l''ID d''échange {0} n'est pas encore confirmée.\n\nVeuillez effectuer une resynchronisation SPV à \"Paramètres/Info sur le réseau\" pour voir si la transaction est valide. -error.closedTradeWithNoDepositTx=La transaction de dépôt de l'échange fermé avec l''ID d'échange {0} est nulle.\n\nVeuillez redémarrer l''application pour nettoyer la liste des transactions fermées. +error.closedTradeWithUnconfirmedDepositTx=La transaction de dépôt de l'échange fermé avec l'ID d'échange {0} n'est pas encore confirmée.\n\nVeuillez effectuer une resynchronisation SPV à \"Paramètres/Info sur le réseau\" pour voir si la transaction est valide. +error.closedTradeWithNoDepositTx=La transaction de dépôt de l'échange fermé avec l'ID d'échange {0} est nulle.\n\nVeuillez redémarrer l'application pour nettoyer la liste des transactions fermées. popup.warning.walletNotInitialized=Le portefeuille n'est pas encore initialisé popup.warning.osxKeyLoggerWarning=En raison de mesures de sécurité plus strictes dans MacOS 10.14 et dans la version supérieure, le lancement d'une application Java (Haveno utilise Java) provoquera un avertissement pop-up dans MacOS (« Haveno souhaite recevoir les frappes de toute application »). \n\nPour éviter ce problème, veuillez ouvrir «Paramètres MacOS», puis allez dans «Sécurité et confidentialité» -> «Confidentialité» -> «Surveillance des entrées», puis supprimez «Haveno» de la liste de droite. \n\nUne fois les limitations techniques résolues (le packager Java de la version Java requise n'a pas été livré), Haveno effectuera une mise à niveau vers la nouvelle version Java pour éviter ce problème. -popup.warning.wrongVersion=Vous avez probablement une mauvaise version de Haveno sur cet ordinateur.\nL''architecture de votre ordinateur est: {0}.\nLa binary Haveno que vous avez installé est: {1}.\nVeuillez éteindre et réinstaller une bonne version ({2}). +popup.warning.wrongVersion=Vous avez probablement une mauvaise version de Haveno sur cet ordinateur.\nL'architecture de votre ordinateur est: {0}.\nLa binary Haveno que vous avez installé est: {1}.\nVeuillez éteindre et réinstaller une bonne version ({2}). popup.warning.incompatibleDB=Nous avons détecté un fichier de base de données incompatible!\n\nCes fichiers de base de données ne sont pas compatibles avec notre base de code actuelle: {0}\n\nNous avons sauvegardé les fichiers endommagés et appliqué les valeurs par défaut à la nouvelle version de la base de données.\n\nLa sauvegarde se trouve dans: \n\n{1} / db / backup_of_corrupted_data. \n\nVeuillez vérifier si vous avez installé la dernière version de Haveno. \n\nVous pouvez télécharger: \n\n[HYPERLINK:https://haveno.exchange/downloads] \n\nVeuillez redémarrer l'application. popup.warning.startupFailed.twoInstances=Haveno est déjà lancé. Vous ne pouvez pas lancer deux instances de haveno. -popup.warning.tradePeriod.halfReached=Votre transaction avec ID {0} a atteint la moitié de la période de trading maximale autorisée et n''est toujours pas terminée.\n\nLa période de trade se termine le {1}.\n\nVeuillez vérifier l''état de votre transaction dans \"Portfolio/échanges en cours\" pour obtenir de plus amples informations. -popup.warning.tradePeriod.ended=Votre échange avec l''ID {0} a atteint la période de trading maximale autorisée et n''est pas terminé.\n\nLa période d''échange s''est terminée le {1}.\n\nVeuillez vérifier votre transaction sur \"Portfolio/Echanges en cours\" pour contacter le médiateur. +popup.warning.tradePeriod.halfReached=Votre transaction avec ID {0} a atteint la moitié de la période de trading maximale autorisée et n'est toujours pas terminée.\n\nLa période de trade se termine le {1}.\n\nVeuillez vérifier l'état de votre transaction dans \"Portfolio/échanges en cours\" pour obtenir de plus amples informations. +popup.warning.tradePeriod.ended=Votre échange avec l'ID {0} a atteint la période de trading maximale autorisée et n'est pas terminé.\n\nLa période d'échange s'est terminée le {1}.\n\nVeuillez vérifier votre transaction sur \"Portfolio/Echanges en cours\" pour contacter le médiateur. popup.warning.noTradingAccountSetup.headline=Vous n'avez pas configuré de compte de trading popup.warning.noTradingAccountSetup.msg=Vous devez configurer une devise nationale ou un compte crypto avant de pouvoir créer un ordre.\nVoulez-vous configurer un compte ? popup.warning.noArbitratorsAvailable=Les arbitres ne sont pas disponibles. popup.warning.noMediatorsAvailable=Il n'y a pas de médiateurs disponibles. popup.warning.notFullyConnected=Vous devez attendre d'être complètement connecté au réseau.\nCela peut prendre jusqu'à 2 minutes au démarrage. -popup.warning.notSufficientConnectionsToXmrNetwork=Vous devez attendre d''avoir au minimum {0} connexions au réseau Monero. +popup.warning.notSufficientConnectionsToXmrNetwork=Vous devez attendre d'avoir au minimum {0} connexions au réseau Monero. popup.warning.downloadNotComplete=Vous devez attendre que le téléchargement des blocs Monero manquants soit terminé. popup.warning.walletNotSynced=Le portefeuille Haveno n'est pas synchronisé avec la hauteur la plus récente de la blockchain. Veuillez patienter jusqu'à ce que le portefeuille soit synchronisé ou vérifiez votre connexion. popup.warning.removeOffer=Vous êtes certain de vouloir retirer cet ordre? @@ -1623,7 +1618,7 @@ popup.warning.examplePercentageValue=Merci de saisir un nombre sous la forme d'u popup.warning.noPriceFeedAvailable=Il n'y a pas de flux pour le prix de disponible pour cette devise. Vous ne pouvez pas utiliser un prix basé sur un pourcentage.\nVeuillez sélectionner le prix fixé. popup.warning.sendMsgFailed=L'envoi du message à votre partenaire d'échange a échoué.\nMerci d'essayer de nouveau et si l'échec persiste merci de reporter le bug. 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\". +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\". popup.warning.makerTxInvalid=Cette offre n'est pas valide. Veuillez choisir une autre offre.\n\n takeOffer.cancelButton=Annuler la prise de l'offre @@ -1636,19 +1631,19 @@ popup.warning.priceRelay=Relais de prix popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Veuillez faire une mise à jour vers la dernière version de Haveno. Une mise à jour obligatoire a été publiée, laquelle désactive le trading sur les anciennes versions. Veuillez consulter le Forum Haveno pour obtenir plus d'informations. popup.warning.noFilter=Nous n'avons pas reçu d'objet de filtre des nœuds de seed. Veuillez informer les administrateurs du réseau d'enregistrer un objet de filtre. -popup.warning.burnXMR=Cette transaction n''est pas possible, car les frais de minage de {0} dépasseraient le montant à transférer de {1}. Veuillez patienter jusqu''à ce que les frais de minage soient de nouveau bas ou jusqu''à ce que vous ayez accumulé plus de XMR à transférer. +popup.warning.burnXMR=Cette transaction n'est pas possible, car les frais de minage de {0} dépasseraient le montant à transférer de {1}. Veuillez patienter jusqu'à ce que les frais de minage soient de nouveau bas ou jusqu'à ce que vous ayez accumulé plus de XMR à transférer. -popup.warning.openOffer.makerFeeTxRejected=La transaction de frais de maker pour l''offre avec ID {0} a été rejetée par le réseau Monero.\nID de transaction={1}.\nL''offre a été retirée pour éviter d''autres problèmes.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l''aide, le canal support de l''équipe Haveno disposible sur Keybase. +popup.warning.openOffer.makerFeeTxRejected=La transaction de frais de maker pour l'offre avec ID {0} a été rejetée par le réseau Monero.\nID de transaction={1}.\nL'offre a été retirée pour éviter d'autres problèmes.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l'aide, le canal support de l'équipe Haveno disposible sur Keybase. popup.warning.trade.txRejected.tradeFee=frais de transaction popup.warning.trade.txRejected.deposit=dépôt -popup.warning.trade.txRejected=La transaction {0} pour le trade qui a pour ID {1} a été rejetée par le réseau Monero.\nID de transaction={2}.\nLe trade a été déplacé vers les échanges échoués.\nAllez dans \"Paramètres/Info sur le réseau\" et effectuez une resynchronisation SPV.\nPour obtenir de l''aide, le canal support de l'équipe Haveno est disponible sur Keybase. +popup.warning.trade.txRejected=La transaction {0} pour le trade qui a pour ID {1} a été rejetée par le réseau Monero.\nID de transaction={2}.\nLe trade a été déplacé vers les échanges échoués.\nAllez dans \"Paramètres/Info sur le réseau\" et effectuez une resynchronisation SPV.\nPour obtenir de l'aide, le canal support de l'équipe Haveno est disponible sur Keybase. -popup.warning.openOfferWithInvalidMakerFeeTx=La transaction de frais de maker pour l''offre avec ID {0} n''est pas valide.\nID de transaction={1}.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l''aide, le canal support de l''équipe Haveno est disponible sur Keybase. +popup.warning.openOfferWithInvalidMakerFeeTx=La transaction de frais de maker pour l'offre avec ID {0} n'est pas valide.\nID de transaction={1}.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l'aide, le canal support de l'équipe Haveno est disponible sur Keybase. popup.info.securityDepositInfo=Afin de s'assurer que les deux traders suivent le protocole de trading, les deux traders doivent payer un dépôt de garantie.\n\nCe dépôt est conservé dans votre portefeuille d'échange jusqu'à ce que votre transaction soit terminée avec succès, et ensuite il vous sera restitué.\n\nRemarque : si vous créez un nouvel ordre, Haveno doit être en cours d'exécution pour qu'un autre trader puisse l'accepter. Pour garder vos ordres en ligne, laissez Haveno en marche et assurez-vous que cet ordinateur reste en ligne aussi (pour cela, assurez-vous qu'il ne passe pas en mode veille....le mode veille du moniteur ne pose aucun problème). -popup.info.cashDepositInfo=Veuillez vous assurer d''avoir une succursale de l''établissement bancaire dans votre région afin de pouvoir effectuer le dépôt en espèces.\nL''identifiant bancaire (BIC/SWIFT) de la banque du vendeur est: {0}. +popup.info.cashDepositInfo=Veuillez vous assurer d'avoir une succursale de l'établissement bancaire dans votre région afin de pouvoir effectuer le dépôt en espèces.\nL'identifiant bancaire (BIC/SWIFT) de la banque du vendeur est: {0}. popup.info.cashDepositInfo.confirm=Je confirme que je peux effectuer le dépôt. popup.info.shutDownWithOpenOffers=Haveno est en cours de fermeture, mais des ordres sont en attente.\n\nCes ordres ne seront pas disponibles sur le réseau P2P si Haveno est éteint, mais ils seront republiés sur le réseau P2P la prochaine fois que vous lancerez Haveno.\n\nPour garder vos ordres en ligne, laissez Haveno en marche et assurez-vous que cet ordinateur reste aussi en ligne (pour cela, assurez-vous qu'il ne passe pas en mode veille...la veille du moniteur ne pose aucun problème). popup.info.qubesOSSetupInfo=Il semble que vous exécutez Haveno sous Qubes OS.\n\nVeuillez vous assurer que votre Haveno qube est mis en place de la manière expliquée dans notre guide [LIEN:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. @@ -1664,7 +1659,7 @@ popup.xmrLocalNode.msg=Haveno a détecté un nœud Monero en cours d'exécution popup.shutDownInProgress.headline=Fermeture en cours popup.shutDownInProgress.msg=La fermeture de l'application nécessite quelques secondes.\nVeuillez ne pas interrompre ce processus. -popup.attention.forTradeWithId=Attention requise la transaction avec l''ID {0} +popup.attention.forTradeWithId=Attention requise la transaction avec l'ID {0} popup.attention.reasonForPaymentRuleChange=La version 1.5.5 introduit un changement critique de règle de trade concernant le champ \"raison du paiement\" dans les transferts banquaires. Veuillez laisser ce champ vide -- N'UTILISEZ PAS l'ID de trade comme \"raison de paiement\". popup.info.multiplePaymentAccounts.headline=Comptes de paiement multiples disponibles @@ -1688,8 +1683,8 @@ popup.accountSigning.success.headline=Félicitations popup.accountSigning.success.description=Tous les {0} comptes de paiement ont été signés avec succès ! popup.accountSigning.generalInformation=Vous trouverez l'état de signature de tous vos comptes dans la section compte.\n\nPour plus d'informations, veuillez consulter [LIEN:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=Un de vos comptes de paiement a été vérifié et signé par un arbitre. Echanger avec ce compte signera automatiquement le compte de votre pair de trading après un échange réussi.\n\n{0} -popup.accountSigning.signedByPeer=Un de vos comptes de paiement a été vérifié et signé par un pair de trading. Votre limite de trading initiale sera levée et vous pourrez signer d''autres comptes dans les {0} jours à venir.\n\n{1} -popup.accountSigning.peerLimitLifted=La limite initiale pour l''un de vos comptes a été levée.\n\n{0} +popup.accountSigning.signedByPeer=Un de vos comptes de paiement a été vérifié et signé par un pair de trading. Votre limite de trading initiale sera levée et vous pourrez signer d'autres comptes dans les {0} jours à venir.\n\n{1} +popup.accountSigning.peerLimitLifted=La limite initiale pour l'un de vos comptes a été levée.\n\n{0} popup.accountSigning.peerSigner=Un de vos comptes est suffisamment mature pour signer d'autres comptes de paiement et la limite initiale pour un de vos comptes a été levée.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Importer le témoin non-signé de l'âge du compte @@ -1712,8 +1707,8 @@ popup.info.buyerAsTakerWithoutDeposit=Votre offre ne nécessitera pas de dépôt # Notifications #################################################################### -notification.trade.headline=Notification pour la transaction avec l''ID {0} -notification.ticket.headline=Ticket de support pour l''échange avec l''ID {0} +notification.trade.headline=Notification pour la transaction avec l'ID {0} +notification.ticket.headline=Ticket de support pour l'échange avec l'ID {0} notification.trade.completed=La transaction est maintenant terminée et vous pouvez retirer vos fonds. notification.trade.accepted=Votre ordre a été accepté par un XMR {0}. notification.trade.unlocked=Votre échange avait au moins une confirmation sur la blockchain.\nVous pouvez effectuer le paiement maintenant. @@ -1723,7 +1718,7 @@ notification.trade.peerOpenedDispute=Votre pair de trading a ouvert un {0}. notification.trade.disputeClosed=Le {0} a été fermé notification.walletUpdate.headline=Mise à jour du portefeuille de trading notification.walletUpdate.msg=Votre portefeuille de trading est suffisamment approvisionné.\nMontant: {0} -notification.takeOffer.walletUpdate.msg=Votre portefeuille de trading était déjà suffisamment approvisionné à la suite d''une précédente tentative d''achat de l'ordre.\nMontant: {0} +notification.takeOffer.walletUpdate.msg=Votre portefeuille de trading était déjà suffisamment approvisionné à la suite d'une précédente tentative d'achat de l'ordre.\nMontant: {0} notification.tradeCompleted.headline=Le trade est terminé notification.tradeCompleted.msg=Vous pouvez retirer vos fonds vers un portefeuille Monero externe ou les conserver dans votre portefeuille Haveno. @@ -1736,25 +1731,25 @@ systemTray.show=Montrer la fenêtre de l'application systemTray.hide=Cacher la fenêtre de l'application systemTray.info=Informations au sujet de Haveno systemTray.exit=Sortir -systemTray.tooltip=Haveno: Une plateforme d''échange décentralisée sur le réseau monero +systemTray.tooltip=Haveno: Une plateforme d'échange décentralisée sur le réseau monero #################################################################### # GUI Util #################################################################### -guiUtil.accountExport.savedToPath=Les comptes de trading sont sauvegardés vers l''arborescence:\n{0} +guiUtil.accountExport.savedToPath=Les comptes de trading sont sauvegardés vers l'arborescence:\n{0} guiUtil.accountExport.noAccountSetup=Vous n'avez pas de comptes de trading configurés pour exportation. -guiUtil.accountExport.selectPath=Sélectionner l''arborescence vers {0} +guiUtil.accountExport.selectPath=Sélectionner l'arborescence vers {0} # suppress inspection "TrailingSpacesInProperty" -guiUtil.accountExport.tradingAccount=Compte de trading avec l''ID {0}\n +guiUtil.accountExport.tradingAccount=Compte de trading avec l'ID {0}\n # suppress inspection "TrailingSpacesInProperty" -guiUtil.accountImport.noImport=Nous n''avons pas importé de compte de trading avec l''id {0} car il existe déjà.\n -guiUtil.accountExport.exportFailed=Echec de l''export à CSV à cause d'une erreur.\nErreur = {0} +guiUtil.accountImport.noImport=Nous n'avons pas importé de compte de trading avec l'id {0} car il existe déjà.\n +guiUtil.accountExport.exportFailed=Echec de l'export à CSV à cause d'une erreur.\nErreur = {0} guiUtil.accountExport.selectExportPath=Sélectionner l'arborescence d'export -guiUtil.accountImport.imported=Compte de trading importé depuis l''arborescence:\n{0}\n\nComptes importés:\n{1} -guiUtil.accountImport.noAccountsFound=Aucun compte de trading exporté n''a été trouvé sur l''arborescence {0}.\nLe nom du fichier est {1}." -guiUtil.openWebBrowser.warning=Vous allez ouvrir une page Web dans le navigateur Web de votre système.\nVoulez-vous ouvrir la page web maintenant ?\n\nSi vous n''utilisez pas le \"Navigateur Tor\" comme navigateur web par défaut, vous vous connecterez à la page web en clair.\n\nURL: \"{0}\" +guiUtil.accountImport.imported=Compte de trading importé depuis l'arborescence:\n{0}\n\nComptes importés:\n{1} +guiUtil.accountImport.noAccountsFound=Aucun compte de trading exporté n'a été trouvé sur l'arborescence {0}.\nLe nom du fichier est {1}." +guiUtil.openWebBrowser.warning=Vous allez ouvrir une page Web dans le navigateur Web de votre système.\nVoulez-vous ouvrir la page web maintenant ?\n\nSi vous n'utilisez pas le \"Navigateur Tor\" comme navigateur web par défaut, vous vous connecterez à la page web en clair.\n\nURL: \"{0}\" guiUtil.openWebBrowser.doOpen=Ouvrir la page web et ne plus me le demander guiUtil.openWebBrowser.copyUrl=Copier l'URL et annuler guiUtil.ofTradeAmount=du montant du trade @@ -1776,13 +1771,13 @@ table.placeholder.processingData=Traitement des données en cours... peerInfoIcon.tooltip.tradePeer=Du pair de trading peerInfoIcon.tooltip.maker=du maker peerInfoIcon.tooltip.trade.traded={0} adresse onion: {1}\nVous avez déjà échangé a {2} reprise(s) avec ce pair\n{3} -peerInfoIcon.tooltip.trade.notTraded={0} adresse onion: {1}\nvous n''avez pas échangé avec ce pair jusqu''à présent.\n{2} +peerInfoIcon.tooltip.trade.notTraded={0} adresse onion: {1}\nvous n'avez pas échangé avec ce pair jusqu'à présent.\n{2} peerInfoIcon.tooltip.age=Compte de paiement créé il y a {0}. peerInfoIcon.tooltip.unknownAge=Ancienneté du compte de paiement inconnue. tooltip.openPopupForDetails=Ouvrir le popup pour obtenir des détails tooltip.invalidTradeState.warning=Le trade est dans un état invalide. Ouvrez la fenêtre des détails pour plus d'informations -tooltip.openBlockchainForAddress=Ouvrir un explorateur de blockchain externe pour l''adresse: {0} +tooltip.openBlockchainForAddress=Ouvrir un explorateur de blockchain externe pour l'adresse: {0} tooltip.openBlockchainForTx=Ouvrir un explorateur de blockchain externe pour la transaction: {0} confidence.unknown=Statut de transaction inconnu @@ -1966,7 +1961,7 @@ payment.accountNr=Numéro de compte payment.emailOrMobile=Email ou N° de portable payment.useCustomAccountName=Utiliser un nom de compte personnalisé payment.maxPeriod=Durée d'échange max. autorisée -payment.maxPeriodAndLimit=Durée maximale de l''échange : {0} / Achat maximum : {1} / Vente maximum : {2} / Âge du compte : {3} +payment.maxPeriodAndLimit=Durée maximale de l'échange : {0} / Achat maximum : {1} / Vente maximum : {2} / Âge du compte : {3} payment.maxPeriodAndLimitCrypto=Durée maximale de trade: {0} / Limite maximale de trading {1} payment.currencyWithSymbol=Devise: {0} payment.nameOfAcceptedBank=Nom de la banque acceptée @@ -1993,7 +1988,7 @@ payment.halCash.info=Lors de l'utilisation de HalCash, l'acheteur de XMR doit en # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Sachez que tous les virements bancaires comportent un certain risque de rétrofacturation. Pour mitiger ce risque, Haveno fixe des limites par trade en fonction du niveau estimé de risque de rétrofacturation pour la méthode de paiement utilisée.\n\nPour cette méthode de paiement, votre limite de trading pour l'achat et la vente est de {2}.\n\nCette limite ne s'applique qu'à la taille d'une seule transaction. Vous pouvez effectuer autant de transactions que vous le souhaitez.\n\nVous trouverez plus de détails sur le wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=Afin de limiter le risque de rétrofacturation des achats, Haveno fixe des limites d'achat par transaction pour ce compte de paiement basé sur les 2 facteurs suivants :\n\n1. Risque de rétrofacturation pour le mode de paiement\n2. Statut de signature du compte\n\nCe compte de paiement n'est pas encore signé, il est donc limité à l'achat de {0} par trade. Après sa signature, les limites d'achat augmenteront comme suit :\n\n● Avant la signature, et jusqu'à 30 jours après la signature, votre limite d'achat par trade sera de {0}\n● 30 jours après la signature, votre limite d'achat par trade sera de {1}\n● 60 jours après la signature, votre limite d'achat par trade sera de {2}\n\nLes limites de vente ne sont pas affectées par la signature du compte. Vous pouvez vendre {2} en un seul trade immédiatement.\n\nCes limites s'appliquent uniquement à la taille d'un seul trade-vous pouvez placer autant de trades que vous voulez.\n\n Pour plus d''nformations, rendez vous à [LIEN:https://docs.haveno.exchange/the-project/account_limits]. +payment.limits.info.withSigning=Afin de limiter le risque de rétrofacturation des achats, Haveno fixe des limites d'achat par transaction pour ce compte de paiement basé sur les 2 facteurs suivants :\n\n1. Risque de rétrofacturation pour le mode de paiement\n2. Statut de signature du compte\n\nCe compte de paiement n'est pas encore signé, il est donc limité à l'achat de {0} par trade. Après sa signature, les limites d'achat augmenteront comme suit :\n\n● Avant la signature, et jusqu'à 30 jours après la signature, votre limite d'achat par trade sera de {0}\n● 30 jours après la signature, votre limite d'achat par trade sera de {1}\n● 60 jours après la signature, votre limite d'achat par trade sera de {2}\n\nLes limites de vente ne sont pas affectées par la signature du compte. Vous pouvez vendre {2} en un seul trade immédiatement.\n\nCes limites s'appliquent uniquement à la taille d'un seul trade-vous pouvez placer autant de trades que vous voulez.\n\n Pour plus d'nformations, rendez vous à [LIEN:https://docs.haveno.exchange/the-project/account_limits]. 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. @@ -2216,8 +2211,8 @@ validation.negative=Une valeur négative n'est pas autorisée. validation.traditional.tooSmall=La saisie d'une valeur plus petite que le montant minimal possible n'est pas autorisée. validation.traditional.tooLarge=La saisie d'une valeur supérieure au montant maximal possible n'est pas autorisée. validation.xmr.fraction=L'entrée résultera dans une valeur monero plus petite qu'1 satoshi -validation.xmr.tooLarge=La saisie d''une valeur supérieure à {0} n''est pas autorisée. -validation.xmr.tooSmall=La saisie d''une valeur inférieure à {0} n''est pas autorisée. +validation.xmr.tooLarge=La saisie d'une valeur supérieure à {0} n'est pas autorisée. +validation.xmr.tooSmall=La saisie d'une valeur inférieure à {0} n'est pas autorisée. validation.passwordTooShort=Le mot de passe que vous avez saisi est trop court. Il doit comporter un minimum de 8 caractères. validation.passwordTooLong=Le mot de passe que vous avez saisi est trop long. Il ne doit pas contenir plus de 50 caractères. validation.sortCodeNumber={0} doit être composer de {1} chiffres. @@ -2225,17 +2220,17 @@ validation.sortCodeChars={0} doit être composer de {1} caractères. validation.bankIdNumber={0} doit être composer de {1} chiffres. validation.accountNr=Le numéro du compte doit comporter {0} chiffres. validation.accountNrChars=Le numéro du compte doit comporter {0} caractères. -validation.xmr.invalidAddress=L''adresse n''est pas correcte. Veuillez vérifier le format de l''adresse. +validation.xmr.invalidAddress=L'adresse n'est pas correcte. Veuillez vérifier le format de l'adresse. validation.integerOnly=Veuillez seulement entrer des nombres entiers. validation.inputError=Votre saisie a causé une erreur:\n{0} -validation.xmr.exceedsMaxTradeLimit=Votre seuil maximum d''échange est {0}. +validation.xmr.exceedsMaxTradeLimit=Votre seuil maximum d'échange est {0}. validation.nationalAccountId={0} doit être composé de {1} nombres. #new validation.invalidInput=La valeur saisie est invalide: {0} validation.accountNrFormat=Le numéro du compte doit être au format: {0} # suppress inspection "UnusedProperty" -validation.crypto.wrongStructure=La validation de l''adresse a échoué car elle ne concorde pas avec la structure d''une adresse {0}. +validation.crypto.wrongStructure=La validation de l'adresse a échoué car elle ne concorde pas avec la structure d'une adresse {0}. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=L'adresse LTZ doit commencer par L. Les adresses commençant par z ne sont pas supportées. # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 784b65234f..1e7119606a 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=Sotto % del prezzo di mercato shared.aboveInPercent=Sopra % del prezzo di mercato shared.enterPercentageValue=Immetti il valore % shared.OR=OPPURE -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=In attesa dei fondi... shared.TheXMRBuyer=L'acquirente di XMR shared.You=Tu @@ -643,7 +638,7 @@ portfolio.pending.step2_buyer.postal=Invia {0} tramite \"Vaglia Postale Statunit # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Contatta il venditore XMR tramite il contatto fornito e organizza un incontro per pagare {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Inizia il pagamento utilizzando {0} @@ -675,7 +670,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=Informazioni di contatto dell'ac portfolio.pending.step2_seller.waitPayment.msg=La transazione di deposito necessita di almeno una conferma blockchain.\nDevi attendere fino a quando l'acquirente XMR invia il pagamento {0}. portfolio.pending.step2_seller.warn=L'acquirente XMR non ha ancora effettuato il pagamento {0}.\nDevi aspettare fino a quando non invia il pagamento.\nSe lo scambio non sarà completato il {1}, l'arbitro comincierà ad indagare. portfolio.pending.step2_seller.openForDispute=L'acquirente XMR non ha ancora inviato il pagamento!\nIl periodo massimo consentito per lo scambio è trascorso.\nPuoi aspettare più a lungo e dare più tempo al partner di scambio oppure puoi contattare il mediatore per ricevere assistenza. -tradeChat.chatWindowTitle=Finestra di chat per scambi con ID '' {0} '' +tradeChat.chatWindowTitle=Finestra di chat per scambi con ID ' {0} ' tradeChat.openChat=Apri la finestra di chat tradeChat.rules=Puoi comunicare con il tuo peer di trading per risolvere potenziali problemi con questo scambio.\nNon è obbligatorio rispondere nella chat.\nSe un trader viola una delle seguenti regole, apri una controversia ed effettua una segnalazione al mediatore o all'arbitro.\n\nRegole della chat:\n● Non inviare nessun link (rischio di malware). È possibile inviare l'ID transazione e il nome di un block explorer.\n● Non inviare parole del seed, chiavi private, password o altre informazioni sensibili!\n● Non incoraggiare il trading al di fuori di Haveno (non garantisce nessuna sicurezza).\n● Non intraprendere alcuna forma di tentativo di frode di ingegneria sociale.\n● Se un peer non risponde e preferisce non comunicare tramite chat, rispettane la decisione.\n● Limita l'ambito della conversazione allo scambio. Questa chat non è una sostituzione di messenger o un troll-box.\n● Mantieni la conversazione amichevole e rispettosa.\n  @@ -812,8 +807,8 @@ portfolio.pending.mediationResult.info.peerAccepted=Il tuo pari commerciale ha a portfolio.pending.mediationResult.button=Visualizza la risoluzione proposta portfolio.pending.mediationResult.popup.headline=Risultato della mediazione per gli scambi con ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Il tuo pari commerciale ha accettato il suggerimento del mediatore per lo scambio {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Rifiuta e richiedi l'arbitrato portfolio.pending.mediationResult.popup.alreadyAccepted=Hai già accettato @@ -987,7 +982,7 @@ support.youOpenedDisputeForMediation=Hai richiesto la mediazione.\n\n{0}\n\nVers support.peerOpenedTicket=Il tuo peer di trading ha richiesto supporto a causa di problemi tecnici.\n\n{0}\n\nVersione Haveno: {1} support.peerOpenedDispute=Il tuo peer di trading ha richiesto una controversia.\n\n{0}\n\nVersione Haveno: {1} support.peerOpenedDisputeForMediation=Il tuo peer di trading ha richiesto la mediazione.\n\n{0}\n\nVersione Haveno: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Indirizzo nodo del mediatore: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? @@ -1114,19 +1109,19 @@ setting.about.subsystems.label=Versioni di sottosistemi setting.about.subsystems.val=Versione di rete: {0}; Versione del messaggio P2P: {1}; Versione DB locale: {2}; Versione del protocollo di scambio: {3} setting.about.shortcuts=Scorciatoie -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' o ''alt + {0}'' o ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' o 'alt + {0}' o 'cmd + {0}' setting.about.shortcuts.menuNav=Naviga il menu principale setting.about.shortcuts.menuNav.value=Per navigare nel menu principale premere: 'Ctrl' o 'alt' o 'cmd' con un tasto numerico tra '1-9' setting.about.shortcuts.close=Chiudi Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' o ''cmd + {0}'' o ''Ctrl + {1}'' o ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' o 'cmd + {0}' o 'Ctrl + {1}' o 'cmd + {1}' setting.about.shortcuts.closePopup=Chiudi popup o finestra di dialogo setting.about.shortcuts.closePopup.value=Tasto 'ESC' setting.about.shortcuts.chatSendMsg=Invia messaggio chat al trader -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' o ''alt + ENTER'' o ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' o 'alt + ENTER' o 'cmd + ENTER' setting.about.shortcuts.openDispute=Apri disputa setting.about.shortcuts.openDispute.value=Seleziona lo scambio in sospeso e fai clic: {0} @@ -1317,7 +1312,7 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Tipo di offerta account.notifications.marketAlert.message.title=Avviso di offerta account.notifications.marketAlert.message.msg.below=sotto account.notifications.marketAlert.message.msg.above=sopra -account.notifications.marketAlert.message.msg=Una nuova ''{0} {1}'' offerta con prezzo {2} ({3} {4} prezzo di mercato) e metodo di pagamento ''{5}'' è stata pubblicata sulla pagina delle offerte Haveno.\nID offerta: {6}. +account.notifications.marketAlert.message.msg=Una nuova '{0} {1}' offerta con prezzo {2} ({3} {4} prezzo di mercato) e metodo di pagamento '{5}' è stata pubblicata sulla pagina delle offerte Haveno.\nID offerta: {6}. account.notifications.priceAlert.message.title=Avviso di prezzo per {0} account.notifications.priceAlert.message.msg=Il tuo avviso di prezzo è stato attivato. L'attuale prezzo {0} è {1} {2} account.notifications.noWebCamFound.warning=Nessuna webcam trovata.\n\nUtilizzare l'opzione e-mail per inviare il token e la chiave di crittografia dal telefono cellulare all'applicazione Haveno. @@ -1978,7 +1973,7 @@ payment.accountType=Tipologia conto payment.checking=Verifica payment.savings=Risparmi payment.personalId=ID personale -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Alcune banche hanno iniziato a verificare il nome completo del destinatario per i trasferimenti di Faster Payments (UK). Il tuo attuale account Faster Payments non specifica un nome completo.\n\nTi consigliamo di ricreare il tuo account Faster Payments in Haveno per fornire ai futuri acquirenti {0} un nome completo.\n\nQuando si ricrea l'account, assicurarsi di copiare il codice di ordinamento preciso, il numero di account e i valori salt della verifica dell'età dal vecchio account al nuovo account. Ciò garantirà il mantenimento dell'età del tuo account esistente e lo stato della firma.\n  payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. @@ -1991,7 +1986,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade payment.cashDeposit.info=Conferma che la tua banca ti consente di inviare depositi in contanti su conti di altre persone. Ad esempio, Bank of America e Wells Fargo non consentono più tali depositi. payment.revolut.info=Revolut requires the 'Username' 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 have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Si prega di notare che Cash App ha un rischio di chargeback più elevato rispetto alla maggior parte dei bonifici bancari. @@ -2025,7 +2020,7 @@ payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Per la tua protezione, sconsigliamo vivamente di utilizzare i PIN di Paysafecard per i pagamenti.\n\n\ Le transazioni effettuate tramite PIN non possono essere verificate in modo indipendente per la risoluzione delle controversie. Se si verifica un problema, il recupero dei fondi potrebbe non essere possibile.\n\n\ Per garantire la sicurezza delle transazioni con risoluzione delle controversie, utilizza sempre metodi di pagamento che forniscono registrazioni verificabili. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index ccc24ab657..045417f7c4 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -676,7 +671,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=買い手の連絡先 portfolio.pending.step2_seller.waitPayment.msg=デポジットトランザクションには、少なくとも1つのブロックチェーン承認があります。\nXMRの買い手が{0}の支払いを開始するまで待つ必要があります。 portfolio.pending.step2_seller.warn=XMRの買い手はまだ{0}の支払いを行っていません。\n支払いが開始されるまで待つ必要があります。\n取引が{1}で完了していない場合は、調停人が調査します。 portfolio.pending.step2_seller.openForDispute=XMRの買い手は支払いを開始していません!\nトレードの許可された最大期間が経過しました。\nもっと長く待ってトレードピアにもっと時間を与えるか、助けを求めるために調停者に連絡することができます。 -tradeChat.chatWindowTitle=トレードID '{0}'' のチャットウィンドウ +tradeChat.chatWindowTitle=トレードID '{0}' のチャットウィンドウ tradeChat.openChat=チャットウィンドウを開く tradeChat.rules=このトレードに対する潜在的な問題を解決するため、トレードピアと連絡できます。\nチャットに返事する義務はありません。\n取引者が以下のルールを破ると、係争を開始して調停者や調停人に報告して下さい。\n\nチャット・ルール:\n\t●リンクを送らないこと(マルウェアの危険性)。トランザクションIDとブロックチェーンエクスプローラの名前を送ることができます。\n\t●シードワード、プライベートキー、パスワードなどの機密な情報を送らないこと。\n\t●Haveno外のトレードを助長しないこと(セキュリティーがありません)。\n\t●ソーシャル・エンジニアリングや詐欺の行為に参加しないこと。\n\t●チャットで返事されない場合、それともチャットでの連絡が断られる場合、ピアの決断を尊重すること。\n\t●チャットの範囲をトレードに集中しておくこと。チャットはメッセンジャーの代わりや釣りをする場所ではありません。\n\t●礼儀正しく丁寧に話すこと。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index c28a530456..59b24342db 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=% abaixo do preço de mercado shared.aboveInPercent=% acima do preço de mercado shared.enterPercentageValue=Insira a % shared.OR=OU -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Aguardando pagamento... shared.TheXMRBuyer=O comprador de XMR shared.You=Você @@ -646,7 +641,7 @@ portfolio.pending.step2_buyer.postal=Envie {0} através de \"US Postal Money Ord # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Por favor, entre em contato com o vendedor de XMR através do contato fornecido e combine um encontro para pagá-lo {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Iniciar pagamento usando {0} @@ -815,8 +810,8 @@ portfolio.pending.mediationResult.info.peerAccepted=O seu parceiro de negociaç portfolio.pending.mediationResult.button=Ver solução proposta portfolio.pending.mediationResult.popup.headline=Resultado da mediação para a negociação com ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=O seu parceiro de negociação aceitou a sugestão do mediador para a negociação {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Rejeitar e solicitar arbitramento portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou @@ -989,7 +984,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 Haveno: {1} support.peerOpenedDispute=O seu parceiro de negociação solicitou uma disputa.\n\n{0}\n\nVersão do Haveno: {1} support.peerOpenedDisputeForMediation=O seu parceiro de negociação solicitou mediação.\n\n{0}\n\nVersão do Haveno: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Endereço do nó do mediador: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? @@ -1116,19 +1111,19 @@ setting.about.subsystems.label=Versões dos subsistemas setting.about.subsystems.val=Versão da rede: {0}; Versão de mensagens P2P: {1}; Versão do banco de dados local: {2}; Versão do protocolo de negociação: {3} setting.about.shortcuts=atalhos -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' ou ''alt + {0}'' ou ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' ou 'alt + {0}' ou 'cmd + {0}' setting.about.shortcuts.menuNav=Navegar para o menu principal setting.about.shortcuts.menuNav.value=Para ir ao menu principal, pressione: "ctr" ou "alt" ou "cmd" com um botão numérico de 1 a 9 setting.about.shortcuts.close=Fechar Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' ou ''cmd + {0}'' ou ''Ctrl + {1}'' ou ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' ou 'cmd + {0}' ou 'Ctrl + {1}' ou 'cmd + {1}' setting.about.shortcuts.closePopup=Fechar popup ou janela de diálogo setting.about.shortcuts.closePopup.value=botão "Esc" setting.about.shortcuts.chatSendMsg=Enviar mensagem de chat ao negociador -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' ou ''alt + ENTER'' ou ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' ou 'alt + ENTER' ou 'cmd + ENTER' setting.about.shortcuts.openDispute=Abrir disputa setting.about.shortcuts.openDispute.value=Selecione negociação pendente e clique: {0} @@ -1319,7 +1314,7 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Tipo de oferta account.notifications.marketAlert.message.title=Alerta de oferta account.notifications.marketAlert.message.msg.below=abaixo account.notifications.marketAlert.message.msg.above=acima -account.notifications.marketAlert.message.msg=Uma nova oferta ''{0} {1}'' com preço {2} ({3} {4} preço de mercado) e com o método de pagamento ''{5}'' foi publicada no livro de ofertas do Haveno.\nID da oferta: {6}. +account.notifications.marketAlert.message.msg=Uma nova oferta '{0} {1}' com preço {2} ({3} {4} preço de mercado) e com o método de pagamento '{5}' foi publicada no livro de ofertas do Haveno.\nID da oferta: {6}. account.notifications.priceAlert.message.title=Alerta de preço para {0} account.notifications.priceAlert.message.msg=O seu preço de alerta foi atingido. O preço atual da {0} é {1} {2} account.notifications.noWebCamFound.warning=Nenhuma webcam foi encontrada.\n\nPor favor, use a opção e-mail para enviar o token e a chave de criptografia do seu celular para o Haveno @@ -1985,8 +1980,8 @@ payment.accountType=Tipo de conta payment.checking=Conta Corrente payment.savings=Poupança payment.personalId=Identificação pessoal -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Ao usar o HalCash, o comprador de XMR precisa enviar ao vendedor de XMR o código HalCash através de uma mensagem de texto do seu telefone.\n\nPor favor, certifique-se de não exceder a quantia máxima que seu banco lhe permite enviar com o HalCash. O valor mínimo de saque é de 10 euros e valor máximo é de 600 EUR. Para saques repetidos é de 3000 euros por destinatário por dia e 6000 euros por destinatário por mês. Por favor confirme esses limites com seu banco para ter certeza de que eles usam os mesmos limites mencionados aqui.\n\nO valor de saque deve ser um múltiplo de 10 euros, pois você não pode sacar notas diferentes de uma ATM. Esse valor em XMR será ajustado na telas de criar e aceitar ofertas para que a quantia de EUR esteja correta. Você não pode usar o preço com base no mercado, pois o valor do EUR estaria mudando com a variação dos preços.\n\nEm caso de disputa, o comprador de XMR precisa fornecer a prova de que enviou o EUR. @@ -1998,7 +1993,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade 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=Revolut requires the 'Username' 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 have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Por favor, esteja ciente de que o Cash App tem um risco maior de estorno do que a maioria das transferências bancárias. @@ -2032,7 +2027,7 @@ payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Para sua proteção, desaconselhamos fortemente o uso de PINs do Paysafecard para pagamento.\n\n\ Transações feitas por PINs não podem ser verificadas de forma independente para resolução de disputas. Se ocorrer um problema, a recuperação de fundos pode não ser possível.\n\n\ Para garantir a segurança das transações com resolução de disputas, sempre utilize métodos de pagamento que forneçam registros verificáveis. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index d14b41668c..682b825894 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=Abaixo % do preço de mercado shared.aboveInPercent=Acima % do preço de mercado shared.enterPercentageValue=Insira % do valor shared.OR=OU -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Esperando pelos fundos... shared.TheXMRBuyer=O comprador de XMR shared.You=Você @@ -643,7 +638,7 @@ portfolio.pending.step2_buyer.postal=Por favor envie {0} por \"US Postal Money O # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Por favor contacte o vendedor de XMR pelo contacto fornecido e marque um encontro para pagar {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Iniciar pagamento usando {0} @@ -675,7 +670,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=Informação do contacto do comp portfolio.pending.step2_seller.waitPayment.msg=A transação de depósito tem pelo menos uma confirmação da blockchain.\nVocê precisa esperar até que o comprador de XMR inicie o pagamento {0}. portfolio.pending.step2_seller.warn=O comprador do XMR ainda não efetuou o pagamento de {0}.\nVocê precisa esperar até que eles tenham iniciado o pagamento.\nSe o negócio não for concluído em {1}, o árbitro irá investigar. portfolio.pending.step2_seller.openForDispute=O comprador de XMR não iniciou o seu pagamento!\nO período máx. permitido para o negócio acabou.\nVocê pode esperar e dar mais tempo ao seu par de negociação ou entrar em contacto com o mediador para assistência. -tradeChat.chatWindowTitle=Janela de chat para o negócio com o ID ''{0}'' +tradeChat.chatWindowTitle=Janela de chat para o negócio com o ID '{0}' tradeChat.openChat=Abrir janela de chat tradeChat.rules=Você pode comunicar com o seu par de negociação para resolver problemas com este negócio.\nNão é obrigatório responder no chat.\nSe algum negociante infringir alguma das regras abaixo, abra uma disputa e reporte-o ao mediador ou ao árbitro.\n\nRegras do chat:\n\t● Não envie nenhum link (risco de malware). Você pode enviar o ID da transação e o nome de um explorador de blocos.\n\t● Não envie as suas palavras-semente, chaves privadas, senhas ou outra informação sensitiva!\n\t● Não encoraje negócios fora do Haveno (sem segurança).\n\t● Não engaje em nenhuma forma de scams de engenharia social.\n\t● Se um par não responde e prefere não comunicar pelo chat, respeite a sua decisão.\n\t● Mantenha o âmbito da conversa limitado ao negócio. Este chat não é um substituto para o messenger ou uma caixa para trolls.\n\t● Mantenha a conversa amigável e respeitosa. @@ -812,8 +807,8 @@ portfolio.pending.mediationResult.info.peerAccepted=O seu par de negócio aceito portfolio.pending.mediationResult.button=Ver a resolução proposta portfolio.pending.mediationResult.popup.headline=Resultado da mediação para o negócio com o ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=O seu par de negócio aceitou a sugestão do mediador para o negócio {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Rejeitar e solicitar arbitragem portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou @@ -986,7 +981,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 Haveno: {1} support.peerOpenedDispute=O seu par de negociação solicitou uma disputa.\n\n{0}\n\nVersão Haveno: {1} support.peerOpenedDisputeForMediation=O seu par de negociação solicitou uma mediação.\n\n{0}\n\nVersão Haveno: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Endereço do nó do mediador: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? @@ -1113,19 +1108,19 @@ setting.about.subsystems.label=Versão de subsistemas setting.about.subsystems.val=Versão da rede: {0}; Versão de mensagem P2P: {1}; Versão da base de dados local: {2}; Versão do protocolo de negócio: {3} setting.about.shortcuts=Atalhos -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' ou ''alt + {0}'' ou ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' ou 'alt + {0}' ou 'cmd + {0}' setting.about.shortcuts.menuNav=Navigar o menu principal setting.about.shortcuts.menuNav.value=Para navigar o menu principal pressione:: 'Ctrl' ou 'alt' ou 'cmd' juntamente com uma tecla numérica entre '1-9' setting.about.shortcuts.close=Fechar o Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' ou ''cmd + {0}'' ou ''Ctrl + {1}'' ou ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' ou 'cmd + {0}' ou 'Ctrl + {1}' ou 'cmd + {1}' setting.about.shortcuts.closePopup=Fechar popup ou janela de diálogo setting.about.shortcuts.closePopup.value=Tecla "ESCAPE" setting.about.shortcuts.chatSendMsg=Enviar uma mensagem ao negociador -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' ou ''alt + ENTER'' ou ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' ou 'alt + ENTER' ou 'cmd + ENTER' setting.about.shortcuts.openDispute=Abrir disputa setting.about.shortcuts.openDispute.value=Selecionar negócio pendente e clicar: {0} @@ -1316,7 +1311,7 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Tipo de oferta account.notifications.marketAlert.message.title=Alerta de oferta account.notifications.marketAlert.message.msg.below=abaixo de account.notifications.marketAlert.message.msg.above=acima de -account.notifications.marketAlert.message.msg=Uma nova ''{0} {1}'' com o preço de {2} ({3} {4} preço de mercado) e método de pagamento ''{5}'' foi publicada no livro de ofertas do Haveno.\nID da oferta: {6}. +account.notifications.marketAlert.message.msg=Uma nova '{0} {1}' com o preço de {2} ({3} {4} preço de mercado) e método de pagamento '{5}' foi publicada no livro de ofertas do Haveno.\nID da oferta: {6}. account.notifications.priceAlert.message.title=Alerta de preço para {0} account.notifications.priceAlert.message.msg=O teu alerta de preço foi desencadeado. O preço atual de {0} é de {1} {2} account.notifications.noWebCamFound.warning=Nenhuma webcam foi encontrada.\n\nPor favor use a opção email para enviar o token e a chave de criptografia do seu telemóvel para o programa da Haveno. @@ -1975,8 +1970,8 @@ payment.accountType=Tipo de conta payment.checking=Conta Corrente payment.savings=Poupança payment.personalId=ID pessoal -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Ao usar o HalCash, o comprador de XMR precisa enviar ao vendedor de XMR o código HalCash através de uma mensagem de texto do seu telemóvel.\n\nPor favor, certifique-se de não exceder a quantia máxima que seu banco lhe permite enviar com o HalCash. A quantia mín. de levantamento é de 10 euros e a quantia máx. é de 600 EUR. Para levantamentos repetidos é de 3000 euros por recipiente por dia e 6000 euros por recipiente por mês. Por favor confirme esses limites com seu banco para ter certeza de que eles usam os mesmos limites mencionados aqui.\n\nA quantia de levantamento deve ser um múltiplo de 10 euros, pois você não pode levantar outras quantias de uma ATM. A interface do utilizador no ecrã para criar oferta e aceitar ofertas ajustará a quantia de XMR para que a quantia de EUR esteja correta. Você não pode usar o preço com base no mercado, pois o valor do EUR estaria mudando com a variação dos preços.\n\nEm caso de disputa, o comprador de XMR precisa fornecer a prova de que enviou o EUR. @@ -1988,7 +1983,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade 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=Revolut requires the 'Username' 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 have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Esteja ciente de que o Cash App tem um risco de estorno maior do que a maioria das transferências bancárias. @@ -2022,7 +2017,7 @@ payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Para sua proteção, desaconselhamos fortemente o uso de PINs do Paysafecard para pagamento.\n\n\ Transações feitas por PINs não podem ser verificadas de forma independente para resolução de disputas. Se ocorrer um problema, a recuperação dos fundos pode não ser possível.\n\n\ Para garantir a segurança das transações com resolução de disputas, sempre use métodos de pagamento que forneçam registros verificáveis. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 848b765ae2..aac587efda 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=% ниже рыночного курса shared.aboveInPercent=% выше рыночного курса shared.enterPercentageValue=Ввести величину в % shared.OR=ИЛИ -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Ожидание средств... shared.TheXMRBuyer=Покупатель ВТС shared.You=Вы @@ -359,7 +354,7 @@ offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=Купить XMR с помощью: offerbook.sellXmrFor=Продать XMR за: -offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} дн. offerbook.timeSinceSigning.notSigned.noNeed=Н/Д @@ -399,7 +394,7 @@ offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due t offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Этот способ оплаты временно ограничен до {0} до {1}, поскольку все покупатели имеют новые аккаунты.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Ваше предложение будет ограничено для покупателей с подписанными и старыми аккаунтами, потому что оно превышает {0}.\n\n{1} @@ -643,7 +638,7 @@ portfolio.pending.step2_buyer.postal=Отправьте {0} \«Почтовым # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Свяжитесь с продавцом XMR с помощью указанных контактных данных и договоритесь о встрече для оплаты {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Начать оплату, используя {0} @@ -675,7 +670,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=Контактная инфор portfolio.pending.step2_seller.waitPayment.msg=Депозитная транзакция подтверждена в блокчейне не менее одного раза.\nДождитесь начала платежа в {0} покупателем XMR. portfolio.pending.step2_seller.warn=Покупатель XMR все еще не завершил платеж в {0}.\nДождитесь начала оплаты.\nЕсли сделка не завершится {1}, арбитр начнет разбирательство. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. -tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. @@ -715,7 +710,7 @@ portfolio.pending.step3_seller.westernUnion=Покупатель обязан о portfolio.pending.step3_seller.halCash=Покупатель должен отправить вам код HalCash в текстовом сообщении. Кроме того, вы получите сообщение от HalCash с информацией, необходимой для снятия EUR в банкомате, поддерживающем HalCash.\n\nПосле того, как вы заберете деньги из банкомата, подтвердите получение платежа в приложении! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. -portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Подтвердите получение платежа @@ -737,7 +732,7 @@ portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Вы получили платеж в {0} от своего контрагента?\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Подтвердите получение платежа @@ -811,9 +806,9 @@ portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediat portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} -portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted @@ -977,7 +972,7 @@ support.sellerMaker=Продавец ВТС/мейкер support.buyerTaker=Покупатель ВТС/тейкер support.sellerTaker=Продавец XMR/тейкер -support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=Системное сообщение: {0} support.youOpenedTicket=Вы запросили поддержку.\n\n{0}\n\nВерсия Haveno: {1} support.youOpenedDispute=Вы начали спор.\n\n{0}\n\nВерсия Haveno: {1} @@ -985,8 +980,8 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno v support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} -support.mediatorsAddress=Mediator''s node address: {0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} +support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. @@ -1112,19 +1107,19 @@ setting.about.subsystems.label=Версии подсистем setting.about.subsystems.val=Версия сети: {0}; версия P2P-сообщений: {1}; версия локальной базы данных: {2}; версия торгового протокола: {3} setting.about.shortcuts=Short cuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} @@ -1199,7 +1194,7 @@ account.arbitratorRegistration.registerSuccess=You have successfully registered account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=Ваши альткойн-счета -account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=Я понимаю и подтверждаю, что знаю, какой кошелёк нужно использовать. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. @@ -1680,8 +1675,8 @@ popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} -popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} @@ -1976,8 +1971,8 @@ payment.accountType=Тип счёта payment.checking=Текущий payment.savings=Сберегательный payment.personalId=Личный идентификатор -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Используя HalCash, покупатель XMR обязуется отправить продавцу XMR код HalCash через СМС с мобильного телефона.\n\nУбедитесь, что не вы не превысили максимальную сумму, которую ваш банк позволяет отправить с HalCash. Минимальная сумма на вывод средств составляет 10 EUR, а и максимальная — 600 EUR. При повторном выводе средств лимит составляет 3000 EUR на получателя в день и 6000 EUR на получателя в месяц. Просьба сверить эти лимиты с вашим банком и убедиться, что лимиты банка соответствуют лимитам, указанным здесь.\n\nВыводимая сумма должна быть кратна 10 EUR, так как другие суммы снять из банкомата невозможно. Приложение само отрегулирует сумму XMR, чтобы она соответствовала сумме в EUR, во время создания или принятия предложения. Вы не сможете использовать текущий рыночный курс, так как сумма в EUR будет меняться с изменением курса.\n\nВ случае спора покупателю XMR необходимо предоставить доказательство отправки EUR. @@ -1989,7 +1984,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade payment.cashDeposit.info=Убедитесь, что ваш банк позволяет отправлять денежные переводы на счета других лиц. Например, Bank of America и Wells Fargo больше не разрешают такие переводы. payment.revolut.info=Revolut requires the 'Username' 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 have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Обратите внимание, что Cash App имеет более высокий риск возврата платежей, чем большинство банковских переводов. @@ -2024,7 +2019,7 @@ payment.japan.recipient=Имя payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Для вашей защиты мы настоятельно не рекомендуем использовать PIN-коды Paysafecard для платежей.\n\n\ Транзакции, выполненные с помощью PIN-кодов, не могут быть независимо подтверждены для разрешения споров. В случае возникновения проблемы возврат средств может быть невозможен.\n\n\ Чтобы обеспечить безопасность транзакций с возможностью разрешения споров, всегда используйте методы оплаты, предоставляющие проверяемые записи. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index df37fc3f28..8da8c3c0ff 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=ต่ำกว่า % จากราคาตลาด shared.aboveInPercent=สูงกว่า % จากราคาตาด shared.enterPercentageValue=เข้าสู่ % ตามมูลค่า shared.OR=หรือ -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=กำลังรอเงิน ... shared.TheXMRBuyer=ผู้ซื้อ XMR shared.You=คุณ @@ -359,7 +354,7 @@ offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=ซื้อ XMR ด้วย: offerbook.sellXmrFor=ขาย XMR สำหรับ: -offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} วัน offerbook.timeSinceSigning.notSigned.noNeed=ไม่พร้อมใช้งาน @@ -399,7 +394,7 @@ offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due t offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=วิธีการชำระเงินนี้ถูก จำกัด ชั่วคราวไปยัง {0} จนถึง {1} เนื่องจากผู้ซื้อทุกคนมีบัญชีใหม่\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=ข้อเสนอของคุณจะถูก จำกัด เฉพาะผู้ซื้อที่มีบัญชีที่ได้ลงนามและมีอายุ เนื่องจากมันเกิน {0}.\n\n{1} @@ -488,7 +483,7 @@ createOffer.currencyForFee=ค่าธรรมเนียมการซื createOffer.setDeposit=Set buyer's security deposit (%) createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) -createOffer.securityDepositInfo=Your buyer''s security deposit will be {0} +createOffer.securityDepositInfo=Your buyer's security deposit will be {0} createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} createOffer.minSecurityDepositUsed=เงินประกันความปลอดภัยขั้นต่ำถูกใช้ createOffer.buyerAsTakerWithoutDeposit=ไม่ต้องวางมัดจำจากผู้ซื้อ (ป้องกันด้วยรหัสผ่าน) @@ -643,7 +638,7 @@ portfolio.pending.step2_buyer.postal=โปรดส่ง {0} โดยธน # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=กรุณาติดต่อผู้ขายของ XMR ตามรายชื่อที่ได้รับและนัดประชุมเพื่อจ่ายเงิน {0}\n\n portfolio.pending.step2_buyer.startPaymentUsing=เริ่มต้นการชำระเงินโดยใช้ {0} @@ -675,7 +670,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=ข้อมูลการต portfolio.pending.step2_seller.waitPayment.msg=ธุรกรรมการฝากเงินมีการยืนยันบล็อกเชนอย่างน้อยหนึ่งรายการ\nคุณต้องรอจนกว่าผู้ซื้อ XMR จะเริ่มการชำระเงิน {0} portfolio.pending.step2_seller.warn=ผู้ซื้อ XMR ยังไม่ได้ทำ {0} การชำระเงิน\nคุณต้องรอจนกว่าผู้ซื้อจะเริ่มชำระเงิน\nหากการซื้อขายยังไม่เสร็จสิ้นในวันที่ {1} ผู้ไกล่เกลี่ยจะดำเนินการตรวจสอบ portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. -tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. @@ -715,7 +710,7 @@ portfolio.pending.step3_seller.westernUnion=ผู้ซื้อต้องส portfolio.pending.step3_seller.halCash=ผู้ซื้อต้องส่งข้อความรหัส HalCash ให้คุณ ในขณะเดียวกันคุณจะได้รับข้อความจาก HalCash พร้อมกับคำขอข้อมูลจำเป็นในการถอนเงินยูโรุจากตู้เอทีเอ็มที่รองรับ HalCash \n\n หลังจากที่คุณได้รับเงินจากตู้เอทีเอ็มโปรดยืนยันใบเสร็จรับเงินจากการชำระเงินที่นี่ ! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. -portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=ใบเสร็จยืนยันการชำระเงิน @@ -737,7 +732,7 @@ portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=คุณได้รับ {0} การชำระเงินจากคู่ค้าของคุณหรือไม่\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=ยืนยันว่าคุณได้รับการชำระเงินแล้ว @@ -811,9 +806,9 @@ portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediat portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} -portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted @@ -977,7 +972,7 @@ support.sellerMaker= XMR ผู้ขาย/ ผู้สร้าง support.buyerTaker=XMR ผู้ซื้อ / ผู้รับ support.sellerTaker=XMR ผู้ขาย / ผู้รับ -support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=ระบบข้อความ: {0} support.youOpenedTicket=You opened a request for support.\n\n{0}\n\nHaveno version: {1} support.youOpenedDispute=You opened a request for a dispute.\n\n{0}\n\nHaveno version: {1} @@ -985,8 +980,8 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno v support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} -support.mediatorsAddress=Mediator''s node address: {0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} +support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. @@ -1112,19 +1107,19 @@ setting.about.subsystems.label=เวอร์ชั่นของระบบ setting.about.subsystems.val=เวอร์ชั่นของเครือข่าย: {0}; เวอร์ชั่นข้อความ P2P: {1}; เวอร์ชั่นฐานข้อมูลท้องถิ่น: {2}; เวอร์ชั่นโปรโตคอลการซื้อขาย: {3} setting.about.shortcuts=Short cuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} @@ -1199,7 +1194,7 @@ account.arbitratorRegistration.registerSuccess=You have successfully registered account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=บัญชี crypto (เหรียญทางเลือก) ของคุณ -account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=ฉันเข้าใจและยืนยันว่าฉันรู้ว่า wallet ใดที่ฉันต้องการใช้ # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. @@ -1631,7 +1626,7 @@ popup.warning.priceRelay=ราคาผลัดเปลี่ยน popup.warning.seed=รหัสลับเพื่อกู้ข้อมูล popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. popup.warning.noFilter=เราไม่ได้รับวัตถุกรองจากโหนดต้นทาง กรุณาแจ้งผู้ดูแลระบบเครือข่ายให้ลงทะเบียนวัตถุกรอง -popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. +popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you've accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. @@ -1680,8 +1675,8 @@ popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} -popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} @@ -1976,8 +1971,8 @@ payment.accountType=ประเภทบัญชี payment.checking=การตรวจสอบ payment.savings=ออมทรัพย์ payment.personalId=รหัส ID ประจำตัวบุคคล -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=เมื่อมีการใช้งาน HalCash ผู้ซื้อ XMR จำเป็นต้องส่งรหัส Halcash ให้กับผู้ขายทางข้อความโทรศัพท์มือถือ\n\nโปรดตรวจสอบว่าไม่เกินจำนวนเงินสูงสุดที่ธนาคารของคุณอนุญาตให้คุณส่งด้วย HalCash จำนวนเงินขั้นต่ำในการเบิกถอนคือ 10 EUR และสูงสุดในจำนวนเงิน 600 EUR สำหรับการถอนซ้ำเป็น 3000 EUR ต่อผู้รับและต่อวัน และ 6000 EUR ต่อผู้รับและต่อเดือน โปรดตรวจสอบข้อจำกัดจากทางธนาคารคุณเพื่อให้มั่นใจได้ว่าทางธนาคารได้มีการใช้มาตรฐานข้อกำหนดเดียวกันกับดังที่ระบุไว้ ณ ที่นี่\n\nจำนวนเงินที่ถอนจะต้องเป็นจำนวนเงินหลาย 10 EUR เนื่องจากคุณไม่สามารถถอนเงินอื่น ๆ ออกจากตู้เอทีเอ็มได้ UI ในหน้าจอสร้างข้อเสนอและรับข้อเสนอจะปรับจำนวนเงิน XMR เพื่อให้จำนวนเงิน EUR ถูกต้อง คุณไม่สามารถใช้ราคาตลาดเป็นจำนวนเงิน EUR ซึ่งจะเปลี่ยนแปลงไปตามราคาที่มีการปรับเปลี่ยน\n\nในกรณีที่มีข้อพิพาทผู้ซื้อ XMR ต้องแสดงหลักฐานว่าได้ส่ง EUR แล้ว @@ -1989,7 +1984,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade payment.cashDeposit.info=โปรดยืนยันว่าธนาคารของคุณได้อนุมัติให้คุณสามารถส่งเงินสดให้กับบัญชีบุคคลอื่นได้ ตัวอย่างเช่น บางธนาคารที่ไม่ได้มีการบริการถ่ายโอนเงินสดอย่าง Bank of America และ Wells Fargo payment.revolut.info=Revolut requires the 'Username' 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 have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=โปรดทราบว่า Cash App มีความเสี่ยงในการเรียกเงินคืนสูงกว่าการโอนเงินผ่านธนาคารส่วนใหญ่ @@ -2023,7 +2018,7 @@ payment.japan.recipient=ชื่อ payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=เพื่อความปลอดภัยของคุณ เราขอแนะนำอย่างยิ่งให้หลีกเลี่ยงการใช้ Paysafecard PINs ในการชำระเงิน\n\n\ ธุรกรรมที่ดำเนินการผ่าน PIN ไม่สามารถตรวจสอบได้อย่างอิสระสำหรับการระงับข้อพิพาท หากเกิดปัญหา อาจไม่สามารถกู้คืนเงินได้\n\n\ เพื่อความปลอดภัยของธุรกรรมและรองรับการระงับข้อพิพาท โปรดใช้วิธีการชำระเงินที่มีบันทึกการทำธุรกรรมที่ตรวจสอบได้ diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 9b7f12e466..723e108b86 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -752,8 +747,8 @@ portfolio.pending.step2_seller.f2fInfo.headline=Alıcının iletişim bilgileri portfolio.pending.step2_seller.waitPayment.msg=Yatırım işlemi kilidi açıldı.\nXMR alıcısının {0} ödemesini başlatmasını beklemeniz gerekiyor. portfolio.pending.step2_seller.warn=XMR alıcısı hala {0} ödemesini yapmadı.\nÖdeme başlatılana kadar beklemeniz gerekiyor.\nİşlem {1} tarihinde tamamlanmadıysa, arabulucu durumu inceleyecektir. portfolio.pending.step2_seller.openForDispute=XMR alıcısı ödemesine başlamadı!\nİşlem için izin verilen maksimum süre doldu.\nKarşı tarafa daha fazla zaman tanıyabilir veya arabulucu ile iletişime geçebilirsiniz. -disputeChat.chatWindowTitle=İşlem ID''si ile ilgili uyuşmazlık sohbet penceresi ''{0}'' -tradeChat.chatWindowTitle=İşlem ID''si ile ilgili trader sohbet penceresi ''{0}'' +disputeChat.chatWindowTitle=İşlem ID'si ile ilgili uyuşmazlık sohbet penceresi '{0}' +tradeChat.chatWindowTitle=İşlem ID'si ile ilgili trader sohbet penceresi '{0}' tradeChat.openChat=Sohbet penceresini aç tradeChat.rules=Bu işlemle ilgili olası sorunları çözmek için işlem ortağınızla iletişim kurabilirsiniz.\n\ Sohbette yanıt vermek zorunlu değildir.\n\ @@ -1387,19 +1382,19 @@ setting.about.subsystems.label=Alt sistemlerin sürümleri setting.about.subsystems.val=Ağ sürümü: {0}; P2P mesaj sürümü: {1}; Yerel DB sürümü: {2}; Ticaret protokolü sürümü: {3} setting.about.shortcuts=Kısayollar -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' veya ''alt + {0}'' veya ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' veya 'alt + {0}' veya 'cmd + {0}' setting.about.shortcuts.menuNav=Ana menüde gezin setting.about.shortcuts.menuNav.value=Ana menüde gezinmek için şuna basın: 'Ctrl' veya 'alt' veya 'cmd' ve '1-9' arasındaki bir sayı tuşu setting.about.shortcuts.close=Haveno'yu kapat -setting.about.shortcuts.close.value=''Ctrl + {0}'' veya ''cmd + {0}'' veya ''Ctrl + {1}'' veya ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' veya 'cmd + {0}' veya 'Ctrl + {1}' veya 'cmd + {1}' setting.about.shortcuts.closePopup=Açılır pencereyi veya iletişim penceresini kapat setting.about.shortcuts.closePopup.value='ESCAPE' tuşu setting.about.shortcuts.chatSendMsg=Trader sohbet mesajı gönder -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' veya ''alt + ENTER'' veya ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' veya 'alt + ENTER' veya 'cmd + ENTER' setting.about.shortcuts.openDispute=Uyuşmazlık aç setting.about.shortcuts.openDispute.value=Bekleyen işlemi seçin ve tıklayın: {0} @@ -1784,7 +1779,7 @@ account.notifications.marketAlert.message.title=Teklif uyarısı account.notifications.marketAlert.message.msg.below=altında account.notifications.marketAlert.message.msg.above=üstünde account.notifications.marketAlert.message.msg=Haveno teklif defterine {2} ({3} {4} piyasa fiyatı) fiyatıyla \ - ve ödeme yöntemi ''{5}'' olan yeni bir ''{0} {1}'' teklifi yayınlandı.\n\ + ve ödeme yöntemi '{5}' olan yeni bir '{0} {1}' teklifi yayınlandı.\n\ Teklif ID: {6}. account.notifications.priceAlert.message.title={0} için fiyat uyarısı account.notifications.priceAlert.message.msg=Fiyat uyarınız tetiklendi. Mevcut {0} fiyatı {1} {2} diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 52854a4208..6962df5af2 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=Thấp hơn % so với giá thị trường shared.aboveInPercent=Cao hơn % so với giá thị trường shared.enterPercentageValue=Nhập giá trị % shared.OR=HOẶC -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Đợi nộp tiền... shared.TheXMRBuyer=Người mua XMR shared.You=Bạn @@ -359,7 +354,7 @@ offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=Mua XMR với: offerbook.sellXmrFor=Bán XMR để: -offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} ngày offerbook.timeSinceSigning.notSigned.noNeed=Không áp dụng @@ -399,7 +394,7 @@ offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due t offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Phương thức thanh toán này tạm thời chỉ được giới hạn đến {0} cho đến {1} vì tất cả các người mua đều có tài khoản mới.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Đề nghị của bạn sẽ bị giới hạn chỉ đối với các người mua có tài khoản đã ký và có tuổi vì nó vượt quá {0}.\n\n{1} @@ -643,7 +638,7 @@ portfolio.pending.step2_buyer.postal=Hãy gửi {0} bằng \"Phiếu chuyển ti # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Vui lòng liên hệ người bán XMR và cung cấp số liên hệ và sắp xếp cuộc hẹn để thanh toán {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Thanh toán bắt đầu sử dụng {0} @@ -675,7 +670,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=Thông tin liên lạc của ng portfolio.pending.step2_seller.waitPayment.msg=Giao dịch đặt cọc có ít nhất một xác nhận blockchain.\nBạn cần phải đợi cho đến khi người mua XMR bắt đầu thanh toán {0}. portfolio.pending.step2_seller.warn=Người mua XMR vẫn chưa thanh toán {0}.\nBạn cần phải đợi cho đến khi người mua bắt đầu thanh toán.\nNếu giao dịch không được hoàn thành vào {1} trọng tài sẽ điều tra. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. -tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. @@ -715,7 +710,7 @@ portfolio.pending.step3_seller.westernUnion=Người mua phải gửi cho bạn portfolio.pending.step3_seller.halCash=Người mua phải gửi mã HalCash cho bạn bằng tin nhắn. Ngoài ra, bạn sẽ nhận được một tin nhắn từ HalCash với thông tin cần thiết để rút EUR từ một máy ATM có hỗ trợ HalCash. \n\nSau khi nhận được tiền từ ATM vui lòng xác nhận lại biên lai thanh toán tại đây! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. -portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Xác nhận đã nhận được thanh toán @@ -737,7 +732,7 @@ portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Bạn đã nhận được thanh toán {0} từ Đối tác giao dịch của bạn?\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Xác nhận rằng bạn đã nhận được thanh toán @@ -811,9 +806,9 @@ portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediat portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} -portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted @@ -979,7 +974,7 @@ support.sellerMaker=Người bán XMR/Người tạo support.buyerTaker=Người mua XMR/Người nhận support.sellerTaker=Người bán XMR/Người nhận -support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=Tin nhắn hệ thống: {0} support.youOpenedTicket=Bạn đã mở yêu cầu hỗ trợ.\n\n{0}\n\nPhiên bản Haveno: {1} support.youOpenedDispute=Bạn đã mở yêu cầu giải quyết tranh chấp.\n\n{0}\n\nPhiên bản Haveno: {1} @@ -987,8 +982,8 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno v support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} -support.mediatorsAddress=Mediator''s node address: {0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} +support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. @@ -1114,19 +1109,19 @@ setting.about.subsystems.label=Các phiên bản của hệ thống con setting.about.subsystems.val=Phiên bản mạng: {0}; Phiên bản tin nhắn P2P: {1}; Phiên bản DB nội bộ: {2}; Phiên bản giao thức giao dịch: {3} setting.about.shortcuts=Short cuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} @@ -1201,7 +1196,7 @@ account.arbitratorRegistration.registerSuccess=You have successfully registered account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=Tài khoản crypto của bạn -account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=Tôi hiểu và xác nhận rằng tôi đã biết loại ví mình cần sử dụng. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. @@ -1317,7 +1312,7 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Loại chào gi account.notifications.marketAlert.message.title=Thông báo chào giá account.notifications.marketAlert.message.msg.below=cao hơn account.notifications.marketAlert.message.msg.above=thấp hơn -account.notifications.marketAlert.message.msg=một ''{0} {1}'' chào giá mới với giá {2} ({3} {4} giá thị trường) và hình thức thanh toán ''{5}''đã được đăng lên danh mục chào giá của Haveno.\nMã chào giá: {6}. +account.notifications.marketAlert.message.msg=một '{0} {1}' chào giá mới với giá {2} ({3} {4} giá thị trường) và hình thức thanh toán '{5}'đã được đăng lên danh mục chào giá của Haveno.\nMã chào giá: {6}. account.notifications.priceAlert.message.title=Thông báo giá cho {0} account.notifications.priceAlert.message.msg=Thông báo giá của bạn đã được kích hoạt. Giá {0} hiện tại là {1} {2} account.notifications.noWebCamFound.warning=Không tìm thấy webcam.\n\nVui lòng sử dụng lựa chọn email để gửi mã bảo mật và khóa mã hóa từ điện thoại di động của bạn tới ứng dùng Haveno. @@ -1682,8 +1677,8 @@ popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} -popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} @@ -1978,8 +1973,8 @@ payment.accountType=Loại tài khoản payment.checking=Đang kiểm tra payment.savings=Tiết kiệm payment.personalId=ID cá nhân -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Khi sử dụng HalCash người mua XMR cần phải gửi cho người bán XMR mã HalCash bằng tin nhắn điện thoại.\n\nVui lòng đảm bảo là lượng tiền này không vượt quá số lượng tối đa mà ngân hàng của bạn cho phép gửi khi dùng HalCash. Số lượng rút tối thiểu là 10 EUR và tối đa là 600 EUR. Nếu rút nhiều lần thì giới hạn sẽ là 3000 EUR/ người nhận/ ngày và 6000 EUR/người nhận/tháng. Vui lòng kiểm tra chéo những giới hạn này với ngân hàng của bạn để chắc chắn là họ cũng dùng những giới hạn như ghi ở đây.\n\nSố tiền rút phải là bội số của 10 EUR vì bạn không thể rút các mệnh giá khác từ ATM. Giao diện người dùng ở phần 'tạo chào giá' và 'chấp nhận chào giá' sẽ điều chỉnh lượng btc sao cho lượng EUR tương ứng sẽ chính xác. Bạn không thể dùng giá thị trường vì lượng EUR có thể sẽ thay đổi khi giá thay đổi.\n\nTrường hợp tranh chấp, người mua XMR cần phải cung cấp bằng chứng chứng minh mình đã gửi EUR. @@ -1991,7 +1986,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade 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=Revolut requires the 'Username' 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 have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Vui lòng lưu ý rằng Cash App có rủi ro bồi hoàn cao hơn so với hầu hết các chuyển khoản ngân hàng. @@ -2025,7 +2020,7 @@ payment.japan.recipient=Tên payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Vì sự bảo vệ của bạn, chúng tôi khuyến cáo không nên sử dụng mã PIN Paysafecard để thanh toán.\n\n\ Các giao dịch được thực hiện bằng mã PIN không thể được xác minh độc lập để giải quyết tranh chấp. Nếu có vấn đề xảy ra, có thể không thể khôi phục số tiền đã mất.\n\n\ Để đảm bảo an toàn giao dịch và có thể giải quyết tranh chấp, hãy luôn sử dụng các phương thức thanh toán có hồ sơ xác minh được. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 7e6b9be4d5..daf103a968 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -644,7 +639,7 @@ portfolio.pending.step2_buyer.postal=请用“美国邮政汇票”发送 {0} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=请通过提供的联系人与 XMR 卖家联系,并安排会议支付 {0}。\n\n portfolio.pending.step2_buyer.startPaymentUsing=使用 {0} 开始付款 @@ -1160,7 +1155,7 @@ setting.about.shortcuts.sendPrivateNotification=发送私人通知到对等点 setting.about.shortcuts.sendPrivateNotification.value=点击交易伙伴头像并按下:{0} 以显示更多信息 setting.info.headline=新 XMR 自动确认功能 -setting.info.msg=当你完成 XMR/XMR 交易时,您可以使用自动确认功能来验证是否向您的钱包中发送了正确数量的 XMR,以便 Haveno 可以自动将交易标记为完成,从而使每个人都可以更快地进行交易。\n\n自动确认使用 XMR 发送方提供的交易密钥在至少 2 个 XMR 区块浏览器节点上检查 XMR 交易。在默认情况下,Haveno 使用由 Haveno 贡献者运行的区块浏览器节点,但是我们建议运行您自己的 XMR 区块浏览器节点以最大程度地保护隐私和安全。\n\n您还可以在``设置''中将每笔交易的最大 XMR 数量设置为自动确认以及所需确认的数量。\n\n在 Haveno Wiki 上查看更多详细信息(包括如何设置自己的区块浏览器节点):https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades +setting.info.msg=当你完成 XMR/XMR 交易时,您可以使用自动确认功能来验证是否向您的钱包中发送了正确数量的 XMR,以便 Haveno 可以自动将交易标记为完成,从而使每个人都可以更快地进行交易。\n\n自动确认使用 XMR 发送方提供的交易密钥在至少 2 个 XMR 区块浏览器节点上检查 XMR 交易。在默认情况下,Haveno 使用由 Haveno 贡献者运行的区块浏览器节点,但是我们建议运行您自己的 XMR 区块浏览器节点以最大程度地保护隐私和安全。\n\n您还可以在``设置'中将每笔交易的最大 XMR 数量设置为自动确认以及所需确认的数量。\n\n在 Haveno Wiki 上查看更多详细信息(包括如何设置自己的区块浏览器节点):https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades #################################################################### # Account #################################################################### @@ -1603,7 +1598,7 @@ error.closedTradeWithUnconfirmedDepositTx=交易 ID 为 {0} 的已关闭交易 error.closedTradeWithNoDepositTx=交易 ID 为 {0} 的保证金交易已被确认。\n\n请重新启动应用程序来清理已关闭的交易列表。 popup.warning.walletNotInitialized=钱包至今未初始化 -popup.warning.osxKeyLoggerWarning=由于 MacOS 10.14 及更高版本中的安全措施更加严格,因此启动 Java 应用程序(Haveno 使用Java)会在 MacOS 中引发弹出警告(``Haveno 希望从任何应用程序接收击键'').\n\n为了避免该问题,请打开“ MacOS 设置”,然后转到“安全和隐私”->“隐私”->“输入监视”,然后从右侧列表中删除“ Haveno”。\n\n一旦解决了技术限制(所需的 Java 版本的 Java 打包程序尚未交付),Haveno将升级到新的 Java 版本,以避免该问题。 +popup.warning.osxKeyLoggerWarning=由于 MacOS 10.14 及更高版本中的安全措施更加严格,因此启动 Java 应用程序(Haveno 使用Java)会在 MacOS 中引发弹出警告(``Haveno 希望从任何应用程序接收击键').\n\n为了避免该问题,请打开“ MacOS 设置”,然后转到“安全和隐私”->“隐私”->“输入监视”,然后从右侧列表中删除“ Haveno”。\n\n一旦解决了技术限制(所需的 Java 版本的 Java 打包程序尚未交付),Haveno将升级到新的 Java 版本,以避免该问题。 popup.warning.wrongVersion=您这台电脑上可能有错误的 Haveno 版本。\n您的电脑的架构是:{0}\n您安装的 Haveno 二进制文件是:{1}\n请关闭并重新安装正确的版本({2})。 popup.warning.incompatibleDB=我们检测到不兼容的数据库文件!\n\n那些数据库文件与我们当前的代码库不兼容:\n{0}\n\n我们对损坏的文件进行了备份,并将默认值应用于新的数据库版本。\n\n备份位于:\n{1}/db/backup_of_corrupted_data。\n\n请检查您是否安装了最新版本的 Haveno\n您可以下载:\nhttps://haveno.exchange/downloads\n\n请重新启动应用程序。 popup.warning.startupFailed.twoInstances=Haveno 已经在运行。 您不能运行两个 Haveno 实例。 @@ -1998,7 +1993,7 @@ payment.limits.info.withSigning=为了降低这一风险,Haveno 基于两个 payment.cashDeposit.info=请确认您的银行允许您将现金存款汇入他人账户。例如,美国银行和富国银行不再允许此类存款。 payment.revolut.info=Revolut 要求使用“用户名”作为帐户 ID,而不是像以往的电话号码或电子邮件。 -payment.account.revolut.addUserNameInfo={0}\n您现有的 Revolut 帐户({1})尚未设置“用户名”。\n请输入您的 Revolut ``用户名''以更新您的帐户数据。\n这不会影响您的账龄验证状态。 +payment.account.revolut.addUserNameInfo={0}\n您现有的 Revolut 帐户({1})尚未设置“用户名”。\n请输入您的 Revolut ``用户名'以更新您的帐户数据。\n这不会影响您的账龄验证状态。 payment.revolut.addUserNameInfo.headLine=更新 Revolut 账户 payment.cashapp.info=请注意,Cash App 的退款风险高于大多数银行转账。 @@ -2035,7 +2030,7 @@ payment.japan.recipient=名称 payment.australia.payid=PayID payment.payid=PayID 需链接至金融机构。例如电子邮件地址或手机。 payment.payid.info=PayID,如电话号码、电子邮件地址或澳大利亚商业号码(ABN),您可以安全地连接到您的银行、信用合作社或建立社会帐户。你需要在你的澳大利亚金融机构创建一个 PayID。发送和接收金融机构都必须支持 PayID。更多信息请查看[HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=为了保障您的安全,我们强烈不建议使用 Paysafecard PIN 进行支付。\n\n\ 通过 PIN 进行的交易无法被独立验证以解决争议。如果出现问题,资金可能无法追回。\n\n\ 为确保交易安全并支持争议解决,请始终使用提供可验证记录的支付方式。 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index a1694e3999..a816482877 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -644,7 +639,7 @@ portfolio.pending.step2_buyer.postal=請用“美國郵政匯票”發送 {0} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=請通過提供的聯繫人與 XMR 賣家聯繫,並安排會議支付 {0}。\n\n portfolio.pending.step2_buyer.startPaymentUsing=使用 {0} 開始付款 @@ -1160,7 +1155,7 @@ setting.about.shortcuts.sendPrivateNotification=發送私人通知到對等點 setting.about.shortcuts.sendPrivateNotification.value=點擊交易夥伴頭像並按下:{0} 以顯示更多信息 setting.info.headline=新 XMR 自動確認功能 -setting.info.msg=當你完成 XMR/XMR 交易時,您可以使用自動確認功能來驗證是否向您的錢包中發送了正確數量的 XMR,以便 Haveno 可以自動將交易標記為完成,從而使每個人都可以更快地進行交易。\n\n自動確認使用 XMR 發送方提供的交易密鑰在至少 2 個 XMR 區塊瀏覽器節點上檢查 XMR 交易。在默認情況下,Haveno 使用由 Haveno 貢獻者運行的區塊瀏覽器節點,但是我們建議運行您自己的 XMR 區塊瀏覽器節點以最大程度地保護隱私和安全。\n\n您還可以在``設置''中將每筆交易的最大 XMR 數量設置為自動確認以及所需確認的數量。\n\n在 Haveno Wiki 上查看更多詳細信息(包括如何設置自己的區塊瀏覽器節點):https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades +setting.info.msg=當你完成 XMR/XMR 交易時,您可以使用自動確認功能來驗證是否向您的錢包中發送了正確數量的 XMR,以便 Haveno 可以自動將交易標記為完成,從而使每個人都可以更快地進行交易。\n\n自動確認使用 XMR 發送方提供的交易密鑰在至少 2 個 XMR 區塊瀏覽器節點上檢查 XMR 交易。在默認情況下,Haveno 使用由 Haveno 貢獻者運行的區塊瀏覽器節點,但是我們建議運行您自己的 XMR 區塊瀏覽器節點以最大程度地保護隱私和安全。\n\n您還可以在``設置'中將每筆交易的最大 XMR 數量設置為自動確認以及所需確認的數量。\n\n在 Haveno Wiki 上查看更多詳細信息(包括如何設置自己的區塊瀏覽器節點):https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades #################################################################### # Account #################################################################### @@ -1601,7 +1596,7 @@ error.closedTradeWithUnconfirmedDepositTx=交易 ID 為 {0} 的已關閉交易 error.closedTradeWithNoDepositTx=交易 ID 為 {0} 的保證金交易已被確認。\n\n請重新啟動應用程序來清理已關閉的交易列表。 popup.warning.walletNotInitialized=錢包至今未初始化 -popup.warning.osxKeyLoggerWarning=由於 MacOS 10.14 及更高版本中的安全措施更加嚴格,因此啟動 Java 應用程序(Haveno 使用Java)會在 MacOS 中引發彈出警吿(``Haveno 希望從任何應用程序接收擊鍵'').\n\n為了避免該問題,請打開“ MacOS 設置”,然後轉到“安全和隱私”->“隱私”->“輸入監視”,然後從右側列表中刪除“ Haveno”。\n\n一旦解決了技術限制(所需的 Java 版本的 Java 打包程序尚未交付),Haveno將升級到新的 Java 版本,以避免該問題。 +popup.warning.osxKeyLoggerWarning=由於 MacOS 10.14 及更高版本中的安全措施更加嚴格,因此啟動 Java 應用程序(Haveno 使用Java)會在 MacOS 中引發彈出警吿(``Haveno 希望從任何應用程序接收擊鍵').\n\n為了避免該問題,請打開“ MacOS 設置”,然後轉到“安全和隱私”->“隱私”->“輸入監視”,然後從右側列表中刪除“ Haveno”。\n\n一旦解決了技術限制(所需的 Java 版本的 Java 打包程序尚未交付),Haveno將升級到新的 Java 版本,以避免該問題。 popup.warning.wrongVersion=您這台電腦上可能有錯誤的 Haveno 版本。\n您的電腦的架構是:{0}\n您安裝的 Haveno 二進制文件是:{1}\n請關閉並重新安裝正確的版本({2})。 popup.warning.incompatibleDB=我們檢測到不兼容的數據庫文件!\n\n那些數據庫文件與我們當前的代碼庫不兼容:\n{0}\n\n我們對損壞的文件進行了備份,並將默認值應用於新的數據庫版本。\n\n備份位於:\n{1}/db/backup_of_corrupted_data。\n\n請檢查您是否安裝了最新版本的 Haveno\n您可以下載:\nhttps://haveno.exchange/downloads\n\n請重新啟動應用程序。 popup.warning.startupFailed.twoInstances=Haveno 已經在運行。 您不能運行兩個 Haveno 實例。 @@ -1992,7 +1987,7 @@ payment.limits.info.withSigning=為了降低這一風險,Haveno 基於兩個 payment.cashDeposit.info=請確認您的銀行允許您將現金存款匯入他人賬户。例如,美國銀行和富國銀行不再允許此類存款。 payment.revolut.info=Revolut 要求使用“用户名”作為帳户 ID,而不是像以往的電話號碼或電子郵件。 -payment.account.revolut.addUserNameInfo={0}\n您現有的 Revolut 帳户({1})尚未設置“用户名”。\n請輸入您的 Revolut ``用户名''以更新您的帳户數據。\n這不會影響您的賬齡驗證狀態。 +payment.account.revolut.addUserNameInfo={0}\n您現有的 Revolut 帳户({1})尚未設置“用户名”。\n請輸入您的 Revolut ``用户名'以更新您的帳户數據。\n這不會影響您的賬齡驗證狀態。 payment.revolut.addUserNameInfo.headLine=更新 Revolut 賬户 payment.cashapp.info=請注意,Cash App 的退款風險高於大多數銀行轉帳。 @@ -2029,7 +2024,7 @@ payment.japan.recipient=名稱 payment.australia.payid=PayID payment.payid=PayID 需鏈接至金融機構。例如電子郵件地址或手機。 payment.payid.info=PayID,如電話號碼、電子郵件地址或澳大利亞商業號碼(ABN),您可以安全地連接到您的銀行、信用合作社或建立社會帳户。你需要在你的澳大利亞金融機構創建一個 PayID。發送和接收金融機構都必須支持 PayID。更多信息請查看[HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=為了保護您的安全,我們強烈不建議使用 Paysafecard PIN 進行付款。\n\n\ 透過 PIN 進行的交易無法獨立驗證以進行爭議解決。如果發生問題,可能無法追回資金。\n\n\ 為確保交易安全並支持爭議解決,請始終使用可驗證記錄的付款方式。 From 5141574d7082a2454974653f97f317ca7d93627e Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 22 Jun 2025 08:38:43 -0400 Subject: [PATCH 321/371] fix error on export table columns --- .../haveno/desktop/main/funds/locked/LockedView.java | 2 +- .../desktop/main/funds/reserved/ReservedView.java | 2 +- .../main/funds/transactions/TransactionsView.java | 2 +- .../desktop/main/market/trades/TradesChartsView.java | 2 +- desktop/src/main/java/haveno/desktop/util/GUIUtil.java | 10 ++++++++++ 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java b/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java index 5570ca1bef..8cb80b16f7 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java @@ -165,7 +165,7 @@ public class LockedView extends ActivatableView { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { - ObservableList> tableColumns = tableView.getColumns(); + ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); int reportColumns = tableColumns.size(); CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; diff --git a/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java b/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java index 3ed2dfea85..e71fae8d89 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java @@ -165,7 +165,7 @@ public class ReservedView extends ActivatableView { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { - ObservableList> tableColumns = tableView.getColumns(); + ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); int reportColumns = tableColumns.size(); CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java index 66434a8663..eb97c325ab 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java @@ -203,7 +203,7 @@ public class TransactionsView extends ActivatableView { numItems.setText(Res.get("shared.numItemsLabel", sortedDisplayedTransactions.size())); exportButton.setOnAction(event -> { - final ObservableList> tableColumns = tableView.getColumns(); + final ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); final int reportColumns = tableColumns.size() - 1; // CSV report excludes the last column (an icon) CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; diff --git a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java index 7470cf1c4b..bb75c80684 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java @@ -397,7 +397,7 @@ public class TradesChartsView extends ActivatableViewAndModel> tableColumns = tableView.getColumns(); + ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); int reportColumns = tableColumns.size() + 1; boolean showAllTradeCurrencies = model.showAllTradeCurrenciesProperty.get(); diff --git a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java index 3097780c1a..4aad28f464 100644 --- a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java @@ -1281,6 +1281,16 @@ public class GUIUtil { } } + public static ObservableList> getContentColumns(TableView tableView) { + ObservableList> contentColumns = FXCollections.observableArrayList(); + for (TableColumn column : tableView.getColumns()) { + if (!column.getStyleClass().contains("first-column") && !column.getStyleClass().contains("last-column")) { + contentColumns.add(column); + } + } + return contentColumns; + } + public static ImageView getCurrencyIcon(String currencyCode) { return getCurrencyIcon(currencyCode, 24); } From b289a256eeef0bade8bdb8a237471c9ce2f6e62c Mon Sep 17 00:00:00 2001 From: jermanuts <109705802+jermanuts@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:42:14 +0200 Subject: [PATCH 322/371] fix donation permalink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9469a16e2f..ab232aafa7 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ See the [developer guide](docs/developer-guide.md) to get started developing for See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for our styling guides. -If you are not able to contribute code and want to contribute development resources, [donations](#support-and-sponsorships) fund development bounties. +If you are not able to contribute code and want to contribute development resources, [donations](#support) fund development bounties. ## Bounties From 8cca2cbb52379973840ed806ab4fd781a9e337c0 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 28 Jun 2025 10:17:38 -0400 Subject: [PATCH 323/371] do not color currency code in offer book volume column --- .../desktop/main/offer/offerbook/OfferBookView.java | 5 +++-- .../main/offer/offerbook/OfferBookViewModel.java | 10 +++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index e3ed68ccf1..7cb2bc7bdb 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -946,8 +946,9 @@ abstract public class OfferBookView Date: Sun, 29 Jun 2025 13:49:12 -0400 Subject: [PATCH 324/371] fix average calculation in trade charts view --- .../desktop/main/market/trades/ChartCalculations.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java index c8ba1af3af..39b67d1942 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java +++ b/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java @@ -76,7 +76,7 @@ public class ChartCalculations { dateMapsPerTickUnit.forEach((tick, map) -> { HashMap priceMap = new HashMap<>(); - map.forEach((date, tradeStatisticsList) -> priceMap.put(date, getAveragePrice(tradeStatisticsList))); + map.forEach((date, tradeStatisticsList) -> priceMap.put(date, getAverageTraditionalPrice(tradeStatisticsList))); usdAveragePriceMapsPerTickUnit.put(tick, priceMap); }); return usdAveragePriceMapsPerTickUnit; @@ -210,7 +210,7 @@ public class ChartCalculations { return roundToTick(time.toInstant().atZone(ChartCalculations.ZONE_ID).toLocalDateTime(), tickUnit); } - private static long getAveragePrice(List tradeStatisticsList) { + private static long getAverageTraditionalPrice(List tradeStatisticsList) { long accumulatedAmount = 0; // TODO: use BigInteger long accumulatedVolume = 0; for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { @@ -262,7 +262,7 @@ public class ChartCalculations { boolean isBullish; if (CurrencyUtil.isCryptoCurrency(currencyCode)) { isBullish = close < open; - double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, 4 + CryptoMoney.SMALLEST_UNIT_EXPONENT); + double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, CryptoMoney.SMALLEST_UNIT_EXPONENT - 4); averagePrice = MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / accumulatedVolume); } else { isBullish = close > open; From 33c9628df343f134ea700356dcdd7e3c902fdb14 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 29 Jun 2025 15:10:10 -0400 Subject: [PATCH 325/371] use BigInteger for average chart calculations --- .../java/haveno/common/util/MathUtils.java | 11 ++++++ .../main/market/trades/ChartCalculations.java | 36 ++++++++++--------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/common/src/main/java/haveno/common/util/MathUtils.java b/common/src/main/java/haveno/common/util/MathUtils.java index 25c91ed254..a89e4bc01e 100644 --- a/common/src/main/java/haveno/common/util/MathUtils.java +++ b/common/src/main/java/haveno/common/util/MathUtils.java @@ -22,6 +22,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigDecimal; +import java.math.BigInteger; import java.math.RoundingMode; import java.util.ArrayDeque; import java.util.Deque; @@ -85,6 +86,11 @@ public class MathUtils { return ((double) value) * factor; } + public static BigInteger scaleUpByPowerOf10(BigInteger value, int exponent) { + BigInteger factor = BigInteger.TEN.pow(exponent); + return value.multiply(factor); + } + public static double scaleDownByPowerOf10(double value, int exponent) { double factor = Math.pow(10, exponent); return value / factor; @@ -95,6 +101,11 @@ public class MathUtils { return ((double) value) / factor; } + public static BigInteger scaleDownByPowerOf10(BigInteger value, int exponent) { + BigInteger factor = BigInteger.TEN.pow(exponent); + return value.divide(factor); + } + public static double exactMultiply(double value1, double value2) { return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue(); } diff --git a/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java index 39b67d1942..0863ab28be 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java +++ b/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java @@ -22,13 +22,16 @@ import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.TraditionalMoney; +import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatistics3; import haveno.desktop.main.market.trades.charts.CandleData; import haveno.desktop.util.DisplayUtils; import javafx.scene.chart.XYChart; import javafx.util.Pair; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import java.math.BigInteger; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; @@ -47,6 +50,7 @@ import java.util.stream.Collectors; import static haveno.desktop.main.market.trades.TradesChartsViewModel.MAX_TICKS; +@Slf4j public class ChartCalculations { static final ZoneId ZONE_ID = ZoneId.systemDefault(); @@ -211,15 +215,15 @@ public class ChartCalculations { } private static long getAverageTraditionalPrice(List tradeStatisticsList) { - long accumulatedAmount = 0; // TODO: use BigInteger - long accumulatedVolume = 0; + BigInteger accumulatedAmount = BigInteger.ZERO; + BigInteger accumulatedVolume = BigInteger.ZERO; for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { - accumulatedAmount += tradeStatistics.getAmount(); - accumulatedVolume += tradeStatistics.getTradeVolume().getValue(); + accumulatedAmount = accumulatedAmount.add(BigInteger.valueOf(tradeStatistics.getAmount())); + accumulatedVolume = accumulatedVolume.add(BigInteger.valueOf(tradeStatistics.getTradeVolume().getValue())); } - double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, 4 + TraditionalMoney.SMALLEST_UNIT_EXPONENT); - return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / accumulatedAmount); + BigInteger accumulatedVolumeAsBI = MathUtils.scaleUpByPowerOf10(accumulatedVolume, TraditionalMoney.SMALLEST_UNIT_EXPONENT + 4); + return MathUtils.roundDoubleToLong(HavenoUtils.divide(accumulatedVolumeAsBI, accumulatedAmount)); } @VisibleForTesting @@ -232,8 +236,8 @@ public class ChartCalculations { long close = 0; long high = 0; long low = 0; - long accumulatedVolume = 0; // TODO: use BigInteger - long accumulatedAmount = 0; + BigInteger accumulatedVolume = BigInteger.ZERO; + BigInteger accumulatedAmount = BigInteger.ZERO; long numTrades = set.size(); List tradePrices = new ArrayList<>(); for (TradeStatistics3 item : set) { @@ -242,8 +246,8 @@ public class ChartCalculations { low = (low != 0) ? Math.min(low, tradePriceAsLong) : tradePriceAsLong; high = (high != 0) ? Math.max(high, tradePriceAsLong) : tradePriceAsLong; - accumulatedVolume += item.getTradeVolume().getValue(); - accumulatedAmount += item.getTradeAmount().longValueExact(); + accumulatedVolume = accumulatedVolume.add(BigInteger.valueOf(item.getTradeVolume().getValue())); + accumulatedAmount = accumulatedAmount.add(item.getTradeAmount()); tradePrices.add(tradePriceAsLong); } Collections.sort(tradePrices); @@ -262,12 +266,12 @@ public class ChartCalculations { boolean isBullish; if (CurrencyUtil.isCryptoCurrency(currencyCode)) { isBullish = close < open; - double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, CryptoMoney.SMALLEST_UNIT_EXPONENT - 4); - averagePrice = MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / accumulatedVolume); + BigInteger accumulatedAmountAsBI = MathUtils.scaleUpByPowerOf10(accumulatedAmount, CryptoMoney.SMALLEST_UNIT_EXPONENT - 4); + averagePrice = MathUtils.roundDoubleToLong(HavenoUtils.divide(accumulatedAmountAsBI, accumulatedVolume)); } else { isBullish = close > open; - double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, 4 + TraditionalMoney.SMALLEST_UNIT_EXPONENT); - averagePrice = MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / accumulatedAmount); + BigInteger accumulatedVolumeAsBI = MathUtils.scaleUpByPowerOf10(accumulatedVolume, TraditionalMoney.SMALLEST_UNIT_EXPONENT + 4); + averagePrice = MathUtils.roundDoubleToLong(HavenoUtils.divide(accumulatedVolumeAsBI, accumulatedAmount)); } Date dateFrom = new Date(getTimeFromTickIndex(tick, itemsPerInterval)); @@ -278,10 +282,10 @@ public class ChartCalculations { // We do not need precision, so we scale down before multiplication otherwise we could get an overflow. averageUsdPrice = (long) MathUtils.scaleDownByPowerOf10((double) averageUsdPrice, TraditionalMoney.SMALLEST_UNIT_EXPONENT); - long volumeInUsd = averageUsdPrice * (long) MathUtils.scaleDownByPowerOf10((double) accumulatedAmount, 4); + long volumeInUsd = averageUsdPrice * MathUtils.scaleDownByPowerOf10(accumulatedAmount, 4).longValue(); // We store USD value without decimals as its only total volume, no precision is needed. volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, TraditionalMoney.SMALLEST_UNIT_EXPONENT); - return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume, + return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount.longValueExact(), accumulatedVolume.longValueExact(), numTrades, isBullish, dateString, volumeInUsd); } From e3e0256f7ac575bb86227ef340328b0d582bc5b5 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 19 Jun 2025 09:15:12 -0400 Subject: [PATCH 326/371] support wechat pay grpc api --- .../java/haveno/core/api/model/PaymentAccountForm.java | 3 ++- .../main/java/haveno/core/payment/WeChatPayAccount.java | 9 ++++++++- .../java/haveno/core/payment/payload/PaymentMethod.java | 3 ++- proto/src/main/proto/pb.proto | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java index 6b1b494047..239f17abd4 100644 --- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java @@ -78,7 +78,8 @@ public final class PaymentAccountForm implements PersistablePayload { CASH_APP, PAYPAL, VENMO, - PAYSAFE; + PAYSAFE, + WECHAT_PAY; public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); diff --git a/core/src/main/java/haveno/core/payment/WeChatPayAccount.java b/core/src/main/java/haveno/core/payment/WeChatPayAccount.java index 297968ef0c..9071aa7aea 100644 --- a/core/src/main/java/haveno/core/payment/WeChatPayAccount.java +++ b/core/src/main/java/haveno/core/payment/WeChatPayAccount.java @@ -38,6 +38,13 @@ public final class WeChatPayAccount extends PaymentAccount { new TraditionalCurrency("GBP") ); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.ACCOUNT_NR, + PaymentAccountFormField.FieldId.TRADE_CURRENCIES, + PaymentAccountFormField.FieldId.SALT + ); + public WeChatPayAccount() { super(PaymentMethod.WECHAT_PAY); } @@ -54,7 +61,7 @@ public final class WeChatPayAccount extends PaymentAccount { @Override public @NonNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public void setAccountNr(String accountNr) { diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index 4a8246fe78..e3830d8ddb 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -369,7 +369,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 022031d08d..45fe43415d 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1903,6 +1903,7 @@ message PaymentAccountForm { PAYPAL = 17; VENMO = 18; PAYSAFE = 19; + WECHAT_PAY = 20; } FormId id = 1; repeated PaymentAccountFormField fields = 2; From bd40067deb2be29fad37b55ca0b821bdd2d59e37 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 19 Jun 2025 09:24:26 -0400 Subject: [PATCH 327/371] support alipay grpc api --- .../java/haveno/core/api/model/PaymentAccountForm.java | 3 ++- .../src/main/java/haveno/core/payment/AliPayAccount.java | 9 ++++++++- .../java/haveno/core/payment/payload/PaymentMethod.java | 3 ++- proto/src/main/proto/pb.proto | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java index 239f17abd4..d13a9b4300 100644 --- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java @@ -79,7 +79,8 @@ public final class PaymentAccountForm implements PersistablePayload { PAYPAL, VENMO, PAYSAFE, - WECHAT_PAY; + WECHAT_PAY, + ALI_PAY; public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); diff --git a/core/src/main/java/haveno/core/payment/AliPayAccount.java b/core/src/main/java/haveno/core/payment/AliPayAccount.java index 1bff92b5cd..af2f617313 100644 --- a/core/src/main/java/haveno/core/payment/AliPayAccount.java +++ b/core/src/main/java/haveno/core/payment/AliPayAccount.java @@ -60,6 +60,13 @@ public final class AliPayAccount extends PaymentAccount { new TraditionalCurrency("ZAR") ); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.ACCOUNT_NR, + PaymentAccountFormField.FieldId.TRADE_CURRENCIES, + PaymentAccountFormField.FieldId.SALT + ); + public AliPayAccount() { super(PaymentMethod.ALI_PAY); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); @@ -77,7 +84,7 @@ public final class AliPayAccount extends PaymentAccount { @Override public @NonNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public void setAccountNr(String accountNr) { diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index e3830d8ddb..d4a67ef9e1 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -370,7 +370,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 45fe43415d..94ffb0447e 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1904,6 +1904,7 @@ message PaymentAccountForm { VENMO = 18; PAYSAFE = 19; WECHAT_PAY = 20; + ALI_PAY = 21; } FormId id = 1; repeated PaymentAccountFormField fields = 2; From 59e509d3e3cb47ae097dab4fecfa597d317348e4 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:05:24 -0400 Subject: [PATCH 328/371] support swish payment account over grpc api --- .../core/api/model/PaymentAccountForm.java | 3 ++- .../haveno/core/payment/PaymentAccount.java | 4 +++- .../haveno/core/payment/SwishAccount.java | 23 ++++++++++++++++++- .../core/payment/payload/PaymentMethod.java | 3 ++- proto/src/main/proto/pb.proto | 1 + 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java index d13a9b4300..bd23e18ee7 100644 --- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java @@ -80,7 +80,8 @@ public final class PaymentAccountForm implements PersistablePayload { VENMO, PAYSAFE, WECHAT_PAY, - ALI_PAY; + ALI_PAY, + SWISH; public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); diff --git a/core/src/main/java/haveno/core/payment/PaymentAccount.java b/core/src/main/java/haveno/core/payment/PaymentAccount.java index 14dd88482b..61a332336d 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccount.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccount.java @@ -755,7 +755,9 @@ public abstract class PaymentAccount implements PersistablePayload { field.setLabel(Res.get("payment.swift.swiftCode.intermediary")); break; case MOBILE_NR: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.TEXT); + field.setLabel(Res.get("payment.mobile")); + break; case NATIONAL_ACCOUNT_ID: throw new IllegalArgumentException("Not implemented"); case PAYID: diff --git a/core/src/main/java/haveno/core/payment/SwishAccount.java b/core/src/main/java/haveno/core/payment/SwishAccount.java index a726a9a14a..eb2e10de87 100644 --- a/core/src/main/java/haveno/core/payment/SwishAccount.java +++ b/core/src/main/java/haveno/core/payment/SwishAccount.java @@ -17,12 +17,14 @@ package haveno.core.payment; +import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SwishAccountPayload; +import haveno.core.payment.validation.SwishValidator; import lombok.EqualsAndHashCode; import lombok.NonNull; @@ -33,6 +35,13 @@ public final class SwishAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("SEK")); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.MOBILE_NR, + PaymentAccountFormField.FieldId.HOLDER_NAME, + PaymentAccountFormField.FieldId.SALT + ); + public SwishAccount() { super(PaymentMethod.SWISH); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); @@ -50,7 +59,7 @@ public final class SwishAccount extends PaymentAccount { @Override public @NonNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public void setMobileNr(String mobileNr) { @@ -68,4 +77,16 @@ public final class SwishAccount extends PaymentAccount { public String getHolderName() { return ((SwishAccountPayload) paymentAccountPayload).getHolderName(); } + + @Override + public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { + switch (fieldId) { + case MOBILE_NR: + processValidationResult(new SwishValidator().validate(value)); + break; + default: + super.validateFormField(form, fieldId, value); + break; + } + } } diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index d4a67ef9e1..d3c05d5d48 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -371,7 +371,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 94ffb0447e..2df4245b71 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1905,6 +1905,7 @@ message PaymentAccountForm { PAYSAFE = 19; WECHAT_PAY = 20; ALI_PAY = 21; + SWISH = 22; } FormId id = 1; repeated PaymentAccountFormField fields = 2; From 3e9a55d7841c8990f11ad47d5fcdeb5ddfdd9b2c Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 20 Jun 2025 08:01:12 -0400 Subject: [PATCH 329/371] support wise usd payment account over grpc api --- .../core/api/model/PaymentAccountForm.java | 3 ++- .../haveno/core/payment/PaymentAccount.java | 11 +++++---- .../core/payment/TransferwiseUsdAccount.java | 23 ++++++++++++++++--- .../core/payment/payload/PaymentMethod.java | 3 ++- .../TransferwiseUsdAccountPayload.java | 10 ++++---- .../paymentmethods/TransferwiseUsdForm.java | 4 ++-- proto/src/main/proto/pb.proto | 3 ++- 7 files changed, 40 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java index bd23e18ee7..37da866031 100644 --- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java @@ -81,7 +81,8 @@ public final class PaymentAccountForm implements PersistablePayload { PAYSAFE, WECHAT_PAY, ALI_PAY, - SWISH; + SWISH, + TRANSFERWISE_USD; public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); diff --git a/core/src/main/java/haveno/core/payment/PaymentAccount.java b/core/src/main/java/haveno/core/payment/PaymentAccount.java index 61a332336d..0f32918fed 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccount.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccount.java @@ -518,7 +518,8 @@ public abstract class PaymentAccount implements PersistablePayload { case EXTRA_INFO: break; case HOLDER_ADDRESS: - throw new IllegalArgumentException("Not implemented"); + processValidationResult(new LengthValidator(0, 100).validate(value)); + break; case HOLDER_EMAIL: throw new IllegalArgumentException("Not implemented"); case HOLDER_NAME: @@ -668,11 +669,11 @@ public abstract class PaymentAccount implements PersistablePayload { break; case BENEFICIARY_ACCOUNT_NR: field.setComponent(PaymentAccountFormField.Component.TEXT); - field.setLabel(Res.get("payment.swift.account")); + field.setLabel(Res.get("payment.swift.account")); // TODO: this is specific to swift break; case BENEFICIARY_ADDRESS: field.setComponent(PaymentAccountFormField.Component.TEXTAREA); - field.setLabel(Res.get("payment.swift.address.beneficiary")); + field.setLabel(Res.get("payment.swift.address.beneficiary")); // TODO: this is specific to swift break; case BENEFICIARY_CITY: field.setComponent(PaymentAccountFormField.Component.TEXT); @@ -717,7 +718,9 @@ public abstract class PaymentAccount implements PersistablePayload { field.setLabel(Res.get("payment.shared.optionalExtra")); break; case HOLDER_ADDRESS: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.TEXTAREA); + field.setLabel(Res.get("payment.account.owner.address")); + break; case HOLDER_EMAIL: throw new IllegalArgumentException("Not implemented"); case HOLDER_NAME: diff --git a/core/src/main/java/haveno/core/payment/TransferwiseUsdAccount.java b/core/src/main/java/haveno/core/payment/TransferwiseUsdAccount.java index 94491ddbf0..2702ffbaca 100644 --- a/core/src/main/java/haveno/core/payment/TransferwiseUsdAccount.java +++ b/core/src/main/java/haveno/core/payment/TransferwiseUsdAccount.java @@ -19,6 +19,7 @@ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; +import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; @@ -33,6 +34,15 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.EMAIL, + PaymentAccountFormField.FieldId.HOLDER_NAME, + PaymentAccountFormField.FieldId.HOLDER_ADDRESS, + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.COUNTRY, + PaymentAccountFormField.FieldId.SALT + ); + public TransferwiseUsdAccount() { super(PaymentMethod.TRANSFERWISE_USD); // this payment method is currently restricted to United States/USD @@ -61,11 +71,11 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount { } public void setBeneficiaryAddress(String address) { - ((TransferwiseUsdAccountPayload) paymentAccountPayload).setBeneficiaryAddress(address); + ((TransferwiseUsdAccountPayload) paymentAccountPayload).setHolderAddress(address); } public String getBeneficiaryAddress() { - return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getBeneficiaryAddress(); + return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderAddress(); } @Override @@ -90,6 +100,13 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount { @Override public @NotNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; + } + + @Override + protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { + var field = super.getEmptyFormField(fieldId); + if (field.getId() == PaymentAccountFormField.FieldId.HOLDER_ADDRESS) field.setLabel(field.getLabel() + " " + Res.get("payment.transferwiseUsd.address")); + return field; } } diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index d3c05d5d48..f48aeed3c9 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -372,7 +372,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } diff --git a/core/src/main/java/haveno/core/payment/payload/TransferwiseUsdAccountPayload.java b/core/src/main/java/haveno/core/payment/payload/TransferwiseUsdAccountPayload.java index 1daba610db..bdc3243a30 100644 --- a/core/src/main/java/haveno/core/payment/payload/TransferwiseUsdAccountPayload.java +++ b/core/src/main/java/haveno/core/payment/payload/TransferwiseUsdAccountPayload.java @@ -39,7 +39,7 @@ import java.util.Map; public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAccountPayload { private String email = ""; private String holderName = ""; - private String beneficiaryAddress = ""; + private String holderAddress = ""; public TransferwiseUsdAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); @@ -51,7 +51,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco List acceptedCountryCodes, String email, String holderName, - String beneficiaryAddress, + String holderAddress, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, @@ -63,7 +63,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco this.email = email; this.holderName = holderName; - this.beneficiaryAddress = beneficiaryAddress; + this.holderAddress = holderAddress; } @Override @@ -71,7 +71,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco protobuf.TransferwiseUsdAccountPayload.Builder builder = protobuf.TransferwiseUsdAccountPayload.newBuilder() .setEmail(email) .setHolderName(holderName) - .setBeneficiaryAddress(beneficiaryAddress); + .setHolderAddress(holderAddress); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setTransferwiseUsdAccountPayload(builder); @@ -89,7 +89,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), accountPayloadPB.getEmail(), accountPayloadPB.getHolderName(), - accountPayloadPB.getBeneficiaryAddress(), + accountPayloadPB.getHolderAddress(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/TransferwiseUsdForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/TransferwiseUsdForm.java index 922b5aec75..6ed90bfebf 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/TransferwiseUsdForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/TransferwiseUsdForm.java @@ -57,7 +57,7 @@ public class TransferwiseUsdForm extends PaymentMethodForm { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 1, Res.get("payment.email"), ((TransferwiseUsdAccountPayload) paymentAccountPayload).getEmail()); - String address = ((TransferwiseUsdAccountPayload) paymentAccountPayload).getBeneficiaryAddress(); + String address = ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderAddress(); if (address.length() > 0) { TextArea textAddress = addCompactTopLabelTextArea(gridPane, gridRow, 0, Res.get("payment.account.address"), "").second; textAddress.setMinHeight(70); @@ -96,7 +96,7 @@ public class TransferwiseUsdForm extends PaymentMethodForm { updateFromInputs(); }); - String addressLabel = Res.get("payment.account.owner.address") + Res.get("payment.transferwiseUsd.address"); + String addressLabel = Res.get("payment.account.owner.address") + " " + Res.get("payment.transferwiseUsd.address"); TextArea addressTextArea = addTopLabelTextArea(gridPane, ++gridRow, addressLabel, addressLabel).second; addressTextArea.setMinHeight(70); addressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 2df4245b71..eb82fdf203 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1163,7 +1163,7 @@ message TransferwiseAccountPayload { message TransferwiseUsdAccountPayload { string email = 1; string holder_name = 2; - string beneficiary_address = 3; + string holder_address = 3; } message PayseraAccountPayload { @@ -1906,6 +1906,7 @@ message PaymentAccountForm { WECHAT_PAY = 20; ALI_PAY = 21; SWISH = 22; + TRANSFERWISE_USD = 23; } FormId id = 1; repeated PaymentAccountFormField fields = 2; From d19610b93a7048ea36c1227b6a66823841904ee2 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:57:11 -0400 Subject: [PATCH 330/371] support amazon e-gift card payment account over grpc api --- .../core/api/model/PaymentAccountForm.java | 3 ++- .../core/payment/AmazonGiftCardAccount.java | 17 ++++++++++++++++- .../core/payment/PaymentAccountTypeAdapter.java | 3 +++ .../core/payment/payload/PaymentMethod.java | 3 ++- .../paymentmethods/AmazonGiftCardForm.java | 2 +- proto/src/main/proto/pb.proto | 1 + 6 files changed, 25 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java index 37da866031..d280ced31f 100644 --- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java @@ -82,7 +82,8 @@ public final class PaymentAccountForm implements PersistablePayload { WECHAT_PAY, ALI_PAY, SWISH, - TRANSFERWISE_USD; + TRANSFERWISE_USD, + AMAZON_GIFT_CARD; public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); diff --git a/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java b/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java index cb3eb35c70..65cf5719ae 100644 --- a/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java +++ b/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java @@ -46,6 +46,14 @@ public final class AmazonGiftCardAccount extends PaymentAccount { new TraditionalCurrency("USD") ); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR, + PaymentAccountFormField.FieldId.COUNTRY, + PaymentAccountFormField.FieldId.TRADE_CURRENCIES, + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.SALT + ); + @Nullable private Country country; @@ -65,7 +73,7 @@ public final class AmazonGiftCardAccount extends PaymentAccount { @Override public @NotNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public String getEmailOrMobileNr() { @@ -97,4 +105,11 @@ public final class AmazonGiftCardAccount extends PaymentAccount { private AmazonGiftCardAccountPayload getAmazonGiftCardAccountPayload() { return (AmazonGiftCardAccountPayload) paymentAccountPayload; } + + @Override + protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { + var field = super.getEmptyFormField(fieldId); + if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); + return field; + } } diff --git a/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java b/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java index 0226fe4530..6ca6ea2bed 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java @@ -50,6 +50,7 @@ import static haveno.common.util.Utilities.decodeFromHex; import static haveno.core.locale.CountryUtil.findCountryByCode; import static haveno.core.locale.CurrencyUtil.getTradeCurrenciesInList; import static haveno.core.locale.CurrencyUtil.getTradeCurrency; +import static haveno.core.payment.payload.PaymentMethod.AMAZON_GIFT_CARD_ID; import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID; import static java.lang.String.format; import static java.util.Arrays.stream; @@ -438,6 +439,8 @@ class PaymentAccountTypeAdapter extends TypeAdapter { // account.setSingleTradeCurrency(fiatCurrency); } else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) { ((MoneyGramAccount) account).setCountry(country.get()); + } else if (account.hasPaymentMethodWithId(AMAZON_GIFT_CARD_ID)) { + ((AmazonGiftCardAccount) account).setCountry(country.get()); } else { String errMsg = format("cannot set the country on a %s", paymentAccountType.getSimpleName()); diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index f48aeed3c9..1768848669 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -373,7 +373,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AmazonGiftCardForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AmazonGiftCardForm.java index 3bcdc44075..6bd796bf7a 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AmazonGiftCardForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AmazonGiftCardForm.java @@ -120,7 +120,7 @@ public class AmazonGiftCardForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(amazonGiftCardAccount.getEmailOrMobileNr()); + setAccountNameWithString(amazonGiftCardAccount.getEmailOrMobileNr() == null ? "" : amazonGiftCardAccount.getEmailOrMobileNr()); } @Override diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index eb82fdf203..5df4b5324c 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1907,6 +1907,7 @@ message PaymentAccountForm { ALI_PAY = 21; SWISH = 22; TRANSFERWISE_USD = 23; + AMAZON_GIFT_CARD = 24; } FormId id = 1; repeated PaymentAccountFormField fields = 2; From a8ba57f4445bb0b4dcaefa3b312a2a3e26e34660 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:57:13 -0400 Subject: [PATCH 331/371] support ach transfer payment account over grpc api --- .../core/api/model/PaymentAccountForm.java | 3 ++- .../core/payment/AchTransferAccount.java | 25 ++++++++++++++++++- .../haveno/core/payment/PaymentAccount.java | 14 ++++++++--- .../core/payment/payload/PaymentMethod.java | 3 ++- proto/src/main/proto/pb.proto | 1 + 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java index d280ced31f..72f9dbc51a 100644 --- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java @@ -83,7 +83,8 @@ public final class PaymentAccountForm implements PersistablePayload { ALI_PAY, SWISH, TRANSFERWISE_USD, - AMAZON_GIFT_CARD; + AMAZON_GIFT_CARD, + ACH_TRANSFER; public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); diff --git a/core/src/main/java/haveno/core/payment/AchTransferAccount.java b/core/src/main/java/haveno/core/payment/AchTransferAccount.java index 9f04e4e076..8c9c1eee62 100644 --- a/core/src/main/java/haveno/core/payment/AchTransferAccount.java +++ b/core/src/main/java/haveno/core/payment/AchTransferAccount.java @@ -19,6 +19,7 @@ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; +import haveno.core.locale.BankUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.AchTransferAccountPayload; import haveno.core.payment.payload.BankAccountPayload; @@ -34,6 +35,19 @@ public final class AchTransferAccount extends CountryBasedPaymentAccount impleme public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.HOLDER_NAME, + PaymentAccountFormField.FieldId.HOLDER_ADDRESS, + PaymentAccountFormField.FieldId.BANK_NAME, + PaymentAccountFormField.FieldId.BRANCH_ID, + PaymentAccountFormField.FieldId.ACCOUNT_NR, + PaymentAccountFormField.FieldId.ACCOUNT_TYPE, + PaymentAccountFormField.FieldId.COUNTRY, + PaymentAccountFormField.FieldId.TRADE_CURRENCIES, + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.SALT + ); + public AchTransferAccount() { super(PaymentMethod.ACH_TRANSFER); } @@ -79,6 +93,15 @@ public final class AchTransferAccount extends CountryBasedPaymentAccount impleme @Override public @NonNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; + } + + @Override + protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { + var field = super.getEmptyFormField(fieldId); + if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); + if (field.getId() == PaymentAccountFormField.FieldId.BRANCH_ID) field.setLabel(BankUtil.getBranchIdLabel("US")); + if (field.getId() == PaymentAccountFormField.FieldId.ACCOUNT_TYPE) field.setLabel(BankUtil.getAccountTypeLabel("US")); + return field; } } diff --git a/core/src/main/java/haveno/core/payment/PaymentAccount.java b/core/src/main/java/haveno/core/payment/PaymentAccount.java index 0f32918fed..8a7b5f24c1 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccount.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccount.java @@ -435,7 +435,8 @@ public abstract class PaymentAccount implements PersistablePayload { processValidationResult(new LengthValidator(2, 100).validate(value)); break; case ACCOUNT_TYPE: - throw new IllegalArgumentException("Not implemented"); + processValidationResult(new LengthValidator(2, 100).validate(value)); + break; case ANSWER: throw new IllegalArgumentException("Not implemented"); case BANK_ACCOUNT_NAME: @@ -491,7 +492,8 @@ public abstract class PaymentAccount implements PersistablePayload { processValidationResult(new BICValidator().validate(value)); break; case BRANCH_ID: - throw new IllegalArgumentException("Not implemented"); + processValidationResult(new LengthValidator(2, 34).validate(value)); + break; case CITY: processValidationResult(new LengthValidator(2, 34).validate(value)); break; @@ -624,7 +626,9 @@ public abstract class PaymentAccount implements PersistablePayload { field.setLabel(Res.get("payment.account.owner")); break; case ACCOUNT_TYPE: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); + field.setLabel(Res.get("payment.select.account")); + break; case ANSWER: throw new IllegalArgumentException("Not implemented"); case BANK_ACCOUNT_NAME: @@ -692,7 +696,9 @@ public abstract class PaymentAccount implements PersistablePayload { field.setLabel("BIC"); break; case BRANCH_ID: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.TEXT); + //field.setLabel("Not implemented"); // expected to be overridden by subclasses + break; case CITY: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.city")); diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index 1768848669..c4226a97b6 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -374,7 +374,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 5df4b5324c..46a6361301 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1908,6 +1908,7 @@ message PaymentAccountForm { SWISH = 22; TRANSFERWISE_USD = 23; AMAZON_GIFT_CARD = 24; + ACH_TRANSFER = 25; } FormId id = 1; repeated PaymentAccountFormField fields = 2; From 9932ae84f4a0f53d5e217d060ef62e8abf2830fe Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:41:16 -0400 Subject: [PATCH 332/371] support interac e-transfer payment accounts over grpc api --- .../core/api/CorePaymentAccountsService.java | 11 ++++-- .../core/api/model/PaymentAccountForm.java | 3 +- .../core/payment/InteracETransferAccount.java | 36 +++++++++++++++++-- .../haveno/core/payment/PaymentAccount.java | 8 +++-- .../InteracETransferAccountPayload.java | 16 ++++----- .../core/payment/payload/PaymentMethod.java | 3 +- .../java/haveno/core/trade/HavenoUtils.java | 2 ++ .../paymentmethods/InteracETransferForm.java | 2 +- proto/src/main/proto/pb.proto | 3 +- 9 files changed, 65 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java index b506a00ac1..b6afd098b7 100644 --- a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java @@ -36,6 +36,8 @@ import haveno.core.payment.InstantCryptoCurrencyAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountFactory; import haveno.core.payment.payload.PaymentMethod; +import haveno.core.payment.validation.InteracETransferValidator; +import haveno.core.trade.HavenoUtils; import haveno.core.user.User; import java.io.File; import static java.lang.String.format; @@ -48,19 +50,24 @@ import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j -class CorePaymentAccountsService { +public class CorePaymentAccountsService { private final CoreAccountService accountService; private final AccountAgeWitnessService accountAgeWitnessService; private final User user; + public final InteracETransferValidator interacETransferValidator; @Inject public CorePaymentAccountsService(CoreAccountService accountService, AccountAgeWitnessService accountAgeWitnessService, - User user) { + User user, + InteracETransferValidator interacETransferValidator) { this.accountService = accountService; this.accountAgeWitnessService = accountAgeWitnessService; this.user = user; + this.interacETransferValidator = interacETransferValidator; + + HavenoUtils.corePaymentAccountService = this; } PaymentAccount createPaymentAccount(PaymentAccountForm form) { diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java index 72f9dbc51a..34ed649266 100644 --- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java @@ -84,7 +84,8 @@ public final class PaymentAccountForm implements PersistablePayload { SWISH, TRANSFERWISE_USD, AMAZON_GIFT_CARD, - ACH_TRANSFER; + ACH_TRANSFER, + INTERAC_E_TRANSFER; public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); diff --git a/core/src/main/java/haveno/core/payment/InteracETransferAccount.java b/core/src/main/java/haveno/core/payment/InteracETransferAccount.java index 579167c276..e1929b09e4 100644 --- a/core/src/main/java/haveno/core/payment/InteracETransferAccount.java +++ b/core/src/main/java/haveno/core/payment/InteracETransferAccount.java @@ -17,12 +17,15 @@ package haveno.core.payment; +import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.InteracETransferAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; +import haveno.core.payment.validation.InteracETransferValidator; +import haveno.core.trade.HavenoUtils; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; @@ -33,9 +36,22 @@ public final class InteracETransferAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CAD")); + private final InteracETransferValidator interacETransferValidator; + + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.HOLDER_NAME, + PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR, + PaymentAccountFormField.FieldId.QUESTION, + PaymentAccountFormField.FieldId.ANSWER, + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.SALT + ); + public InteracETransferAccount() { super(PaymentMethod.INTERAC_E_TRANSFER); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); + this.interacETransferValidator = HavenoUtils.corePaymentAccountService.interacETransferValidator; + if (interacETransferValidator == null) throw new IllegalArgumentException("InteracETransferValidator cannot be null"); } @Override @@ -50,15 +66,15 @@ public final class InteracETransferAccount extends PaymentAccount { @Override public @NotNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public void setEmail(String email) { - ((InteracETransferAccountPayload) paymentAccountPayload).setEmail(email); + ((InteracETransferAccountPayload) paymentAccountPayload).setEmailOrMobileNr(email); } public String getEmail() { - return ((InteracETransferAccountPayload) paymentAccountPayload).getEmail(); + return ((InteracETransferAccountPayload) paymentAccountPayload).getEmailOrMobileNr(); } public void setAnswer(String answer) { @@ -84,4 +100,18 @@ public final class InteracETransferAccount extends PaymentAccount { public String getHolderName() { return ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName(); } + + public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { + switch (fieldId) { + case QUESTION: + processValidationResult(interacETransferValidator.questionValidator.validate(value)); + break; + case ANSWER: + processValidationResult(interacETransferValidator.answerValidator.validate(value)); + break; + default: + super.validateFormField(form, fieldId, value); + } + + } } diff --git a/core/src/main/java/haveno/core/payment/PaymentAccount.java b/core/src/main/java/haveno/core/payment/PaymentAccount.java index 8a7b5f24c1..88e6956915 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccount.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccount.java @@ -630,7 +630,9 @@ public abstract class PaymentAccount implements PersistablePayload { field.setLabel(Res.get("payment.select.account")); break; case ANSWER: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.TEXT); + field.setLabel(Res.get("payment.answer")); + break; case BANK_ACCOUNT_NAME: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.owner")); @@ -782,7 +784,9 @@ public abstract class PaymentAccount implements PersistablePayload { case PROMPT_PAY_ID: throw new IllegalArgumentException("Not implemented"); case QUESTION: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.TEXT); + field.setLabel(Res.get("payment.secret")); + break; case REQUIREMENTS: throw new IllegalArgumentException("Not implemented"); case SALT: diff --git a/core/src/main/java/haveno/core/payment/payload/InteracETransferAccountPayload.java b/core/src/main/java/haveno/core/payment/payload/InteracETransferAccountPayload.java index 26105e7478..751ecbf045 100644 --- a/core/src/main/java/haveno/core/payment/payload/InteracETransferAccountPayload.java +++ b/core/src/main/java/haveno/core/payment/payload/InteracETransferAccountPayload.java @@ -36,7 +36,7 @@ import java.util.Map; @Getter @Slf4j public final class InteracETransferAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { - private String email = ""; + private String emailOrMobileNr = ""; private String holderName = ""; private String question = ""; private String answer = ""; @@ -52,7 +52,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload private InteracETransferAccountPayload(String paymentMethod, String id, - String email, + String emailOrMobileNr, String holderName, String question, String answer, @@ -62,7 +62,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload id, maxTradePeriod, excludeFromJsonDataMap); - this.email = email; + this.emailOrMobileNr = emailOrMobileNr; this.holderName = holderName; this.question = question; this.answer = answer; @@ -72,7 +72,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setInteracETransferAccountPayload(protobuf.InteracETransferAccountPayload.newBuilder() - .setEmail(email) + .setEmailOrMobileNr(emailOrMobileNr) .setHolderName(holderName) .setQuestion(question) .setAnswer(answer)) @@ -82,7 +82,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload public static InteracETransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new InteracETransferAccountPayload(proto.getPaymentMethodId(), proto.getId(), - proto.getInteracETransferAccountPayload().getEmail(), + proto.getInteracETransferAccountPayload().getEmailOrMobileNr(), proto.getInteracETransferAccountPayload().getHolderName(), proto.getInteracETransferAccountPayload().getQuestion(), proto.getInteracETransferAccountPayload().getAnswer(), @@ -98,21 +98,21 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + - Res.get("payment.email") + " " + email + ", " + Res.getWithCol("payment.secret") + " " + + Res.get("payment.email") + " " + emailOrMobileNr + ", " + Res.getWithCol("payment.secret") + " " + question + ", " + Res.getWithCol("payment.answer") + " " + answer; } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + - Res.getWithCol("payment.email") + " " + email + "\n" + + Res.getWithCol("payment.email") + " " + emailOrMobileNr + "\n" + Res.getWithCol("payment.secret") + " " + question + "\n" + Res.getWithCol("payment.answer") + " " + answer; } @Override public byte[] getAgeWitnessInputData() { - return super.getAgeWitnessInputData(ArrayUtils.addAll(email.getBytes(StandardCharsets.UTF_8), + return super.getAgeWitnessInputData(ArrayUtils.addAll(emailOrMobileNr.getBytes(StandardCharsets.UTF_8), ArrayUtils.addAll(question.getBytes(StandardCharsets.UTF_8), answer.getBytes(StandardCharsets.UTF_8)))); } diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index c4226a97b6..76cd97630e 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -375,7 +375,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index 32a6d76824..b022cf3cfc 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -31,6 +31,7 @@ import haveno.common.file.FileUtil; import haveno.common.util.Base64; import haveno.common.util.Utilities; import haveno.core.api.CoreNotificationService; +import haveno.core.api.CorePaymentAccountsService; import haveno.core.api.XmrConnectionService; import haveno.core.app.HavenoSetup; import haveno.core.offer.OfferPayload; @@ -132,6 +133,7 @@ public class HavenoUtils { public static XmrConnectionService xmrConnectionService; public static OpenOfferManager openOfferManager; public static CoreNotificationService notificationService; + public static CorePaymentAccountsService corePaymentAccountService; public static Preferences preferences; public static boolean isSeedNode() { diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/InteracETransferForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/InteracETransferForm.java index 8447a1a01a..f57ac5cc09 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/InteracETransferForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/InteracETransferForm.java @@ -44,7 +44,7 @@ public class InteracETransferForm extends PaymentMethodForm { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName()); addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.emailOrMobile"), - ((InteracETransferAccountPayload) paymentAccountPayload).getEmail()); + ((InteracETransferAccountPayload) paymentAccountPayload).getEmailOrMobileNr()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.secret"), ((InteracETransferAccountPayload) paymentAccountPayload).getQuestion()); addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.answer"), diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 46a6361301..e0f05c4ef7 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1047,7 +1047,7 @@ message FasterPaymentsAccountPayload { } message InteracETransferAccountPayload { - string email = 1; + string email_or_mobile_nr = 1; string holder_name = 2; string question = 3; string answer = 4; @@ -1909,6 +1909,7 @@ message PaymentAccountForm { TRANSFERWISE_USD = 23; AMAZON_GIFT_CARD = 24; ACH_TRANSFER = 25; + INTERAC_E_TRANSFER = 26; } FormId id = 1; repeated PaymentAccountFormField fields = 2; From 4a6043841aba3969ae01a3666bd82a6d7f6e40d1 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 21 Jun 2025 14:48:56 -0400 Subject: [PATCH 333/371] support US postal money order accounts over grpc api --- .../java/haveno/core/api/model/PaymentAccountForm.java | 3 ++- .../haveno/core/payment/USPostalMoneyOrderAccount.java | 9 ++++++++- .../java/haveno/core/payment/payload/PaymentMethod.java | 3 ++- proto/src/main/proto/pb.proto | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java index 34ed649266..f7dc4a5063 100644 --- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java @@ -85,7 +85,8 @@ public final class PaymentAccountForm implements PersistablePayload { TRANSFERWISE_USD, AMAZON_GIFT_CARD, ACH_TRANSFER, - INTERAC_E_TRANSFER; + INTERAC_E_TRANSFER, + US_POSTAL_MONEY_ORDER; public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); diff --git a/core/src/main/java/haveno/core/payment/USPostalMoneyOrderAccount.java b/core/src/main/java/haveno/core/payment/USPostalMoneyOrderAccount.java index 41667563cf..4cf6e20608 100644 --- a/core/src/main/java/haveno/core/payment/USPostalMoneyOrderAccount.java +++ b/core/src/main/java/haveno/core/payment/USPostalMoneyOrderAccount.java @@ -33,6 +33,13 @@ public final class USPostalMoneyOrderAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.HOLDER_NAME, + PaymentAccountFormField.FieldId.POSTAL_ADDRESS, + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.SALT + ); + public USPostalMoneyOrderAccount() { super(PaymentMethod.US_POSTAL_MONEY_ORDER); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); @@ -50,7 +57,7 @@ public final class USPostalMoneyOrderAccount extends PaymentAccount { @Override public @NonNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public void setPostalAddress(String postalAddress) { diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index 76cd97630e..dd842af7c3 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -376,7 +376,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index e0f05c4ef7..647272b261 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1910,6 +1910,7 @@ message PaymentAccountForm { AMAZON_GIFT_CARD = 24; ACH_TRANSFER = 25; INTERAC_E_TRANSFER = 26; + US_POSTAL_MONEY_ORDER = 27; } FormId id = 1; repeated PaymentAccountFormField fields = 2; From e3d7499004766641892ecf01958fcab8eb87a114 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 1 Jul 2025 09:45:42 -0400 Subject: [PATCH 334/371] fix vertical alignment of price column in offer book view --- .../java/haveno/desktop/main/offer/offerbook/OfferBookView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index 7cb2bc7bdb..6be82a1c81 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -912,7 +912,7 @@ abstract public class OfferBookView Date: Wed, 2 Jul 2025 08:33:05 -0400 Subject: [PATCH 335/371] add payment methods to trade statistics PaymentMethodMapper --- .../java/haveno/core/trade/statistics/TradeStatistics3.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java index 209fad38cf..4e581b8291 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java @@ -178,7 +178,9 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl TRANSFERWISE_USD, ACH_TRANSFER, DOMESTIC_WIRE_TRANSFER, - PAYPAL + PAYPAL, + PAYSAFE, + CASH_AT_ATM } @Getter From e089a6f2a4fe5cb786c482a866276f7328fd1866 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:44:27 -0400 Subject: [PATCH 336/371] wallet poll requests connection changes off thread to avoid deadlock --- core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index b2c4ce90e2..e08d1a2bbc 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -2061,7 +2061,7 @@ public class XmrWalletService extends XmrWalletBase { if (System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); lastLogPollErrorTimestamp = System.currentTimeMillis(); - if (System.currentTimeMillis() - lastPollTxsTimestamp > POLL_TXS_TOLERANCE_MS) requestSwitchToNextBestConnection(sourceConnection); + if (System.currentTimeMillis() - lastPollTxsTimestamp > POLL_TXS_TOLERANCE_MS) ThreadUtils.submitToPool(() -> requestSwitchToNextBestConnection(sourceConnection)); } } } From c6ef499ced338eab293d26c93f5d253e7113a66e Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 3 Jul 2025 09:29:48 -0400 Subject: [PATCH 337/371] fix vertical alignment of text field with icon --- .../main/java/haveno/desktop/components/TextFieldWithIcon.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java b/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java index 1db14cf73a..9c7e8a823d 100644 --- a/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java +++ b/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java @@ -54,7 +54,7 @@ public class TextFieldWithIcon extends AnchorPane { iconLabel = new Label(); iconLabel.setLayoutX(0); - iconLabel.setLayoutY(Layout.FLOATING_ICON_Y); + iconLabel.setLayoutY(Layout.FLOATING_ICON_Y - 2); dummyTextField.widthProperty().addListener((observable, oldValue, newValue) -> { iconLabel.setLayoutX(dummyTextField.widthProperty().get() + 20 + Layout.FLOATING_ICON_Y); From 19d4df8b2ec3d862e8f63a266410a26ba2a3a082 Mon Sep 17 00:00:00 2001 From: jermanuts <109705802+jermanuts@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:45:29 +0200 Subject: [PATCH 338/371] Add translation guide to CONTRIBUTING.md --- docs/CONTRIBUTING.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index caa76ac1a9..76a09c62ff 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -20,4 +20,14 @@ When you have something new built for Haveno, submit a pull request for review t ## Developer guide -See the [developer guide](developer-guide.md) to get started developing Haveno. \ No newline at end of file +See the [developer guide](developer-guide.md) to get started developing Haveno. + +## Translation + +Existing translation files are in [core/src/main/resources/i18n/](/core/src/main/resources/i18n/), feel free to update or improve them if needed. + +To add a new locale translations, follow these steps: + +- Add your [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) standard country language code to [core/src/main/java/haveno/core/locale/LanguageUtil.java](/core/src/main/java/haveno/core/locale/LanguageUtil.java) and remove it from "// not translated yet" if it is there. + +- Copy [displayStrings.properties](/core/src/main/resources/i18n/displayStrings.properties), create new file in [core/src/main/resources/i18n/](/core/src/main/resources/i18n/) in this format: `displayStrings_[insertLocaleName].properties` and then add translations. From 93f6337e6ac8d95bf96a0b343a83d68442cd2153 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 5 Jul 2025 10:56:02 -0400 Subject: [PATCH 339/371] fix opening matrix.to link under support button by escaping --- .../main/java/haveno/desktop/util/GUIUtil.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java index 4aad28f464..2193cec831 100644 --- a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java @@ -124,6 +124,8 @@ import java.io.OutputStreamWriter; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; @@ -721,13 +723,26 @@ public class GUIUtil { private static void doOpenWebPage(String target) { try { - Utilities.openURI(new URI(target)); + Utilities.openURI(safeParse(target)); } catch (Exception e) { e.printStackTrace(); log.error(e.getMessage()); } } + private static URI safeParse(String url) throws URISyntaxException { + int hashIndex = url.indexOf('#'); + + if (hashIndex >= 0 && hashIndex < url.length() - 1) { + String base = url.substring(0, hashIndex); + String fragment = url.substring(hashIndex + 1); + String encodedFragment = URLEncoder.encode(fragment, StandardCharsets.UTF_8); + return new URI(base + "#" + encodedFragment); + } + + return new URI(url); // no fragment + } + public static String getPercentageOfTradeAmount(BigInteger fee, BigInteger tradeAmount) { String result = " (" + getPercentage(fee, tradeAmount) + " " + Res.get("guiUtil.ofTradeAmount") + ")"; From da17bcc76d211ec694316dfe12b2bc1e2f8caf08 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 5 Jul 2025 10:56:12 -0400 Subject: [PATCH 340/371] fix alignment of market price pct when taking offer --- .../java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java | 1 - 1 file changed, 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index cb1611841b..73b3aa526e 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -1134,7 +1134,6 @@ public class TakeOfferView extends ActivatableViewAndModel Date: Sat, 5 Jul 2025 10:56:25 -0400 Subject: [PATCH 341/371] always use 'currency name (code)' format --- .../main/offer/offerbook/OfferBookView.java | 2 +- .../settings/preferences/PreferencesView.java | 2 +- .../haveno/desktop/util/CurrencyListItem.java | 2 +- .../java/haveno/desktop/util/GUIUtil.java | 30 +------------------ .../java/haveno/desktop/util/GUIUtilTest.java | 22 -------------- 5 files changed, 4 insertions(+), 54 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index 6be82a1c81..d10e6b6562 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -511,7 +511,7 @@ abstract public class OfferBookView() { @Override public String toString(TradeCurrency object) { - return object.getCode() + " - " + object.getName(); + return object.getName() + " (" + object.getCode() + ")"; } @Override diff --git a/desktop/src/main/java/haveno/desktop/util/CurrencyListItem.java b/desktop/src/main/java/haveno/desktop/util/CurrencyListItem.java index 5a85a80d08..3aea50a638 100644 --- a/desktop/src/main/java/haveno/desktop/util/CurrencyListItem.java +++ b/desktop/src/main/java/haveno/desktop/util/CurrencyListItem.java @@ -61,7 +61,7 @@ public class CurrencyListItem { if (isSpecialShowAllItem()) return Res.get(GUIUtil.SHOW_ALL_FLAG); else - return tradeCurrency.getCode() + " - " + tradeCurrency.getName(); + return tradeCurrency.getName() + " (" + tradeCurrency.getCode() + ")"; } private boolean isSpecialShowAllItem() { diff --git a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java index 2193cec831..2710c00f6a 100644 --- a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java @@ -387,7 +387,7 @@ public class GUIUtil { String code = item.getCode(); AnchorPane pane = new AnchorPane(); - Label currency = new AutoTooltipLabel(code + " - " + item.getName()); + Label currency = new AutoTooltipLabel(item.getName() + " (" + item.getCode() + ")"); currency.getStyleClass().add("currency-label-selected"); AnchorPane.setLeftAnchor(currency, 0.0); pane.getChildren().add(currency); @@ -422,34 +422,6 @@ public class GUIUtil { }; } - public static StringConverter getTradeCurrencyConverter(String postFixSingle, - String postFixMulti, - Map offerCounts) { - return new StringConverter<>() { - @Override - public String toString(TradeCurrency tradeCurrency) { - String code = tradeCurrency.getCode(); - Optional offerCountOptional = Optional.ofNullable(offerCounts.get(code)); - final String displayString; - displayString = offerCountOptional - .map(offerCount -> CurrencyUtil.getNameAndCode(code) - + " - " + offerCount + " " + (offerCount == 1 ? postFixSingle : postFixMulti)) - .orElseGet(() -> CurrencyUtil.getNameAndCode(code)); - // http://boschista.deviantart.com/journal/Cool-ASCII-Symbols-214218618 - if (code.equals(GUIUtil.SHOW_ALL_FLAG)) - return "▶ " + Res.get("list.currency.showAll"); - else if (code.equals(GUIUtil.EDIT_FLAG)) - return "▼ " + Res.get("list.currency.editList"); - return tradeCurrency.getDisplayPrefix() + displayString; - } - - @Override - public TradeCurrency fromString(String s) { - return null; - } - }; - } - public static Callback, ListCell> getTradeCurrencyCellFactory(String postFixSingle, String postFixMulti, Map offerCounts) { diff --git a/desktop/src/test/java/haveno/desktop/util/GUIUtilTest.java b/desktop/src/test/java/haveno/desktop/util/GUIUtilTest.java index dc7d68c873..f5b00a7fb6 100644 --- a/desktop/src/test/java/haveno/desktop/util/GUIUtilTest.java +++ b/desktop/src/test/java/haveno/desktop/util/GUIUtilTest.java @@ -19,21 +19,15 @@ package haveno.desktop.util; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; -import haveno.core.locale.TradeCurrency; import haveno.core.trade.HavenoUtils; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; -import javafx.util.StringConverter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.math.BigInteger; -import java.util.HashMap; import java.util.Locale; -import java.util.Map; -import static haveno.desktop.maker.TradeCurrencyMakers.euro; -import static haveno.desktop.maker.TradeCurrencyMakers.monero; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -49,22 +43,6 @@ public class GUIUtilTest { Res.setBaseCurrencyName("Bitcoin"); } - @Test - public void testTradeCurrencyConverter() { - Map offerCounts = new HashMap<>() {{ - put("XMR", 11); - put("EUR", 10); - }}; - StringConverter tradeCurrencyConverter = GUIUtil.getTradeCurrencyConverter( - Res.get("shared.oneOffer"), - Res.get("shared.multipleOffers"), - offerCounts - ); - - assertEquals("✦ Monero (XMR) - 11 offers", tradeCurrencyConverter.toString(monero)); - assertEquals("★ Euro (EUR) - 10 offers", tradeCurrencyConverter.toString(euro)); - } - @Test public void testOpenURLWithCampaignParameters() { Preferences preferences = mock(Preferences.class); From 61122493b51a85127f955e7ddddb135bb7a456cb Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 5 Jul 2025 10:56:36 -0400 Subject: [PATCH 342/371] remove unused progress bar below network info --- .../src/main/java/haveno/desktop/main/MainView.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index 2a5b994cdb..99e75b2a69 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -124,7 +124,7 @@ public class MainView extends InitializableView { private ChangeListener splashP2PNetworkVisibleListener; private BusyAnimation splashP2PNetworkBusyAnimation; private Label splashP2PNetworkLabel; - private ProgressBar xmrSyncIndicator, p2pNetworkProgressBar; + private ProgressBar xmrSyncIndicator; private Label xmrSplashInfo; private Popup p2PNetworkWarnMsgPopup, xmrNetworkWarnMsgPopup; private final TorNetworkSettingsWindow torNetworkSettingsWindow; @@ -851,18 +851,13 @@ public class MainView extends InitializableView { model.getUpdatedDataReceived().addListener((observable, oldValue, newValue) -> UserThread.execute(() -> { p2PNetworkIcon.setOpacity(1); - p2pNetworkProgressBar.setProgress(0); })); - p2pNetworkProgressBar = new ProgressBar(-1); - p2pNetworkProgressBar.setMaxHeight(2); - p2pNetworkProgressBar.prefWidthProperty().bind(p2PNetworkLabel.widthProperty()); - VBox vBox = new VBox(); vBox.setAlignment(Pos.CENTER_RIGHT); - vBox.getChildren().addAll(p2PNetworkLabel, p2pNetworkProgressBar); + vBox.getChildren().addAll(p2PNetworkLabel); setRightAnchor(vBox, networkIconRightAnchor + 45); - setBottomAnchor(vBox, 5d); + setBottomAnchor(vBox, 7d); return new AnchorPane(separator, xmrInfoLabel, versionBox, vBox, p2PNetworkStatusIcon, p2PNetworkIcon, useDarkModeIcon) {{ setId("footer-pane"); From 55032f94e792f37def52eda34a9c0361d5d35044 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 5 Jul 2025 11:58:08 -0400 Subject: [PATCH 343/371] use logo for fiat currencies --- .../src/main/java/haveno/desktop/images.css | 4 +++ .../main/java/haveno/desktop/theme-dark.css | 4 +++ .../java/haveno/desktop/util/GUIUtil.java | 31 +++++++++++------- .../resources/images/fiat_logo_dark_mode.png | Bin 0 -> 12822 bytes .../resources/images/fiat_logo_light_mode.png | Bin 0 -> 13458 bytes 5 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 desktop/src/main/resources/images/fiat_logo_dark_mode.png create mode 100644 desktop/src/main/resources/images/fiat_logo_light_mode.png diff --git a/desktop/src/main/java/haveno/desktop/images.css b/desktop/src/main/java/haveno/desktop/images.css index 1e36427ee9..c721f2ee7b 100644 --- a/desktop/src/main/java/haveno/desktop/images.css +++ b/desktop/src/main/java/haveno/desktop/images.css @@ -313,6 +313,10 @@ -fx-image: url("../../images/settings.png"); } +#image-fiat-logo { + -fx-image: url("../../images/fiat_logo_light_mode.png"); +} + #image-btc-logo { -fx-image: url("../../images/btc_logo.png"); } diff --git a/desktop/src/main/java/haveno/desktop/theme-dark.css b/desktop/src/main/java/haveno/desktop/theme-dark.css index 5316735b69..11fc14cdbf 100644 --- a/desktop/src/main/java/haveno/desktop/theme-dark.css +++ b/desktop/src/main/java/haveno/desktop/theme-dark.css @@ -643,3 +643,7 @@ .regular-text-color { -fx-text-fill: -bs-text-color; } + +#image-fiat-logo { + -fx-image: url("../../images/fiat_logo_dark_mode.png"); +} diff --git a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java index 2710c00f6a..514b90d134 100644 --- a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java @@ -349,10 +349,11 @@ public class GUIUtil { break; default: - // use icons for crypto - if (CurrencyUtil.isCryptoCurrency(code)) { + // use icon if available + ImageView currencyIcon = getCurrencyIcon(code); + if (currencyIcon != null) { label1.setText(""); - StackPane iconWrapper = new StackPane(getCurrencyIcon(code)); // TODO: icon must be wrapped in StackPane for reliable rendering on linux + StackPane iconWrapper = new StackPane(currencyIcon); // TODO: icon must be wrapped in StackPane for reliable rendering on linux label1.setGraphic(iconWrapper); } @@ -459,10 +460,12 @@ public class GUIUtil { break; default: - // use icons for crypto - if (CurrencyUtil.isCryptoCurrency(item.getCode())) { + // use icon if available + ImageView currencyIcon = getCurrencyIcon(code); + if (currencyIcon != null) { label1.setText(""); - label1.setGraphic(getCurrencyIcon(item.getCode())); + StackPane iconWrapper = new StackPane(currencyIcon); // TODO: icon must be wrapped in StackPane for reliable rendering on linux + label1.setGraphic(iconWrapper); } boolean isCrypto = CurrencyUtil.isCryptoCurrency(code); @@ -502,10 +505,12 @@ public class GUIUtil { Label label2 = new AutoTooltipLabel(item.getNameAndCode()); label2.getStyleClass().add("currency-label"); - // use icons for crypto - if (CurrencyUtil.isCryptoCurrency(item.getCode())) { + // use icon if available + ImageView currencyIcon = getCurrencyIcon(item.getCode()); + if (currencyIcon != null) { label1.setText(""); - label1.setGraphic(getCurrencyIcon(item.getCode())); + StackPane iconWrapper = new StackPane(currencyIcon); // TODO: icon must be wrapped in StackPane for reliable rendering on linux + label1.setGraphic(iconWrapper); } box.getChildren().addAll(label1, label2); @@ -1284,12 +1289,14 @@ public class GUIUtil { public static ImageView getCurrencyIcon(String currencyCode, double size) { if (currencyCode == null) return null; + String imageId = getImageId(currencyCode); + if (imageId == null) return null; ImageView iconView = new ImageView(); iconView.setFitWidth(size); iconView.setPreserveRatio(true); iconView.setSmooth(true); iconView.setCache(true); - iconView.setId(getImageId(currencyCode)); + iconView.setId(imageId); return iconView; } @@ -1327,7 +1334,9 @@ public class GUIUtil { private static String getImageId(String currencyCode) { if (currencyCode == null) return null; - return "image-" + currencyCode.toLowerCase() + "-logo"; + if (CurrencyUtil.isCryptoCurrency(currencyCode)) return "image-" + currencyCode.toLowerCase() + "-logo"; + if (CurrencyUtil.isFiatCurrency(currencyCode)) return "image-fiat-logo"; + return null; } public static void adjustHeightAutomatically(TextArea textArea) { diff --git a/desktop/src/main/resources/images/fiat_logo_dark_mode.png b/desktop/src/main/resources/images/fiat_logo_dark_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..b6fbaf02f325388180992903456ebd28e0796d78 GIT binary patch literal 12822 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%0D*0>ij+y2QI3Nm|*p24oyl+ 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 G{Qm&Igi@3M literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/fiat_logo_light_mode.png b/desktop/src/main/resources/images/fiat_logo_light_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..a40b3e632d736c96de2735a23bb30c380e9cb3b9 GIT binary patch literal 13458 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 vh Date: Sat, 5 Jul 2025 12:23:38 -0400 Subject: [PATCH 344/371] use stackpane for currency icons --- .../java/haveno/desktop/util/GUIUtil.java | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java index 514b90d134..cb8111af33 100644 --- a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java @@ -350,11 +350,10 @@ public class GUIUtil { default: // use icon if available - ImageView currencyIcon = getCurrencyIcon(code); + StackPane currencyIcon = getCurrencyIcon(code); if (currencyIcon != null) { label1.setText(""); - StackPane iconWrapper = new StackPane(currencyIcon); // TODO: icon must be wrapped in StackPane for reliable rendering on linux - label1.setGraphic(iconWrapper); + label1.setGraphic(currencyIcon); } if (preferences.isSortMarketCurrenciesNumerically() && item.numTrades > 0) { @@ -461,11 +460,10 @@ public class GUIUtil { default: // use icon if available - ImageView currencyIcon = getCurrencyIcon(code); + StackPane currencyIcon = getCurrencyIcon(code); if (currencyIcon != null) { label1.setText(""); - StackPane iconWrapper = new StackPane(currencyIcon); // TODO: icon must be wrapped in StackPane for reliable rendering on linux - label1.setGraphic(iconWrapper); + label1.setGraphic(currencyIcon); } boolean isCrypto = CurrencyUtil.isCryptoCurrency(code); @@ -506,11 +504,10 @@ public class GUIUtil { label2.getStyleClass().add("currency-label"); // use icon if available - ImageView currencyIcon = getCurrencyIcon(item.getCode()); + StackPane currencyIcon = getCurrencyIcon(item.getCode()); if (currencyIcon != null) { label1.setText(""); - StackPane iconWrapper = new StackPane(currencyIcon); // TODO: icon must be wrapped in StackPane for reliable rendering on linux - label1.setGraphic(iconWrapper); + label1.setGraphic(currencyIcon); } box.getChildren().addAll(label1, label2); @@ -1283,21 +1280,31 @@ public class GUIUtil { return contentColumns; } - public static ImageView getCurrencyIcon(String currencyCode) { - return getCurrencyIcon(currencyCode, 24); + private static ImageView getCurrencyImageView(String currencyCode) { + return getCurrencyImageView(currencyCode, 24); } - public static ImageView getCurrencyIcon(String currencyCode, double size) { + private static ImageView getCurrencyImageView(String currencyCode, double size) { if (currencyCode == null) return null; String imageId = getImageId(currencyCode); if (imageId == null) return null; - ImageView iconView = new ImageView(); - iconView.setFitWidth(size); - iconView.setPreserveRatio(true); - iconView.setSmooth(true); - iconView.setCache(true); - iconView.setId(imageId); - return iconView; + ImageView icon = new ImageView(); + icon.setFitWidth(size); + icon.setPreserveRatio(true); + icon.setSmooth(true); + icon.setCache(true); + icon.setId(imageId); + return icon; + } + + public static StackPane getCurrencyIcon(String currencyCode) { + ImageView icon = getCurrencyImageView(currencyCode); + return icon == null ? null : new StackPane(icon); + } + + public static StackPane getCurrencyIcon(String currencyCode, double size) { + ImageView icon = getCurrencyImageView(currencyCode, size); + return icon == null ? null : new StackPane(icon); } public static StackPane getCurrencyIconWithBorder(String currencyCode) { @@ -1307,12 +1314,9 @@ public class GUIUtil { public static StackPane getCurrencyIconWithBorder(String currencyCode, double size, double borderWidth) { if (currencyCode == null) return null; - ImageView icon = getCurrencyIcon(currencyCode, size); + ImageView icon = getCurrencyImageView(currencyCode, size); icon.setFitWidth(size - 2 * borderWidth); icon.setFitHeight(size - 2 * borderWidth); - icon.setPreserveRatio(true); - icon.setSmooth(true); - icon.setCache(true); StackPane circleWrapper = new StackPane(icon); circleWrapper.setPrefSize(size, size); From ea506ecaf2af4129e63d086df1fb14c0c03b2158 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 7 Jul 2025 20:44:55 -0400 Subject: [PATCH 345/371] widen currency text box when creating offer --- .../main/java/haveno/desktop/main/offer/MutableOfferView.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index a580d24802..97861a9c79 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -605,6 +605,7 @@ public abstract class MutableOfferView> exten paymentTitledGroupBg.managedProperty().bind(paymentTitledGroupBg.visibleProperty()); currencyComboBox.prefWidthProperty().bind(paymentAccountsComboBox.widthProperty()); currencyComboBox.managedProperty().bind(currencyComboBox.visibleProperty()); + currencyTextFieldBox.prefWidthProperty().bind(paymentAccountsComboBox.widthProperty()); currencyTextFieldBox.managedProperty().bind(currencyTextFieldBox.visibleProperty()); } @@ -1038,6 +1039,7 @@ public abstract class MutableOfferView> exten final Tuple3 currencyTextFieldTuple = addTopLabelTextField(gridPane, gridRow, Res.get("shared.currency"), "", 5d); currencyTextField = currencyTextFieldTuple.second; currencyTextFieldBox = currencyTextFieldTuple.third; + currencyTextFieldBox.setMaxWidth(tradingAccountBoxTuple.first.getMinWidth() / 2); currencyTextFieldBox.setVisible(false); editOfferElements.add(currencyTextFieldBox); From 384e771712c8824d0851ab9aefa0d100775eb7f7 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 7 Jul 2025 20:45:19 -0400 Subject: [PATCH 346/371] fix vertical alignment of fixed price swap arrows --- .../main/java/haveno/desktop/main/offer/MutableOfferView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 97861a9c79..68d585e3f5 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -1523,7 +1523,7 @@ public abstract class MutableOfferView> exten // Fixed/Percentage toggle priceTypeToggleButton = getIconButton(MaterialDesignIcon.SWAP_VERTICAL); editOfferElements.add(priceTypeToggleButton); - HBox.setMargin(priceTypeToggleButton, new Insets(16, 1.5, 0, 0)); + HBox.setMargin(priceTypeToggleButton, new Insets(25, 1.5, 0, 0)); priceTypeToggleButton.setOnAction((actionEvent) -> updatePriceToggleButtons(model.getDataModel().getUseMarketBasedPrice().getValue())); From 75b8eb1dc937e24968445e7ff783851981c40c2b Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 8 Jul 2025 07:08:30 -0400 Subject: [PATCH 347/371] provide trade start time, duration, and deadline in grpc api --- .../java/haveno/core/api/model/TradeInfo.java | 18 ++++++++++++++++++ .../api/model/builder/TradeInfoV1Builder.java | 18 ++++++++++++++++++ .../src/main/java/haveno/core/trade/Trade.java | 8 ++++++-- proto/src/main/proto/grpc.proto | 3 +++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/api/model/TradeInfo.java b/core/src/main/java/haveno/core/api/model/TradeInfo.java index 8df26368ba..a423bfa470 100644 --- a/core/src/main/java/haveno/core/api/model/TradeInfo.java +++ b/core/src/main/java/haveno/core/api/model/TradeInfo.java @@ -99,6 +99,9 @@ public class TradeInfo implements Payload { private final boolean isCompleted; private final String contractAsJson; private final ContractInfo contract; + private final long startTime; + private final long maxDurationMs; + private final long deadlineTime; public TradeInfo(TradeInfoV1Builder builder) { this.offer = builder.getOffer(); @@ -140,6 +143,9 @@ public class TradeInfo implements Payload { this.isCompleted = builder.isCompleted(); this.contractAsJson = builder.getContractAsJson(); this.contract = builder.getContract(); + this.startTime = builder.getStartTime(); + this.maxDurationMs = builder.getMaxDurationMs(); + this.deadlineTime = builder.getDeadlineTime(); } public static TradeInfo toTradeInfo(Trade trade) { @@ -202,6 +208,9 @@ public class TradeInfo implements Payload { .withContractAsJson(trade.getContractAsJson()) .withContract(contractInfo) .withOffer(toOfferInfo(trade.getOffer())) + .withStartTime(trade.getStartDate().getTime()) + .withMaxDurationMs(trade.getMaxTradePeriod()) + .withDeadlineTime(trade.getMaxTradePeriodDate().getTime()) .build(); } @@ -251,6 +260,9 @@ public class TradeInfo implements Payload { .setIsPayoutUnlocked(isPayoutUnlocked) .setContractAsJson(contractAsJson == null ? "" : contractAsJson) .setContract(contract.toProtoMessage()) + .setStartTime(startTime) + .setMaxDurationMs(maxDurationMs) + .setDeadlineTime(deadlineTime) .build(); } @@ -295,6 +307,9 @@ public class TradeInfo implements Payload { .withIsPayoutUnlocked(proto.getIsPayoutUnlocked()) .withContractAsJson(proto.getContractAsJson()) .withContract((ContractInfo.fromProto(proto.getContract()))) + .withStartTime(proto.getStartTime()) + .withMaxDurationMs(proto.getMaxDurationMs()) + .withDeadlineTime(proto.getDeadlineTime()) .build(); } @@ -339,6 +354,9 @@ public class TradeInfo implements Payload { ", offer=" + offer + "\n" + ", contractAsJson=" + contractAsJson + "\n" + ", contract=" + contract + "\n" + + ", startTime=" + startTime + "\n" + + ", maxDurationMs=" + maxDurationMs + "\n" + + ", deadlineTime=" + deadlineTime + "\n" + '}'; } } diff --git a/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java b/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java index dcf40b8a69..dfb99f820b 100644 --- a/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java +++ b/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java @@ -73,6 +73,9 @@ public final class TradeInfoV1Builder { private String contractAsJson; private ContractInfo contract; private String closingStatus; + private long startTime; + private long maxDurationMs; + private long deadlineTime; public TradeInfoV1Builder withOffer(OfferInfo offer) { this.offer = offer; @@ -284,6 +287,21 @@ public final class TradeInfoV1Builder { return this; } + public TradeInfoV1Builder withStartTime(long startTime) { + this.startTime = startTime; + return this; + } + + public TradeInfoV1Builder withMaxDurationMs(long maxDurationMs) { + this.maxDurationMs = maxDurationMs; + return this; + } + + public TradeInfoV1Builder withDeadlineTime(long deadlineTime) { + this.deadlineTime = deadlineTime; + return this; + } + public TradeInfo build() { return new TradeInfo(this); } diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 29df35e45f..83002928bb 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2153,6 +2153,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + public long getMaxTradePeriod() { + return getOffer().getPaymentMethod().getMaxTradePeriod(); + } + public Date getHalfTradePeriodDate() { return new Date(getStartTime() + getMaxTradePeriod() / 2); } @@ -2161,8 +2165,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return new Date(getStartTime() + getMaxTradePeriod()); } - private long getMaxTradePeriod() { - return getOffer().getPaymentMethod().getMaxTradePeriod(); + public Date getStartDate() { + return new Date(getStartTime()); } private long getStartTime() { diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index b2af139042..882e0a0623 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -908,6 +908,9 @@ message TradeInfo { string maker_deposit_tx_id = 37; string taker_deposit_tx_id = 38; string payout_tx_id = 39; + uint64 start_time = 40; + uint64 max_duration_ms = 41; + uint64 deadline_time = 42; } message ContractInfo { From 0b4f5bf4ed494e4bbaa4b4e98fc4b798e9b8c138 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 8 Jul 2025 07:11:24 -0400 Subject: [PATCH 348/371] adjust payment accounts list height dynamically --- .../main/account/content/PaymentAccountsView.java | 13 +++++++++++++ .../content/cryptoaccounts/CryptoAccountsView.java | 4 +--- .../TraditionalAccountsView.java | 4 +--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/PaymentAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/PaymentAccountsView.java index cd22e98a3f..cd89273c9b 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/PaymentAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/PaymentAccountsView.java @@ -14,7 +14,9 @@ import haveno.desktop.components.InfoAutoTooltipLabel; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.ImageUtil; +import haveno.desktop.util.Layout; import javafx.beans.value.ChangeListener; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.scene.Node; @@ -67,6 +69,10 @@ public abstract class PaymentAccountsView) change -> { + setPaymentAccountsListHeight(); + }); } @Override @@ -153,6 +159,13 @@ public abstract class PaymentAccountsView, VBox> tuple = addTopLabelListView(root, gridRow, Res.get("account.crypto.yourCryptoAccounts"), Layout.FIRST_ROW_DISTANCE); paymentAccountsListView = tuple.second; - int prefNumRows = Math.min(4, Math.max(2, model.dataModel.getNumPaymentAccounts())); - paymentAccountsListView.setMinHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 34); - paymentAccountsListView.setMaxHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 34); + setPaymentAccountsListHeight(); setPaymentAccountsCellFactory(); Tuple3 tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java index 939a271366..3a758f1657 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java @@ -455,9 +455,7 @@ public class TraditionalAccountsView extends PaymentAccountsView, VBox> tuple = addTopLabelListView(root, gridRow, Res.get("account.traditional.yourTraditionalAccounts"), Layout.FIRST_ROW_DISTANCE); paymentAccountsListView = tuple.second; - int prefNumRows = Math.min(4, Math.max(2, model.dataModel.getNumPaymentAccounts())); - paymentAccountsListView.setMinHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 34); - paymentAccountsListView.setMaxHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 34); + setPaymentAccountsListHeight(); setPaymentAccountsCellFactory(); Tuple3 tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), From 19b8aaaf23b1b04353732c873ac4b51b310649b8 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 8 Jul 2025 10:31:56 -0400 Subject: [PATCH 349/371] transfer open offer's challenge when upgraded --- core/src/main/java/haveno/core/offer/OpenOfferManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 03327b1d09..1ab83d85e0 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -2057,6 +2057,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe updatedOffer.setPriceFeedService(priceFeedService); OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice()); + updatedOpenOffer.setChallenge(originalOpenOffer.getChallenge()); addOpenOffer(updatedOpenOffer); requestPersistence(); From da9cf540e6038173d7c30d1b5521065789d53d4c Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 9 Jul 2025 06:54:42 -0400 Subject: [PATCH 350/371] instruct to build v1.1.2 --- docs/installing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installing.md b/docs/installing.md index eefd844ce6..da1e3bf3a7 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -39,7 +39,7 @@ If it's the first time you are building Haveno, run the following commands to do ``` git clone https://github.com/haveno-dex/haveno.git cd haveno -git checkout master +git checkout v1.1.2 make ``` @@ -48,7 +48,7 @@ make If you are updating from a previous version, run from the root of the repository: ``` -git checkout master +git checkout v1.1.2 git pull make clean && make ``` From 100b2136a7c225fa23b7883c42a9f70178fad2e6 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 9 Jul 2025 06:55:09 -0400 Subject: [PATCH 351/371] fix error loading interac e-transfer offers --- .../haveno/core/offer/placeoffer/tasks/ValidateOffer.java | 4 ++++ .../java/haveno/core/payment/InteracETransferAccount.java | 5 +---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java index 3b1a01beeb..bc8a274f6d 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java @@ -23,6 +23,7 @@ import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.placeoffer.PlaceOfferModel; +import haveno.core.payment.PaymentAccount; import haveno.core.trade.HavenoUtils; import haveno.core.trade.messages.TradeMessage; import haveno.core.user.User; @@ -96,6 +97,9 @@ public class ValidateOffer extends Task { /*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0, "MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/ + PaymentAccount paymentAccount = user.getPaymentAccount(offer.getMakerPaymentAccountId()); + checkArgument(paymentAccount != null, "Payment account is null. makerPaymentAccountId=" + offer.getMakerPaymentAccountId()); + long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit()); checkArgument(offer.getAmount().longValueExact() <= maxAmount, "Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR"); diff --git a/core/src/main/java/haveno/core/payment/InteracETransferAccount.java b/core/src/main/java/haveno/core/payment/InteracETransferAccount.java index e1929b09e4..456124aac6 100644 --- a/core/src/main/java/haveno/core/payment/InteracETransferAccount.java +++ b/core/src/main/java/haveno/core/payment/InteracETransferAccount.java @@ -36,8 +36,6 @@ public final class InteracETransferAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CAD")); - private final InteracETransferValidator interacETransferValidator; - private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR, @@ -50,8 +48,6 @@ public final class InteracETransferAccount extends PaymentAccount { public InteracETransferAccount() { super(PaymentMethod.INTERAC_E_TRANSFER); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); - this.interacETransferValidator = HavenoUtils.corePaymentAccountService.interacETransferValidator; - if (interacETransferValidator == null) throw new IllegalArgumentException("InteracETransferValidator cannot be null"); } @Override @@ -102,6 +98,7 @@ public final class InteracETransferAccount extends PaymentAccount { } public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { + InteracETransferValidator interacETransferValidator = HavenoUtils.corePaymentAccountService.interacETransferValidator; switch (fieldId) { case QUESTION: processValidationResult(interacETransferValidator.questionValidator.validate(value)); From a2f54215deb9a37970849112b893183e1c2ec383 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 9 Jul 2025 06:55:26 -0400 Subject: [PATCH 352/371] always show tx withdraw window --- .../desktop/main/funds/withdrawal/WithdrawalView.java | 9 ++------- .../desktop/main/overlays/windows/TxWithdrawWindow.java | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java index 04a960f486..64ab188454 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java @@ -40,7 +40,6 @@ import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradeManager; import haveno.core.trade.protocol.TradeProtocol; -import haveno.core.user.DontShowAgainLookup; import haveno.core.util.validation.BtcAddressValidator; import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.setup.WalletsSetup; @@ -330,12 +329,8 @@ public class WithdrawalView extends ActivatableView { try { xmrWalletService.getWallet().relayTx(tx); xmrWalletService.getWallet().setTxNote(tx.getHash(), withdrawMemoTextField.getText()); // TODO (monero-java): tx note does not persist when tx created then relayed - String key = "showTransactionSent"; - if (DontShowAgainLookup.showAgain(key)) { - new TxWithdrawWindow(tx.getHash(), withdrawToAddress, HavenoUtils.formatXmr(receiverAmount, true), HavenoUtils.formatXmr(fee, true), xmrWalletService.getWallet().getTxNote(tx.getHash())) - .dontShowAgainId(key) - .show(); - } + new TxWithdrawWindow(tx.getHash(), withdrawToAddress, HavenoUtils.formatXmr(receiverAmount, true), HavenoUtils.formatXmr(fee, true), xmrWalletService.getWallet().getTxNote(tx.getHash())) + .show(); log.debug("onWithdraw onSuccess tx ID:{}", tx.getHash()); } catch (Exception e) { e.printStackTrace(); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxWithdrawWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxWithdrawWindow.java index f3dfc654b8..f6f153eb8a 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxWithdrawWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxWithdrawWindow.java @@ -52,7 +52,6 @@ public class TxWithdrawWindow extends Overlay { addHeadLine(); addContent(); addButtons(); - addDontShowAgainCheckBox(); applyStyles(); display(); } From 181bb2aa2610f075d52ea9db638d88fc76d2deea Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 11 Jul 2025 09:44:26 -0400 Subject: [PATCH 353/371] remove 'revert tx' column from transactions view --- .../funds/transactions/TransactionsView.fxml | 1 - .../funds/transactions/TransactionsView.java | 40 ++----------------- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml index 3f8aa752fd..15d2177129 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml @@ -40,7 +40,6 @@ - diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java index eb97c325ab..8e1e999f30 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java @@ -20,7 +20,6 @@ package haveno.desktop.main.funds.transactions; import com.google.inject.Inject; import com.googlecode.jcsv.writer.CSVEntryConverter; import de.jensd.fx.fontawesome.AwesomeIcon; -import haveno.common.util.Utilities; import haveno.core.api.XmrConnectionService; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; @@ -47,13 +46,11 @@ import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.Scene; -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.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; @@ -70,7 +67,7 @@ public class TransactionsView extends ActivatableView { @FXML TableView tableView; @FXML - TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, confidenceColumn, memoColumn, revertTxColumn; + TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, confidenceColumn, memoColumn; @FXML Label numItems; @FXML @@ -137,7 +134,6 @@ public class TransactionsView extends ActivatableView { txFeeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txFee", Res.getBaseCurrencyCode()))); confidenceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations", Res.getBaseCurrencyCode()))); memoColumn.setGraphic(new AutoTooltipLabel(Res.get("funds.tx.memo"))); - revertTxColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.revert", Res.getBaseCurrencyCode()))); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.tx.noTxAvailable"))); @@ -151,7 +147,6 @@ public class TransactionsView extends ActivatableView { setTxFeeColumnCellFactory(); setConfidenceColumnCellFactory(); setMemoColumnCellFactory(); - setRevertTxColumnCellFactory(); dateColumn.setComparator(Comparator.comparing(TransactionsListItem::getDate)); detailsColumn.setComparator((o1, o2) -> { @@ -170,10 +165,7 @@ public class TransactionsView extends ActivatableView { tableView.getSortOrder().add(dateColumn); keyEventEventHandler = event -> { - // Not intended to be public to users as the feature is not well tested - if (Utilities.isAltOrCtrlPressed(KeyCode.R, event)) { - revertTxColumn.setVisible(!revertTxColumn.isVisible()); - } + // unused }; HBox.setHgrow(spacer, Priority.ALWAYS); @@ -204,7 +196,7 @@ public class TransactionsView extends ActivatableView { numItems.setText(Res.get("shared.numItemsLabel", sortedDisplayedTransactions.size())); exportButton.setOnAction(event -> { final ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); - final int reportColumns = tableColumns.size() - 1; // CSV report excludes the last column (an icon) + final int reportColumns = tableColumns.size(); CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; for (int i = 0; i < columns.length; i++) @@ -501,31 +493,5 @@ public class TransactionsView extends ActivatableView { } }); } - - private void setRevertTxColumnCellFactory() { - revertTxColumn.setCellValueFactory((addressListItem) -> - new ReadOnlyObjectWrapper<>(addressListItem.getValue())); - revertTxColumn.setCellFactory( - new Callback<>() { - - @Override - public TableCell call(TableColumn column) { - return new TableCell<>() { - Button button; - - @Override - public void updateItem(final TransactionsListItem item, boolean empty) { - super.updateItem(item, empty); - setGraphic(null); - if (button != null) { - button.setOnAction(null); - button = null; - } - } - }; - } - }); - } } From 68005c4daaf342a2ab03cb55dd6b6586889aaa2e Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 11 Jul 2025 10:36:57 -0400 Subject: [PATCH 354/371] widen offer details window for confirm and cancel buttons --- .../desktop/main/overlays/windows/OfferDetailsWindow.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index b0c00ca60f..b885726cf4 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -128,7 +128,7 @@ public class OfferDetailsWindow extends Overlay { this.tradePrice = tradePrice; rowIndex = -1; - width = Layout.DETAILS_WINDOW_WIDTH; + width = 1050; createGridPane(); addContent(); display(); @@ -137,7 +137,7 @@ public class OfferDetailsWindow extends Overlay { public void show(Offer offer) { this.offer = offer; rowIndex = -1; - width = Layout.DETAILS_WINDOW_WIDTH; + width = 1050; createGridPane(); addContent(); display(); From 953157c965193836f720b37691560bcabafd1fcc Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 12 Jul 2025 08:10:34 -0400 Subject: [PATCH 355/371] set vertical spacing of pending trades view to 10 for consistency --- .../desktop/main/portfolio/pendingtrades/PendingTradesView.fxml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml index c0240455f2..a9e35c48f1 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml @@ -25,7 +25,7 @@ + spacing="10" xmlns:fx="http://javafx.com/fxml"> From 0cf34f3170030103f0d209510422fb3da53e8c50 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 12 Jul 2025 08:26:17 -0400 Subject: [PATCH 356/371] round offer amounts within min and max range --- .../haveno/core/api/CoreTradesService.java | 11 +-- .../haveno/core/offer/CreateOfferService.java | 8 +- .../main/java/haveno/core/offer/Offer.java | 10 ++- .../java/haveno/core/offer/OfferUtil.java | 6 +- .../java/haveno/core/util/coin/CoinUtil.java | 73 ++++++++++--------- .../haveno/core/util/coin/CoinUtilTest.java | 24 +++--- .../main/offer/MutableOfferDataModel.java | 18 +++-- .../main/offer/MutableOfferViewModel.java | 14 ++-- .../offer/takeoffer/TakeOfferDataModel.java | 18 ++--- .../offer/takeoffer/TakeOfferViewModel.java | 22 +++--- .../overlays/windows/OfferDetailsWindow.java | 2 +- .../OfferBookChartViewModelTest.java | 16 ++-- .../offerbook/OfferBookListItemMaker.java | 10 +-- .../offerbook/OfferBookViewModelTest.java | 8 +- .../java/haveno/desktop/maker/OfferMaker.java | 6 +- 15 files changed, 127 insertions(+), 119 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java index 5e53ff64e4..30775633e2 100644 --- a/core/src/main/java/haveno/core/api/CoreTradesService.java +++ b/core/src/main/java/haveno/core/api/CoreTradesService.java @@ -125,15 +125,12 @@ class CoreTradesService { // adjust amount for fixed-price offer (based on TakeOfferViewModel) String currencyCode = offer.getCurrencyCode(); OfferDirection direction = offer.getOfferPayload().getDirection(); - long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit()); + BigInteger maxAmount = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit()); if (offer.getPrice() != null) { if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) { - amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit); - } else if (offer.isTraditionalOffer() - && !amount.equals(offer.getMinAmount()) && !amount.equals(amount)) { - // We only apply the rounding if the amount is variable (minAmount is lower as amount). - // Otherwise we could get an amount lower then the minAmount set by rounding - amount = CoinUtil.getRoundedAmount(amount, offer.getPrice(), maxTradeLimit, offer.getCurrencyCode(), offer.getPaymentMethodId()); + amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), offer.getMinAmount(), maxAmount); + } else if (offer.isTraditionalOffer() && offer.isRange()) { + amount = CoinUtil.getRoundedAmount(amount, offer.getPrice(), offer.getMinAmount(), maxAmount, offer.getCounterCurrencyCode(), offer.getPaymentMethodId()); } } diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index 161bf69a16..89f1149447 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -158,8 +158,8 @@ public class CreateOfferService { } // adjust amount and min amount - amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); - minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); + amount = CoinUtil.getRoundedAmount(amount, fixedPrice, minAmount, amount, currencyCode, paymentAccount.getPaymentMethod().getId()); + minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, minAmount, amount, currencyCode, paymentAccount.getPaymentMethod().getId()); // generate one-time challenge for private offer String challenge = null; @@ -184,7 +184,7 @@ public class CreateOfferService { List acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount); long maxTradePeriod = paymentAccount.getMaxTradePeriod(); boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit; - long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit); + long maxTradeLimitAsLong = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit).longValueExact(); boolean useAutoClose = false; boolean useReOpenAfterAutoClose = false; long lowerClosePrice = 0; @@ -221,7 +221,7 @@ public class CreateOfferService { acceptedBanks, Version.VERSION, xmrWalletService.getHeight(), - maxTradeLimit, + maxTradeLimitAsLong, maxTradePeriod, useAutoClose, useReOpenAfterAutoClose, diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index 8df8511b3a..4ddcaa21a5 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -40,6 +40,7 @@ import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import haveno.core.util.VolumeUtil; +import haveno.core.util.coin.CoinUtil; import haveno.network.p2p.NodeAddress; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyStringProperty; @@ -251,12 +252,13 @@ public class Offer implements NetworkPayload, PersistablePayload { } @Nullable - public Volume getVolumeByAmount(BigInteger amount) { + public Volume getVolumeByAmount(BigInteger amount, BigInteger minAmount, BigInteger maxAmount) { Price price = getPrice(); if (price == null || amount == null) { return null; } - Volume volumeByAmount = price.getVolumeByAmount(amount); + BigInteger adjustedAmount = CoinUtil.getRoundedAmount(amount, price, minAmount, maxAmount, getCounterCurrencyCode(), getPaymentMethodId()); + Volume volumeByAmount = price.getVolumeByAmount(adjustedAmount); volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, getPaymentMethod().getId()); return volumeByAmount; @@ -385,12 +387,12 @@ public class Offer implements NetworkPayload, PersistablePayload { @Nullable public Volume getVolume() { - return getVolumeByAmount(getAmount()); + return getVolumeByAmount(getAmount(), getMinAmount(), getAmount()); } @Nullable public Volume getMinVolume() { - return getVolumeByAmount(getMinAmount()); + return getVolumeByAmount(getMinAmount(), getMinAmount(), getAmount()); } public boolean isBuyOffer() { diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java index 2e2644630a..8f7c1957aa 100644 --- a/core/src/main/java/haveno/core/offer/OfferUtil.java +++ b/core/src/main/java/haveno/core/offer/OfferUtil.java @@ -120,13 +120,13 @@ public class OfferUtil { return direction == OfferDirection.BUY; } - public long getMaxTradeLimit(PaymentAccount paymentAccount, + public BigInteger getMaxTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) { - return paymentAccount != null + return BigInteger.valueOf(paymentAccount != null ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit) - : 0; + : 0); } /** diff --git a/core/src/main/java/haveno/core/util/coin/CoinUtil.java b/core/src/main/java/haveno/core/util/coin/CoinUtil.java index 6c163ede6a..eef242da7c 100644 --- a/core/src/main/java/haveno/core/util/coin/CoinUtil.java +++ b/core/src/main/java/haveno/core/util/coin/CoinUtil.java @@ -75,19 +75,19 @@ public class CoinUtil { return BigDecimal.valueOf(percent).multiply(new BigDecimal(amount)).setScale(8, RoundingMode.DOWN).toBigInteger(); } - public static BigInteger getRoundedAmount(BigInteger amount, Price price, Long maxTradeLimit, String currencyCode, String paymentMethodId) { + public static BigInteger getRoundedAmount(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount, String currencyCode, String paymentMethodId) { if (price != null) { if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) { - return getRoundedAtmCashAmount(amount, price, maxTradeLimit); + return getRoundedAtmCashAmount(amount, price, minAmount, maxAmount); } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) { - return getRoundedAmountUnit(amount, price, maxTradeLimit); + return getRoundedAmountUnit(amount, price, minAmount, maxAmount); } } - return getRoundedAmount4Decimals(amount, maxTradeLimit); + return getRoundedAmount4Decimals(amount); } - public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, Long maxTradeLimit) { - return getAdjustedAmount(amount, price, maxTradeLimit, 10); + public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount) { + return getAdjustedAmount(amount, price, minAmount, maxAmount, 10); } /** @@ -96,14 +96,15 @@ public class CoinUtil { * * @param amount Monero amount which is a candidate for getting rounded. * @param price Price used in relation to that amount. - * @param maxTradeLimit The max. trade limit of the users account, in atomic units. + * @param minAmount The minimum amount. + * @param maxAmount The maximum amount. * @return The adjusted amount */ - public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, Long maxTradeLimit) { - return getAdjustedAmount(amount, price, maxTradeLimit, 1); + public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount) { + return getAdjustedAmount(amount, price, minAmount, maxAmount, 1); } - public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Long maxTradeLimit) { + public static BigInteger getRoundedAmount4Decimals(BigInteger amount) { DecimalFormat decimalFormat = new DecimalFormat("#.####", HavenoUtils.DECIMAL_FORMAT_SYMBOLS); double roundedXmrAmount = Double.parseDouble(decimalFormat.format(HavenoUtils.atomicUnitsToXmr(amount))); return HavenoUtils.xmrToAtomicUnits(roundedXmrAmount); @@ -115,44 +116,40 @@ public class CoinUtil { * * @param amount amount which is a candidate for getting rounded. * @param price Price used in relation to that amount. - * @param maxTradeLimit The max. trade limit of the users account, in satoshis. + * @param minAmount The minimum amount. + * @param maxAmount The maximum amount. * @param factor The factor used for rounding. E.g. 1 means rounded to units of * 1 EUR, 10 means rounded to 10 EUR, etc. * @return The adjusted amount */ @VisibleForTesting - static BigInteger getAdjustedAmount(BigInteger amount, Price price, Long maxTradeLimit, int factor) { + static BigInteger getAdjustedAmount(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount, int factor) { checkArgument( amount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(), - "amount needs to be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr" + "amount needs to be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(amount) + " xmr" + ); + if (minAmount == null) minAmount = Restrictions.getMinTradeAmount(); + checkArgument( + minAmount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(), + "minAmount needs to be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(minAmount) + " xmr" ); checkArgument( factor > 0, "factor needs to be positive" ); - // Amount must result in a volume of min factor units of the fiat currency, e.g. 1 EUR or - // 10 EUR in case of HalCash. + + // Amount must result in a volume of min factor units of the fiat currency, e.g. 1 EUR or 10 EUR in case of HalCash. Volume smallestUnitForVolume = Volume.parse(String.valueOf(factor), price.getCurrencyCode()); - if (smallestUnitForVolume.getValue() <= 0) - return BigInteger.ZERO; - + if (smallestUnitForVolume.getValue() <= 0) return BigInteger.ZERO; BigInteger smallestUnitForAmount = price.getAmountByVolume(smallestUnitForVolume); - long minTradeAmount = Restrictions.getMinTradeAmount().longValueExact(); - - checkArgument( - minTradeAmount >= Restrictions.getMinTradeAmount().longValueExact(), - "MinTradeAmount must be at least " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr" - ); - smallestUnitForAmount = BigInteger.valueOf(Math.max(minTradeAmount, smallestUnitForAmount.longValueExact())); - // We don't allow smaller amount values than smallestUnitForAmount - boolean useSmallestUnitForAmount = amount.compareTo(smallestUnitForAmount) < 0; + smallestUnitForAmount = BigInteger.valueOf(Math.max(minAmount.longValueExact(), smallestUnitForAmount.longValueExact())); // We get the adjusted volume from our amount + boolean useSmallestUnitForAmount = amount.compareTo(smallestUnitForAmount) < 0; Volume volume = useSmallestUnitForAmount ? getAdjustedVolumeUnit(price.getVolumeByAmount(smallestUnitForAmount), factor) : getAdjustedVolumeUnit(price.getVolumeByAmount(amount), factor); - if (volume.getValue() <= 0) - return BigInteger.ZERO; + if (volume.getValue() <= 0) return BigInteger.ZERO; // From that adjusted volume we calculate back the amount. It might be a bit different as // the amount used as input before due rounding. @@ -161,15 +158,23 @@ public class CoinUtil { // For the amount we allow only 4 decimal places long adjustedAmount = HavenoUtils.centinerosToAtomicUnits(Math.round(HavenoUtils.atomicUnitsToCentineros(amountByVolume) / 10000d) * 10000).longValueExact(); - // If we are above our trade limit we reduce the amount by the smallestUnitForAmount + // If we are below the minAmount we increase the amount by the smallestUnitForAmount BigInteger smallestUnitForAmountUnadjusted = price.getAmountByVolume(smallestUnitForVolume); - if (maxTradeLimit != null) { - while (adjustedAmount > maxTradeLimit) { + if (minAmount != null) { + while (adjustedAmount < minAmount.longValueExact()) { + adjustedAmount += smallestUnitForAmountUnadjusted.longValueExact(); + } + } + + // If we are above our trade limit we reduce the amount by the smallestUnitForAmount + if (maxAmount != null) { + while (adjustedAmount > maxAmount.longValueExact()) { adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact(); } } - adjustedAmount = Math.max(minTradeAmount, adjustedAmount); - if (maxTradeLimit != null) adjustedAmount = Math.min(maxTradeLimit, adjustedAmount); + + adjustedAmount = Math.max(minAmount.longValueExact(), adjustedAmount); + if (maxAmount != null) adjustedAmount = Math.min(maxAmount.longValueExact(), adjustedAmount); return BigInteger.valueOf(adjustedAmount); } } diff --git a/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java b/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java index eb8e2e124d..1a1a33435c 100644 --- a/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java +++ b/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java @@ -76,7 +76,8 @@ public class CoinUtilTest { BigInteger result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), - HavenoUtils.xmrToAtomicUnits(0.2).longValueExact(), + HavenoUtils.xmrToAtomicUnits(0.1), + HavenoUtils.xmrToAtomicUnits(0.2), 1); assertEquals( HavenoUtils.formatXmr(Restrictions.MIN_TRADE_AMOUNT, true), @@ -88,12 +89,13 @@ public class CoinUtilTest { CoinUtil.getAdjustedAmount( BigInteger.ZERO, Price.valueOf("USD", 1000_0000), - HavenoUtils.xmrToAtomicUnits(0.2).longValueExact(), + HavenoUtils.xmrToAtomicUnits(0.1), + HavenoUtils.xmrToAtomicUnits(0.2), 1); fail("Expected IllegalArgumentException to be thrown when amount is too low."); } catch (IllegalArgumentException iae) { assertEquals( - "amount needs to be above minimum of 0.1 xmr", + "amount needs to be above minimum of 0.1 xmr but was 0.0 xmr", iae.getMessage(), "Unexpected exception message." ); @@ -102,7 +104,8 @@ public class CoinUtilTest { result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), - HavenoUtils.xmrToAtomicUnits(0.2).longValueExact(), + HavenoUtils.xmrToAtomicUnits(0.1), + HavenoUtils.xmrToAtomicUnits(0.2), 1); assertEquals( "0.10 XMR", @@ -113,7 +116,8 @@ public class CoinUtilTest { result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), - HavenoUtils.xmrToAtomicUnits(0.25).longValueExact(), + HavenoUtils.xmrToAtomicUnits(0.1), + HavenoUtils.xmrToAtomicUnits(0.25), 1); assertEquals( "0.10 XMR", @@ -121,18 +125,14 @@ public class CoinUtilTest { "Minimum trade amount allowed should respect maxTradeLimit and factor, if possible." ); - // TODO(chirhonul): The following seems like it should raise an exception or otherwise fail. - // We are asking for the smallest allowed BTC trade when price is 1000 USD each, and the - // max trade limit is 5k sat = 0.00005 BTC. But the returned amount 0.00005 BTC, or - // 0.05 USD worth, which is below the factor of 1 USD, but does respect the maxTradeLimit. - // Basically the given constraints (maxTradeLimit vs factor) are impossible to both fulfill.. result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), - HavenoUtils.xmrToAtomicUnits(0.00005).longValueExact(), + HavenoUtils.xmrToAtomicUnits(0.1), + HavenoUtils.xmrToAtomicUnits(0.5), 1); assertEquals( - "0.00005 XMR", + "0.10 XMR", HavenoUtils.formatXmr(result, true), "Minimum trade amount allowed with low maxTradeLimit should still respect that limit, even if result does not respect the factor specified." ); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index 47bfee0006..2aaf7e46b5 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -333,7 +333,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { setSuggestedSecurityDeposit(getPaymentAccount()); if (amount.get() != null && this.allowAmountUpdate) - this.amount.set(amount.get().min(BigInteger.valueOf(getMaxTradeLimit()))); + this.amount.set(amount.get().min(getMaxTradeLimit())); } } @@ -472,17 +472,17 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return marketPriceMarginPct; } - long getMaxTradeLimit() { + BigInteger getMaxTradeLimit() { // disallow offers which no buyer can take due to trade limits on release if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), OfferDirection.BUY, buyerAsTakerWithoutDeposit.get()); + return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), OfferDirection.BUY, buyerAsTakerWithoutDeposit.get())); } if (paymentAccount != null) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction, buyerAsTakerWithoutDeposit.get()); + return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction, buyerAsTakerWithoutDeposit.get())); } else { - return 0; + return BigInteger.ZERO; } } @@ -543,9 +543,11 @@ public abstract class MutableOfferDataModel extends OfferDataModel { // if the volume != amount * price, we need to adjust the amount if (amount.get() == null || !volumeBefore.equals(price.get().getVolumeByAmount(amount.get()))) { BigInteger value = price.get().getAmountByVolume(volumeBefore); - value = value.min(BigInteger.valueOf(getMaxTradeLimit())); // adjust if above maximum - value = value.max(Restrictions.getMinTradeAmount()); // adjust if below minimum - value = CoinUtil.getRoundedAmount(value, price.get(), getMaxTradeLimit(), tradeCurrencyCode.get(), paymentAccount.getPaymentMethod().getId()); + BigInteger maxAmount = getMaxTradeLimit(); + BigInteger minAmount = Restrictions.getMinTradeAmount(); + value = value.min(maxAmount); // adjust if above maximum + value = value.max(minAmount); // adjust if below minimum + value = CoinUtil.getRoundedAmount(value, price.get(), minAmount, maxAmount, tradeCurrencyCode.get(), paymentAccount.getPaymentMethod().getId()); amount.set(value); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index c29781c3bf..3dc2849872 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -492,7 +492,7 @@ public abstract class MutableOfferViewModel ext buyerAsTakerWithoutDepositListener = (ov, oldValue, newValue) -> { if (dataModel.paymentAccount != null) xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); if (amount.get() != null) amountValidationResult.set(isXmrInputValid(amount.get())); updateSecurityDeposit(); setSecurityDepositToModel(); @@ -610,7 +610,7 @@ public abstract class MutableOfferViewModel ext } if (dataModel.paymentAccount != null) xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); xmrValidator.setMinValue(Restrictions.getMinTradeAmount()); final boolean isBuy = dataModel.getDirection() == OfferDirection.BUY; @@ -700,7 +700,7 @@ public abstract class MutableOfferViewModel ext amountValidationResult.set(isXmrInputValid(amount.get())); xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); securityDepositValidator.setPaymentAccount(paymentAccount); } @@ -1199,10 +1199,10 @@ public abstract class MutableOfferViewModel ext if (amount.get() != null && !amount.get().isEmpty()) { BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), xmrFormatter)); - long maxTradeLimit = dataModel.getMaxTradeLimit(); + BigInteger maxTradeLimit = dataModel.getMaxTradeLimit(); Price price = dataModel.getPrice().get(); if (price != null && price.isPositive()) { - amount = CoinUtil.getRoundedAmount(amount, price, maxTradeLimit, tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); + amount = CoinUtil.getRoundedAmount(amount, price, dataModel.getMinAmount().get(), maxTradeLimit, tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); } dataModel.setAmount(amount); if (syncMinAmountWithAmount || @@ -1221,9 +1221,9 @@ public abstract class MutableOfferViewModel ext BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), xmrFormatter)); Price price = dataModel.getPrice().get(); - long maxTradeLimit = dataModel.getMaxTradeLimit(); + BigInteger maxTradeLimit = dataModel.getMaxTradeLimit(); if (price != null && price.isPositive()) { - minAmount = CoinUtil.getRoundedAmount(minAmount, price, maxTradeLimit, tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); + minAmount = CoinUtil.getRoundedAmount(minAmount, price, dataModel.getMinAmount().get(), maxTradeLimit, tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); } dataModel.setMinAmount(minAmount); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 1a548f1b1f..590a9a2c31 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -183,7 +183,7 @@ class TakeOfferDataModel extends OfferDataModel { checkArgument(!possiblePaymentAccounts.isEmpty(), "possiblePaymentAccounts.isEmpty()"); paymentAccount = getLastSelectedPaymentAccount(); - this.amount.set(BigInteger.valueOf(getMaxTradeLimit())); + this.amount.set(getMaxTradeLimit()); updateSecurityDeposit(); @@ -293,7 +293,7 @@ class TakeOfferDataModel extends OfferDataModel { if (paymentAccount != null) { this.paymentAccount = paymentAccount; - this.amount.set(BigInteger.valueOf(getMaxTradeLimit())); + this.amount.set(getMaxTradeLimit()); preferences.setTakeOfferSelectedPaymentAccountId(paymentAccount.getId()); } @@ -338,17 +338,17 @@ class TakeOfferDataModel extends OfferDataModel { .orElse(firstItem); } - long getMyMaxTradeLimit() { + BigInteger getMyMaxTradeLimit() { if (paymentAccount != null) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, getCurrencyCode(), - offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); + return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, getCurrencyCode(), + offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit())); } else { - return 0; + return BigInteger.ZERO; } } - long getMaxTradeLimit() { - return Math.min(offer.getAmount().longValueExact(), getMyMaxTradeLimit()); + BigInteger getMaxTradeLimit() { + return offer.getAmount().min(getMyMaxTradeLimit()); } boolean canTakeOffer() { @@ -388,7 +388,7 @@ class TakeOfferDataModel extends OfferDataModel { } void maybeApplyAmount(BigInteger amount) { - if (amount.compareTo(offer.getMinAmount()) >= 0 && amount.compareTo(BigInteger.valueOf(getMaxTradeLimit())) <= 0) { + if (amount.compareTo(offer.getMinAmount()) >= 0 && amount.compareTo(getMaxTradeLimit()) <= 0) { this.amount.set(amount); } calculateTotalToPay(); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java index 1ad0f9547a..9e1b4c8441 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java @@ -208,7 +208,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im errorMessage.set(offer.getErrorMessage()); xmrValidator.setMaxValue(offer.getAmount()); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); xmrValidator.setMinValue(offer.getMinAmount()); } @@ -237,7 +237,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im public void onPaymentAccountSelected(PaymentAccount paymentAccount) { dataModel.onPaymentAccountSelected(paymentAccount); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); updateButtonDisableState(); } @@ -297,12 +297,13 @@ class TakeOfferViewModel extends ActivatableWithDataModel im calculateVolume(); Price tradePrice = dataModel.tradePrice; - long maxTradeLimit = dataModel.getMaxTradeLimit(); + BigInteger minAmount = dataModel.getOffer().getMinAmount(); + BigInteger maxAmount = dataModel.getMaxTradeLimit(); if (PaymentMethod.isRoundedForAtmCash(dataModel.getPaymentMethod().getId())) { - BigInteger adjustedAmountForAtm = CoinUtil.getRoundedAtmCashAmount(dataModel.getAmount().get(), tradePrice, maxTradeLimit); + BigInteger adjustedAmountForAtm = CoinUtil.getRoundedAtmCashAmount(dataModel.getAmount().get(), tradePrice, minAmount, maxAmount); dataModel.maybeApplyAmount(adjustedAmountForAtm); - } else if (dataModel.getOffer().isTraditionalOffer()) { - BigInteger roundedAmount = CoinUtil.getRoundedAmount(dataModel.getAmount().get(), tradePrice, maxTradeLimit, dataModel.getOffer().getCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); + } else if (dataModel.getOffer().isTraditionalOffer() && dataModel.getOffer().isRange()) { + BigInteger roundedAmount = CoinUtil.getRoundedAmount(dataModel.getAmount().get(), tradePrice, minAmount, maxAmount, dataModel.getOffer().getCounterCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); dataModel.maybeApplyAmount(roundedAmount); } amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); @@ -568,13 +569,14 @@ class TakeOfferViewModel extends ActivatableWithDataModel im private void setAmountToModel() { if (amount.get() != null && !amount.get().isEmpty()) { BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), xmrFormatter)); - long maxTradeLimit = dataModel.getMaxTradeLimit(); + BigInteger minAmount = dataModel.getOffer().getMinAmount(); + BigInteger maxAmount = dataModel.getMaxTradeLimit(); Price price = dataModel.tradePrice; if (price != null) { if (dataModel.isRoundedForAtmCash()) { - amount = CoinUtil.getRoundedAtmCashAmount(amount, price, maxTradeLimit); - } else if (dataModel.getOffer().isTraditionalOffer()) { - amount = CoinUtil.getRoundedAmount(amount, price, maxTradeLimit, dataModel.getOffer().getCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); + amount = CoinUtil.getRoundedAtmCashAmount(amount, price, minAmount, maxAmount); + } else if (dataModel.getOffer().isTraditionalOffer() && dataModel.getOffer().isRange()) { + amount = CoinUtil.getRoundedAmount(amount, price, minAmount, maxAmount, dataModel.getOffer().getCounterCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); } } dataModel.maybeApplyAmount(amount); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index b885726cf4..c414df2222 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -239,7 +239,7 @@ public class OfferDetailsWindow extends Overlay { HavenoUtils.formatXmr(tradeAmount, true)); addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, VolumeUtil.formatVolumeLabel(currencyCode) + counterCurrencyDirectionInfo, - VolumeUtil.formatVolumeWithCode(offer.getVolumeByAmount(tradeAmount))); + VolumeUtil.formatVolumeWithCode(offer.getVolumeByAmount(tradeAmount, offer.getMinAmount(), tradeAmount))); } else { addConfirmationLabelLabel(gridPane, ++rowIndex, amount + xmrDirectionInfo, HavenoUtils.formatXmr(offer.getAmount(), true)); diff --git a/desktop/src/test/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModelTest.java index 0aa4b545a7..09f9c466c4 100644 --- a/desktop/src/test/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModelTest.java @@ -88,7 +88,7 @@ public class OfferBookChartViewModelTest { final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); - assertEquals(7, model.maxPlacesForBuyPrice.intValue()); + assertEquals(9, model.maxPlacesForBuyPrice.intValue()); offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.price, 940164750000L)))); assertEquals(9, model.maxPlacesForBuyPrice.intValue()); // 9401.6475 offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.price, 1010164750000L)))); @@ -117,11 +117,11 @@ public class OfferBookChartViewModelTest { final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); - assertEquals(1, model.maxPlacesForBuyVolume.intValue()); //0 + assertEquals(3, model.maxPlacesForBuyVolume.intValue()); //0 offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.amount, 1000000000000L)))); - assertEquals(2, model.maxPlacesForBuyVolume.intValue()); //10 + assertEquals(4, model.maxPlacesForBuyVolume.intValue()); //10 offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.amount, 221286000000000L)))); - assertEquals(4, model.maxPlacesForBuyVolume.intValue()); //2213 + assertEquals(6, model.maxPlacesForBuyVolume.intValue()); //2213 } @Test @@ -166,7 +166,7 @@ public class OfferBookChartViewModelTest { final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); - assertEquals(7, model.maxPlacesForSellPrice.intValue()); // 10.0000 default price + assertEquals(9, model.maxPlacesForSellPrice.intValue()); // 10.0000 default price offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.price, 940164750000L)))); assertEquals(9, model.maxPlacesForSellPrice.intValue()); // 9401.6475 offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.price, 1010164750000L)))); @@ -195,10 +195,10 @@ public class OfferBookChartViewModelTest { final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); - assertEquals(1, model.maxPlacesForSellVolume.intValue()); //0 + assertEquals(3, model.maxPlacesForSellVolume.intValue()); //0 offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.amount, 1000000000000L)))); - assertEquals(2, model.maxPlacesForSellVolume.intValue()); //10 + assertEquals(4, model.maxPlacesForSellVolume.intValue()); //10 offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.amount, 221286000000000L)))); - assertEquals(4, model.maxPlacesForSellVolume.intValue()); //2213 + assertEquals(6, model.maxPlacesForSellVolume.intValue()); //2213 } } diff --git a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookListItemMaker.java b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookListItemMaker.java index eea8787a2f..cb98dcd763 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookListItemMaker.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookListItemMaker.java @@ -42,9 +42,9 @@ public class OfferBookListItemMaker { public static final Instantiator OfferBookListItem = lookup -> new OfferBookListItem(make(xmrUsdOffer.but( - with(OfferMaker.price, lookup.valueOf(price, 1000000000L)), - with(OfferMaker.amount, lookup.valueOf(amount, 1000000000L)), - with(OfferMaker.minAmount, lookup.valueOf(amount, 1000000000L)), + with(OfferMaker.price, lookup.valueOf(price, 100000000000L)), + with(OfferMaker.amount, lookup.valueOf(amount, 100000000000L)), + with(OfferMaker.minAmount, lookup.valueOf(amount, 100000000000L)), with(OfferMaker.direction, lookup.valueOf(direction, OfferDirection.BUY)), with(OfferMaker.useMarketBasedPrice, lookup.valueOf(useMarketBasedPrice, false)), with(OfferMaker.marketPriceMargin, lookup.valueOf(marketPriceMargin, 0.0)), @@ -56,8 +56,8 @@ public class OfferBookListItemMaker { public static final Instantiator OfferBookListItemWithRange = lookup -> new OfferBookListItem(make(xmrUsdOffer.but( MakeItEasy.with(OfferMaker.price, lookup.valueOf(price, 100000L)), - with(OfferMaker.minAmount, lookup.valueOf(minAmount, 1000000000L)), - with(OfferMaker.amount, lookup.valueOf(amount, 2000000000L))))); + with(OfferMaker.minAmount, lookup.valueOf(minAmount, 100000000000L)), + with(OfferMaker.amount, lookup.valueOf(amount, 200000000000L))))); public static final Maker xmrBuyItem = a(OfferBookListItem); public static final Maker xmrSellItem = a(OfferBookListItem, with(direction, OfferDirection.SELL)); diff --git a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java index b7cc852ee1..dfc68c4739 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java @@ -310,9 +310,9 @@ public class OfferBookViewModelTest { null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); - assertEquals(5, model.maxPlacesForVolume.intValue()); - offerBookListItems.addAll(make(xmrBuyItem.but(with(amount, 20000000000000L)))); assertEquals(7, model.maxPlacesForVolume.intValue()); + offerBookListItems.addAll(make(xmrBuyItem.but(with(amount, 20000000000000L)))); + assertEquals(9, model.maxPlacesForVolume.intValue()); } @Test @@ -360,7 +360,7 @@ public class OfferBookViewModelTest { null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); - assertEquals(7, model.maxPlacesForPrice.intValue()); + assertEquals(9, model.maxPlacesForPrice.intValue()); offerBookListItems.addAll(make(xmrBuyItem.but(with(price, 1495582400000L)))); //149558240 assertEquals(10, model.maxPlacesForPrice.intValue()); offerBookListItems.addAll(make(xmrBuyItem.but(with(price, 149558240000L)))); //149558240 @@ -453,7 +453,7 @@ public class OfferBookViewModelTest { assertEquals("12557.2046", model.getPrice(lowItem)); assertEquals("(1.00%)", model.getPriceAsPercentage(lowItem)); - assertEquals("10.0000", model.getPrice(fixedItem)); + assertEquals("1000.0000", model.getPrice(fixedItem)); offerBookListItems.addAll(item); assertEquals("14206.1304", model.getPrice(item)); assertEquals("(-12.00%)", model.getPriceAsPercentage(item)); diff --git a/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java b/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java index 2496dbdbbd..28c2f52739 100644 --- a/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java +++ b/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java @@ -80,8 +80,8 @@ public class OfferMaker { lookup.valueOf(price, 100000L), lookup.valueOf(marketPriceMargin, 0.0), lookup.valueOf(useMarketBasedPrice, false), - lookup.valueOf(amount, 100000L), - lookup.valueOf(minAmount, 100000L), + lookup.valueOf(amount, 100000000000L), + lookup.valueOf(minAmount, 100000000000L), lookup.valueOf(makerFeePct, .0015), lookup.valueOf(takerFeePct, .0075), lookup.valueOf(penaltyFeePct, 0.03), @@ -97,7 +97,7 @@ public class OfferMaker { }}), null, null, - "2", + "3", lookup.valueOf(blockHeight, 700000L), lookup.valueOf(tradeLimit, 0L), lookup.valueOf(maxTradePeriod, 0L), From 8f505ab17bee70afda9c1407d503c58ae1716cb3 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 8 Jul 2025 09:29:52 -0400 Subject: [PATCH 357/371] enable volume input when taking range offer --- .../main/offer/MutableOfferViewModel.java | 1 - .../offer/takeoffer/TakeOfferDataModel.java | 35 ++++++ .../main/offer/takeoffer/TakeOfferView.java | 51 ++++---- .../offer/takeoffer/TakeOfferViewModel.java | 110 +++++++++++++++++- 4 files changed, 168 insertions(+), 29 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index 3dc2849872..e563af3601 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -207,7 +207,6 @@ public abstract class MutableOfferViewModel ext @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter, OfferUtil offerUtil) { super(dataModel); - this.fiatVolumeValidator = fiatVolumeValidator; this.amountValidator4Decimals = amountValidator4Decimals; this.amountValidator8Decimals = amountValidator8Decimals; diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 590a9a2c31..e8da31e3bf 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -41,6 +41,7 @@ import haveno.core.trade.handlers.TradeResultHandler; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.VolumeUtil; +import haveno.core.util.coin.CoinUtil; import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; @@ -59,6 +60,7 @@ import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.Set; +import java.util.function.Predicate; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @@ -91,7 +93,10 @@ class TakeOfferDataModel extends OfferDataModel { private XmrBalanceListener balanceListener; private PaymentAccount paymentAccount; private boolean isTabSelected; + protected boolean allowAmountUpdate = true; Price tradePrice; + private final Predicate isNonZeroPrice = (p) -> p != null && !p.isZero(); + private final Predicate> isNonZeroVolume = (v) -> v.get() != null && !v.get().isZero(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -317,6 +322,10 @@ class TakeOfferDataModel extends OfferDataModel { return offer; } + ReadOnlyObjectProperty getVolume() { + return volume; + } + ObservableList getPossiblePaymentAccounts() { Set paymentAccounts = user.getPaymentAccounts(); checkNotNull(paymentAccounts, "paymentAccounts must not be null"); @@ -387,6 +396,32 @@ class TakeOfferDataModel extends OfferDataModel { } } + void calculateAmount() { + if (isNonZeroPrice.test(tradePrice) && isNonZeroVolume.test(volume) && allowAmountUpdate) { + try { + Volume volumeBefore = volume.get(); + calculateVolume(); + + // if the volume != amount * price, we need to adjust the amount + if (amount.get() == null || !volumeBefore.equals(tradePrice.getVolumeByAmount(amount.get()))) { + BigInteger value = tradePrice.getAmountByVolume(volumeBefore); + value = value.min(offer.getAmount()); // adjust if above maximum + value = value.max(offer.getMinAmount()); // adjust if below minimum + value = CoinUtil.getRoundedAmount(value, tradePrice, offer.getMinAmount(), getMaxTradeLimit(), offer.getCounterCurrencyCode(), paymentAccount.getPaymentMethod().getId()); + amount.set(value); + } + + calculateTotalToPay(); + } catch (Throwable t) { + log.error(t.toString()); + } + } + } + + protected void setVolume(Volume volume) { + this.volume.set(volume); + } + void maybeApplyAmount(BigInteger amount) { if (amount.compareTo(offer.getMinAmount()) >= 0 && amount.compareTo(getMaxTradeLimit()) <= 0) { this.amount.set(amount); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index 73b3aa526e..bf929d6f3b 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -144,9 +144,9 @@ public class TakeOfferView extends ActivatableViewAndModel missingCoinListener; @@ -170,7 +170,7 @@ public class TakeOfferView extends ActivatableViewAndModel amountFocusedListener, getShowWalletFundedNotificationListener; + private ChangeListener amountFocusedListener, volumeFocusedListener, getShowWalletFundedNotificationListener; private InfoInputTextField volumeInfoTextField; @@ -208,21 +208,6 @@ public class TakeOfferView extends ActivatableViewAndModel { - model.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText()); - amountTextField.setText(model.amount.get()); - }; - - getShowWalletFundedNotificationListener = (observable, oldValue, newValue) -> { - if (newValue) { - Notification walletFundedNotification = new Notification() - .headLine(Res.get("notification.walletUpdate.headline")) - .notification(Res.get("notification.walletUpdate.msg", HavenoUtils.formatXmr(model.dataModel.getTotalToPay().get(), true))) - .autoClose(); - - walletFundedNotification.show(); - } - }; GUIUtil.focusWhenAddedToScene(amountTextField); } @@ -342,6 +327,7 @@ public class TakeOfferView extends ActivatableViewAndModel CurrencyUtil.getCounterCurrency(model.dataModel.getCurrencyCode()))); priceAsPercentageLabel.prefWidthProperty().bind(priceCurrencyLabel.widthProperty()); nextButton.disableProperty().bind(model.isNextButtonDisabled); @@ -703,10 +690,10 @@ public class TakeOfferView extends ActivatableViewAndModel { + showWarningInvalidXmrDecimalPlacesSubscription = EasyBind.subscribe(model.showWarningInvalidXmrDecimalPlaces, newValue -> { if (newValue) { new Popup().warning(Res.get("takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces")).show(); - model.showWarningInvalidBtcDecimalPlaces.set(false); + model.showWarningInvalidXmrDecimalPlaces.set(false); } }); @@ -742,13 +729,31 @@ public class TakeOfferView extends ActivatableViewAndModel { + model.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText()); + amountTextField.setText(model.amount.get()); + }; + getShowWalletFundedNotificationListener = (observable, oldValue, newValue) -> { + if (newValue) { + Notification walletFundedNotification = new Notification() + .headLine(Res.get("notification.walletUpdate.headline")) + .notification(Res.get("notification.walletUpdate.msg", HavenoUtils.formatXmr(model.dataModel.getTotalToPay().get(), true))) + .autoClose(); + + walletFundedNotification.show(); + } + }; + volumeFocusedListener = (o, oldValue, newValue) -> { + model.onFocusOutVolumeTextField(oldValue, newValue); + volumeTextField.setText(model.volume.get()); + }; missingCoinListener = (observable, oldValue, newValue) -> { if (!newValue.toString().equals("")) { updateQrCode(); @@ -758,12 +763,14 @@ public class TakeOfferView extends ActivatableViewAndModel im private final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final CoinFormatter xmrFormatter; + private final FiatVolumeValidator fiatVolumeValidator; + private final AmountValidator4Decimals amountValidator4Decimals; + private final AmountValidator8Decimals amountValidator8Decimals; private String amountRange; private String paymentLabel; - private boolean takeOfferRequested; + private boolean takeOfferRequested, ignoreVolumeStringListener; private Trade trade; - private Offer offer; + protected Offer offer; private String price; private String amountDescription; @@ -101,15 +110,18 @@ class TakeOfferViewModel extends ActivatableWithDataModel im final BooleanProperty isTakeOfferButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty isNextButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty isWaitingForFunds = new SimpleBooleanProperty(); - final BooleanProperty showWarningInvalidBtcDecimalPlaces = new SimpleBooleanProperty(); + final BooleanProperty showWarningInvalidXmrDecimalPlaces = new SimpleBooleanProperty(); final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); final BooleanProperty takeOfferCompleted = new SimpleBooleanProperty(); final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty(); final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); private ChangeListener amountStrListener; private ChangeListener amountListener; + private ChangeListener volumeStringListener; + private ChangeListener volumeListener; private ChangeListener isWalletFundedListener; private ChangeListener tradeStateListener; private ChangeListener offerStateListener; @@ -124,6 +136,9 @@ class TakeOfferViewModel extends ActivatableWithDataModel im @Inject public TakeOfferViewModel(TakeOfferDataModel dataModel, + FiatVolumeValidator fiatVolumeValidator, + AmountValidator4Decimals amountValidator4Decimals, + AmountValidator8Decimals amountValidator8Decimals, OfferUtil offerUtil, XmrValidator btcValidator, P2PService p2PService, @@ -138,6 +153,9 @@ class TakeOfferViewModel extends ActivatableWithDataModel im this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.xmrFormatter = btcFormatter; + this.fiatVolumeValidator = fiatVolumeValidator; + this.amountValidator4Decimals = amountValidator4Decimals; + this.amountValidator8Decimals = amountValidator8Decimals; createListeners(); } @@ -210,6 +228,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel im xmrValidator.setMaxValue(offer.getAmount()); xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); xmrValidator.setMinValue(offer.getMinAmount()); + + setVolumeToModel(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -288,7 +308,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im InputValidator.ValidationResult result = isXmrInputValid(amount.get()); amountValidationResult.set(result); if (result.isValid) { - showWarningInvalidBtcDecimalPlaces.set(!DisplayUtils.hasBtcValidDecimals(userInput, xmrFormatter)); + if (userInput != null) showWarningInvalidXmrDecimalPlaces.set(!DisplayUtils.hasBtcValidDecimals(userInput, xmrFormatter)); // only allow max 4 decimal places for xmr values setAmountToModel(); // reformat input @@ -338,6 +358,30 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } } + void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) { + if (oldValue && !newValue) { + InputValidator.ValidationResult result = isVolumeInputValid(volume.get()); + volumeValidationResult.set(result); + if (result.isValid) { + setVolumeToModel(); + ignoreVolumeStringListener = true; + + Volume volume = dataModel.getVolume().get(); + if (volume != null) { + volume = VolumeUtil.getAdjustedVolume(volume, offer.getPaymentMethod().getId()); + this.volume.set(VolumeUtil.formatVolume(volume)); + } + + ignoreVolumeStringListener = false; + + dataModel.calculateAmount(); + + if (amount.get() != null) + amountValidationResult.set(isXmrInputValid(amount.get())); + } + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // States /////////////////////////////////////////////////////////////////////////////////////////// @@ -454,14 +498,12 @@ class TakeOfferViewModel extends ActivatableWithDataModel im /////////////////////////////////////////////////////////////////////////////////////////// private void addBindings() { - volume.bind(createStringBinding(() -> VolumeUtil.formatVolume(dataModel.volume.get()), dataModel.volume)); totalToPay.bind(createStringBinding(() -> HavenoUtils.formatXmr(dataModel.getTotalToPay().get(), true), dataModel.getTotalToPay())); } private void removeBindings() { volumeDescriptionLabel.unbind(); - volume.unbind(); totalToPay.unbind(); } @@ -475,10 +517,33 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } updateButtonDisableState(); }; + amountListener = (ov, oldValue, newValue) -> { amount.set(HavenoUtils.formatXmr(newValue)); applyTakerFee(); }; + + volumeStringListener = (ov, oldValue, newValue) -> { + if (!ignoreVolumeStringListener) { + if (isVolumeInputValid(newValue).isValid) { + setVolumeToModel(); + dataModel.calculateAmount(); + dataModel.calculateTotalToPay(); + } + updateButtonDisableState(); + } + }; + + volumeListener = (ov, oldValue, newValue) -> { + ignoreVolumeStringListener = true; + if (newValue != null) + volume.set(VolumeUtil.formatVolume(newValue)); + else + volume.set(""); + + ignoreVolumeStringListener = false; + }; + isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); tradeStateListener = (ov, oldValue, newValue) -> applyTradeState(); @@ -527,9 +592,11 @@ class TakeOfferViewModel extends ActivatableWithDataModel im // Bidirectional bindings are used for all input fields: amount, price, volume and minAmount // We do volume/amount calculation during input, so user has immediate feedback amount.addListener(amountStrListener); + volume.addListener(volumeStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().addListener(amountListener); + dataModel.getVolume().addListener(volumeListener); dataModel.getIsXmrWalletFunded().addListener(isWalletFundedListener); p2PService.getNetworkNode().addConnectionListener(connectionListener); @@ -541,9 +608,11 @@ class TakeOfferViewModel extends ActivatableWithDataModel im private void removeListeners() { amount.removeListener(amountStrListener); + volume.removeListener(volumeStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().removeListener(amountListener); + dataModel.getVolume().removeListener(volumeListener); dataModel.getIsXmrWalletFunded().removeListener(isWalletFundedListener); if (offer != null) { @@ -583,6 +652,35 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } } + private void setVolumeToModel() { + if (volume.get() != null && !volume.get().isEmpty()) { + try { + dataModel.setVolume(Volume.parse(volume.get(), offer.getCounterCurrencyCode())); + } catch (Throwable t) { + log.debug(t.getMessage()); + } + } else { + dataModel.setVolume(null); + } + } + + private InputValidator.ValidationResult isVolumeInputValid(String input) { + return getVolumeValidator().validate(input); + } + + // TODO: replace with VolumeUtils? + + private MonetaryValidator getVolumeValidator() { + final String code = offer.getCounterCurrencyCode(); + if (CurrencyUtil.isFiatCurrency(code)) { + return fiatVolumeValidator; + } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(code)) { + return amountValidator4Decimals; + } else { + return amountValidator8Decimals; + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// From 10347ae4884adbee190636a68f0ba2e7edbf2304 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 12 Jul 2025 10:43:23 -0400 Subject: [PATCH 358/371] restore last combo box selection on focus out --- .../components/AutocompleteComboBox.java | 33 ++++++++++++++++++- .../TraditionalAccountsView.java | 2 ++ .../main/market/trades/TradesChartsView.java | 8 ++--- .../main/offer/offerbook/OfferBookView.java | 4 +-- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java b/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java index e6701cdb4c..e6ef5dca7c 100644 --- a/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java +++ b/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java @@ -46,6 +46,7 @@ public class AutocompleteComboBox extends JFXComboBox { private List matchingList; private JFXComboBoxListViewSkin comboBoxListViewSkin; private boolean selectAllShortcut = false; + private T lastCommittedValue; public AutocompleteComboBox() { this(FXCollections.observableArrayList()); @@ -59,6 +60,30 @@ public class AutocompleteComboBox extends JFXComboBox { fixSpaceKey(); setAutocompleteItems(items); reactToQueryChanges(); + + // Store last committed value so we can restore it if nothing selected + valueProperty().addListener((obs, oldVal, newVal) -> { + if (newVal != null) + lastCommittedValue = newVal; + }); + + // Restore last committed value when editor loses focus if no matches + getEditor().focusedProperty().addListener((obs, wasFocused, isNowFocused) -> { + if (!isNowFocused) { + String input = getEditor().getText(); + T matched = getConverter().fromString(input); + + boolean matchFound = getItems().stream() + .anyMatch(item -> item.equals(matched)); + + if (!matchFound) { + UserThread.execute(() -> { + getSelectionModel().select(lastCommittedValue); + getEditor().setText(asString(lastCommittedValue)); + }); + } + } + }); } /** @@ -100,10 +125,16 @@ public class AutocompleteComboBox extends JFXComboBox { return; } - // Case 2: fire if the text is empty to support special "show all" case + // Case 2: fire if the text is empty if (inputText.isEmpty()) { eh.handle(e); getParent().requestFocus(); + + // Restore the last committed value + UserThread.execute(() -> { + getSelectionModel().select(lastCommittedValue); + getEditor().setText(asString(lastCommittedValue)); + }); } }); } diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java index 3a758f1657..b6e8a575bc 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java @@ -501,6 +501,8 @@ public class TraditionalAccountsView extends PaymentAccountsView { + if (paymentMethodComboBox.getEditor().getText().isEmpty()) + return; if (paymentMethodForm != null) { FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); diff --git a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java index bb75c80684..d087329942 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java @@ -116,15 +116,11 @@ public class TradesChartsView extends ActivatableViewAndModel currencyItem.codeDashNameString().equals(query)). findAny().orElse(null); } - - private CurrencyListItem specialShowAllItem() { - return comboBox.getItems().get(0); - } } private final User user; @@ -291,7 +287,7 @@ public class TradesChartsView extends ActivatableViewAndModel UserThread.execute(() -> { if (currencyComboBox.getEditor().getText().isEmpty()) - currencyComboBox.getSelectionModel().select(SHOW_ALL); + return; CurrencyListItem selectedItem = currencyComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { model.onSetTradeCurrency(selectedItem.tradeCurrency); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index d10e6b6562..2129f3c6dc 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -347,7 +347,7 @@ abstract public class OfferBookView { if (currencyComboBox.getEditor().getText().isEmpty()) - currencyComboBox.getSelectionModel().select(SHOW_ALL); + return; model.onSetTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()); paymentMethodComboBox.setAutocompleteItems(model.getPaymentMethods()); model.updateSelectedPaymentMethod(); @@ -500,7 +500,7 @@ abstract public class OfferBookView asString(item).equals(query)). findAny().orElse(null); From 4627960c05358259d8f292ae6f48d5db9089a454 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 12 Jul 2025 10:44:10 -0400 Subject: [PATCH 359/371] recover trade funds via dispute if deposit transaction dropped (#1838) --- .../haveno/core/api/CoreDisputesService.java | 12 ++++++++++++ .../core/support/dispute/DisputeManager.java | 9 +++++++++ .../overlays/windows/DisputeSummaryWindow.java | 16 ++-------------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreDisputesService.java b/core/src/main/java/haveno/core/api/CoreDisputesService.java index f4bb4c803d..ece5a816cc 100644 --- a/core/src/main/java/haveno/core/api/CoreDisputesService.java +++ b/core/src/main/java/haveno/core/api/CoreDisputesService.java @@ -241,12 +241,24 @@ public class CoreDisputesService { } else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) { disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7) disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO); + if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(trade.getWallet().getBalance()) > 0) { // in case peer's deposit transaction is not confirmed + log.warn("Payout amount for buyer is more than wallet's balance. Decreasing payout amount from {} to {}", + HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost()), + HavenoUtils.formatXmr(trade.getWallet().getBalance())); + disputeResult.setBuyerPayoutAmountBeforeCost(trade.getWallet().getBalance()); + } } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) { disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit)); } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) { disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit)); + if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(trade.getWallet().getBalance()) > 0) { // in case peer's deposit transaction is not confirmed + log.warn("Payout amount for seller is more than wallet's balance. Decreasing payout amount from {} to {}", + HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost()), + HavenoUtils.formatXmr(trade.getWallet().getBalance())); + disputeResult.setSellerPayoutAmountBeforeCost(trade.getWallet().getBalance()); + } } else if (payoutSuggestion == PayoutSuggestion.CUSTOM) { if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance"); long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact(); diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index ac0ede6e35..5d5cc84040 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -403,6 +403,15 @@ public abstract class DisputeManager> extends Sup chatMessage.setSystemMessage(true); dispute.addAndPersistChatMessage(chatMessage); + // export multisig hex if needed + if (trade.getSelf().getUpdatedMultisigHex() == null) { + try { + trade.exportMultisigHex(); + } catch (Exception e) { + log.error("Failed to export multisig hex", e); + } + } + // create dispute opened message NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java index 49931e202b..dc64af37d5 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -34,7 +34,6 @@ import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.mediation.MediationManager; -import haveno.core.support.dispute.refund.RefundManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; @@ -355,22 +354,11 @@ public class DisputeSummaryWindow extends Overlay { BigInteger sellerAmount = HavenoUtils.parseXmr(sellerPayoutAmountInputTextField.getText()); Contract contract = dispute.getContract(); BigInteger tradeAmount = contract.getTradeAmount(); - BigInteger available = tradeAmount + BigInteger expected = tradeAmount .add(trade.getBuyer().getSecurityDeposit()) .add(trade.getSeller().getSecurityDeposit()); BigInteger totalAmount = buyerAmount.add(sellerAmount); - - boolean isRefundAgent = getDisputeManager(dispute) instanceof RefundManager; - if (isRefundAgent) { - // We allow to spend less in case of RefundAgent or even zero to both, so in that case no payout tx will - // be made - return totalAmount.compareTo(available) <= 0; - } else { - if (totalAmount.compareTo(BigInteger.ZERO) <= 0) { - return false; - } - return totalAmount.compareTo(available) == 0; - } + return totalAmount.compareTo(expected) == 0 || totalAmount.compareTo(trade.getWallet().getBalance()) == 0; // allow spending the expected amount or full wallet balance in case a deposit transaction was dropped } private void applyCustomAmounts(InputTextField inputTextField, boolean oldFocusValue, boolean newFocusValue) { From dff7e884280461bcaec8537daf02aaa243b55dca Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 13 Jul 2025 10:28:15 -0400 Subject: [PATCH 360/371] fix min amount creating offer after setting volume (#1858) --- .../java/haveno/desktop/main/offer/MutableOfferDataModel.java | 4 ++++ .../java/haveno/desktop/main/offer/MutableOfferViewModel.java | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index 2aaf7e46b5..327640968c 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -486,6 +486,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } } + BigInteger getMinTradeLimit() { + return Restrictions.getMinTradeAmount(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index e563af3601..b015cabd48 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -1220,9 +1220,8 @@ public abstract class MutableOfferViewModel ext BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), xmrFormatter)); Price price = dataModel.getPrice().get(); - BigInteger maxTradeLimit = dataModel.getMaxTradeLimit(); if (price != null && price.isPositive()) { - minAmount = CoinUtil.getRoundedAmount(minAmount, price, dataModel.getMinAmount().get(), maxTradeLimit, tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); + minAmount = CoinUtil.getRoundedAmount(minAmount, price, dataModel.getMinTradeLimit(), dataModel.getMaxTradeLimit(), tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); } dataModel.setMinAmount(minAmount); From fd664d1d30daae5ef102a58c541bd10ffaefe778 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 14 Jul 2025 08:37:44 -0400 Subject: [PATCH 361/371] remove min. non-dust from preferences (#1860) --- .../settings/preferences/PreferencesView.java | 34 +++---------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java index 89d147f0fa..2590e6a12a 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java @@ -17,7 +17,6 @@ package haveno.desktop.main.settings.preferences; -import static com.google.common.base.Preconditions.checkArgument; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; @@ -47,7 +46,6 @@ import haveno.core.util.ParsingUtils; import haveno.core.util.validation.IntegerValidator; import haveno.core.util.validation.RegexValidator; import haveno.core.util.validation.RegexValidatorFactory; -import haveno.core.xmr.wallet.Restrictions; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; @@ -112,7 +110,7 @@ public class PreferencesView extends ActivatableViewAndModel allCryptoCurrencies; private ObservableList tradeCurrencies; private InputTextField deviationInputTextField; - private ChangeListener deviationListener, ignoreTradersListListener, ignoreDustThresholdListener, + private ChangeListener deviationListener, ignoreTradersListListener, rpcUserListener, rpcPwListener, blockNotifyPortListener, autoConfTradeLimitListener, autoConfServiceAddressListener; private ChangeListener deviationFocusedListener; @@ -209,7 +207,7 @@ public class PreferencesView extends ActivatableViewAndModel { - try { - int value = Integer.parseInt(newValue); - checkArgument(value >= Restrictions.getMinNonDustOutput().value, - "Input must be at least " + Restrictions.getMinNonDustOutput().value); - checkArgument(value <= 2000, - "Input must not be higher than 2000 Satoshis"); - if (!newValue.equals(oldValue)) { - preferences.setIgnoreDustThreshold(value); - } - } catch (Throwable ignore) { - } - }; - if (displayStandbyModeFeature) { // AvoidStandbyModeService feature works only on OSX & Windows avoidStandbyMode = addSlideToggleButton(root, ++gridRow, @@ -287,7 +264,7 @@ public class PreferencesView extends ActivatableViewAndModel referralIdInputTextField.setText(referralId)); referralIdInputTextField.setPromptText(Res.get("setting.preferences.refererId.prompt"));*/ - ignoreDustThresholdInputTextField.setText(String.valueOf(preferences.getIgnoreDustThreshold())); userLanguageComboBox.setItems(languageCodes); userLanguageComboBox.getSelectionModel().select(preferences.getUserLanguage()); userLanguageComboBox.setConverter(new StringConverter<>() { @@ -696,7 +672,6 @@ public class PreferencesView extends ActivatableViewAndModel Date: Mon, 14 Jul 2025 11:53:50 -0400 Subject: [PATCH 362/371] sort traditional currencies after fiat currencies in combined pull down (#1864) --- core/src/main/java/haveno/core/locale/CurrencyUtil.java | 6 +++--- .../src/main/java/haveno/desktop/util/CurrencyList.java | 7 ++++++- .../main/java/haveno/desktop/util/CurrencyPredicates.java | 4 ++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java index c94d55c70b..09783b7580 100644 --- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java +++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java @@ -93,7 +93,7 @@ public class CurrencyUtil { public static Collection getAllSortedFiatCurrencies(Comparator comparator) { return getAllSortedTraditionalCurrencies(comparator).stream() .filter(currency -> CurrencyUtil.isFiatCurrency(currency.getCode())) - .collect(Collectors.toList()); // sorted by currency name + .collect(Collectors.toList()); // sorted by currency name } public static List getAllFiatCurrencies() { @@ -105,11 +105,11 @@ public class CurrencyUtil { public static List getAllSortedFiatCurrencies() { return getAllSortedTraditionalCurrencies().stream() .filter(currency -> CurrencyUtil.isFiatCurrency(currency.getCode())) - .collect(Collectors.toList()); // sorted by currency name + .collect(Collectors.toList()); // sorted by currency name } public static Collection getAllSortedTraditionalCurrencies() { - return traditionalCurrencyMapSupplier.get().values(); // sorted by currency name + return traditionalCurrencyMapSupplier.get().values(); // sorted by currency name } public static List getAllTraditionalCurrencies() { diff --git a/desktop/src/main/java/haveno/desktop/util/CurrencyList.java b/desktop/src/main/java/haveno/desktop/util/CurrencyList.java index 88bef5ab1c..17186603e8 100644 --- a/desktop/src/main/java/haveno/desktop/util/CurrencyList.java +++ b/desktop/src/main/java/haveno/desktop/util/CurrencyList.java @@ -65,6 +65,7 @@ public class CurrencyList { private List getPartitionedSortedItems(List currencies) { Map tradesPerCurrency = countTrades(currencies); + List fiatCurrencies = new ArrayList<>(); List traditionalCurrencies = new ArrayList<>(); List cryptoCurrencies = new ArrayList<>(); @@ -73,7 +74,9 @@ public class CurrencyList { Integer count = entry.getValue(); CurrencyListItem item = new CurrencyListItem(currency, count); - if (predicates.isTraditionalCurrency(currency)) { + if (predicates.isFiatCurrency(currency)) { + fiatCurrencies.add(item); + } else if (predicates.isTraditionalCurrency(currency)) { traditionalCurrencies.add(item); } @@ -83,10 +86,12 @@ public class CurrencyList { } Comparator comparator = getComparator(); + fiatCurrencies.sort(comparator); traditionalCurrencies.sort(comparator); cryptoCurrencies.sort(comparator); List result = new ArrayList<>(); + result.addAll(fiatCurrencies); result.addAll(traditionalCurrencies); result.addAll(cryptoCurrencies); diff --git a/desktop/src/main/java/haveno/desktop/util/CurrencyPredicates.java b/desktop/src/main/java/haveno/desktop/util/CurrencyPredicates.java index a201bd6f36..15449dcb87 100644 --- a/desktop/src/main/java/haveno/desktop/util/CurrencyPredicates.java +++ b/desktop/src/main/java/haveno/desktop/util/CurrencyPredicates.java @@ -25,6 +25,10 @@ class CurrencyPredicates { return CurrencyUtil.isCryptoCurrency(currency.getCode()); } + boolean isFiatCurrency(TradeCurrency currency) { + return CurrencyUtil.isFiatCurrency(currency.getCode()); + } + boolean isTraditionalCurrency(TradeCurrency currency) { return CurrencyUtil.isTraditionalCurrency(currency.getCode()); } From 3f5dc9c07745f489f2fe37ba004d405a8120dc36 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 14 Jul 2025 11:54:01 -0400 Subject: [PATCH 363/371] darken primary blue color (#1863) --- .../src/main/java/haveno/desktop/theme-dark.css | 5 +++-- .../src/main/java/haveno/desktop/theme-light.css | 14 +++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/theme-dark.css b/desktop/src/main/java/haveno/desktop/theme-dark.css index 11fc14cdbf..aaa93fc35f 100644 --- a/desktop/src/main/java/haveno/desktop/theme-dark.css +++ b/desktop/src/main/java/haveno/desktop/theme-dark.css @@ -1,4 +1,6 @@ .root { + -bs-color-primary: rgb(28, 96, 220); + /* javafx main color palette */ -fx-base: #29292a; -fx-background: #29292a; @@ -9,7 +11,7 @@ -fx-text-fill: #dadada; /* javafx elements */ - -fx-accent: #0b65da; + -fx-accent: -bs-color-primary; -fx-box-border: transparent; -fx-focus-color: #0c59bd; -fx-faint-focus-color: #0c59bd; @@ -18,7 +20,6 @@ -fx-default-button: derive(-fx-accent, 95%); /* haveno main colors */ - -bs-color-primary: #0b65da; -bs-color-primary-dark: #0c59bd; -bs-text-color: white; -bs-background-color: black; diff --git a/desktop/src/main/java/haveno/desktop/theme-light.css b/desktop/src/main/java/haveno/desktop/theme-light.css index 44e703ef69..8542066595 100644 --- a/desktop/src/main/java/haveno/desktop/theme-light.css +++ b/desktop/src/main/java/haveno/desktop/theme-light.css @@ -1,5 +1,5 @@ .root { - -bs-color-primary: #0b65da; + -bs-color-primary: rgb(28, 96, 220); -bs-color-primary-dark: #0c59bd; -bs-text-color: #000000; -bs-background-color: #ffffff; @@ -38,17 +38,17 @@ -bs-yellow-light: derive(-bs-yellow, 81%); -bs-blue-transparent: #0f87c344; -bs-bg-green: #99ba9c; - -bs-rd-green: #0b65da; + -bs-rd-green: -bs-color-primary; -bs-rd-green-dark: #3EA34A; - -bs-rd-nav-selected: #0b65da; + -bs-rd-nav-selected: -bs-color-primary; -bs-rd-nav-deselected: rgba(255, 255, 255, 1); -bs-rd-nav-secondary-selected: -fx-accent; -bs-rd-nav-secondary-deselected: -bs-rd-font-light; - -bs-rd-nav-background: #0b65da; - -bs-rd-nav-primary-background: #0b65da; + -bs-rd-nav-background: -bs-color-primary; + -bs-rd-nav-primary-background: -bs-color-primary; -bs-rd-nav-button-hover: derive(-bs-rd-nav-background, 10%); -bs-rd-nav-primary-button-hover: derive(-bs-rd-nav-primary-background, 10%); - -bs-rd-nav-primary-border: #0B65DA; + -bs-rd-nav-primary-border: -bs-color-primary; -bs-rd-nav-border: #535353; -bs-rd-nav-border-color: rgba(255, 255, 255, 0.31); -bs-rd-nav-hover-text: white; @@ -94,7 +94,7 @@ -bs-cancel: #dddddd; -bs-cancel-focus: derive(-bs-cancel, -50%); -bs-cancel-hover: derive(-bs-cancel, -10%); - -fx-accent: #0b65da; + -fx-accent: -bs-color-primary; -fx-box-border: #e9e9e9; -bs-green-soft: derive(-bs-rd-green, 60%); -bs-red-soft: derive(-bs-rd-error-red, 60%); From b0ecc628e3caa4e8a1b5c64781c312f568cf9471 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 14 Jul 2025 11:54:13 -0400 Subject: [PATCH 364/371] remove 1px border on bottom of navigation bar (#1862) --- desktop/src/main/java/haveno/desktop/haveno.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/haveno.css b/desktop/src/main/java/haveno/desktop/haveno.css index 7fc6948fda..3eae67086b 100644 --- a/desktop/src/main/java/haveno/desktop/haveno.css +++ b/desktop/src/main/java/haveno/desktop/haveno.css @@ -771,7 +771,7 @@ tree-table-view:focused { /* Main navigation */ .top-navigation { -fx-background-color: -bs-rd-nav-background; - -fx-border-width: 0 0 1 0; + -fx-border-width: 0 0 0 0; -fx-border-color: -bs-rd-nav-primary-border; -fx-background-radius: 999; -fx-border-radius: 999; From ccbe2d2fe1a401c07dcbc30e7600ac40f6f48bd5 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 14 Jul 2025 11:56:11 -0400 Subject: [PATCH 365/371] add logs when trade taken and payment account decrypted (#1861) --- .../main/java/haveno/core/notifications/alerts/TradeEvents.java | 2 +- .../trade/protocol/tasks/ProcessDepositsConfirmedMessage.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java b/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java index 0dd6fe58ac..a1274a3813 100644 --- a/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java +++ b/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java @@ -59,7 +59,7 @@ public class TradeEvents { } private void setTradePhaseListener(Trade trade) { - if (isInitialized) log.info("We got a new trade. id={}", trade.getId()); + if (isInitialized) log.info("We got a new trade, tradeId={}", trade.getId(), "hasBuyerAsTakerWithoutDeposit=" + trade.getOffer().hasBuyerAsTakerWithoutDeposit()); if (!trade.isPayoutPublished()) { trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> { String msg = null; diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java index 7e0c85af2d..1d3632a74e 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java @@ -55,7 +55,7 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { // decrypt seller payment account payload if key given if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) { - log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key"); + log.info("Decrypting seller payment account payload for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey()); } From ca1dde03f573141232fff98ee0b5832bb09efa90 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 15 Jul 2025 06:35:37 -0400 Subject: [PATCH 366/371] peers publish deposit tx for redundancy (#1839) --- core/src/main/java/haveno/core/trade/Trade.java | 2 ++ .../trade/protocol/tasks/MaybeSendSignContractRequest.java | 2 ++ .../core/trade/protocol/tasks/ProcessDepositResponse.java | 7 +++++++ .../core/trade/protocol/tasks/SendDepositRequest.java | 4 ++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 83002928bb..63ca7a6e8a 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1578,6 +1578,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { peer.setUpdatedMultisigHex(null); peer.setDisputeClosedMessage(null); peer.setPaymentSentMessage(null); + peer.setDepositTxHex(null); + peer.setDepositTxKey(null); if (peer.isPaymentReceivedMessageReceived()) peer.setPaymentReceivedMessage(null); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index 6f10625e35..b9138fdbc2 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -156,6 +156,8 @@ public class MaybeSendSignContractRequest extends TradeTask { trade.getSelf().setDepositTx(depositTx); trade.getSelf().setDepositTxHash(depositTx.getHash()); trade.getSelf().setDepositTxFee(depositTx.getFee()); + trade.getSelf().setDepositTxHex(depositTx.getFullHex()); + trade.getSelf().setDepositTxKey(depositTx.getKey()); trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx)); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java index 454763e15b..387da42b82 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java @@ -48,6 +48,13 @@ public class ProcessDepositResponse extends TradeTask { return; } + // publish deposit transaction for redundancy + try { + model.getXmrWalletService().getDaemon().submitTxHex(trade.getSelf().getDepositTxHex()); + } catch (Exception e) { + log.error("Failed to redundantly publish deposit transaction for {} {}", trade.getClass().getSimpleName(), trade.getShortId(), e); + } + // record security deposits trade.getBuyer().setSecurityDeposit(BigInteger.valueOf(message.getBuyerSecurityDeposit())); trade.getSeller().setSecurityDeposit(BigInteger.valueOf(message.getSellerSecurityDeposit())); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java index 7101c488a5..74153a8dbc 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java @@ -82,8 +82,8 @@ public class SendDepositRequest extends TradeTask { Version.getP2PMessageVersion(), new Date().getTime(), trade.getSelf().getContractSignature(), - trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getFullHex(), - trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getKey(), + trade.getSelf().getDepositTxHex(), + trade.getSelf().getDepositTxKey(), trade.getSelf().getPaymentAccountKey()); // update trade state From 922fb6df247aaf5ba3bcf9573b237553411c929c Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 18 Jul 2025 12:56:25 -0400 Subject: [PATCH 367/371] add preference to clear sensitive data after days (#1859) --- .../haveno/core/api/CoreTradesService.java | 1 - .../haveno/core/support/dispute/Dispute.java | 2 +- .../main/java/haveno/core/trade/Contract.java | 10 +--- .../main/java/haveno/core/trade/Trade.java | 15 +++++- .../core/trade/protocol/ProcessModel.java | 11 +++++ .../java/haveno/core/user/Preferences.java | 8 +++- .../haveno/core/user/PreferencesPayload.java | 2 +- .../resources/i18n/displayStrings.properties | 10 ++++ .../i18n/displayStrings_cs.properties | 3 ++ .../i18n/displayStrings_de.properties | 3 ++ .../i18n/displayStrings_es.properties | 3 ++ .../i18n/displayStrings_fa.properties | 3 ++ .../i18n/displayStrings_fr.properties | 3 ++ .../i18n/displayStrings_it.properties | 3 ++ .../i18n/displayStrings_ja.properties | 3 ++ .../i18n/displayStrings_pt-br.properties | 3 ++ .../i18n/displayStrings_pt.properties | 3 ++ .../i18n/displayStrings_ru.properties | 3 ++ .../i18n/displayStrings_th.properties | 3 ++ .../i18n/displayStrings_tr.properties | 3 ++ .../i18n/displayStrings_vi.properties | 3 ++ .../i18n/displayStrings_zh-hans.properties | 3 ++ .../i18n/displayStrings_zh-hant.properties | 3 ++ .../settings/preferences/PreferencesView.java | 46 +++++++++++++++++-- 24 files changed, 132 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java index 30775633e2..177571d0d3 100644 --- a/core/src/main/java/haveno/core/api/CoreTradesService.java +++ b/core/src/main/java/haveno/core/api/CoreTradesService.java @@ -189,7 +189,6 @@ class CoreTradesService { verifyTradeIsNotClosed(tradeId); var trade = getOpenTrade(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); - log.info("Keeping funds received from trade {}", tradeId); tradeManager.onTradeCompleted(trade); } diff --git a/core/src/main/java/haveno/core/support/dispute/Dispute.java b/core/src/main/java/haveno/core/support/dispute/Dispute.java index 15595b8893..01f0493aa3 100644 --- a/core/src/main/java/haveno/core/support/dispute/Dispute.java +++ b/core/src/main/java/haveno/core/support/dispute/Dispute.java @@ -392,7 +392,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload { change += "chat messages;"; } if (change.length() > 0) { - log.info("cleared sensitive data from {} of dispute for trade {}", change, Utilities.getShortId(getTradeId())); + log.info("Cleared sensitive data from {} of dispute for trade {}", change, Utilities.getShortId(getTradeId())); } } diff --git a/core/src/main/java/haveno/core/trade/Contract.java b/core/src/main/java/haveno/core/trade/Contract.java index 9a88eaff56..1e350d305b 100644 --- a/core/src/main/java/haveno/core/trade/Contract.java +++ b/core/src/main/java/haveno/core/trade/Contract.java @@ -261,18 +261,12 @@ public final class Contract implements NetworkPayload { } public boolean maybeClearSensitiveData() { - return false; // TODO: anything to clear? + return false; // nothing to clear } // edits a contract json string public static String sanitizeContractAsJson(String contractAsJson) { - return contractAsJson - .replaceAll( - "\"takerPaymentAccountPayload\": \\{[^}]*}", - "\"takerPaymentAccountPayload\": null") - .replaceAll( - "\"makerPaymentAccountPayload\": \\{[^}]*}", - "\"makerPaymentAccountPayload\": null"); + return contractAsJson; // nothing to sanitize because the contract does not contain the payment account payloads } public void printDiff(@Nullable String peersContractAsJson) { diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 63ca7a6e8a..6437b40726 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1586,11 +1586,24 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public void maybeClearSensitiveData() { String change = ""; + if (contract != null && contract.maybeClearSensitiveData()) { + change += "contract;"; + } + if (processModel != null && processModel.maybeClearSensitiveData()) { + change += "processModel;"; + } + if (contractAsJson != null) { + String edited = Contract.sanitizeContractAsJson(contractAsJson); + if (!edited.equals(contractAsJson)) { + contractAsJson = edited; + change += "contractAsJson;"; + } + } if (removeAllChatMessages()) { change += "chat messages;"; } if (change.length() > 0) { - log.info("cleared sensitive data from {} of trade {}", change, getShortId()); + log.info("Cleared sensitive data from {} of {} {}", change, getClass().getSimpleName(), getShortId()); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java index 4f5ce9e66a..7339585560 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -326,6 +326,17 @@ public class ProcessModel implements Model, PersistablePayload { getAccountAgeWitnessService().getAccountAgeWitnessUtils().witnessDebugLog(trade, null); } + public boolean maybeClearSensitiveData() { + boolean changed = false; + for (TradePeer tradingPeer : getTradePeers()) { + if (tradingPeer.getPaymentAccountPayload() != null || tradingPeer.getContractAsJson() != null) { + tradingPeer.setPaymentAccountPayload(null); + tradingPeer.setContractAsJson(null); + changed = true; + } + } + return changed; + } /////////////////////////////////////////////////////////////////////////////////////////// // Delegates diff --git a/core/src/main/java/haveno/core/user/Preferences.java b/core/src/main/java/haveno/core/user/Preferences.java index 02b1d82aaf..85f49195e1 100644 --- a/core/src/main/java/haveno/core/user/Preferences.java +++ b/core/src/main/java/haveno/core/user/Preferences.java @@ -109,8 +109,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid )); public static final boolean USE_SYMMETRIC_SECURITY_DEPOSIT = true; - public static final int CLEAR_DATA_AFTER_DAYS_INITIAL = 99999; // feature effectively disabled until user agrees to settings notification - public static final int CLEAR_DATA_AFTER_DAYS_DEFAULT = 60; // used when user has agreed to settings notification + public static final int CLEAR_DATA_AFTER_DAYS_DEFAULT = 60; // used with new instance or when existing user has agreed to settings notification + public static final int CLEAR_DATA_AFTER_DAYS_DISABLED = 99999; // feature effectively disabled until existing user agrees to settings notification // payload is initialized so the default values are available for Property initialization. @@ -309,6 +309,10 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid setIgnoreDustThreshold(600); } + if (prefPayload.getClearDataAfterDays() < 1) { + setClearDataAfterDays(Preferences.CLEAR_DATA_AFTER_DAYS_DISABLED); + } + // For users from old versions the 4 flags a false but we want to have it true by default // PhoneKeyAndToken is also null so we can use that to enable the flags if (prefPayload.getPhoneKeyAndToken() == null) { diff --git a/core/src/main/java/haveno/core/user/PreferencesPayload.java b/core/src/main/java/haveno/core/user/PreferencesPayload.java index 44e6aef509..35739037c1 100644 --- a/core/src/main/java/haveno/core/user/PreferencesPayload.java +++ b/core/src/main/java/haveno/core/user/PreferencesPayload.java @@ -122,7 +122,7 @@ public final class PreferencesPayload implements PersistableEnvelope { private String takeOfferSelectedPaymentAccountId; private double securityDepositAsPercent = getDefaultSecurityDepositAsPercent(); private int ignoreDustThreshold = 600; - private int clearDataAfterDays = Preferences.CLEAR_DATA_AFTER_DAYS_INITIAL; + private int clearDataAfterDays = Preferences.CLEAR_DATA_AFTER_DAYS_DEFAULT; private double securityDepositAsPercentForCrypto = getDefaultSecurityDepositAsPercent(); private int blockNotifyPort; private boolean tacAcceptedV120; diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 1ebbbb2829..1b73ad94a6 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1337,6 +1337,7 @@ setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=Reset all \"Don't show again\" flags settings.preferences.languageChange=To apply the language change to all screens requires a restart. settings.preferences.supportLanguageWarning=In case of a dispute, please note that arbitration is handled in {0}. +setting.preferences.clearDataAfterDays=Clear sensitive data after (days) settings.preferences.editCustomExplorer.headline=Explorer Settings settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or \ customize to suit your own preferences. @@ -1346,6 +1347,15 @@ settings.preferences.editCustomExplorer.name=Name settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=New data-privacy feature +settings.preferences.sensitiveDataRemoval.msg=To protect the privacy of yourself and other traders, Haveno intends to \ + remove sensitive data from old trades. This is particularly important for fiat trades which may include bank \ + account details.\n\n\ + The threshold for data removal can be configured on this screen via the field "Clear sensitive data after (days)". \ + It is recommended to set it as low as possible, for example 60 days. That means trades from more than 60 \ + days ago will have sensitive data cleared, as long as they are completed. Completed trades are found in the \ + Portfolio / History tab. + settings.net.xmrHeader=Monero network settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=My onion address diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 14cd7b0812..c78fefe199 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -1309,6 +1309,9 @@ settings.preferences.editCustomExplorer.name=Jméno settings.preferences.editCustomExplorer.txUrl=Transakční URL settings.preferences.editCustomExplorer.addressUrl=Adresa URL +setting.info.headline=Nová funkce ochrany osobních údajů +settings.preferences.sensitiveDataRemoval.msg=Aby byla chráněna vaše soukromí i soukromí ostatních obchodníků, Haveno zamýšlí odstranit citlivá data ze starých obchodů.\n\nDoporučuje se nastavit tento limit co nejníže, například na 60 dní. To znamená, že obchody starší než 60 dní budou mít citlivá data odstraněna, pokud jsou dokončené. Dokončené obchody najdete na záložce Portfolio / Historie. + settings.net.xmrHeader=Síť Monero settings.net.p2pHeader=Síť Haveno settings.net.onionAddressLabel=Moje onion adresa diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index d04889babd..c3dd7119ab 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1044,6 +1044,9 @@ settings.preferences.editCustomExplorer.name=Name settings.preferences.editCustomExplorer.txUrl=Transaktions-URL settings.preferences.editCustomExplorer.addressUrl=Adress-URL +setting.info.headline=Neue Datenschutzfunktion +settings.preferences.sensitiveDataRemoval.msg=Zum Schutz der Privatsphäre von Ihnen und anderen Händlern beabsichtigt Haveno, sensible Daten aus alten Trades zu entfernen. Dies ist besonders wichtig bei Fiat-Trades, die Bankkontodaten enthalten können.\n\nEs wird empfohlen, den Wert so niedrig wie möglich zu setzen, zum Beispiel 60 Tage. Das bedeutet, dass Trades, die älter als 60 Tage sind, sensible Daten gelöscht bekommen, sofern sie abgeschlossen sind. Abgeschlossene Trades finden Sie im Portfolio- / Verlauf-Reiter. + settings.net.xmrHeader=Monero-Netzwerk settings.net.p2pHeader=Haveno-Netzwerk settings.net.onionAddressLabel=Meine Onion-Adresse diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index a1bb56e29d..a6a5ff1cbd 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -1045,6 +1045,9 @@ settings.preferences.editCustomExplorer.name=Nombre settings.preferences.editCustomExplorer.txUrl=URL de transacción settings.preferences.editCustomExplorer.addressUrl=URL de la dirección +setting.info.headline=Nueva función de privacidad de datos +settings.preferences.sensitiveDataRemoval.msg=Para proteger la privacidad de usted y otros traders, Haveno pretende eliminar datos sensibles de las operaciones antiguas. Esto es especialmente importante para las operaciones con fiat que pueden incluir detalles de cuentas bancarias.\n\nSe recomienda configurarlo lo más bajo posible, por ejemplo, 60 días. Esto significa que las operaciones de hace más de 60 días tendrán los datos sensibles eliminados, siempre que estén completadas. Las operaciones completadas se encuentran en la pestaña Portafolio / Historial. + settings.net.xmrHeader=Red Monero settings.net.p2pHeader=Red Haveno settings.net.onionAddressLabel=Mi dirección onion diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index b6eb0cc2f4..c5444d64ca 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1041,6 +1041,9 @@ settings.preferences.editCustomExplorer.name=نام settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=ویژگی جدید حفظ حریم خصوصی داده‌ها +settings.preferences.sensitiveDataRemoval.msg=برای محافظت از حریم خصوصی شما و سایر معامله‌گران، Haveno قصد دارد داده‌های حساس مربوط به معاملات قدیمی را حذف کند. این موضوع به ویژه برای معاملات فیات که ممکن است شامل جزئیات حساب بانکی باشند اهمیت دارد.\n\nتوصیه می‌شود این مقدار را تا حد امکان پایین تنظیم کنید، مثلاً ۶۰ روز. این بدان معناست که معاملاتی که بیش از ۶۰ روز از آنها گذشته و تکمیل شده‌اند، داده‌های حساس آنها پاک خواهد شد. معاملات تکمیل شده در تب «پرتفوی / تاریخچه» یافت می‌شوند. + settings.net.xmrHeader=شبکه بیتکوین settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=آدرس onion من diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 5d11a8a6fb..87dc02915e 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -1046,6 +1046,9 @@ settings.preferences.editCustomExplorer.name=Nom settings.preferences.editCustomExplorer.txUrl=URL de la transaction settings.preferences.editCustomExplorer.addressUrl=Addresse URL +setting.info.headline=Nouvelle fonctionnalité de confidentialité des données +settings.preferences.sensitiveDataRemoval.msg=Pour protéger la vie privée de vous-même et des autres traders, Haveno a l'intention de supprimer les données sensibles des anciennes transactions. Cela est particulièrement important pour les transactions en fiat qui peuvent inclure des informations bancaires.\n\nIl est recommandé de régler ce délai aussi bas que possible, par exemple 60 jours. Cela signifie que les transactions datant de plus de 60 jours verront leurs données sensibles supprimées, tant qu'elles sont terminées. Les transactions terminées se trouvent dans l'onglet Portefeuille / Historique. + settings.net.xmrHeader=Réseau Monero settings.net.p2pHeader=Le réseau Haveno settings.net.onionAddressLabel=Mon adresse onion diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 1e7119606a..a57af5a967 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1043,6 +1043,9 @@ settings.preferences.editCustomExplorer.name=Nome settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=Nuova funzione per la privacy dei dati +settings.preferences.sensitiveDataRemoval.msg=Per proteggere la privacy tua e degli altri trader, Haveno intende rimuovere i dati sensibili dalle vecchie transazioni. Questo è particolarmente importante per le transazioni in valuta fiat che possono includere dettagli del conto bancario.\n\nSi consiglia di impostare questo valore il più basso possibile, ad esempio 60 giorni. Questo significa che le transazioni completate da più di 60 giorni avranno i dati sensibili cancellati. Le transazioni completate si trovano nella scheda Portafoglio / Cronologia. + settings.net.xmrHeader=Network Monero settings.net.p2pHeader=Rete Haveno settings.net.onionAddressLabel=Il mio indirizzo onion diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 045417f7c4..0a3393a0a0 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -1044,6 +1044,9 @@ settings.preferences.editCustomExplorer.name=名義 settings.preferences.editCustomExplorer.txUrl=トランザクションURL settings.preferences.editCustomExplorer.addressUrl=アドレスURL +setting.info.headline=新しいデータプライバシー機能 +settings.preferences.sensitiveDataRemoval.msg=ご自身および他のトレーダーのプライバシーを保護するため、Havenoは過去の取引から機密データを削除する予定です。これは銀行口座情報を含む可能性のある法定通貨の取引で特に重要です。\n\n可能な限り低く設定することをお勧めします。例えば60日です。これは、60日以上前の完了した取引の機密データが削除されることを意味します。完了した取引はポートフォリオ/履歴タブで確認できます。 + settings.net.xmrHeader=ビットコインのネットワーク settings.net.p2pHeader=Havenoネットワーク settings.net.onionAddressLabel=私のonionアドレス diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 59b24342db..c60eed1247 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -1045,6 +1045,9 @@ settings.preferences.editCustomExplorer.name=Nome settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=Nova funcionalidade de privacidade de dados +settings.preferences.sensitiveDataRemoval.msg=Para proteger a privacidade sua e de outros traders, o Haveno pretende remover dados sensíveis de negociações antigas. Isso é particularmente importante para negociações com moedas fiduciárias que podem incluir detalhes de conta bancária.\n\nÉ recomendado definir o menor valor possível, por exemplo, 60 dias. Isso significa que negociações com mais de 60 dias terão os dados sensíveis removidos, desde que estejam concluídas. Negociações concluídas podem ser encontradas na aba Portfólio / Histórico. + settings.net.xmrHeader=Rede Monero settings.net.p2pHeader=Rede Haveno settings.net.onionAddressLabel=Meu endereço onion diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 682b825894..eade3dc2ef 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1042,6 +1042,9 @@ settings.preferences.editCustomExplorer.name=Nome settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=Nova funcionalidade de privacidade de dados +settings.preferences.sensitiveDataRemoval.msg=Para proteger a privacidade sua e de outros negociadores, o Haveno pretende remover dados sensíveis de negociações antigas. Isso é particularmente importante para negociações com moedas fiduciárias que podem incluir detalhes de conta bancária.\n\nRecomenda-se definir o prazo o mais baixo possível, por exemplo, 60 dias. Isso significa que negociações com mais de 60 dias terão os dados sensíveis removidos, desde que estejam concluídas. Negociações concluídas podem ser encontradas na aba Portfólio / Histórico. + settings.net.xmrHeader=Rede Monero settings.net.p2pHeader=Rede do Haveno settings.net.onionAddressLabel=O meu endereço onion diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index aac587efda..6cf97b8432 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1041,6 +1041,9 @@ settings.preferences.editCustomExplorer.name=Имя settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=Новая функция защиты данных +settings.preferences.sensitiveDataRemoval.msg=Для защиты вашей и других трейдеров конфиденциальности Haveno намерен удалять конфиденциальные данные из старых сделок. Это особенно важно для сделок с фиатной валютой, которые могут включать данные банковских счетов.\n\nРекомендуется установить минимальный срок, например, 60 дней. Это означает, что сделки старше 60 дней будут очищены от конфиденциальных данных, при условии, что они завершены. Завершённые сделки находятся во вкладке «Портфель / История». + settings.net.xmrHeader=Сеть Биткойн settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=Мой onion-адрес diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 8da8c3c0ff..392d0bde39 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1041,6 +1041,9 @@ settings.preferences.editCustomExplorer.name=ชื่อ settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=คุณสมบัติความเป็นส่วนตัวข้อมูลใหม่ +settings.preferences.sensitiveDataRemoval.msg=เพื่อปกป้องความเป็นส่วนตัวของคุณและผู้ซื้อขายอื่นๆ Haveno ตั้งใจที่จะลบข้อมูลที่ละเอียดอ่อนจากการซื้อขายเก่า ซึ่งมีความสำคัญอย่างยิ่งสำหรับการซื้อขายฟิอาทที่อาจมีรายละเอียดบัญชีธนาคาร\n\nแนะนำให้ตั้งค่านี้ให้ต่ำที่สุดเท่าที่จะเป็นไปได้ เช่น 60 วัน ซึ่งหมายความว่าการซื้อขายที่เกิน 60 วันจะถูกลบข้อมูลที่ละเอียดอ่อน ตราบใดที่การซื้อขายนั้นเสร็จสมบูรณ์ การซื้อขายที่เสร็จสมบูรณ์สามารถดูได้ในแท็บพอร์ตโฟลิโอ / ประวัติ + settings.net.xmrHeader=เครือข่าย Monero settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=ที่อยู่ onion ของฉัน diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 723e108b86..eea3d55cc1 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -1305,6 +1305,9 @@ settings.preferences.editCustomExplorer.name=İsim settings.preferences.editCustomExplorer.txUrl=İşlem URL'si settings.preferences.editCustomExplorer.addressUrl=Adres URL'si +setting.info.headline=Yeni veri gizliliği özelliği +settings.preferences.sensitiveDataRemoval.msg=Kendinizin ve diğer tacirlerin gizliliğini korumak için Haveno, eski işlemlerden hassas verileri kaldırmayı planlamaktadır. Bu, banka hesap bilgilerini içerebilecek fiat işlemleri için özellikle önemlidir.\n\nVeri silme eşiğinin mümkün olduğunca düşük, örneğin 60 gün olarak ayarlanması önerilir. Bu, 60 günden eski ve tamamlanmış işlemlerde hassas verilerin temizleneceği anlamına gelir. Tamamlanmış işlemler Portföy / Geçmiş sekmesinde bulunabilir. + settings.net.xmrHeader=Monero ağı settings.net.p2pHeader=Haveno ağı settings.net.onionAddressLabel=Onion adresim diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 6962df5af2..06d91a6a9d 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1043,6 +1043,9 @@ settings.preferences.editCustomExplorer.name=Tên settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=Tính năng bảo mật dữ liệu mới +settings.preferences.sensitiveDataRemoval.msg=Để bảo vệ quyền riêng tư của bạn và các nhà giao dịch khác, Haveno dự định sẽ xóa dữ liệu nhạy cảm khỏi các giao dịch cũ. Điều này đặc biệt quan trọng đối với các giao dịch tiền pháp định có thể bao gồm thông tin tài khoản ngân hàng.\n\nBạn nên đặt giá trị này càng thấp càng tốt, ví dụ 60 ngày. Điều đó có nghĩa là các giao dịch đã hoàn thành từ hơn 60 ngày trước sẽ bị xóa dữ liệu nhạy cảm. Các giao dịch đã hoàn thành có thể được tìm thấy trong tab Danh mục đầu tư / Lịch sử. + settings.net.xmrHeader=Mạng Monero settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=Địa chỉ onion của tôi diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index daf103a968..003292245e 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -1044,6 +1044,9 @@ settings.preferences.editCustomExplorer.name=名称 settings.preferences.editCustomExplorer.txUrl=交易 URL settings.preferences.editCustomExplorer.addressUrl=地址 URL +setting.info.headline=新的数据隐私功能 +settings.preferences.sensitiveDataRemoval.msg=为了保护您和其他交易者的隐私,Haveno 计划从旧交易中删除敏感数据。这对于可能包含银行账户信息的法币交易尤其重要。\n\n建议将其设置得尽可能低,例如 60 天。这意味着超过 60 天且已完成的交易将被清除敏感数据。已完成的交易可在“投资组合 / 历史”标签中找到。 + settings.net.xmrHeader=比特币网络 settings.net.p2pHeader=Haveno 网络 settings.net.onionAddressLabel=我的匿名地址 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index a816482877..ba735ecfb5 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -1044,6 +1044,9 @@ settings.preferences.editCustomExplorer.name=名稱 settings.preferences.editCustomExplorer.txUrl=交易 URL settings.preferences.editCustomExplorer.addressUrl=地址 URL +setting.info.headline=新的資料隱私功能 +settings.preferences.sensitiveDataRemoval.msg=為了保護您與其他交易者的隱私,Haveno 計劃從舊交易中移除敏感資料。這對於可能包含銀行帳戶資訊的法幣交易尤其重要。\n\n建議將此設定設為盡可能低,例如 60 天。這表示只要交易已完成且超過 60 天,敏感資料將被清除。已完成的交易可在「投資組合 / 歷史」標籤中找到。 + settings.net.xmrHeader=比特幣網絡 settings.net.p2pHeader=Haveno 網絡 settings.net.onionAddressLabel=我的匿名地址 diff --git a/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java index 2590e6a12a..c0e0862fd0 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java @@ -39,6 +39,7 @@ import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.XmrValidator; import haveno.core.trade.HavenoUtils; +import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; @@ -111,7 +112,7 @@ public class PreferencesView extends ActivatableViewAndModel tradeCurrencies; private InputTextField deviationInputTextField; private ChangeListener deviationListener, ignoreTradersListListener, - rpcUserListener, rpcPwListener, blockNotifyPortListener, + rpcUserListener, rpcPwListener, blockNotifyPortListener, clearDataAfterDaysListener, autoConfTradeLimitListener, autoConfServiceAddressListener; private ChangeListener deviationFocusedListener; private final boolean displayStandbyModeFeature; @@ -184,6 +185,24 @@ public class PreferencesView extends ActivatableViewAndModel { + DontShowAgainLookup.dontShowAgain(key, true); + // user has acknowledged, enable the feature with a reasonable default value + preferences.setClearDataAfterDays(Preferences.CLEAR_DATA_AFTER_DAYS_DEFAULT); + clearDataAfterDaysInputTextField.setText(String.valueOf(preferences.getClearDataAfterDays())); + }) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } + // We want to have it updated in case an asset got removed allCryptoCurrencies = FXCollections.observableArrayList(CurrencyUtil.getActiveSortedCryptoCurrencies(filterManager)); allCryptoCurrencies.removeAll(cryptoCurrencies); @@ -207,7 +226,7 @@ public class PreferencesView extends ActivatableViewAndModel { + try { + int value = Integer.parseInt(newValue); + if (!newValue.equals(oldValue)) { + preferences.setClearDataAfterDays(value); + } + } catch (Throwable ignore) { + } + }; + if (displayStandbyModeFeature) { // AvoidStandbyModeService feature works only on OSX & Windows avoidStandbyMode = addSlideToggleButton(root, ++gridRow, @@ -264,7 +299,7 @@ public class PreferencesView extends ActivatableViewAndModel referralIdInputTextField.setText(referralId)); referralIdInputTextField.setPromptText(Res.get("setting.preferences.refererId.prompt"));*/ + clearDataAfterDaysInputTextField.setText(String.valueOf(preferences.getClearDataAfterDays())); userLanguageComboBox.setItems(languageCodes); userLanguageComboBox.getSelectionModel().select(preferences.getUserLanguage()); userLanguageComboBox.setConverter(new StringConverter<>() { @@ -672,6 +708,7 @@ public class PreferencesView extends ActivatableViewAndModel Date: Fri, 18 Jul 2025 17:13:00 -0400 Subject: [PATCH 368/371] update tails readme to use installer instead of zip archive (#1869) --- scripts/install_tails/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/install_tails/README.md b/scripts/install_tails/README.md index 05b6470fd8..4179194dc7 100644 --- a/scripts/install_tails/README.md +++ b/scripts/install_tails/README.md @@ -8,13 +8,13 @@ After you already have a [Tails USB](https://tails.net/install/linux/index.en.ht 4. Execute the following command in the terminal to download and execute the installation script. ``` - curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh + curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh ``` - Replace the binary zip URL and PGP fingerprint for the network you're using. For example: + Replace the installer URL and PGP fingerprint for the network you're using. For example: ``` - curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh https://github.com/havenoexample/haveno-example/releases/latest/download/haveno-linux-deb.zip FAA24D878B8D36C90120A897CA02DAC12DAE2D0F + curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh https://github.com/havenoexample/haveno-example/releases/latest/download/haveno-v1.1.2-linux-x86_64-installer.deb FAA24D878B8D36C90120A897CA02DAC12DAE2D0F ``` 5. Start Haveno by finding the icon in the launcher under **Applications > Other**. From 071659aec12a90cb03d4d0b1fe8f70f0125bcb1d Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 19 Jul 2025 08:00:46 -0400 Subject: [PATCH 369/371] fix amount adjustment when creating new offer (#1868) --- .../haveno/core/offer/CreateOfferService.java | 8 ++++---- .../main/java/haveno/core/offer/OfferUtil.java | 18 ++++++++++++++++++ .../java/haveno/core/trade/HavenoUtils.java | 16 +++++++++++----- .../java/haveno/core/util/coin/CoinUtil.java | 17 ++++++++++++++--- .../haveno/core/util/coin/CoinUtilTest.java | 2 +- .../main/offer/MutableOfferDataModel.java | 12 +----------- 6 files changed, 49 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index 89f1149447..4a35f3c43c 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -35,6 +35,7 @@ import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.User; import haveno.core.util.coin.CoinUtil; +import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; @@ -92,7 +93,6 @@ public class CreateOfferService { Version.VERSION.replace(".", ""); } - // TODO: add trigger price? public Offer createAndGetOffer(String offerId, OfferDirection direction, String currencyCode, @@ -158,8 +158,9 @@ public class CreateOfferService { } // adjust amount and min amount - amount = CoinUtil.getRoundedAmount(amount, fixedPrice, minAmount, amount, currencyCode, paymentAccount.getPaymentMethod().getId()); - minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, minAmount, amount, currencyCode, paymentAccount.getPaymentMethod().getId()); + BigInteger maxTradeLimit = offerUtil.getMaxTradeLimitForRelease(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit); + amount = CoinUtil.getRoundedAmount(amount, fixedPrice, Restrictions.getMinTradeAmount(), maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId()); + minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, Restrictions.getMinTradeAmount(), maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId()); // generate one-time challenge for private offer String challenge = null; @@ -241,7 +242,6 @@ public class CreateOfferService { return offer; } - // TODO: add trigger price? public Offer createClonedOffer(Offer sourceOffer, String currencyCode, Price fixedPrice, diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java index 8f7c1957aa..64de6fec9e 100644 --- a/core/src/main/java/haveno/core/offer/OfferUtil.java +++ b/core/src/main/java/haveno/core/offer/OfferUtil.java @@ -56,6 +56,7 @@ import haveno.core.payment.PayPalAccount; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; +import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.ReferralIdService; import haveno.core.user.AutoConfirmSettings; import haveno.core.user.Preferences; @@ -269,4 +270,21 @@ public class OfferUtil { public static boolean isCryptoOffer(Offer offer) { return offer.getCounterCurrencyCode().equals("XMR"); } + + public BigInteger getMaxTradeLimitForRelease(PaymentAccount paymentAccount, + String currencyCode, + OfferDirection direction, + boolean buyerAsTakerWithoutDeposit) { + + // disallow offers which no buyer can take due to trade limits on release + if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) { + return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, OfferDirection.BUY, buyerAsTakerWithoutDeposit)); + } + + if (paymentAccount != null) { + return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)); + } else { + return BigInteger.ZERO; + } + } } diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index b022cf3cfc..24ee1c1dda 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -148,11 +148,17 @@ public class HavenoUtils { @SuppressWarnings("unused") public static Date getReleaseDate() { if (RELEASE_DATE == null) return null; - try { - return DATE_FORMAT.parse(RELEASE_DATE); - } catch (Exception e) { - log.error("Failed to parse release date: " + RELEASE_DATE, e); - throw new IllegalArgumentException(e); + return parseDate(RELEASE_DATE); + } + + private static Date parseDate(String date) { + synchronized (DATE_FORMAT) { + try { + return DATE_FORMAT.parse(date); + } catch (Exception e) { + log.error("Failed to parse date: " + date, e); + throw new IllegalArgumentException(e); + } } } diff --git a/core/src/main/java/haveno/core/util/coin/CoinUtil.java b/core/src/main/java/haveno/core/util/coin/CoinUtil.java index eef242da7c..bc079c9b5e 100644 --- a/core/src/main/java/haveno/core/util/coin/CoinUtil.java +++ b/core/src/main/java/haveno/core/util/coin/CoinUtil.java @@ -126,16 +126,27 @@ public class CoinUtil { static BigInteger getAdjustedAmount(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount, int factor) { checkArgument( amount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(), - "amount needs to be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(amount) + " xmr" + "amount must be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(amount) + " xmr" ); if (minAmount == null) minAmount = Restrictions.getMinTradeAmount(); checkArgument( minAmount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(), - "minAmount needs to be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(minAmount) + " xmr" + "minAmount must be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(minAmount) + " xmr" ); + if (maxAmount != null) { + checkArgument( + amount.longValueExact() <= maxAmount.longValueExact(), + "amount must be below maximum of " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(amount) + " xmr" + ); + checkArgument( + maxAmount.longValueExact() >= minAmount.longValueExact(), + "maxAmount must be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " xmr" + ); + } + checkArgument( factor > 0, - "factor needs to be positive" + "factor must be positive" ); // Amount must result in a volume of min factor units of the fiat currency, e.g. 1 EUR or 10 EUR in case of HalCash. diff --git a/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java b/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java index 1a1a33435c..f14f41afdc 100644 --- a/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java +++ b/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java @@ -95,7 +95,7 @@ public class CoinUtilTest { fail("Expected IllegalArgumentException to be thrown when amount is too low."); } catch (IllegalArgumentException iae) { assertEquals( - "amount needs to be above minimum of 0.1 xmr but was 0.0 xmr", + "amount must be above minimum of 0.1 xmr but was 0.0 xmr", iae.getMessage(), "Unexpected exception message." ); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index 327640968c..b8bbdcbd41 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -473,17 +473,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } BigInteger getMaxTradeLimit() { - - // disallow offers which no buyer can take due to trade limits on release - if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) { - return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), OfferDirection.BUY, buyerAsTakerWithoutDeposit.get())); - } - - if (paymentAccount != null) { - return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction, buyerAsTakerWithoutDeposit.get())); - } else { - return BigInteger.ZERO; - } + return offerUtil.getMaxTradeLimitForRelease(paymentAccount, tradeCurrencyCode.get(), direction, buyerAsTakerWithoutDeposit.get()); } BigInteger getMinTradeLimit() { From dd1fced7fbd0f9ea57bf5c6034051867e4a3b177 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 19 Jul 2025 08:01:19 -0400 Subject: [PATCH 370/371] cannot set trigger price for fixed price offers over api (#1870) --- .../src/main/java/haveno/core/offer/OpenOfferManager.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 1ab83d85e0..986b177131 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -519,6 +519,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe ErrorMessageHandler errorMessageHandler) { ThreadUtils.execute(() -> { + // cannot set trigger price for fixed price offers + if (triggerPrice != 0 && offer.getOfferPayload().getPrice() != 0) { + errorMessageHandler.handleErrorMessage("Cannot set trigger price for fixed price offers."); + return; + } + // check source offer and clone limit OpenOffer sourceOffer = null; if (sourceOfferId != null) { @@ -526,7 +532,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // get source offer Optional sourceOfferOptional = getOpenOffer(sourceOfferId); if (!sourceOfferOptional.isPresent()) { - errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId); + errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId + "."); return; } sourceOffer = sourceOfferOptional.get(); From 0b04d34909d424fd3c75244b395933d7e08123b5 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 19 Jul 2025 08:01:35 -0400 Subject: [PATCH 371/371] set offer state to invalid on failed validation (#1871) --- core/src/main/java/haveno/core/offer/OpenOfferManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 986b177131..6d33106b4d 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1093,6 +1093,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe try { ValidateOffer.validateOffer(openOffer.getOffer(), accountAgeWitnessService, user); } catch (Exception e) { + openOffer.getOffer().setState(Offer.State.INVALID); errorMessageHandler.handleErrorMessage("Failed to validate offer: " + e.getMessage()); return; }

      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 literal 0 HcmV?d00001 diff --git a/desktop/src/main/resources/images/yellow_circle.png b/desktop/src/main/resources/images/yellow_circle.png index 44e5a272fa5d4c5c0496f592cc505b1b697054d5..0fe6fc0b4e36af3da43dab76f785f720587868fc 100644 GIT binary patch literal 4887 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 JPDHLkV1mB$S%Lrn literal 510 zcmVvUIiu8jY=WyX1KJL97_rHfR){(b%mwwdW;5&QOq_&8? z>WW~v8s9cdTqS-<7tpB8z$FQI^f^4b0MD3*qtC+8XOXp1$-r>gJ&)J52|w(T_-`cO zkT~p83~B4b%@WV^IvjltHT4M^m1)$=lW3Hu0<;M$jzM+~BYwdNOTV_{DC|-UcIjyw zZF;7R;vIP@_yw2d3z%oau#^$7;@B_719Tir8A0_Tit5E<_yu<98Hm^iqtFj34T4HT zpwh!YH3|bDrw>5HzUzXT8iRHchMvCr14R_E>Thxs7g!86IMWNB%GRb-U>ctvUIiu8jY=WyX1KJL97_rHfR){(b%mwwdW;5&QOq_&8? z>WW~v8s9cdTqS-<7tpB8z$FQI^f^4b0MD3*qtC+8XOXp1$-r>gJ&)J52|w(T_-`cO zkT~p83~B4b%@WV^IvjltHT4M^m1)$=lW3Hu0<;M$jzM+~BYwdNOTV_{DC|-UcIjyw zZF;7R;vIP@_yw2d3z%oau#^$7;@B_719Tir8A0_Tit5E<_yu<98Hm^iqtFj34T4HT zpwh!YH3|bDrw>5HzUzXT8iRHchMvCr14R_E>Thxs7g!86IMWNB%GRb-U>ct Date: Fri, 6 Jun 2025 08:24:45 -0400 Subject: [PATCH 306/371] bump version to 1.1.2 --- build.gradle | 2 +- .../src/main/java/haveno/common/app/Version.java | 2 +- .../linux/exchange.haveno.Haveno.metainfo.xml | 2 +- desktop/package/macosx/Info.plist | 4 ++-- scripts/install_whonix_qubes/INSTALL.md | 14 +++++++------- scripts/install_whonix_qubes/README.md | 2 +- .../main/java/haveno/seednode/SeedNodeMain.java | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 0c8a4412f2..f52dd1a10d 100644 --- a/build.gradle +++ b/build.gradle @@ -610,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.1.1-SNAPSHOT' + version = '1.1.2-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index d7be891aea..325f4a7a0e 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.1.1"; + public static final String VERSION = "1.1.2"; /** * Holds a list of the tagged resource files for optimizing the getData requests. diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index 57aeb02f09..277b583389 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -60,6 +60,6 @@ - + diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index c6bf09f2de..e4f331e614 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.1.1 + 1.1.2 CFBundleShortVersionString - 1.1.1 + 1.1.2 CFBundleExecutable Haveno diff --git a/scripts/install_whonix_qubes/INSTALL.md b/scripts/install_whonix_qubes/INSTALL.md index adee6d2e60..fac278a406 100644 --- a/scripts/install_whonix_qubes/INSTALL.md +++ b/scripts/install_whonix_qubes/INSTALL.md @@ -153,7 +153,7 @@ $ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno