diff --git a/common/src/main/java/io/bisq/common/GlobalSettings.java b/common/src/main/java/io/bisq/common/GlobalSettings.java index 82b323b8771..4847b8d7438 100644 --- a/common/src/main/java/io/bisq/common/GlobalSettings.java +++ b/common/src/main/java/io/bisq/common/GlobalSettings.java @@ -26,7 +26,7 @@ public class GlobalSettings { private static boolean useAnimations = true; - private static Locale locale = new Locale("en", "US"); + private static Locale locale = Locale.getDefault(); private static final ObjectProperty localeProperty = new SimpleObjectProperty<>(locale); private static TradeCurrency defaultTradeCurrency; private static String btcDenomination; diff --git a/common/src/main/java/io/bisq/common/locale/CurrencyUtil.java b/common/src/main/java/io/bisq/common/locale/CurrencyUtil.java index f4461597321..9e3887f564c 100644 --- a/common/src/main/java/io/bisq/common/locale/CurrencyUtil.java +++ b/common/src/main/java/io/bisq/common/locale/CurrencyUtil.java @@ -96,12 +96,14 @@ public static List createAllSortedCryptoCurrenciesList() { result.add(new CryptoCurrency("BCH", "Bitcoin Cash")); result.add(new CryptoCurrency("BCHC", "Bitcoin Clashic")); result.add(new CryptoCurrency("BTG", "Bitcoin Gold")); + result.add(new CryptoCurrency("DARX", "BitDaric")); result.add(new CryptoCurrency("BURST", "Burstcoin")); result.add(new CryptoCurrency("GBYTE", "Byte")); result.add(new CryptoCurrency("CAGE", "Cagecoin")); result.add(new CryptoCurrency("XCP", "Counterparty")); result.add(new CryptoCurrency("CREA", "Creativecoin")); result.add(new CryptoCurrency("XCN", "Cryptonite")); + result.add(new CryptoCurrency("DAI", "Dai Stablecoin", true)); result.add(new CryptoCurrency("DNET", "DarkNet")); if (!baseCurrencyCode.equals("DASH")) result.add(new CryptoCurrency("DASH", "Dash")); @@ -131,6 +133,7 @@ public static List createAllSortedCryptoCurrenciesList() { result.add(new CryptoCurrency("NMC", "Namecoin")); result.add(new CryptoCurrency("NBT", "NuBits")); result.add(new CryptoCurrency("NXT", "Nxt")); + result.add(new CryptoCurrency("ODN", "Obsidian")); result.add(new CryptoCurrency("888", "OctoCoin")); result.add(new CryptoCurrency("PART", "Particl")); result.add(new CryptoCurrency("PASC", "Pascal Coin", true)); @@ -146,14 +149,15 @@ public static List createAllSortedCryptoCurrenciesList() { result.add(new CryptoCurrency("SIB", "Sibcoin")); result.add(new CryptoCurrency("XSPEC", "Spectrecoin")); result.add(new CryptoCurrency("STEEM", "STEEM")); - result.add(new CryptoCurrency("STL", "Stellite")); - result.add(new CryptoCurrency("TRC", "Terracoin")); + result.add(new CryptoCurrency("STL", "Stellite")); + result.add(new CryptoCurrency("TRC", "Terracoin")); result.add(new CryptoCurrency("MVT", "The Movement", true)); result.add(new CryptoCurrency("UNO", "Unobtanium")); result.add(new CryptoCurrency("CRED", "Verify", true)); result.add(new CryptoCurrency("WAC", "WACoins")); result.add(new CryptoCurrency("WILD", "WILD Token", true)); + result.add(new CryptoCurrency("YTN", "Yenten")); result.add(new CryptoCurrency("XZC", "Zcoin")); result.add(new CryptoCurrency("ZEC", "Zcash")); result.add(new CryptoCurrency("ZEN", "ZenCash")); diff --git a/common/src/main/resources/i18n/displayStrings.properties b/common/src/main/resources/i18n/displayStrings.properties index 99ce4e5a7f1..705b32c3320 100644 --- a/common/src/main/resources/i18n/displayStrings.properties +++ b/common/src/main/resources/i18n/displayStrings.properties @@ -266,8 +266,8 @@ market.offerBook.leftButtonAltcoin=I want to buy {0} (sell {1}) market.offerBook.rightButtonAltcoin=I want to sell {0} (buy {1}) market.offerBook.leftButtonFiat=I want to buy {0} with {1} market.offerBook.rightButtonFiat=I want to sell {0} for {1} -market.offerBook.leftHeaderLabel=Offers to sell {0} for {1} -market.offerBook.rightHeaderLabel=Offers to buy {0} with {1} +market.offerBook.sellOffersHeaderLabel=Sell {0} to +market.offerBook.buyOffersHeaderLabel=Buy {0} from market.offerBook.buy=I want to buy bitcoin market.offerBook.sell=I want to sell bitcoin @@ -353,7 +353,7 @@ createOffer.fundsBox.offerFee=Trade fee: createOffer.fundsBox.networkFee=Mining fee: createOffer.fundsBox.placeOfferSpinnerInfo=Offer publishing is in progress ... createOffer.fundsBox.paymentLabel=Bisq trade with ID {0} -createOffer.fundsBox.fundsStructure=({0} deposit, {1} trade fee, {2} mining fee) +createOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee, {2} mining fee) createOffer.success.headline=Your offer has been published createOffer.success.info=You can manage your open offers at \"Portfolio/My open offers\". @@ -411,7 +411,7 @@ takeOffer.fundsBox.offerFee=Trade fee: takeOffer.fundsBox.networkFee=Total mining fees: takeOffer.fundsBox.takeOfferSpinnerInfo=Take offer in progress ... takeOffer.fundsBox.paymentLabel=Bisq trade with ID {0} -takeOffer.fundsBox.fundsStructure=({0} deposit, {1} trade fee, {2} mining fee) +takeOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee, {2} mining fee) 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} @@ -533,7 +533,7 @@ The trade ID (\"reason for payment\" text) of the transaction is: \"{2}\" portfolio.pending.step3_seller.cash=\n\nBecause the payment is done via Cash Deposit the BTC buyer has to write \"NO REFUND\" on the paper receipt, tear it in 2 parts and send you a photo by email.\n\n\ To avoid chargeback risk, only confirm if you received the email and if you are sure the paper receipt is valid.\n\ If you are not sure, {0} -portfolio.pending.step3_seller.westernUnion=The buyer has to sent you the MTCN (tracking number) and a photo of the receipt by email.\n\ +portfolio.pending.step3_seller.westernUnion=The buyer has to send you the MTCN (tracking number) and a photo of the receipt by email.\n\ The receipt must clearly show your full name, city, country and the amount. Please check your email if you received the MTCN.\n\n\ After closing that popup you will see the BTC buyer's name and address for picking up the money from Western Union.\n\n\ Only confirm receipt after you have successfully picked up the money! @@ -589,6 +589,10 @@ portfolio.pending.step5_seller.received=You have received: portfolio.pending.role=My role portfolio.pending.tradeInformation=Trade information portfolio.pending.remainingTime=Remaining time +portfolio.pending.remainingTimeDetail={0} (until {1}) +portfolio.pending.tradePeriodInfo=After the first blockchain confirmation, the trade period starts. Based on the payment method used, a different maximum allowed trade period is applied. +portfolio.pending.tradePeriodWarning=If the period is exceeded both trades can open a dispute. +portfolio.pending.tradeNotCompleted=Trade not completed in time (until {0}) portfolio.pending.tradeProcess=Trade process portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the arbitrator arrived (e.g. if you did not got a response after 1 day) feel free to open a dispute again. portfolio.pending.openAgainDispute.button=Open dispute again @@ -963,7 +967,7 @@ account.seed.warn.noPw.msg=You have not setup a wallet password which would prot Do you want to display the seed words? account.seed.warn.noPw.yes=Yes, and don't ask me again account.seed.enterPw=Enter password to view seed words -account.seed.restore.info=Please note that you cannot import a wallet from an old bisq version (any version before 0.5.0), \ +account.seed.restore.info=Please note that you cannot import a wallet from an old Bisq version (any version before 0.5.0), \ because the wallet format has changed!\n\n\ If you want to move the funds from the old version to the new Bisq application send it with a Bitcoin transaction.\n\n\ Also be aware that wallet restore is only for emergency cases and might cause problems with the internal wallet database.\n\ @@ -1290,7 +1294,7 @@ torNetworkSettingWindow.deleteFiles.button=Delete outdated Tor files and shut do torNetworkSettingWindow.deleteFiles.progress=Shut down Tor in progress torNetworkSettingWindow.deleteFiles.success=Outdated Tor files deleted successfully. Please restart. torNetworkSettingWindow.bridges.header=Is Tor blocked? -torNetworkSettingWindow.bridges.info=If Tor is blocked by your internet provider or in your country you can try to use Tor bridges.\n\ +torNetworkSettingWindow.bridges.info=If Tor is blocked by your internet provider or by your country you can try to use Tor bridges.\n\ Visit the Tor web page at: https://bridges.torproject.org/bridges to learn more about \ bridges and pluggable transports. @@ -1514,6 +1518,7 @@ navigation.arbitratorSelection=\"Arbitrator selection\" navigation.funds.availableForWithdrawal=\"Fund/Send funds\" navigation.portfolio.myOpenOffers=\"Portfolio/My open offers\" navigation.portfolio.pending=\"Portfolio/Open trades\" +navigation.portfolio.closedTrades=\"Portfolio/History\" navigation.funds.depositFunds=\"Funds/Receive funds\" navigation.settings.preferences=\"Settings/Preferences\" navigation.funds.transactions=\"Funds/Transactions\" @@ -1526,7 +1531,6 @@ navigation.dao.wallet.receive=\"DAO/BSQ Wallet/Receive\" #################################################################### formatter.formatVolumeLabel={0} amount{1} -formatter.tradePeriodOver=Trade period is over formatter.makerTaker=Maker as {0} {1} / Taker as {2} {3} formatter.youAreAsMaker=You are {0} {1} as maker / Taker is {2} {3} formatter.youAreAsTaker=You are {0} {1} as taker / Maker is {2} {3} @@ -1672,7 +1676,7 @@ payment.altcoin.address.dyn={0} address: payment.accountNr=Account number: payment.emailOrMobile=Email or mobile nr: payment.useCustomAccountName=Use custom account name -payment.maxPeriod=Max. allowed trade period / date: +payment.maxPeriod=Max. allowed trade period: payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. trade limit: {1} / Account age: {2} payment.currencyWithSymbol=Currency: {0} payment.nameOfAcceptedBank=Name of accepted bank diff --git a/common/src/main/resources/i18n/displayStrings_de.properties b/common/src/main/resources/i18n/displayStrings_de.properties index 270fcdeea71..1443c115858 100644 --- a/common/src/main/resources/i18n/displayStrings_de.properties +++ b/common/src/main/resources/i18n/displayStrings_de.properties @@ -166,6 +166,7 @@ shared.viewContractAsJson=Vertrag im JSON-Format ansehen shared.contract.title=Vertrag für den Handel mit der ID: {0} shared.paymentDetails=Zahlungsdetails des BTC-{0}: shared.securityDeposit=Kaution +shared.yourSecurityDeposit=Deine Kaution shared.contract=Vertrag shared.messageArrived=Nachricht angekommen. shared.messageStoredInMailbox=Nachricht in Postfach gespeichert. @@ -258,8 +259,8 @@ market.offerBook.leftButtonAltcoin=Ich möchte {0} kaufen ({1} verkaufen) market.offerBook.rightButtonAltcoin=Ich möchte {0} verkaufen ({1} kaufen) market.offerBook.leftButtonFiat=Ich möchte {0} mit {1} kaufen market.offerBook.rightButtonFiat=Ich möchte {0} für {1} verkaufen -market.offerBook.leftHeaderLabel=Angebote {0} für {1} zu verkaufen -market.offerBook.rightHeaderLabel=Angebote {0} für {1} zu kaufen +market.offerBook.sellOffersHeaderLabel=Verkaufe {0} an +market.offerBook.buyOffersHeaderLabel=Kaufe {0} von market.offerBook.buy=Ich möchte Bitcoins kaufen market.offerBook.sell=Ich möchte Bitcoins verkaufen @@ -341,6 +342,7 @@ createOffer.fundsBox.offerFee=Erstellergebühr: createOffer.fundsBox.networkFee=Mining-Gebühr: createOffer.fundsBox.placeOfferSpinnerInfo=Das Angebot wird veröffentlicht ... createOffer.fundsBox.paymentLabel=Bisq-Handel mit der ID {0} +createOffer.fundsBox.fundsStructure=({0} Kaution, {1} Erstellergebühr, {2} Mining-Gebühr) createOffer.success.headline=Ihr Angebot wurde veröffentlicht createOffer.success.info=Sie können Ihre offenen Angebote unter \"Portfolio/Meine offenen Angebote\" verwalten. @@ -393,6 +395,7 @@ takeOffer.fundsBox.offerFee=Abnehmergebühr: takeOffer.fundsBox.networkFee=Mining-Gebühren (3x): takeOffer.fundsBox.takeOfferSpinnerInfo=Angebot wird angenommen ... takeOffer.fundsBox.paymentLabel=Bisq-Handel mit der ID {0} +takeOffer.fundsBox.fundsStructure=({0} Kaution, {1} Abnehmergebühr, {2} Mining-Gebühr) takeOffer.success.headline=Sie haben erfolgreich ein Angebot angenommen. takeOffer.success.info=Sie können den Status Ihres Angebots unter \"Portfolio/Offene Angebote\" einsehen. takeOffer.error.message=Bei der Angebotsannahme trat ein Fehler auf.\n\n{0} @@ -543,6 +546,10 @@ portfolio.pending.step5_seller.received=Sie haben erhalten: portfolio.pending.role=Meine Rolle portfolio.pending.tradeInformation=Handelsinformationen portfolio.pending.remainingTime=Verbleibende Zeit +portfolio.pending.remainingTimeDetail={0} (bis {1}) +portfolio.pending.tradePeriodInfo=Die Handelsdauer beginnt mit der ersten Blockchain-Bestätigung. Abhängig von der Zahlungart, wird eine maximale Handesldauer gesetzt. +portfolio.pending.tradePeriodWarning=Beim Überschreiten der Handelsdauer kann eine Anfrage auf Konfliktlösung geöffnet werden. +portfolio.pending.tradeNotCompleted=Maximale Handelsdauer wurde überschritten (bis {0}) portfolio.pending.tradeProcess=Handelsprozess portfolio.pending.openAgainDispute.msg=Falls Sie nicht sicher sind, ob Ihre Nachricht den Vermittler erreicht hat (z.B. wenn Sie nach einem Tag noch keine Rückmeldung erhalten haben) können Sie gerne erneut einen Konflikt öffnen. portfolio.pending.openAgainDispute.button=Konflikt erneut öffnen @@ -1275,6 +1282,7 @@ navigation.arbitratorSelection=\"Vermittlerauswahl\" navigation.funds.availableForWithdrawal=\"Gelder/Gelder senden\" navigation.portfolio.myOpenOffers=\"Portfolio/Meine offenen Angebote\" navigation.portfolio.pending=\"Portfolio/Offene Händel\" +navigation.portfolio.closedTrades=\"Portfolio/Verlauf\" navigation.funds.depositFunds=\"Gelder/Gelder erhalten\" navigation.settings.preferences=\"Einstellungen/Voreinstellungen\" navigation.funds.transactions=\"Gelder/Transaktionen\" @@ -1287,7 +1295,6 @@ navigation.dao.wallet.receive=\"DAO/BSQ-Wallet/Erhalten\" #################################################################### formatter.formatVolumeLabel={0} Betrag{1} -formatter.tradePeriodOver=Die Handelsdauer ist abgelaufen formatter.makerTaker=Ersteller als {0} {1} / Abnehmer als {2} {3} formatter.youAreAsMaker=Sie {0} {1} als Ersteller / Abnehmer wird {3} {2} formatter.youAreAsTaker=Sie {0} {1} als Abnehmer / Ersteller wird {3} {2} @@ -1415,8 +1422,8 @@ payment.altcoin.address.dyn={0} Adresse: payment.accountNr=Kontonummer: payment.emailOrMobile=E-Mail oder Mobil: payment.useCustomAccountName=Spezifischen Kontonamen nutzen -payment.maxPeriod=Max. erlaubte Handelsdauer / -datum: -payment.maxPeriodAndLimit=Max. Handelsdauer: {0} / Max. Handelsgrenze: {1} +payment.maxPeriod=Max. erlaubte Handelsdauer: +payment.maxPeriodAndLimit=Max. Handelsdauer: {0} / Max. Handelsgrenze: {1} / Alter des Kontos: {2} payment.currencyWithSymbol=Währung: {0} payment.nameOfAcceptedBank=Name der akzeptierten Bank payment.addAcceptedBank=Akzeptierte Bank hinzufügen diff --git a/common/src/main/resources/i18n/displayStrings_el.properties b/common/src/main/resources/i18n/displayStrings_el.properties index bf2a11e7d82..90e0e211452 100644 --- a/common/src/main/resources/i18n/displayStrings_el.properties +++ b/common/src/main/resources/i18n/displayStrings_el.properties @@ -258,8 +258,8 @@ market.offerBook.leftButtonAltcoin=Θέλω να αγοράσω {0} (πώλησ market.offerBook.rightButtonAltcoin=Θέλω να πουλήσω {0} (αγορά {1}) market.offerBook.leftButtonFiat=Θέλω να αγοράσω {0} με {1} market.offerBook.rightButtonFiat=Θέλω να πουλήσω {0} με {1} -market.offerBook.leftHeaderLabel=Προσφορές πώλησης {0} έναντι {1} -market.offerBook.rightHeaderLabel=Προσφορές αγοράς {0} έναντι {1} +market.offerBook.sellOffersHeaderLabel=Πουλήστε το {0} στο +market.offerBook.buyOffersHeaderLabel=Αγοράστε {0} από market.offerBook.buy=Θέλω να αγοράσω bitcoin market.offerBook.sell=Θέλω να πουλήσω bitcoin @@ -1250,7 +1250,6 @@ navigation.dao.wallet.receive=\"DAO/πορτοφόλι BSQ/Λήψη\" #################################################################### formatter.formatVolumeLabel={0} ποσό{1} -formatter.tradePeriodOver=Λήξη περιόδου συναλλαγής formatter.makerTaker=Maker ως {0} {1} / Taker ως {2} {3} formatter.youAreAsMaker=Είσαι {0} {1} ως maker / Taker είναι {2} {3{} formatter.youAreAsTaker=Είσαι {0} {1} ως taker / Maker είναι {2} {3} @@ -1378,7 +1377,7 @@ payment.altcoin.address.dyn={0} διεύθυνση: payment.accountNr=Αριθμός λογαριασμού: payment.emailOrMobile=Email ή αριθμός κινητού: payment.useCustomAccountName=Χρήση προεπιλεγμένου ονόματος λογαριασμού -payment.maxPeriod=Μέγιστη επιτρεπόμενη χρονική περίοδος συναλλαγής / ημερομηνία: +payment.maxPeriod=Μέγιστη επιτρεπόμενη χρονική περίοδος συναλλαγής: payment.maxPeriodAndLimit=Μέγιστη διάρκεια συναλλαγής: {0} / Μέγιστο όριο συναλλαγής: {1} payment.currencyWithSymbol=Νόμισμα: {0} payment.nameOfAcceptedBank=Όνομα αποδεκτής τράπεζας diff --git a/common/src/main/resources/i18n/displayStrings_es.properties b/common/src/main/resources/i18n/displayStrings_es.properties index 6e4bd4e215f..a9758cb2d8f 100644 --- a/common/src/main/resources/i18n/displayStrings_es.properties +++ b/common/src/main/resources/i18n/displayStrings_es.properties @@ -258,8 +258,8 @@ market.offerBook.leftButtonAltcoin=Quiero comprar {0} (vender {1}) market.offerBook.rightButtonAltcoin=Quiero vender {0} (comprar {1}) market.offerBook.leftButtonFiat=Quiero comprar {0} con {1} market.offerBook.rightButtonFiat=Quiero vender {0} por {1} -market.offerBook.leftHeaderLabel=Ofrece vender {0} por {1} -market.offerBook.rightHeaderLabel=Ofrece comrpar {0} con {1} +market.offerBook.sellOffersHeaderLabel=Vender {0} a +market.offerBook.buyOffersHeaderLabel=Compre {0} desde market.offerBook.buy=Quiero comprar bitcoin market.offerBook.sell=Quiero vender bitcoin @@ -1250,7 +1250,6 @@ navigation.dao.wallet.receive=\"DAO/Monedero BSQ/Recibir\" #################################################################### formatter.formatVolumeLabel={0} cantidad{1} -formatter.tradePeriodOver=El periodo de intercambio se acabó formatter.makerTaker=Creador como {0} {1} / Tomador como {2} {3} formatter.youAreAsMaker=Usted es {0} {1} como creador / Tomador es {2} {3} formatter.youAreAsTaker=Usted es {0} {1} como tomador / Creador es {2} {3} @@ -1378,7 +1377,7 @@ payment.altcoin.address.dyn=Su dirección {0}: payment.accountNr=Número de cuenta: payment.emailOrMobile=Email o número de móvil: payment.useCustomAccountName=Utilizar nombre de cuenta personalizado -payment.maxPeriod=Periodo / fecha máximo de intercambio permitido: +payment.maxPeriod=Periodo máximo de intercambio permitido: payment.maxPeriodAndLimit=Duración máxima de intercambio: {0} / Límite máximo de intercambio: {1} payment.currencyWithSymbol=Moneda: {0} payment.nameOfAcceptedBank=Nombre de banco aceptado diff --git a/common/src/main/resources/i18n/displayStrings_hu.properties b/common/src/main/resources/i18n/displayStrings_hu.properties index 1289b95070c..90b09c2ffb4 100644 --- a/common/src/main/resources/i18n/displayStrings_hu.properties +++ b/common/src/main/resources/i18n/displayStrings_hu.properties @@ -258,8 +258,8 @@ market.offerBook.leftButtonAltcoin=Vásárolni szeretnék {0} (eladni való {1}) market.offerBook.rightButtonAltcoin=Eladni szeretnék {0} ({1} vétel) market.offerBook.leftButtonFiat=Vásárolni szeretnék {0}-ot {1}-ért market.offerBook.rightButtonFiat=Eladni szeretnék {0} cserébe {1}-ért -market.offerBook.leftHeaderLabel=Eladási ajánlatok {0} cserében {1} -market.offerBook.rightHeaderLabel=Vételi ajánlatok {0} cserében {1} +market.offerBook.sellOffersHeaderLabel={0} eladni +market.offerBook.buyOffersHeaderLabel=Vásároljon {0} -tól market.offerBook.buy=Vásárolni szeretnék bitcoinot market.offerBook.sell=Eladni szeretnék bitcoinot @@ -1250,7 +1250,6 @@ navigation.dao.wallet.receive=\"DAO/BSQ Pénztárca/Fogadás\" #################################################################### formatter.formatVolumeLabel={0} összeg{1} -formatter.tradePeriodOver=Tranzakció időszak véget ért formatter.makerTaker=Ajánló mint {0} {1} / Vevő mint {2} {3} formatter.youAreAsMaker=Ön {0} {1} mint ajánló / A vevő {2} {3} formatter.youAreAsTaker=Ön {0} {1} mint vevő / Az ajánló {2} {3} @@ -1378,7 +1377,7 @@ payment.altcoin.address.dyn={0} cím: payment.accountNr=Fiókszám: payment.emailOrMobile=E-mail vagy mobil: payment.useCustomAccountName=Használj egyéni fióknevet -payment.maxPeriod=Max. megengedett tranzakció időszak / dátum: +payment.maxPeriod=Max. megengedett tranzakció időszak: payment.maxPeriodAndLimit=Max. tranzakció időtartama: {0} / Max. tranzakció korlátozás: {1} payment.currencyWithSymbol=Valuta: {0} payment.nameOfAcceptedBank=Elfogadott bank neve diff --git a/common/src/main/resources/i18n/displayStrings_pt.properties b/common/src/main/resources/i18n/displayStrings_pt.properties index 7ec55e74b7d..af663b7a903 100644 --- a/common/src/main/resources/i18n/displayStrings_pt.properties +++ b/common/src/main/resources/i18n/displayStrings_pt.properties @@ -258,8 +258,8 @@ market.offerBook.leftButtonAltcoin=Eu quero comprar {0} (vender {1}) market.offerBook.rightButtonAltcoin=Eu quero vender {0} (comprar {1}) market.offerBook.leftButtonFiat=Eu quero comprar {0} com {1} market.offerBook.rightButtonFiat=Eu quero vender {0} por {1} -market.offerBook.leftHeaderLabel=Ofertas para vender {0} por {1} -market.offerBook.rightHeaderLabel=Ofertas para comprar {0} com {1} +market.offerBook.sellOffersHeaderLabel=Vender {0} para +market.offerBook.buyOffersHeaderLabel=Compre {0} de market.offerBook.buy=Eu quero comprar bitcoin market.offerBook.sell=Eu quero vender bitcoin @@ -1250,7 +1250,6 @@ navigation.dao.wallet.receive=\"DAO/Carteira BSQ/Receber\" #################################################################### formatter.formatVolumeLabel={0} quantia{1} -formatter.tradePeriodOver=O período de negociação acabou formatter.makerTaker=Ofertante como {0} {1} / Aceitador como {2} {3} formatter.youAreAsMaker=Você está {0} {1} como ofertante / Aceitador está {2} {3} formatter.youAreAsTaker=Você está {0} {1} como aceitador / Ofetante é {2} {3} @@ -1378,7 +1377,7 @@ payment.altcoin.address.dyn=Endereço {0}: payment.accountNr=Número da conta: payment.emailOrMobile=Email ou celular: payment.useCustomAccountName=Usar número de conta personalizado: -payment.maxPeriod=Período máximo de negociação / data: +payment.maxPeriod=Período máximo de negociação: payment.maxPeriodAndLimit=Duração máxima de negociação: {0} / Limite de negociação: {1} payment.currencyWithSymbol=Moeda: {0} payment.nameOfAcceptedBank=Nome do banco aceito diff --git a/common/src/main/resources/i18n/displayStrings_ro.properties b/common/src/main/resources/i18n/displayStrings_ro.properties index b76231ff83e..5c6ffe3767a 100644 --- a/common/src/main/resources/i18n/displayStrings_ro.properties +++ b/common/src/main/resources/i18n/displayStrings_ro.properties @@ -264,8 +264,8 @@ market.offerBook.leftButtonAltcoin=Doresc să cumpăr {0} (vând {1}) market.offerBook.rightButtonAltcoin=Doresc să vând {0} (cumpăr {1}) market.offerBook.leftButtonFiat=Doresc să cumpăr {0} cu {1} market.offerBook.rightButtonFiat=Doresc să vând {0} contra {1} -market.offerBook.leftHeaderLabel=Oferte de vânzare {0} contra {1} -market.offerBook.rightHeaderLabel=Oferte de cumpărare {0} cu {1} +market.offerBook.sellOffersHeaderLabel=Vindem {0} la +market.offerBook.buyOffersHeaderLabel=Cumpărați {0} de la market.offerBook.buy=Doresc să cumpăr bitcoin market.offerBook.sell=Doresc să vând bitcoin @@ -1361,7 +1361,6 @@ navigation.dao.wallet.receive=\"Portofel/Încasare DAO/BSQ\" #################################################################### formatter.formatVolumeLabel={0} suma{1} -formatter.tradePeriodOver=Perioada de tranzacționare s-a încheiat formatter.makerTaker=Ofertant ca {0} {1} / Acceptant ca {2} {3} formatter.youAreAsMaker=Tu ești {0} {1} ca ofertant / Acceptantul este {2} {3} formatter.youAreAsTaker=Tu ești {0} {1} ca acceptant / Ofertantul este {2} {3} @@ -1495,7 +1494,7 @@ payment.altcoin.address.dyn=Adresa {0}: payment.accountNr=Număr cont: payment.emailOrMobile=E-mail sau nr. mobil: payment.useCustomAccountName=Folosește nume de cont preferințial -payment.maxPeriod=Data / perioada maximă de tranzacționare permisă: +payment.maxPeriod=Perioada maximă de tranzacționare permisă: payment.maxPeriodAndLimit=Durata maximă de tranzacționare: {0} / Limita maximă de tranzacționare: {1} / Vechimea contului: {2} payment.currencyWithSymbol=Valuta: {0} payment.nameOfAcceptedBank=Numele băncii acceptate diff --git a/common/src/main/resources/i18n/displayStrings_ru.properties b/common/src/main/resources/i18n/displayStrings_ru.properties index 9350dfaa541..2378ec6146a 100644 --- a/common/src/main/resources/i18n/displayStrings_ru.properties +++ b/common/src/main/resources/i18n/displayStrings_ru.properties @@ -258,8 +258,8 @@ market.offerBook.leftButtonAltcoin=Я хочу купить {0} (продать market.offerBook.rightButtonAltcoin=Я хочу продать {0} (купить {1}) market.offerBook.leftButtonFiat=Я хочу купить {0} за {1} market.offerBook.rightButtonFiat=Я хочу продать {0} за {1} -market.offerBook.leftHeaderLabel=Предложения по продаже {0} за {1} -market.offerBook.rightHeaderLabel=Предложения по покупке {0} за {1} +market.offerBook.sellOffersHeaderLabel=Продать {0} до +market.offerBook.buyOffersHeaderLabel=Купите {0} из market.offerBook.buy=Я хочу купить Биткоин market.offerBook.sell=Я хочу продать Биткоин @@ -1250,7 +1250,6 @@ navigation.dao.wallet.receive=\"ДАО/BSQ Кошелёк/Получить\" #################################################################### formatter.formatVolumeLabel={0} сумма{1} -formatter.tradePeriodOver=Время сделки истекло formatter.makerTaker=как создающий предложение {0} {1} / Как принимающий предложение {2} {3} formatter.youAreAsMaker=Вы {0} {1}, как дающий предложение / Принимающий {2} {3} formatter.youAreAsTaker=Вы {0} {1}, как принимающий предложение / Создающий {2} {3} @@ -1378,7 +1377,7 @@ payment.altcoin.address.dyn={0} адрес: payment.accountNr=Номер счёта: payment.emailOrMobile=Электронная почта либо мобильный номер: payment.useCustomAccountName=Использовать собственное название счёта -payment.maxPeriod=Макс. период отведенный на сделку / время: +payment.maxPeriod=Макс. период отведенный на сделку: payment.maxPeriodAndLimit=Макс. продолжительность сделки: {0} / Макс. торговый предел: {1} payment.currencyWithSymbol=Валюта: {0} payment.nameOfAcceptedBank=Название подтверждённого банка diff --git a/common/src/main/resources/i18n/displayStrings_sr.properties b/common/src/main/resources/i18n/displayStrings_sr.properties index a48805ba13d..0e658adf7ec 100644 --- a/common/src/main/resources/i18n/displayStrings_sr.properties +++ b/common/src/main/resources/i18n/displayStrings_sr.properties @@ -258,8 +258,8 @@ market.offerBook.leftButtonAltcoin=Želim da kupim {0} (prodaja {1}) market.offerBook.rightButtonAltcoin=Želim da prodam {0} (kupovina {1}) market.offerBook.leftButtonFiat=Želim da kupim {0} sa {1} market.offerBook.rightButtonFiat=Želim da prodam {0} za {1} -market.offerBook.leftHeaderLabel=Ponude prodaje {0} za {1} -market.offerBook.rightHeaderLabel=Ponude kupovine {0} sa {1} +market.offerBook.sellOffersHeaderLabel=Prodaj {0} u +market.offerBook.buyOffersHeaderLabel=Kupite {0} iz market.offerBook.buy=Želim da kupim bitkoin market.offerBook.sell=Želim da prodam bitkoin @@ -1250,7 +1250,6 @@ navigation.dao.wallet.receive=\"DAO/BSQ Novčanik/Primi\" #################################################################### formatter.formatVolumeLabel={0} iznos{1} -formatter.tradePeriodOver=Period trgovine je završen formatter.makerTaker=Tvorac kao {0} {1} / Uzimalac kao {2} {3} formatter.youAreAsMaker=Vi ste {0} {1} kao tvorac / Uzimalac je {2} {3} formatter.youAreAsTaker=Vi ste {0} {1} kao uzimalac / Tvorac je {2} {3} @@ -1378,7 +1377,7 @@ payment.altcoin.address.dyn={0} adresa: payment.accountNr=Broj računa: payment.emailOrMobile=Email ili br. mobilnog: payment.useCustomAccountName=Koristi prilagođeno ime računa -payment.maxPeriod=Maks. dozvoljeni period trgovine / datum: +payment.maxPeriod=Maks. dozvoljeni period trgovine: payment.maxPeriodAndLimit=Maks. trajanje trgovine: {0} / Maks. rok trgovine: {1} payment.currencyWithSymbol=Valuta: {0} payment.nameOfAcceptedBank=Ime prihvaćene banke diff --git a/common/src/main/resources/i18n/displayStrings_zh.properties b/common/src/main/resources/i18n/displayStrings_zh.properties index a78001b8ef7..e4c8ff27ef6 100644 --- a/common/src/main/resources/i18n/displayStrings_zh.properties +++ b/common/src/main/resources/i18n/displayStrings_zh.properties @@ -258,8 +258,8 @@ market.offerBook.leftButtonAltcoin=我想要买入 {0} (卖出 {1}) market.offerBook.rightButtonAltcoin=我想要卖出 {0} (买入 {1}) market.offerBook.leftButtonFiat=我想要用 {1} 买入 {0} market.offerBook.rightButtonFiat=我想要用 {1} 卖出 {0} -market.offerBook.leftHeaderLabel={1} 卖出 {0} 列表 -market.offerBook.rightHeaderLabel={1} 买入 {0} 列表 +market.offerBook.sellOffersHeaderLabel=出售 {0} 到 +market.offerBook.buyOffersHeaderLabel=从中购买 {0} market.offerBook.buy=我想要买入比特币 market.offerBook.sell=我想要卖出比特币 @@ -1250,7 +1250,6 @@ navigation.dao.wallet.receive=\"DAO/BSQ 钱包/接收\" #################################################################### formatter.formatVolumeLabel={0} 数量 {1} -formatter.tradePeriodOver=交易期结束 formatter.makerTaker=卖家{0} {1} / 买家 {2} {3} formatter.youAreAsMaker=您是{0} {1}卖家 / 买家是 {2} {3} formatter.youAreAsTaker=您是{0} {1} 买家 / 卖家是 {2} {3} @@ -1378,7 +1377,7 @@ payment.altcoin.address.dyn={0} 地址: payment.accountNr=账号: payment.emailOrMobile=电子邮箱或手机号码: payment.useCustomAccountName=使用自定义名称 -payment.maxPeriod=最大允许时限 / 日期: +payment.maxPeriod=最大允许时限: payment.maxPeriodAndLimit=最大交易期限:{0} /最大交易限额:{1} payment.currencyWithSymbol=货币:{0} payment.nameOfAcceptedBank=接受的银行名称 diff --git a/common/src/main/resources/i18n/in_dev/displayStrings_fr.properties b/common/src/main/resources/i18n/in_dev/displayStrings_fr.properties index e7a7a61bcd4..171f740277d 100644 --- a/common/src/main/resources/i18n/in_dev/displayStrings_fr.properties +++ b/common/src/main/resources/i18n/in_dev/displayStrings_fr.properties @@ -434,8 +434,8 @@ market.offerBook.leftButtonAltcoin=Je veux acheter {0} (vendre {1}) market.offerBook.rightButtonAltcoin=Je veux vendre {0} (buy {1}) market.offerBook.leftButtonFiat=Je veux acheter {0} avec {1} market.offerBook.rightButtonFiat=Je veux vendre {0} pour {1} -market.offerBook.leftHeaderLabel=Offres de vente {0} pour {1} -market.offerBook.rightHeaderLabel=Offres pour acheter {0} avec {1} +market.offerBook.sellOffersHeaderLabel=Vendre {0} à +market.offerBook.buyOffersHeaderLabel=Acheter {0} à partir de market.offerBook.buy=Je veux acheter du bitcoin market.offerBook.sell=Je veux vendre du bitcoin market.spread.numberOfOffersColumn=Toutes les offres ({0}) diff --git a/core/src/main/java/io/bisq/core/btc/wallet/BsqWalletService.java b/core/src/main/java/io/bisq/core/btc/wallet/BsqWalletService.java index 3b53566cba9..f19467b1a2b 100644 --- a/core/src/main/java/io/bisq/core/btc/wallet/BsqWalletService.java +++ b/core/src/main/java/io/bisq/core/btc/wallet/BsqWalletService.java @@ -17,8 +17,6 @@ package io.bisq.core.btc.wallet; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; import io.bisq.core.app.BisqEnvironment; import io.bisq.core.btc.Restrictions; import io.bisq.core.btc.exceptions.TransactionVerificationException; @@ -365,7 +363,7 @@ public void commitTx(Transaction tx) { public Transaction getPreparedSendTx(String receiverAddress, Coin receiverAmount) throws AddressFormatException, - InsufficientMoneyException, WalletException, TransactionVerificationException { + InsufficientBsqException, WalletException, TransactionVerificationException { Transaction tx = new Transaction(params); checkArgument(Restrictions.isAboveDust(receiverAmount), @@ -381,7 +379,11 @@ public Transaction getPreparedSendTx(String receiverAddress, sendRequest.signInputs = false; sendRequest.ensureMinRequiredFee = false; sendRequest.changeAddress = getUnusedAddress(); - wallet.completeTx(sendRequest); + try { + wallet.completeTx(sendRequest); + } catch (InsufficientMoneyException e) { + throw new InsufficientBsqException(e.missing); + } checkWalletConsistency(wallet); verifyTransaction(tx); // printTx("prepareSendTx", tx); @@ -394,7 +396,7 @@ public Transaction getPreparedSendTx(String receiverAddress, /////////////////////////////////////////////////////////////////////////////////////////// public Transaction getPreparedBurnFeeTx(Coin fee) throws - InsufficientMoneyException, ChangeBelowDustException { + InsufficientBsqException, ChangeBelowDustException { Transaction tx = new Transaction(params); // We might have no output if inputs match fee. @@ -404,9 +406,13 @@ public Transaction getPreparedBurnFeeTx(Coin fee) throws // TODO check dust output CoinSelection coinSelection = bsqCoinSelector.select(fee, wallet.calculateAllSpendCandidates()); coinSelection.gathered.stream().forEach(tx::addInput); - Coin change = bsqCoinSelector.getChange(fee, coinSelection); - if (change.isPositive()) - tx.addOutput(change, getUnusedAddress()); + try { + Coin change = bsqCoinSelector.getChange(fee, coinSelection); + if (change.isPositive()) + tx.addOutput(change, getUnusedAddress()); + } catch (InsufficientMoneyException e) { + throw new InsufficientBsqException(e.missing); + } //printTx("getPreparedBurnFeeTx", tx); return tx; diff --git a/core/src/main/java/io/bisq/core/btc/wallet/BtcNodeConverter.java b/core/src/main/java/io/bisq/core/btc/wallet/BtcNodeConverter.java new file mode 100644 index 00000000000..b15aa5312ac --- /dev/null +++ b/core/src/main/java/io/bisq/core/btc/wallet/BtcNodeConverter.java @@ -0,0 +1,132 @@ +/* + * 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 io.bisq.core.btc.wallet; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; +import io.bisq.core.btc.BitcoinNodes.BtcNode; +import io.bisq.network.DnsLookupException; +import io.bisq.network.DnsLookupTor; +import org.bitcoinj.core.PeerAddress; +import org.bitcoinj.net.OnionCat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Objects; + +class BtcNodeConverter { + private static final Logger log = LoggerFactory.getLogger(BtcNodeConverter.class); + + private final Facade facade; + + BtcNodeConverter() { + this.facade = new Facade(); + } + + BtcNodeConverter(Facade facade) { + this.facade = facade; + } + + @Nullable + PeerAddress convertOnionHost(BtcNode node) { + // no DNS lookup for onion addresses + String onionAddress = Objects.requireNonNull(node.getOnionAddress()); + try { + // OnionCat.onionHostToInetAddress converts onion to ipv6 representation + // inetAddress is not used but required for wallet persistence. Throws nullPointer if not set. + InetAddress inetAddress = facade.onionHostToInetAddress(onionAddress); + PeerAddress result = new PeerAddress(onionAddress, node.getPort()); + result.setAddr(inetAddress); + return result; + } catch (UnknownHostException e) { + log.error("Failed to convert node", e); + return null; + } + } + + @Nullable + PeerAddress convertClearNode(BtcNode node) { + int port = node.getPort(); + + PeerAddress result = create(node.getHostNameOrAddress(), port); + if (result == null) { + String address = node.getAddress(); + if (address != null) { + result = create(address, port); + } else { + log.warn("Lookup failed, no address for node", node); + } + } + return result; + } + + @Nullable + PeerAddress convertWithTor(BtcNode node, Socks5Proxy proxy) { + int port = node.getPort(); + + PeerAddress result = create(proxy, node.getHostNameOrAddress(), port); + if (result == null) { + String address = node.getAddress(); + if (address != null) { + result = create(proxy, address, port); + } else { + log.warn("Lookup failed, no address for node", node); + } + } + return result; + } + + @Nullable + private PeerAddress create(Socks5Proxy proxy, String host, int port) { + try { + // We use DnsLookupTor to not leak with DNS lookup + // Blocking call. takes about 600 ms ;-( + InetAddress lookupAddress = facade.torLookup(proxy, host); + InetSocketAddress address = new InetSocketAddress(lookupAddress, port); + return new PeerAddress(address.getAddress(), address.getPort()); + } catch (Exception e) { + log.error("Failed to create peer address", e); + return null; + } + } + + @Nullable + private static PeerAddress create(String hostName, int port) { + try { + InetSocketAddress address = new InetSocketAddress(hostName, port); + return new PeerAddress(address.getAddress(), address.getPort()); + } catch (Exception e) { + log.error("Failed to create peer address", e); + return null; + } + } + + static class Facade { + InetAddress onionHostToInetAddress(String onionAddress) throws UnknownHostException { + return OnionCat.onionHostToInetAddress(onionAddress); + } + + InetAddress torLookup(Socks5Proxy proxy, String host) throws DnsLookupException { + return DnsLookupTor.lookup(proxy, host); + } + } +} + diff --git a/core/src/main/java/io/bisq/core/btc/wallet/InsufficientBsqException.java b/core/src/main/java/io/bisq/core/btc/wallet/InsufficientBsqException.java new file mode 100644 index 00000000000..9c5246bc928 --- /dev/null +++ b/core/src/main/java/io/bisq/core/btc/wallet/InsufficientBsqException.java @@ -0,0 +1,10 @@ +package io.bisq.core.btc.wallet; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; + +public class InsufficientBsqException extends InsufficientMoneyException { + public InsufficientBsqException(Coin missing) { + super(missing); + } +} diff --git a/core/src/main/java/io/bisq/core/btc/wallet/PeerAddressesRepository.java b/core/src/main/java/io/bisq/core/btc/wallet/PeerAddressesRepository.java new file mode 100644 index 00000000000..c2dac3e2188 --- /dev/null +++ b/core/src/main/java/io/bisq/core/btc/wallet/PeerAddressesRepository.java @@ -0,0 +1,89 @@ +/* + * 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 io.bisq.core.btc.wallet; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; +import io.bisq.core.btc.BitcoinNodes.BtcNode; +import org.bitcoinj.core.PeerAddress; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +class PeerAddressesRepository { + private final BtcNodeConverter converter; + private final List nodes; + + PeerAddressesRepository(BtcNodeConverter converter, List nodes) { + this.converter = converter; + this.nodes = nodes; + } + + PeerAddressesRepository(List nodes) { + this(new BtcNodeConverter(), nodes); + } + + List getPeerAddresses(@Nullable Socks5Proxy proxy, boolean isUseClearNodesWithProxies) { + List result; + // We connect to onion nodes only in case we use Tor for BitcoinJ (default) to avoid privacy leaks at + // exit nodes with bloom filters. + if (proxy != null) { + List onionHosts = getOnionHosts(); + result = new ArrayList<>(onionHosts); + + if (isUseClearNodesWithProxies) { + // We also use the clear net nodes (used for monitor) + List torAddresses = getClearNodesBehindProxy(proxy); + result.addAll(torAddresses); + } + } else { + result = getClearNodes(); + } + return result; + } + + private List getClearNodes() { + return nodes.stream() + .filter(BtcNode::hasClearNetAddress) + .flatMap(node -> nullableAsStream(converter.convertClearNode(node))) + .collect(Collectors.toList()); + } + + private List getOnionHosts() { + return nodes.stream() + .filter(BtcNode::hasOnionAddress) + .flatMap(node -> nullableAsStream(converter.convertOnionHost(node))) + .collect(Collectors.toList()); + } + + private List getClearNodesBehindProxy(Socks5Proxy proxy) { + return nodes.stream() + .filter(BtcNode::hasClearNetAddress) + .flatMap(node -> nullableAsStream(converter.convertWithTor(node, proxy))) + .collect(Collectors.toList()); + } + + private static Stream nullableAsStream(@Nullable T item) { + return Optional.ofNullable(item) + .map(Stream::of) + .orElse(Stream.empty()); + } +} diff --git a/core/src/main/java/io/bisq/core/btc/wallet/WalletNetworkConfig.java b/core/src/main/java/io/bisq/core/btc/wallet/WalletNetworkConfig.java new file mode 100644 index 00000000000..b0ffbda92ef --- /dev/null +++ b/core/src/main/java/io/bisq/core/btc/wallet/WalletNetworkConfig.java @@ -0,0 +1,70 @@ +/* + * 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 io.bisq.core.btc.wallet; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; +import io.bisq.network.Socks5MultiDiscovery; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.PeerAddress; +import org.bitcoinj.params.MainNetParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.List; + +class WalletNetworkConfig { + private static final Logger log = LoggerFactory.getLogger(WalletNetworkConfig.class); + + @Nullable + private final Socks5Proxy proxy; + private final WalletConfig delegate; + private final NetworkParameters parameters; + private final int socks5DiscoverMode; + + WalletNetworkConfig(WalletConfig delegate, NetworkParameters parameters, int socks5DiscoverMode, + @Nullable Socks5Proxy proxy) { + this.delegate = delegate; + this.parameters = parameters; + this.socks5DiscoverMode = socks5DiscoverMode; + this.proxy = proxy; + } + + void proposePeers(List peers) { + if (!peers.isEmpty()) { + log.info("You connect with peerAddresses: {}", peers); + PeerAddress[] peerAddresses = peers.toArray(new PeerAddress[peers.size()]); + delegate.setPeerNodes(peerAddresses); + } else if (proxy != null) { + if (log.isWarnEnabled()) { + MainNetParams mainNetParams = MainNetParams.get(); + if (parameters.equals(mainNetParams)) { + log.warn("You use the public Bitcoin network and are exposed to privacy issues " + + "caused by the broken bloom filters. See https://bisq.network/blog/privacy-in-bitsquare/ " + + "for more info. It is recommended to use the provided nodes."); + } + } + // SeedPeers uses hard coded stable addresses (from MainNetParams). It should be updated from time to time. + delegate.setDiscovery(new Socks5MultiDiscovery(proxy, parameters, socks5DiscoverMode)); + } else { + log.warn("You don't use tor and use the public Bitcoin network and are exposed to privacy issues " + + "caused by the broken bloom filters. See https://bisq.network/blog/privacy-in-bitsquare/ " + + "for more info. It is recommended to use Tor and the provided nodes."); + } + } +} diff --git a/core/src/main/java/io/bisq/core/btc/wallet/WalletSetupPreferences.java b/core/src/main/java/io/bisq/core/btc/wallet/WalletSetupPreferences.java new file mode 100644 index 00000000000..29b460f2e91 --- /dev/null +++ b/core/src/main/java/io/bisq/core/btc/wallet/WalletSetupPreferences.java @@ -0,0 +1,101 @@ +/* + * 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 io.bisq.core.btc.wallet; + +import io.bisq.common.util.Utilities; +import io.bisq.core.btc.BitcoinNodes; +import io.bisq.core.btc.BitcoinNodes.BitcoinNodesOption; +import io.bisq.core.btc.BitcoinNodes.BtcNode; +import io.bisq.core.user.Preferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static io.bisq.core.btc.BitcoinNodes.BitcoinNodesOption.CUSTOM; +import static io.bisq.core.btc.wallet.WalletsSetup.DEFAULT_CONNECTIONS; + + +class WalletSetupPreferences { + private static final Logger log = LoggerFactory.getLogger(WalletSetupPreferences.class); + + private final Preferences preferences; + + WalletSetupPreferences(Preferences preferences) { + this.preferences = preferences; + } + + List selectPreferredNodes(BitcoinNodes nodes) { + List result; + + BitcoinNodesOption nodesOption = BitcoinNodesOption.values()[preferences.getBitcoinNodesOptionOrdinal()]; + switch (nodesOption) { + case CUSTOM: + String bitcoinNodes = preferences.getBitcoinNodes(); + Set distinctNodes = Utilities.commaSeparatedListToSet(bitcoinNodes, false); + result = BitcoinNodes.toBtcNodesList(distinctNodes); + if (result.isEmpty()) { + log.warn("Custom nodes is set but no valid nodes are provided. " + + "We fall back to provided nodes option."); + preferences.setBitcoinNodesOptionOrdinal(BitcoinNodesOption.PROVIDED.ordinal()); + result = nodes.getProvidedBtcNodes(); + } + break; + case PUBLIC: + result = Collections.emptyList(); + break; + case PROVIDED: + default: + result = nodes.getProvidedBtcNodes(); + break; + } + + return result; + } + + boolean isUseCustomNodes() { + return CUSTOM.ordinal() == preferences.getBitcoinNodesOptionOrdinal(); + } + + int calculateMinBroadcastConnections(List nodes) { + BitcoinNodesOption nodesOption = BitcoinNodesOption.values()[preferences.getBitcoinNodesOptionOrdinal()]; + int result; + switch (nodesOption) { + case CUSTOM: + // We have set the nodes already above + result = (int) Math.ceil(nodes.size() * 0.5); + // If Tor is set we usually only use onion nodes, + // but if user provides mixed clear net and onion nodes we want to use both + break; + case PUBLIC: + // We keep the empty nodes + result = (int) Math.floor(DEFAULT_CONNECTIONS * 0.8); + break; + case PROVIDED: + default: + // We require only 4 nodes instead of 7 (for 9 max connections) because our provided nodes + // are more reliable than random public nodes. + result = 4; + break; + } + return result; + } + +} diff --git a/core/src/main/java/io/bisq/core/btc/wallet/WalletsSetup.java b/core/src/main/java/io/bisq/core/btc/wallet/WalletsSetup.java index c26ee5915a8..faed22aaf91 100644 --- a/core/src/main/java/io/bisq/core/btc/wallet/WalletsSetup.java +++ b/core/src/main/java/io/bisq/core/btc/wallet/WalletsSetup.java @@ -28,11 +28,10 @@ import io.bisq.common.handlers.ExceptionHandler; import io.bisq.common.handlers.ResultHandler; import io.bisq.common.storage.FileUtil; -import io.bisq.common.util.Utilities; import io.bisq.core.app.BisqEnvironment; import io.bisq.core.btc.*; +import io.bisq.core.btc.BitcoinNodes.BtcNode; import io.bisq.core.user.Preferences; -import io.bisq.network.DnsLookupTor; import io.bisq.network.Socks5MultiDiscovery; import io.bisq.network.Socks5ProxyProvider; import javafx.beans.property.*; @@ -40,8 +39,6 @@ import org.apache.commons.lang3.StringUtils; import org.bitcoinj.core.*; import org.bitcoinj.core.listeners.DownloadProgressTracker; -import org.bitcoinj.net.OnionCat; -import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.RegTestParams; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.DeterministicSeed; @@ -53,7 +50,6 @@ import java.io.File; import java.io.IOException; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.file.Paths; import java.util.ArrayList; @@ -72,7 +68,7 @@ @Slf4j public class WalletsSetup { // We reduce defaultConnections from 12 (PeerGroup.DEFAULT_CONNECTIONS) to 9 nodes - private static final int DEFAULT_CONNECTIONS = 9; + static final int DEFAULT_CONNECTIONS = 9; private static final long STARTUP_TIMEOUT = 180; private static final String BSQ_WALLET_FILE_NAME = "bisq_BSQ.wallet"; @@ -292,128 +288,19 @@ private void configPeerNodesForLocalHostBitcoinNode() { walletConfig.setPeerNodesForLocalHost(); } - private void configPeerNodes(Socks5Proxy socks5Proxy) { - boolean useCustomNodes = false; - List btcNodeList = new ArrayList<>(); + private void configPeerNodes(@Nullable Socks5Proxy proxy) { + WalletSetupPreferences walletSetupPreferences = new WalletSetupPreferences(preferences); - // We prefer to duplicate the check for CUSTOM here as in case the custom nodes lead to an empty list we fall back to the PROVIDED mode. - if (preferences.getBitcoinNodesOptionOrdinal() == BitcoinNodes.BitcoinNodesOption.CUSTOM.ordinal()) { - btcNodeList = BitcoinNodes.toBtcNodesList(Utilities.commaSeparatedListToSet(preferences.getBitcoinNodes(), false)); - if (btcNodeList.isEmpty()) { - log.warn("Custom nodes is set but no valid nodes are provided. We fall back to provided nodes option."); - preferences.setBitcoinNodesOptionOrdinal(BitcoinNodes.BitcoinNodesOption.PROVIDED.ordinal()); - } - } + List nodes = walletSetupPreferences.selectPreferredNodes(bitcoinNodes); + int minBroadcastConnections = walletSetupPreferences.calculateMinBroadcastConnections(nodes); + walletConfig.setMinBroadcastConnections(minBroadcastConnections); - switch (BitcoinNodes.BitcoinNodesOption.values()[preferences.getBitcoinNodesOptionOrdinal()]) { - case CUSTOM: - // We have set the btcNodeList already above - walletConfig.setMinBroadcastConnections((int) Math.ceil(btcNodeList.size() * 0.5)); - // If Tor is set we usually only use onion nodes, but if user provides mixed clear net and onion nodes we want to use both - useCustomNodes = true; - break; - case PUBLIC: - // We keep the empty btcNodeList - walletConfig.setMinBroadcastConnections((int) Math.floor(DEFAULT_CONNECTIONS * 0.8)); - break; - default: - case PROVIDED: - btcNodeList = bitcoinNodes.getProvidedBtcNodes(); - // We require only 4 nodes instead of 7 (for 9 max connections) because our provided nodes - // are more reliable than random public nodes. - walletConfig.setMinBroadcastConnections(4); - break; - } + PeerAddressesRepository repository = new PeerAddressesRepository(nodes); + boolean isUseClearNodesWithProxies = (useAllProvidedNodes || walletSetupPreferences.isUseCustomNodes()); + List peers = repository.getPeerAddresses(proxy, isUseClearNodesWithProxies); - List peerAddressList = new ArrayList<>(); - final boolean useTorForBitcoinJ = socks5Proxy != null; - // We connect to onion nodes only in case we use Tor for BitcoinJ (default) to avoid privacy leaks at - // exit nodes with bloom filters. - if (useTorForBitcoinJ) { - btcNodeList.stream() - .filter(BitcoinNodes.BtcNode::hasOnionAddress) - .forEach(btcNode -> { - // no DNS lookup for onion addresses - log.info("We add a onion node. btcNode={}", btcNode); - final String onionAddress = checkNotNull(btcNode.getOnionAddress()); - try { - // OnionCat.onionHostToInetAddress converts onion to ipv6 representation - // inetAddress is not used but required for wallet persistence. Throws nullPointer if not set. - final InetAddress inetAddress = OnionCat.onionHostToInetAddress(onionAddress); - final PeerAddress peerAddress = new PeerAddress(onionAddress, btcNode.getPort()); - peerAddress.setAddr(inetAddress); - peerAddressList.add(peerAddress); - } catch (UnknownHostException e) { - log.error("OnionCat.onionHostToInetAddress() failed with btcNode={}, error={}", btcNode.toString(), e.toString()); - e.printStackTrace(); - } - }); - if (useAllProvidedNodes || useCustomNodes) { - // We also use the clear net nodes (used for monitor) - btcNodeList.stream() - .filter(BitcoinNodes.BtcNode::hasClearNetAddress) - .forEach(btcNode -> { - try { - // We use DnsLookupTor to not leak with DNS lookup - // Blocking call. takes about 600 ms ;-( - InetSocketAddress address = new InetSocketAddress(DnsLookupTor.lookup(socks5Proxy, btcNode.getHostNameOrAddress()), btcNode.getPort()); - log.info("We add a clear net node (tor is used) with InetAddress={}, btcNode={}", address.getAddress(), btcNode); - peerAddressList.add(new PeerAddress(address.getAddress(), address.getPort())); - } catch (Exception e) { - if (btcNode.getAddress() != null) { - log.warn("Dns lookup failed. We try with provided IP address. BtcNode: {}", btcNode); - try { - InetSocketAddress address = new InetSocketAddress(DnsLookupTor.lookup(socks5Proxy, btcNode.getAddress()), btcNode.getPort()); - log.info("We add a clear net node (tor is used) with InetAddress={}, BtcNode={}", address.getAddress(), btcNode); - peerAddressList.add(new PeerAddress(address.getAddress(), address.getPort())); - } catch (Exception e2) { - log.warn("Dns lookup failed for BtcNode: {}", btcNode); - } - } else { - log.warn("Dns lookup failed. No IP address is provided. BtcNode: {}", btcNode); - } - } - }); - } - } else { - btcNodeList.stream() - .filter(BitcoinNodes.BtcNode::hasClearNetAddress) - .forEach(btcNode -> { - try { - InetSocketAddress address = new InetSocketAddress(btcNode.getHostNameOrAddress(), btcNode.getPort()); - log.info("We add a clear net node (no tor is used) with host={}, btcNode.getPort()={}", btcNode.getHostNameOrAddress(), btcNode.getPort()); - peerAddressList.add(new PeerAddress(address.getAddress(), address.getPort())); - } catch (Throwable t) { - if (btcNode.getAddress() != null) { - log.warn("Dns lookup failed. We try with provided IP address. BtcNode: {}", btcNode); - try { - InetSocketAddress address = new InetSocketAddress(btcNode.getAddress(), btcNode.getPort()); - log.info("We add a clear net node (no tor is used) with host={}, btcNode.getPort()={}", btcNode.getHostNameOrAddress(), btcNode.getPort()); - peerAddressList.add(new PeerAddress(address.getAddress(), address.getPort())); - } catch (Throwable t2) { - log.warn("Failed to create InetSocketAddress from btcNode {}", btcNode); - } - } else { - log.warn("Dns lookup failed. No IP address is provided. BtcNode: {}", btcNode); - } - } - }); - } - - if (!peerAddressList.isEmpty()) { - final PeerAddress[] peerAddresses = peerAddressList.toArray(new PeerAddress[peerAddressList.size()]); - log.info("You connect with peerAddresses: " + peerAddressList.toString()); - walletConfig.setPeerNodes(peerAddresses); - } else if (useTorForBitcoinJ) { - if (params == MainNetParams.get()) - log.warn("You use the public Bitcoin network and are exposed to privacy issues caused by the broken bloom filters." + - "See https://bisq.network/blog/privacy-in-bitsquare/ for more info. It is recommended to use the provided nodes."); - // SeedPeers uses hard coded stable addresses (from MainNetParams). It should be updated from time to time. - walletConfig.setDiscovery(new Socks5MultiDiscovery(socks5Proxy, params, socks5DiscoverMode)); - } else { - log.warn("You don't use gtor and use the public Bitcoin network and are exposed to privacy issues caused by the broken bloom filters." + - "See https://bisq.network/blog/privacy-in-bitsquare/ for more info. It is recommended to use Tor and the provided nodes."); - } + WalletNetworkConfig networkConfig = new WalletNetworkConfig(walletConfig, params, socks5DiscoverMode, proxy); + networkConfig.proposePeers(peers); } diff --git a/core/src/main/java/io/bisq/core/dao/blockchain/parse/BsqBlockChain.java b/core/src/main/java/io/bisq/core/dao/blockchain/parse/BsqBlockChain.java index 2967f29802b..c68fc0be055 100644 --- a/core/src/main/java/io/bisq/core/dao/blockchain/parse/BsqBlockChain.java +++ b/core/src/main/java/io/bisq/core/dao/blockchain/parse/BsqBlockChain.java @@ -384,8 +384,6 @@ public boolean containsTx(String txId) { public Optional findTx(String txId) { Tx tx = getTxMap().get(txId); - if (tx == null) - tx = getGenesisTx(); //todo have to be in txmap already -> remove after check if (tx != null) return Optional.of(tx); else @@ -396,7 +394,6 @@ public int getChainHeadHeight() { return lock.read(() -> chainHeadHeight); } - // Only used for Json Exporter public Map getTxMap() { return lock.read(() -> txMap); } diff --git a/core/src/main/java/io/bisq/core/dao/blockchain/parse/BsqParser.java b/core/src/main/java/io/bisq/core/dao/blockchain/parse/BsqParser.java index d4949b0ba57..22b963cf36f 100644 --- a/core/src/main/java/io/bisq/core/dao/blockchain/parse/BsqParser.java +++ b/core/src/main/java/io/bisq/core/dao/blockchain/parse/BsqParser.java @@ -102,6 +102,7 @@ void parseBsqBlock(BsqBlock bsqBlock, // Parsing with data requested from bsqBlockchainService /////////////////////////////////////////////////////////////////////////////////////////// + @VisibleForTesting void parseBlocks(int startBlockHeight, int chainHeadHeight, int genesisBlockHeight, @@ -112,6 +113,7 @@ void parseBlocks(int startBlockHeight, for (int blockHeight = startBlockHeight; blockHeight <= chainHeadHeight; blockHeight++) { long startTs = System.currentTimeMillis(); Block btcdBlock = rpcService.requestBlock(blockHeight); + List bsqTxsInBlock = findBsqTxsInBlock(btcdBlock, genesisBlockHeight, genesisTxId); diff --git a/core/src/main/java/io/bisq/core/dao/compensation/CompensationRequest.java b/core/src/main/java/io/bisq/core/dao/compensation/CompensationRequest.java index 8ef0cd12982..33284b0e1d0 100644 --- a/core/src/main/java/io/bisq/core/dao/compensation/CompensationRequest.java +++ b/core/src/main/java/io/bisq/core/dao/compensation/CompensationRequest.java @@ -18,17 +18,23 @@ package io.bisq.core.dao.compensation; import io.bisq.common.proto.persistable.PersistablePayload; +import io.bisq.core.btc.wallet.BsqWalletService; import io.bisq.generated.protobuffer.PB; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; import org.springframework.util.CollectionUtils; import javax.annotation.Nullable; import java.util.Map; import java.util.Optional; +import static com.google.common.base.Preconditions.checkNotNull; + // Represents the state of the CompensationRequest data // TODO cleanup @Getter @@ -51,6 +57,14 @@ public final class CompensationRequest implements PersistablePayload { private boolean closed; @Setter //TODO private boolean waitingForVotingPeriod; + @Setter + private Coin compensationRequestFee; + @Setter + private Transaction feeTx; + @Setter + Transaction txWithBtcFee; + @Setter + private Transaction signedTx; @Nullable private Map extraDataMap; @@ -108,4 +122,17 @@ public static CompensationRequest fromProto(PB.CompensationRequest proto) { proto.getWaitingForVotingPeriod(), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); } + + /// API + public Coin getRequestedBsq() { + checkNotNull(payload); + return payload.getRequestedBsq(); + } + + public Address getIssuanceAddress (BsqWalletService bsqWalletService) { + checkNotNull(payload); + // Remove leading 'B' + String underlyingBtcAddress = payload.getBsqAddress().substring(1, payload.getBsqAddress().length()); + return Address.fromBase58(bsqWalletService.getParams(), underlyingBtcAddress); + } } diff --git a/core/src/main/java/io/bisq/core/dao/compensation/CompensationRequestManager.java b/core/src/main/java/io/bisq/core/dao/compensation/CompensationRequestManager.java index 72f67aa5eed..2bc5c0f2a26 100644 --- a/core/src/main/java/io/bisq/core/dao/compensation/CompensationRequestManager.java +++ b/core/src/main/java/io/bisq/core/dao/compensation/CompensationRequestManager.java @@ -17,18 +17,28 @@ package io.bisq.core.dao.compensation; +import com.google.common.util.concurrent.FutureCallback; import com.google.inject.Inject; import io.bisq.common.UserThread; import io.bisq.common.app.DevEnv; +import io.bisq.common.app.Version; +import io.bisq.common.crypto.Hash; import io.bisq.common.crypto.KeyRing; import io.bisq.common.proto.persistable.PersistedDataHost; import io.bisq.common.storage.Storage; +import io.bisq.common.util.Utilities; import io.bisq.core.app.BisqEnvironment; +import io.bisq.core.btc.exceptions.TransactionVerificationException; +import io.bisq.core.btc.exceptions.WalletException; import io.bisq.core.btc.wallet.BsqWalletService; +import io.bisq.core.btc.wallet.BtcWalletService; +import io.bisq.core.btc.wallet.ChangeBelowDustException; +import io.bisq.core.dao.DaoConstants; import io.bisq.core.dao.DaoPeriodService; import io.bisq.core.dao.blockchain.BsqBlockChainChangeDispatcher; import io.bisq.core.dao.blockchain.BsqBlockChainListener; import io.bisq.core.dao.blockchain.parse.BsqBlockChain; +import io.bisq.core.provider.fee.FeeService; import io.bisq.network.p2p.P2PService; import io.bisq.network.p2p.storage.HashMapChangedListener; import io.bisq.network.p2p.storage.payload.ProtectedStorageEntry; @@ -37,21 +47,33 @@ import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.bitcoinj.core.*; +import org.bitcoinj.crypto.DeterministicKey; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.security.PublicKey; import java.util.Optional; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + public class CompensationRequestManager implements PersistedDataHost, BsqBlockChainListener, HashMapChangedListener { private static final Logger log = LoggerFactory.getLogger(CompensationRequestManager.class); private final P2PService p2PService; private final DaoPeriodService daoPeriodService; private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; private final BsqBlockChain bsqBlockChain; private final Storage compensationRequestsStorage; private final PublicKey signaturePubKey; + private final FeeService feeService; @Getter private final ObservableList allRequests = FXCollections.observableArrayList(); @@ -68,16 +90,20 @@ public class CompensationRequestManager implements PersistedDataHost, BsqBlockCh @Inject public CompensationRequestManager(P2PService p2PService, BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, DaoPeriodService daoPeriodService, BsqBlockChain bsqBlockChain, BsqBlockChainChangeDispatcher bsqBlockChainChangeDispatcher, KeyRing keyRing, - Storage compensationRequestsStorage) { + Storage compensationRequestsStorage, + FeeService feeService) { this.p2PService = p2PService; - this.daoPeriodService = daoPeriodService; this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.daoPeriodService = daoPeriodService; this.bsqBlockChain = bsqBlockChain; this.compensationRequestsStorage = compensationRequestsStorage; + this.feeService = feeService; signaturePubKey = keyRing.getPubKeyRing().getSignaturePubKey(); bsqBlockChainChangeDispatcher.addBsqBlockChainListener(this); @@ -114,6 +140,79 @@ public void addToP2PNetwork(CompensationRequestPayload compensationRequestPayloa p2PService.addProtectedStorageEntry(compensationRequestPayload, true); } + public CompensationRequest prepareCompensationRequest(CompensationRequestPayload compensationRequestPayload) + throws InsufficientMoneyException, ChangeBelowDustException, TransactionVerificationException, WalletException, IOException { + CompensationRequest compensationRequest = new CompensationRequest(compensationRequestPayload); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + compensationRequest.setCompensationRequestFee(feeService.getCreateCompensationRequestFee()); + compensationRequest.setFeeTx(bsqWalletService.getPreparedBurnFeeTx(compensationRequest.getCompensationRequestFee())); + + String bsqAddress = compensationRequestPayload.getBsqAddress(); + // Remove initial B + bsqAddress = bsqAddress.substring(1, bsqAddress.length()); + checkArgument(!compensationRequest.getFeeTx().getInputs().isEmpty(), "preparedTx inputs must not be empty"); + + // We use the key of the first BSQ input for signing the data + TransactionOutput connectedOutput = compensationRequest.getFeeTx().getInputs().get(0).getConnectedOutput(); + checkNotNull(connectedOutput, "connectedOutput must not be null"); + DeterministicKey bsqKeyPair = bsqWalletService.findKeyFromPubKeyHash(connectedOutput.getScriptPubKey().getPubKeyHash()); + checkNotNull(bsqKeyPair, "bsqKeyPair must not be null"); + + // We get the JSON of the object excluding signature and feeTxId + String payloadAsJson = StringUtils.deleteWhitespace(Utilities.objectToJson(compensationRequestPayload)); + // Signs a text message using the standard Bitcoin messaging signing format and returns the signature as a base64 + // encoded string. + String signature = bsqKeyPair.signMessage(payloadAsJson); + compensationRequestPayload.setSignature(signature); + + String dataAndSig = payloadAsJson + signature; + byte[] dataAndSigAsBytes = dataAndSig.getBytes(); + outputStream.write(DaoConstants.OP_RETURN_TYPE_COMPENSATION_REQUEST); + outputStream.write(Version.COMPENSATION_REQUEST_VERSION); + outputStream.write(Hash.getSha256Ripemd160hash(dataAndSigAsBytes)); + byte opReturnData[] = outputStream.toByteArray(); + + //TODO should we store the hash in the compensationRequestPayload object? + + //TODO 1 Btc output (small payment to own compensation receiving address) + compensationRequest.setTxWithBtcFee( + btcWalletService.completePreparedCompensationRequestTx( + compensationRequest.getRequestedBsq(), + compensationRequest.getIssuanceAddress(bsqWalletService), + compensationRequest.getFeeTx(), + opReturnData)); + if (contains(compensationRequestPayload)) {log.error("Req found");} + compensationRequest.setSignedTx(bsqWalletService.signTx(compensationRequest.getTxWithBtcFee())); + if (contains(compensationRequestPayload)) {log.error("Req found");} + } + if (contains(compensationRequestPayload)) {log.error("Req found");} + return compensationRequest; + } + + public void commitCompensationRequest(CompensationRequest compensationRequest, FutureCallback callback) { + // We need to create another instance, otherwise the tx would trigger an invalid state exception + // if it gets committed 2 times + // We clone before commit to avoid unwanted side effects + final Transaction clonedTransaction = btcWalletService.getClonedTransaction(compensationRequest.getTxWithBtcFee()); + bsqWalletService.commitTx(compensationRequest.getTxWithBtcFee()); + btcWalletService.commitTx(clonedTransaction); + bsqWalletService.broadcastTx(compensationRequest.getSignedTx(), new FutureCallback() { + @Override + public void onSuccess(@Nullable Transaction transaction) { + checkNotNull(transaction, "Transaction must not be null at broadcastTx callback."); + compensationRequest.getPayload().setTxId(transaction.getHashAsString()); + addToP2PNetwork(compensationRequest.getPayload()); + + callback.onSuccess(transaction); + } + + @Override + public void onFailure(@NotNull Throwable t) { + callback.onFailure(t); + } + }); + } + public boolean removeCompensationRequest(CompensationRequest compensationRequest) { final CompensationRequestPayload payload = compensationRequest.getPayload(); // We allow removal which are not confirmed yet or if it we are in the right phase @@ -230,11 +329,13 @@ private void createCompensationRequest(CompensationRequestPayload compensationRe if (storeLocally) compensationRequestsStorage.queueUpForSave(new CompensationRequestList(getAllRequests()), 500); } else { - log.warn("We have already an item with the same CompensationRequest."); + if (!isMine(compensationRequestPayload)) + log.warn("We already have an item with the same CompensationRequest."); } } private void updateFilteredLists() { + // TODO: Does this only need to be set once to keep the list updated? pastRequests.setPredicate(daoPeriodService::isInPastCycle); activeRequests.setPredicate(compensationRequest -> { return daoPeriodService.isInCurrentCycle(compensationRequest) || diff --git a/core/src/main/java/io/bisq/core/provider/ProvidersRepository.java b/core/src/main/java/io/bisq/core/provider/ProvidersRepository.java index 2142f039c70..166a90103c3 100644 --- a/core/src/main/java/io/bisq/core/provider/ProvidersRepository.java +++ b/core/src/main/java/io/bisq/core/provider/ProvidersRepository.java @@ -91,7 +91,7 @@ public void selectNextProviderBaseUrl() { index++; if (providerList.size() == 1) - log.warn("We oly have one provider"); + log.warn("We only have one provider"); } else { baseUrl = ""; log.warn("We do not have any providers. That can be if all providers are filtered or providersFromProgramArgs is set but empty. " + diff --git a/core/src/main/java/io/bisq/core/trade/statistics/TradeStatisticsManager.java b/core/src/main/java/io/bisq/core/trade/statistics/TradeStatisticsManager.java index 2840254245a..aed627af8fa 100644 --- a/core/src/main/java/io/bisq/core/trade/statistics/TradeStatisticsManager.java +++ b/core/src/main/java/io/bisq/core/trade/statistics/TradeStatisticsManager.java @@ -251,7 +251,12 @@ private void printAllCurrencyStats() { newlyAdded.add("BETR"); newlyAdded.add("MVT"); newlyAdded.add("REF"); - + // v0.6.6 + newlyAdded.add("STL"); + newlyAdded.add("DAI"); + newlyAdded.add("YTN"); + newlyAdded.add("DARX"); + newlyAdded.add("ODN"); coinsWithValidator.addAll(newlyAdded); diff --git a/core/src/test/java/io/bisq/core/btc/wallet/BtcNodeConverterTest.java b/core/src/test/java/io/bisq/core/btc/wallet/BtcNodeConverterTest.java new file mode 100644 index 00000000000..84feafd9f4c --- /dev/null +++ b/core/src/test/java/io/bisq/core/btc/wallet/BtcNodeConverterTest.java @@ -0,0 +1,76 @@ +package io.bisq.core.btc.wallet; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; +import io.bisq.core.btc.BitcoinNodes.BtcNode; +import io.bisq.core.btc.wallet.BtcNodeConverter.Facade; +import io.bisq.network.DnsLookupException; +import org.bitcoinj.core.PeerAddress; +import org.junit.Test; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BtcNodeConverterTest { + @Test + public void testConvertOnionHost() throws UnknownHostException { + BtcNode node = mock(BtcNode.class); + when(node.getOnionAddress()).thenReturn("aaa.onion"); + + InetAddress inetAddress = mock(InetAddress.class); + + Facade facade = mock(Facade.class); + when(facade.onionHostToInetAddress(any())).thenReturn(inetAddress); + + PeerAddress peerAddress = new BtcNodeConverter(facade).convertOnionHost(node); + // noinspection ConstantConditions + assertEquals(inetAddress, peerAddress.getAddr()); + } + + @Test + public void testConvertOnionHostOnFailure() throws UnknownHostException { + BtcNode node = mock(BtcNode.class); + when(node.getOnionAddress()).thenReturn("aaa.onion"); + + Facade facade = mock(Facade.class); + when(facade.onionHostToInetAddress(any())).thenThrow(UnknownHostException.class); + + PeerAddress peerAddress = new BtcNodeConverter(facade).convertOnionHost(node); + assertNull(peerAddress); + } + + @Test + public void testConvertClearNode() { + final String ip = "192.168.0.1"; + + BtcNode node = mock(BtcNode.class); + when(node.getHostNameOrAddress()).thenReturn(ip); + + PeerAddress peerAddress = new BtcNodeConverter().convertClearNode(node); + // noinspection ConstantConditions + InetAddress inetAddress = peerAddress.getAddr(); + assertEquals(ip, inetAddress.getHostName()); + } + + @Test + public void testConvertWithTor() throws DnsLookupException { + InetAddress expected = mock(InetAddress.class); + + Facade facade = mock(Facade.class); + when(facade.torLookup(any(), anyString())).thenReturn(expected); + + BtcNode node = mock(BtcNode.class); + when(node.getHostNameOrAddress()).thenReturn("aaa.onion"); + + PeerAddress peerAddress = new BtcNodeConverter(facade).convertWithTor(node, mock(Socks5Proxy.class)); + + // noinspection ConstantConditions + assertEquals(expected, peerAddress.getAddr()); + } +} diff --git a/core/src/test/java/io/bisq/core/btc/wallet/PeerAddressesRepositoryTest.java b/core/src/test/java/io/bisq/core/btc/wallet/PeerAddressesRepositoryTest.java new file mode 100644 index 00000000000..95050ccd2fa --- /dev/null +++ b/core/src/test/java/io/bisq/core/btc/wallet/PeerAddressesRepositoryTest.java @@ -0,0 +1,83 @@ +package io.bisq.core.btc.wallet; + +import com.google.common.collect.Lists; +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; +import io.bisq.core.btc.BitcoinNodes.BtcNode; +import org.bitcoinj.core.PeerAddress; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class PeerAddressesRepositoryTest { + @Test + public void testGetPeerAddressesWhenClearNodes() { + BtcNode node = mock(BtcNode.class); + when(node.hasClearNetAddress()).thenReturn(true); + + BtcNodeConverter converter = mock(BtcNodeConverter.class, RETURNS_DEEP_STUBS); + PeerAddressesRepository repository = new PeerAddressesRepository(converter, + Collections.singletonList(node)); + + List peers = repository.getPeerAddresses(null, false); + + assertFalse(peers.isEmpty()); + } + + @Test + public void testGetPeerAddressesWhenConverterReturnsNull() { + BtcNodeConverter converter = mock(BtcNodeConverter.class); + when(converter.convertClearNode(any())).thenReturn(null); + + BtcNode node = mock(BtcNode.class); + when(node.hasClearNetAddress()).thenReturn(true); + + PeerAddressesRepository repository = new PeerAddressesRepository(converter, + Collections.singletonList(node)); + + List peers = repository.getPeerAddresses(null, false); + + verify(converter).convertClearNode(any()); + assertTrue(peers.isEmpty()); + } + + @Test + public void testGetPeerAddressesWhenProxyAndClearNodes() { + BtcNode node = mock(BtcNode.class); + when(node.hasClearNetAddress()).thenReturn(true); + + BtcNode onionNode = mock(BtcNode.class); + when(node.hasOnionAddress()).thenReturn(true); + + BtcNodeConverter converter = mock(BtcNodeConverter.class, RETURNS_DEEP_STUBS); + PeerAddressesRepository repository = new PeerAddressesRepository(converter, + Lists.newArrayList(node, onionNode)); + + List peers = repository.getPeerAddresses(mock(Socks5Proxy.class), true); + + assertEquals(2, peers.size()); + } + + @Test + public void testGetPeerAddressesWhenOnionNodesOnly() { + BtcNode node = mock(BtcNode.class); + when(node.hasClearNetAddress()).thenReturn(true); + + BtcNode onionNode = mock(BtcNode.class); + when(node.hasOnionAddress()).thenReturn(true); + + BtcNodeConverter converter = mock(BtcNodeConverter.class, RETURNS_DEEP_STUBS); + PeerAddressesRepository repository = new PeerAddressesRepository(converter, + Lists.newArrayList(node, onionNode)); + + List peers = repository.getPeerAddresses(mock(Socks5Proxy.class), false); + + assertEquals(1, peers.size()); + } +} diff --git a/core/src/test/java/io/bisq/core/btc/wallet/WalletNetworkConfigTest.java b/core/src/test/java/io/bisq/core/btc/wallet/WalletNetworkConfigTest.java new file mode 100644 index 00000000000..142a57b1b18 --- /dev/null +++ b/core/src/test/java/io/bisq/core/btc/wallet/WalletNetworkConfigTest.java @@ -0,0 +1,54 @@ +package io.bisq.core.btc.wallet; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; +import io.bisq.network.Socks5MultiDiscovery; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.PeerAddress; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class WalletNetworkConfigTest { + private static final int MODE = 0; + + private WalletConfig delegate; + + @Before + public void setUp() { + delegate = mock(WalletConfig.class); + } + + @Test + public void testProposePeersWhenProxyPresentAndNoPeers() { + WalletNetworkConfig config = new WalletNetworkConfig(delegate, mock(NetworkParameters.class), MODE, + mock(Socks5Proxy.class)); + config.proposePeers(Collections.emptyList()); + + verify(delegate, never()).setPeerNodes(any()); + verify(delegate).setDiscovery(any(Socks5MultiDiscovery.class)); + } + + @Test + public void testProposePeersWhenProxyNotPresentAndNoPeers() { + WalletNetworkConfig config = new WalletNetworkConfig(delegate, mock(NetworkParameters.class), MODE, + null); + config.proposePeers(Collections.emptyList()); + + verify(delegate, never()).setDiscovery(any(Socks5MultiDiscovery.class)); + verify(delegate, never()).setPeerNodes(any()); + } + + @Test + public void testProposePeersWhenPeersPresent() { + WalletNetworkConfig config = new WalletNetworkConfig(delegate, mock(NetworkParameters.class), MODE, + null); + config.proposePeers(Collections.singletonList(mock(PeerAddress.class))); + + verify(delegate, never()).setDiscovery(any(Socks5MultiDiscovery.class)); + verify(delegate).setPeerNodes(any()); + } +} diff --git a/core/src/test/java/io/bisq/core/btc/wallet/WalletSetupPreferencesTest.java b/core/src/test/java/io/bisq/core/btc/wallet/WalletSetupPreferencesTest.java new file mode 100644 index 00000000000..2d114b7dba2 --- /dev/null +++ b/core/src/test/java/io/bisq/core/btc/wallet/WalletSetupPreferencesTest.java @@ -0,0 +1,45 @@ +package io.bisq.core.btc.wallet; + +import io.bisq.core.btc.BitcoinNodes; +import io.bisq.core.btc.BitcoinNodes.BtcNode; +import io.bisq.core.user.Preferences; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.List; + +import static io.bisq.core.btc.BitcoinNodes.BitcoinNodesOption.CUSTOM; +import static io.bisq.core.btc.BitcoinNodes.BitcoinNodesOption.PUBLIC; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(Preferences.class) +public class WalletSetupPreferencesTest { + @Test + public void testSelectPreferredNodesWhenPublicOption() { + Preferences delegate = mock(Preferences.class); + when(delegate.getBitcoinNodesOptionOrdinal()).thenReturn(PUBLIC.ordinal()); + + WalletSetupPreferences preferences = new WalletSetupPreferences(delegate); + List nodes = preferences.selectPreferredNodes(mock(BitcoinNodes.class)); + + assertTrue(nodes.isEmpty()); + } + + @Test + public void testSelectPreferredNodesWhenCustomOption() { + Preferences delegate = mock(Preferences.class); + when(delegate.getBitcoinNodesOptionOrdinal()).thenReturn(CUSTOM.ordinal()); + when(delegate.getBitcoinNodes()).thenReturn("aaa.onion,bbb.onion"); + + WalletSetupPreferences preferences = new WalletSetupPreferences(delegate); + List nodes = preferences.selectPreferredNodes(mock(BitcoinNodes.class)); + + assertEquals(2, nodes.size()); + } +} diff --git a/core/src/test/java/io/bisq/core/dao/blockchain/parse/BsqParserTest.java b/core/src/test/java/io/bisq/core/dao/blockchain/parse/BsqParserTest.java index 29a065e92cf..05222c6aada 100644 --- a/core/src/test/java/io/bisq/core/dao/blockchain/parse/BsqParserTest.java +++ b/core/src/test/java/io/bisq/core/dao/blockchain/parse/BsqParserTest.java @@ -1,20 +1,25 @@ package io.bisq.core.dao.blockchain.parse; +import com.neemre.btcdcli4j.core.BitcoindException; +import com.neemre.btcdcli4j.core.CommunicationException; +import com.neemre.btcdcli4j.core.domain.Block; import io.bisq.common.proto.persistable.PersistenceProtoResolver; +import io.bisq.core.dao.blockchain.exceptions.BlockNotConnectingException; +import io.bisq.core.dao.blockchain.exceptions.BsqBlockchainException; import io.bisq.core.dao.blockchain.vo.*; import mockit.Expectations; import mockit.Injectable; import mockit.Tested; import mockit.integration.junit4.JMockit; +import org.bitcoinj.core.Coin; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.math.BigDecimal; +import java.util.*; +import static java.util.Arrays.asList; import static org.junit.Assert.*; @RunWith(JMockit.class) @@ -44,13 +49,10 @@ public void testIsBsqTx() { int height = 200; String hash = "abc123"; long time = new Date().getTime(); - List inputs = new ArrayList<>(); - inputs.add(new TxInput("tx1", 0)); - inputs.add(new TxInput("tx1", 1)); - List outputs = new ArrayList<>(); - outputs.add(new TxOutput(0, 101, "tx1", null, null, null, height)); - TxVo txVo = new TxVo("vo", height, hash, time); - Tx tx = new Tx(txVo, inputs, outputs); + Tx tx = new Tx(new TxVo("vo", height, hash, time), + asList(new TxInput("tx1", 0), + new TxInput("tx1", 1)), + asList(new TxOutput(0, 101, "tx1", null, null, null, height))); // Return one spendable txoutputs with value, for three test cases // 1) - null, 0 -> not BSQ transaction @@ -75,4 +77,99 @@ public void testIsBsqTx() { // Third time there is BSQ in the second txout assertTrue(bsqParser.isBsqTx(height, tx)); } + + @Test + public void testParseBlocks() throws BitcoindException, CommunicationException, BlockNotConnectingException, BsqBlockchainException { + // Setup blocks to test, starting before genesis + // Only the transactions related to bsq are relevant, no checks are done on correctness of blocks or other txs + // so hashes and most other data don't matter + long time = new Date().getTime(); + int genesisHeight = 200; + int startHeight = 199; + int headHeight = 201; + Coin issuance = Coin.parseCoin("25"); + + // Blockhashes + String bh199 = "blockhash199"; + String bh200 = "blockhash200"; + String bh201 = "blockhash201"; + + // Block 199 + String cbId199 = "cbid199"; + Tx cbTx199 = new Tx(new TxVo(cbId199, 199, bh199, time), + new ArrayList(), + asList(new TxOutput(0, 25, cbId199, null, null, null, 199))); + Block block199 = new Block(bh199, 10, 10, 199, 2, "root", asList(cbId199), time, Long.parseLong("1234"), "bits", BigDecimal.valueOf(1), "chainwork", "previousBlockHash", bh200); + + // Genesis Block + String cbId200 = "cbid200"; + String genesisId = "genesisId"; + Tx cbTx200 = new Tx(new TxVo(cbId200, 200, bh200, time), + new ArrayList(), + asList(new TxOutput(0, 25, cbId200, null, null, null, 200))); + Tx genesisTx = new Tx(new TxVo(genesisId, 200, bh200, time), + asList(new TxInput("someoldtx", 0)), + asList(new TxOutput(0, issuance.getValue(), genesisId, null, null, null, 200))); + Block block200 = new Block(bh200, 10, 10, 200, 2, "root", asList(cbId200, genesisId), time, Long.parseLong("1234"), "bits", BigDecimal.valueOf(1), "chainwork", bh199, bh201); + + // Block 201 + // Make a bsq transaction + String cbId201 = "cbid201"; + String bsqTx1Id = "bsqtx1"; + long bsqTx1Value1 = Coin.parseCoin("24").getValue(); + long bsqTx1Value2 = Coin.parseCoin("0.4").getValue(); + Tx cbTx201 = new Tx(new TxVo(cbId201, 201, bh201, time), + new ArrayList(), + asList(new TxOutput(0, 25, cbId201, null, null, null, 201))); + Tx bsqTx1 = new Tx(new TxVo(bsqTx1Id, 201, bh201, time), + asList(new TxInput(genesisId, 0)), + asList(new TxOutput(0, bsqTx1Value1, bsqTx1Id, null, null, null, 201), + new TxOutput(1, bsqTx1Value2, bsqTx1Id, null, null, null, 201))); + Block block201 = new Block(bh201, 10, 10, 201, 2, "root", asList(cbId201, bsqTx1Id), time, Long.parseLong("1234"), "bits", BigDecimal.valueOf(1), "chainwork", bh200, "nextBlockHash"); + + new Expectations(rpcService) {{ + rpcService.requestBlock(199); + result = block199; + rpcService.requestBlock(200); + result = block200; + rpcService.requestBlock(201); + result = block201; + + rpcService.requestTx(cbId199, 199); + result = cbTx199; + rpcService.requestTx(cbId200, genesisHeight); + result = cbTx200; + rpcService.requestTx(genesisId, genesisHeight); + result = genesisTx; + rpcService.requestTx(cbId201, 201); + result = cbTx201; + rpcService.requestTx(bsqTx1Id, 201); + result = bsqTx1; + }}; + + // Running parseBlocks to build the bsq blockchain + bsqParser.parseBlocks(startHeight, headHeight, genesisHeight, genesisId, block -> { + }); + + // Verify that the the genesis tx has been added to the bsq blockchain with the correct issuance amount + assertTrue(bsqBlockChain.getGenesisTx() == genesisTx); + assertTrue(bsqBlockChain.getIssuedAmount().getValue() == issuance.getValue()); + + // And that other txs are not added + assertFalse(bsqBlockChain.containsTx(cbId199)); + assertFalse(bsqBlockChain.containsTx(cbId200)); + assertFalse(bsqBlockChain.containsTx(cbId201)); + + // But bsq txs are added + assertTrue(bsqBlockChain.containsTx(bsqTx1Id)); + TxOutput bsqOut1 = bsqBlockChain.getSpendableTxOutput(bsqTx1Id, 0).get(); + assertTrue(bsqOut1.isUnspent()); + assertTrue(bsqOut1.getValue() == bsqTx1Value1); + TxOutput bsqOut2 = bsqBlockChain.getSpendableTxOutput(bsqTx1Id, 1).get(); + assertTrue(bsqOut2.isUnspent()); + assertTrue(bsqOut2.getValue() == bsqTx1Value2); + assertFalse(bsqBlockChain.isTxOutputSpendable(genesisId, 0)); + assertTrue(bsqBlockChain.isTxOutputSpendable(bsqTx1Id, 0)); + assertTrue(bsqBlockChain.isTxOutputSpendable(bsqTx1Id, 1)); + } } diff --git a/gui/src/main/java/io/bisq/gui/app/BisqAppMain.java b/gui/src/main/java/io/bisq/gui/app/BisqAppMain.java index c2e2d9f1162..bcfe401768b 100644 --- a/gui/src/main/java/io/bisq/gui/app/BisqAppMain.java +++ b/gui/src/main/java/io/bisq/gui/app/BisqAppMain.java @@ -25,17 +25,12 @@ import joptsimple.OptionParser; import joptsimple.OptionSet; -import java.util.Locale; - import static io.bisq.core.app.BisqEnvironment.DEFAULT_APP_NAME; import static io.bisq.core.app.BisqEnvironment.DEFAULT_USER_DATA_DIR; public class BisqAppMain extends BisqExecutable { static { - // Need to set default locale initially otherwise we get problems at non-english OS - Locale.setDefault(new Locale("en", Locale.getDefault().getCountry())); - Utilities.removeCryptographyRestrictions(); } diff --git a/gui/src/main/java/io/bisq/gui/bisq.css b/gui/src/main/java/io/bisq/gui/bisq.css index e6dfe46ea11..d1bb3db7440 100644 --- a/gui/src/main/java/io/bisq/gui/bisq.css +++ b/gui/src/main/java/io/bisq/gui/bisq.css @@ -56,6 +56,7 @@ bg color of non edit textFields: fafafa -bs-orange: #ff8a2b; /* 2 usages */ -bs-orange2: #dd6900; /* 1 usages */ -bs-yellow: #ffb60f; /* 2 usages */ + -bs-yellow-light: derive(-bs-yellow, 81%); -bs-bg-grey8: #E1E9E1; /* 1 usages */ -bs-bg-green2:#619865; /* 2 usages */ -bs-bg-green:#99ba9c; /* 4 usages */ @@ -126,11 +127,36 @@ bg color of non edit textFields: fafafa -fx-text-fill: -bs-black; } +.info { + -fx-text-fill: -bs-green; +} + +.info:hover { + -fx-text-fill: -bs-grey; +} + .headline-label { -fx-font-weight: bold; -fx-font-size: 22; } +.info { + -fx-text-fill: -bs-green; +} + +.info:hover { + -fx-text-fill: -bs-grey; +} + +.warning-box { + -fx-background-color: -bs-yellow-light; + -fx-spacing: 6; + -fx-alignment: center; +} + +.warning { + -fx-text-fill: -bs-yellow; +} /* Other UI Elements */ .separator { @@ -362,10 +388,6 @@ textfield */ -fx-padding: 4 4 4 4; } -#address-text-field:hover { - -fx-text-fill: -bs-black; -} - #funds-confidence { -fx-progress-color: -bs-dim-grey; } @@ -502,10 +524,6 @@ textfield */ -fx-text-fill: -bs-medium-grey; } -#clickable-icon:hover { - -fx-text-fill: -bs-grey; -} - /******************************************************************************* * * * Images * @@ -826,6 +844,7 @@ textfield */ #titled-group-bg-label-active { -fx-font-weight: bold; -fx-font-size: 14; + -fx-text-fill: -fx-accent; -fx-background-color: -bs-content-bg-grey; } diff --git a/gui/src/main/java/io/bisq/gui/components/AddressTextField.java b/gui/src/main/java/io/bisq/gui/components/AddressTextField.java index 09b0c6988fb..6228154d61b 100644 --- a/gui/src/main/java/io/bisq/gui/components/AddressTextField.java +++ b/gui/src/main/java/io/bisq/gui/components/AddressTextField.java @@ -87,12 +87,12 @@ public AddressTextField() { Utilities.copyToClipboard(address.get()); })); - AnchorPane.setRightAnchor(copyIcon, 5.0); - AnchorPane.setRightAnchor(extWalletIcon, 30.0); + AnchorPane.setRightAnchor(copyIcon, 30.0); + AnchorPane.setRightAnchor(extWalletIcon, 5.0); AnchorPane.setRightAnchor(textField, 55.0); AnchorPane.setLeftAnchor(textField, 0.0); - getChildren().addAll(textField, extWalletIcon, copyIcon); + getChildren().addAll(textField, copyIcon, extWalletIcon); } private void openWallet() { diff --git a/gui/src/main/java/io/bisq/gui/components/FundsTextField.java b/gui/src/main/java/io/bisq/gui/components/FundsTextField.java index 6d0fd820d01..eacfee5e6c9 100644 --- a/gui/src/main/java/io/bisq/gui/components/FundsTextField.java +++ b/gui/src/main/java/io/bisq/gui/components/FundsTextField.java @@ -19,6 +19,7 @@ import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; +import io.bisq.common.UserThread; import io.bisq.common.locale.Res; import io.bisq.common.util.Utilities; import javafx.beans.binding.Bindings; @@ -33,13 +34,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class FundsTextField extends AnchorPane { +import java.util.concurrent.TimeUnit; + +public class FundsTextField extends InfoTextField { public static final Logger log = LoggerFactory.getLogger(FundsTextField.class); - private final StringProperty amount = new SimpleStringProperty(); private final StringProperty fundsStructure = new SimpleStringProperty(); - private final Label infoIcon; - private PopOver infoPopover; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -47,20 +47,9 @@ public class FundsTextField extends AnchorPane { public FundsTextField() { - - TextField textField = new TextField(); - // might be removed if no styling is necessary - textField.setId("amount-text-field"); - textField.setEditable(false); - textField.setPromptText(Res.get("createOffer.fundsBox.totalsNeeded.prompt")); - textField.textProperty().bind(Bindings.concat(amount, " ", fundsStructure)); - textField.setFocusTraversable(false); - - infoIcon = new Label(); - infoIcon.setLayoutY(3); - infoIcon.setId("clickable-icon"); - infoIcon.getStyleClass().addAll("highlight", "show-hand"); - AwesomeDude.setIcon(infoIcon, AwesomeIcon.INFO_SIGN); + super(); + textField.textProperty().unbind(); + textField.textProperty().bind(Bindings.concat(textProperty(), " ", fundsStructure)); Label copyIcon = new Label(); copyIcon.setLayoutY(3); @@ -68,7 +57,7 @@ public FundsTextField() { Tooltip.install(copyIcon, new Tooltip(Res.get("shared.copyToClipboard"))); AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); copyIcon.setOnMouseClicked(e -> { - String text = getAmount(); + String text = getText(); if (text != null && text.length() > 0) { String copyText; String[] strings = text.split(" "); @@ -81,60 +70,17 @@ public FundsTextField() { } }); - AnchorPane.setRightAnchor(copyIcon, 5.0); - AnchorPane.setRightAnchor(infoIcon, 37.0); - AnchorPane.setRightAnchor(textField, 30.0); - AnchorPane.setLeftAnchor(textField, 0.0); - - getChildren().addAll(textField, infoIcon, copyIcon); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Public - /////////////////////////////////////////////////////////////////////////////////////////// - - public void setContentForInfoPopOver(Node node) { - // As we don't use binding here we need to recreate it on mouse over to reflect the current state - infoIcon.setOnMouseEntered(e -> createInfoPopOver(node)); - infoIcon.setOnMouseExited(e -> { - if (infoPopover != null) - infoPopover.hide(); - }); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Private - /////////////////////////////////////////////////////////////////////////////////////////// - - private void createInfoPopOver(Node node) { - node.getStyleClass().add("default-text"); - - infoPopover = new PopOver(node); - if (infoIcon.getScene() != null) { - infoPopover.setDetachable(false); - infoPopover.setArrowLocation(PopOver.ArrowLocation.RIGHT_TOP); - infoPopover.setArrowIndent(5); + AnchorPane.setRightAnchor(copyIcon, 30.0); + AnchorPane.setRightAnchor(infoIcon, 62.0); + AnchorPane.setRightAnchor(textField, 55.0); - infoPopover.show(infoIcon, -17); - } + getChildren().add(copyIcon); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters/Setters /////////////////////////////////////////////////////////////////////////////////////////// - public void setAmount(String amount) { - this.amount.set(amount); - } - - public String getAmount() { - return amount.get(); - } - - public StringProperty amountProperty() { - return amount; - } - public void setFundsStructure(String fundsStructure) { this.fundsStructure.set(fundsStructure); } diff --git a/gui/src/main/java/io/bisq/gui/components/InfoTextField.java b/gui/src/main/java/io/bisq/gui/components/InfoTextField.java new file mode 100644 index 00000000000..8e69770d597 --- /dev/null +++ b/gui/src/main/java/io/bisq/gui/components/InfoTextField.java @@ -0,0 +1,102 @@ +package io.bisq.gui.components; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; +import io.bisq.common.UserThread; +import io.bisq.common.locale.Res; +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.layout.AnchorPane; +import org.controlsfx.control.PopOver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class InfoTextField extends AnchorPane { + public static final Logger log = LoggerFactory.getLogger(InfoTextField.class); + + private final StringProperty text = new SimpleStringProperty(); + protected final Label infoIcon; + protected final TextField textField; + private Boolean hidePopover; + private PopOver infoPopover; + + public InfoTextField() { + textField = new TextField(); + textField.setEditable(false); + textField.textProperty().bind(text); + textField.setFocusTraversable(false); + + infoIcon = new Label(); + infoIcon.setLayoutY(3); + infoIcon.getStyleClass().addAll("icon", "info"); + AwesomeDude.setIcon(infoIcon, AwesomeIcon.INFO_SIGN); + + AnchorPane.setRightAnchor(infoIcon, 7.0); + AnchorPane.setRightAnchor(textField, 0.0); + AnchorPane.setLeftAnchor(textField, 0.0); + + getChildren().addAll(textField, infoIcon); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setContentForInfoPopOver(Node node) { + // As we don't use binding here we need to recreate it on mouse over to reflect the current state + infoIcon.setOnMouseEntered(e -> { + hidePopover = false; + showInfoPopOver(node); + }); + infoIcon.setOnMouseExited(e -> { + if (infoPopover != null) + infoPopover.hide(); + hidePopover = true; + UserThread.runAfter(() -> { + if (hidePopover) { + infoPopover.hide(); + hidePopover = false; + } + }, 250, TimeUnit.MILLISECONDS); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void showInfoPopOver(Node node) { + node.getStyleClass().add("default-text"); + + infoPopover = new PopOver(node); + if (infoIcon.getScene() != null) { + infoPopover.setDetachable(false); + infoPopover.setArrowLocation(PopOver.ArrowLocation.RIGHT_TOP); + infoPopover.setArrowIndent(5); + + infoPopover.show(infoIcon, -17); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters/Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setText(String text) { + this.text.set(text); + } + + public String getText() { + return text.get(); + } + + public StringProperty textProperty() { + return text; + } +} diff --git a/gui/src/main/java/io/bisq/gui/components/paymentmethods/PaymentMethodForm.java b/gui/src/main/java/io/bisq/gui/components/paymentmethods/PaymentMethodForm.java index af29c0db514..09f22c44ee2 100644 --- a/gui/src/main/java/io/bisq/gui/components/paymentmethods/PaymentMethodForm.java +++ b/gui/src/main/java/io/bisq/gui/components/paymentmethods/PaymentMethodForm.java @@ -26,6 +26,7 @@ import io.bisq.core.payment.AccountAgeWitnessService; import io.bisq.core.payment.CryptoCurrencyAccount; import io.bisq.core.payment.PaymentAccount; +import io.bisq.gui.components.InfoTextField; import io.bisq.gui.components.InputTextField; import io.bisq.gui.main.overlays.popups.Popup; import io.bisq.gui.util.BSFormatter; @@ -111,13 +112,12 @@ protected void addAccountNameTextFieldWithAutoFillCheckBox() { }); } - public static void addOpenTradeDuration(GridPane gridPane, - int gridRow, - Offer offer, - String dateFromBlocks) { + public static InfoTextField addOpenTradeDuration(GridPane gridPane, + int gridRow, + Offer offer) { long hours = offer.getMaxTradePeriod() / 3600_000; - addLabelTextField(gridPane, gridRow, Res.get("payment.maxPeriod"), - getTimeText(hours) + " / " + dateFromBlocks); + return addLabelInfoTextfield(gridPane, gridRow, Res.get("payment.maxPeriod"), + getTimeText(hours)).second; } protected static String getTimeText(long hours) { diff --git a/gui/src/main/java/io/bisq/gui/main/dao/compensation/create/CreateCompensationRequestView.java b/gui/src/main/java/io/bisq/gui/main/dao/compensation/create/CreateCompensationRequestView.java index 68f66d411fc..3cbd54c4c3b 100644 --- a/gui/src/main/java/io/bisq/gui/main/dao/compensation/create/CreateCompensationRequestView.java +++ b/gui/src/main/java/io/bisq/gui/main/dao/compensation/create/CreateCompensationRequestView.java @@ -18,18 +18,12 @@ package io.bisq.gui.main.dao.compensation.create; import com.google.common.util.concurrent.FutureCallback; -import io.bisq.common.app.Version; -import io.bisq.common.crypto.Hash; import io.bisq.common.crypto.KeyRing; import io.bisq.common.locale.Res; -import io.bisq.common.util.Utilities; import io.bisq.core.btc.exceptions.TransactionVerificationException; import io.bisq.core.btc.exceptions.WalletException; -import io.bisq.core.btc.wallet.BsqWalletService; -import io.bisq.core.btc.wallet.BtcWalletService; -import io.bisq.core.btc.wallet.ChangeBelowDustException; -import io.bisq.core.btc.wallet.WalletsSetup; -import io.bisq.core.dao.DaoConstants; +import io.bisq.core.btc.wallet.*; +import io.bisq.core.dao.compensation.CompensationRequest; import io.bisq.core.dao.compensation.CompensationRequestManager; import io.bisq.core.dao.compensation.CompensationRequestPayload; import io.bisq.core.provider.fee.FeeService; @@ -45,20 +39,16 @@ import io.bisq.network.p2p.P2PService; import javafx.scene.control.Button; import javafx.scene.layout.GridPane; -import org.apache.commons.lang3.StringUtils; import org.bitcoinj.core.*; -import org.bitcoinj.crypto.DeterministicKey; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import javax.inject.Inject; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.PublicKey; import java.util.Date; import java.util.UUID; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static io.bisq.gui.util.FormBuilder.addButtonAfterGroup; @@ -136,70 +126,22 @@ protected void activate() { ); boolean walletExceptionMightBeCausedByBtCWallet = false; - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - // TODO move to domain - final Coin compensationRequestFee = feeService.getCreateCompensationRequestFee(); - final Transaction feeTx = bsqWalletService.getPreparedBurnFeeTx(compensationRequestFee); - String bsqAddress = compensationRequestPayload.getBsqAddress(); - // Remove initial B - bsqAddress = bsqAddress.substring(1, bsqAddress.length()); - final Address issuanceAddress = Address.fromBase58(bsqWalletService.getParams(), bsqAddress); - final Coin issuanceAmount = compensationRequestPayload.getRequestedBsq(); - walletExceptionMightBeCausedByBtCWallet = true; - checkArgument(!feeTx.getInputs().isEmpty(), "preparedTx inputs must not be empty"); - - // We use the key of the first BSQ input for signing the data - TransactionOutput connectedOutput = feeTx.getInputs().get(0).getConnectedOutput(); - checkNotNull(connectedOutput, "connectedOutput must not be null"); - DeterministicKey bsqKeyPair = bsqWalletService.findKeyFromPubKeyHash(connectedOutput.getScriptPubKey().getPubKeyHash()); - checkNotNull(bsqKeyPair, "bsqKeyPair must not be null"); - - // We get the JSON of the object excluding signature and feeTxId - String payloadAsJson = StringUtils.deleteWhitespace(Utilities.objectToJson(compensationRequestPayload)); - // Signs a text message using the standard Bitcoin messaging signing format and returns the signature as a base64 - // encoded string. - String signature = bsqKeyPair.signMessage(payloadAsJson); - compensationRequestPayload.setSignature(signature); - - String dataAndSig = payloadAsJson + signature; - byte[] dataAndSigAsBytes = dataAndSig.getBytes(); - outputStream.write(DaoConstants.OP_RETURN_TYPE_COMPENSATION_REQUEST); - outputStream.write(Version.COMPENSATION_REQUEST_VERSION); - outputStream.write(Hash.getSha256Ripemd160hash(dataAndSigAsBytes)); - byte opReturnData[] = outputStream.toByteArray(); - //TODO should we store the hash in the compensationRequestPayload object? - - //TODO 1 Btc output (small payment to own compensation receiving address) - walletExceptionMightBeCausedByBtCWallet = true; - Transaction txWithBtcFee = btcWalletService.completePreparedCompensationRequestTx(issuanceAmount, - issuanceAddress, - feeTx, - opReturnData); - walletExceptionMightBeCausedByBtCWallet = false; - Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); - Coin miningFee = signedTx.getFee(); - int txSize = signedTx.bitcoinSerialize().length; + try { + CompensationRequest compensationRequest = compensationRequestManager.prepareCompensationRequest(compensationRequestPayload); + Coin miningFee = compensationRequest.getSignedTx().getFee(); + int txSize = compensationRequest.getSignedTx().bitcoinSerialize().length; new Popup<>().headLine(Res.get("dao.compensation.create.confirm")) .confirmation(Res.get("dao.compensation.create.confirm.info", - bsqFormatter.formatCoinWithCode(issuanceAmount), - bsqFormatter.formatCoinWithCode(compensationRequestFee), + bsqFormatter.formatCoinWithCode(compensationRequest.getRequestedBsq()), + bsqFormatter.formatCoinWithCode(compensationRequest.getCompensationRequestFee()), btcFormatter.formatCoinWithCode(miningFee), CoinUtil.getFeePerByte(miningFee, txSize), (txSize / 1000d))) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { - // We need to create another instance, otherwise the tx would trigger an invalid state exception - // if it gets committed 2 times - // We clone before commit to avoid unwanted side effects - final Transaction clonedTransaction = btcWalletService.getClonedTransaction(txWithBtcFee); - bsqWalletService.commitTx(txWithBtcFee); - btcWalletService.commitTx(clonedTransaction); - bsqWalletService.broadcastTx(signedTx, new FutureCallback() { + compensationRequestManager.commitCompensationRequest(compensationRequest, new FutureCallback() { @Override public void onSuccess(@Nullable Transaction transaction) { - checkNotNull(transaction, "Transaction must not be null at broadcastTx callback."); - compensationRequestPayload.setTxId(transaction.getHashAsString()); - compensationRequestManager.addToP2PNetwork(compensationRequestPayload); compensationRequestDisplay.clearForm(); new Popup<>().confirmation(Res.get("dao.tx.published.success")).show(); } @@ -214,7 +156,7 @@ public void onFailure(@NotNull Throwable t) { .closeButtonText(Res.get("shared.cancel")) .show(); } catch (InsufficientMoneyException e) { - BSFormatter formatter = walletExceptionMightBeCausedByBtCWallet ? btcFormatter : bsqFormatter; + BSFormatter formatter = e instanceof InsufficientBsqException ? bsqFormatter : btcFormatter; new Popup<>().warning(Res.get("dao.compensation.create.missingFunds", formatter.formatCoinWithCode(e.missing))).show(); } catch (IOException | TransactionVerificationException | WalletException | ChangeBelowDustException e) { log.error(e.toString()); diff --git a/gui/src/main/java/io/bisq/gui/main/market/offerbook/OfferBookChartView.java b/gui/src/main/java/io/bisq/gui/main/market/offerbook/OfferBookChartView.java index 1cb2e965916..dcadc8956dc 100644 --- a/gui/src/main/java/io/bisq/gui/main/market/offerbook/OfferBookChartView.java +++ b/gui/src/main/java/io/bisq/gui/main/market/offerbook/OfferBookChartView.java @@ -207,10 +207,10 @@ public Number fromString(String string) { reverseTableColumns(); } - leftHeaderLabel.setText(Res.get("market.offerBook.leftHeaderLabel", code, Res.getBaseCurrencyCode())); + leftHeaderLabel.setText(Res.get("market.offerBook.buyOffersHeaderLabel", code)); leftButton.setText(Res.get("market.offerBook.leftButtonAltcoin", code, Res.getBaseCurrencyCode())); - rightHeaderLabel.setText(Res.get("market.offerBook.rightHeaderLabel", code, Res.getBaseCurrencyCode())); + rightHeaderLabel.setText(Res.get("market.offerBook.sellOffersHeaderLabel", code)); rightButton.setText(Res.get("market.offerBook.rightButtonAltcoin", code, Res.getBaseCurrencyCode())); priceColumnLabel.set(Res.get("shared.priceWithCur", Res.getBaseCurrencyCode())); @@ -220,10 +220,10 @@ public Number fromString(String string) { reverseTableColumns(); } - leftHeaderLabel.setText(Res.get("market.offerBook.rightHeaderLabel", Res.getBaseCurrencyCode(), code)); + leftHeaderLabel.setText(Res.get("market.offerBook.sellOffersHeaderLabel", Res.getBaseCurrencyCode())); leftButton.setText(Res.get("market.offerBook.rightButtonFiat", Res.getBaseCurrencyCode(), code)); - rightHeaderLabel.setText(Res.get("market.offerBook.leftHeaderLabel", Res.getBaseCurrencyCode(), code)); + rightHeaderLabel.setText(Res.get("market.offerBook.buyOffersHeaderLabel", Res.getBaseCurrencyCode())); rightButton.setText(Res.get("market.offerBook.leftButtonFiat", Res.getBaseCurrencyCode(), code)); priceColumnLabel.set(Res.get("shared.priceWithCur", code)); @@ -438,17 +438,9 @@ public void updateItem(final OfferListItem offerListItem, boolean empty) { } }); */ - if (direction == OfferPayload.Direction.BUY) { - // tableView.getColumns().add(accumulatedColumn); - tableView.getColumns().add(volumeColumn); - tableView.getColumns().add(amountColumn); - tableView.getColumns().add(priceColumn); - } else { - tableView.getColumns().add(priceColumn); - tableView.getColumns().add(amountColumn); - tableView.getColumns().add(volumeColumn); - //tableView.getColumns().add(accumulatedColumn); - } + tableView.getColumns().add(volumeColumn); + tableView.getColumns().add(amountColumn); + tableView.getColumns().add(priceColumn); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); Label placeholder = new AutoTooltipLabel(Res.get("table.placeholder.noItems", Res.get("shared.offers"))); diff --git a/gui/src/main/java/io/bisq/gui/main/offer/createoffer/CreateOfferView.java b/gui/src/main/java/io/bisq/gui/main/offer/createoffer/CreateOfferView.java index c1302af84f0..62ba1e8ff9d 100644 --- a/gui/src/main/java/io/bisq/gui/main/offer/createoffer/CreateOfferView.java +++ b/gui/src/main/java/io/bisq/gui/main/offer/createoffer/CreateOfferView.java @@ -410,7 +410,7 @@ private void onShowPayFundsScreen() { cancelButton2.setVisible(true); totalToPayTextField.setFundsStructure(Res.get("createOffer.fundsBox.fundsStructure", - model.getSecurityDepositPercentage(), model.getMakerFeePercentage(), model.getTxFeePercentage())); + model.getSecurityDepositWithCode(), model.getMakerFeePercentage(), model.getTxFeePercentage())); totalToPayTextField.setContentForInfoPopOver(createInfoPopover()); final byte[] imageBytes = QRCode @@ -496,7 +496,7 @@ private void addBindings() { marketBasedPriceTextField.textProperty().bindBidirectional(model.marketPriceMargin); volumeTextField.textProperty().bindBidirectional(model.volume); volumeTextField.promptTextProperty().bind(model.volumePromptLabel); - totalToPayTextField.amountProperty().bind(model.totalToPay); + totalToPayTextField.textProperty().bind(model.totalToPay); addressTextField.amountAsCoinProperty().bind(model.dataModel.getMissingCoin()); buyerSecurityDepositInputTextField.textProperty().bindBidirectional(model.buyerSecurityDeposit); @@ -544,7 +544,7 @@ private void removeBindings() { marketBasedPriceLabel.prefWidthProperty().unbind(); volumeTextField.textProperty().unbindBidirectional(model.volume); volumeTextField.promptTextProperty().unbindBidirectional(model.volume); - totalToPayTextField.amountProperty().unbind(); + totalToPayTextField.textProperty().unbind(); addressTextField.amountAsCoinProperty().unbind(); buyerSecurityDepositInputTextField.textProperty().unbindBidirectional(model.buyerSecurityDeposit); diff --git a/gui/src/main/java/io/bisq/gui/main/offer/createoffer/CreateOfferViewModel.java b/gui/src/main/java/io/bisq/gui/main/offer/createoffer/CreateOfferViewModel.java index 273aca314e9..288eb6f77f1 100644 --- a/gui/src/main/java/io/bisq/gui/main/offer/createoffer/CreateOfferViewModel.java +++ b/gui/src/main/java/io/bisq/gui/main/offer/createoffer/CreateOfferViewModel.java @@ -831,9 +831,8 @@ public String getSecurityDepositInfo() { GUIUtil.getPercentageOfTradeAmount(dataModel.getSecurityDeposit(), dataModel.getAmount().get(), btcFormatter); } - public String getSecurityDepositPercentage() { - return GUIUtil.getPercentage(dataModel.getSecurityDeposit(), dataModel.getAmount().get(), - btcFormatter); + public String getSecurityDepositWithCode() { + return btcFormatter.formatCoinWithCode(dataModel.getSecurityDeposit()); } public String getMakerFee() { diff --git a/gui/src/main/java/io/bisq/gui/main/offer/takeoffer/TakeOfferView.java b/gui/src/main/java/io/bisq/gui/main/offer/takeoffer/TakeOfferView.java index 3a7e6739cdb..34da719ae41 100644 --- a/gui/src/main/java/io/bisq/gui/main/offer/takeoffer/TakeOfferView.java +++ b/gui/src/main/java/io/bisq/gui/main/offer/takeoffer/TakeOfferView.java @@ -420,8 +420,8 @@ private void onShowPayFundsScreen() { balanceLabel.setVisible(true); balanceTextField.setVisible(true); - totalToPayTextField.setFundsStructure(Res.get("createOffer.fundsBox.fundsStructure", - model.getSecurityDepositPercentage(), model.getMakerFeePercentage(), model.getTxFeePercentage())); + totalToPayTextField.setFundsStructure(Res.get("takeOffer.fundsBox.fundsStructure", + model.getSecurityDepositWithCode(), model.getMakerFeePercentage(), model.getTxFeePercentage())); totalToPayTextField.setContentForInfoPopOver(createInfoPopover()); if (model.dataModel.isWalletFunded.get()) { @@ -462,7 +462,7 @@ private void close() { private void addBindings() { amountTextField.textProperty().bindBidirectional(model.amount); volumeTextField.textProperty().bindBidirectional(model.volume); - totalToPayTextField.amountProperty().bind(model.totalToPay); + totalToPayTextField.textProperty().bind(model.totalToPay); addressTextField.amountAsCoinProperty().bind(model.dataModel.missingCoin); amountTextField.validationResultProperty().bind(model.amountValidationResult); priceCurrencyLabel.textProperty().bind(createStringBinding(() -> formatter.getCurrencyPair(model.dataModel.getCurrencyCode()))); @@ -481,7 +481,7 @@ private void addBindings() { private void removeBindings() { amountTextField.textProperty().unbindBidirectional(model.amount); volumeTextField.textProperty().unbindBidirectional(model.volume); - totalToPayTextField.amountProperty().unbind(); + totalToPayTextField.textProperty().unbind(); addressTextField.amountAsCoinProperty().unbind(); amountTextField.validationResultProperty().unbind(); priceCurrencyLabel.textProperty().unbind(); diff --git a/gui/src/main/java/io/bisq/gui/main/offer/takeoffer/TakeOfferViewModel.java b/gui/src/main/java/io/bisq/gui/main/offer/takeoffer/TakeOfferViewModel.java index 66fd32c93a4..5286f0ea308 100644 --- a/gui/src/main/java/io/bisq/gui/main/offer/takeoffer/TakeOfferViewModel.java +++ b/gui/src/main/java/io/bisq/gui/main/offer/takeoffer/TakeOfferViewModel.java @@ -584,9 +584,8 @@ public String getSecurityDepositInfo() { GUIUtil.getPercentageOfTradeAmount(dataModel.getSecurityDeposit(), dataModel.getAmount().get(), btcFormatter); } - public String getSecurityDepositPercentage() { - return GUIUtil.getPercentage(dataModel.getSecurityDeposit(), dataModel.getAmount().get(), - btcFormatter); + public String getSecurityDepositWithCode() { + return btcFormatter.formatCoinWithCode(dataModel.getSecurityDeposit()); } public String getTakerFee() { diff --git a/gui/src/main/java/io/bisq/gui/main/portfolio/pendingtrades/steps/TradeStepView.java b/gui/src/main/java/io/bisq/gui/main/portfolio/pendingtrades/steps/TradeStepView.java index d0e1e065bde..ee71cdfb184 100644 --- a/gui/src/main/java/io/bisq/gui/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/gui/src/main/java/io/bisq/gui/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -17,12 +17,16 @@ package io.bisq.gui.main.portfolio.pendingtrades.steps; +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; import io.bisq.common.Clock; import io.bisq.common.app.Log; import io.bisq.common.locale.Res; import io.bisq.core.arbitration.Dispute; import io.bisq.core.trade.Trade; import io.bisq.core.user.Preferences; +import io.bisq.gui.components.AutoTooltipLabel; +import io.bisq.gui.components.InfoTextField; import io.bisq.gui.components.TitledGroupBg; import io.bisq.gui.components.TxIdTextField; import io.bisq.gui.components.paymentmethods.PaymentMethodForm; @@ -31,10 +35,15 @@ import io.bisq.gui.main.portfolio.pendingtrades.TradeSubView; import io.bisq.gui.util.Layout; import javafx.beans.value.ChangeListener; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; +import javafx.scene.control.Separator; import javafx.scene.control.TextField; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.slf4j.Logger; @@ -43,6 +52,7 @@ import java.util.Optional; import static com.google.common.base.Preconditions.checkNotNull; +import static io.bisq.gui.components.paymentmethods.PaymentMethodForm.addOpenTradeDuration; import static io.bisq.gui.util.FormBuilder.*; public abstract class TradeStepView extends AnchorPane { @@ -175,9 +185,10 @@ protected void addTradeInfoBlock() { else txIdTextField.cleanup(); - if (model.dataModel.getTrade() != null) - PaymentMethodForm.addOpenTradeDuration(gridPane, ++gridRow, model.dataModel.getTrade().getOffer(), - model.getDateForOpenDispute()); + if (model.dataModel.getTrade() != null) { + InfoTextField infoTextField = addOpenTradeDuration(gridPane, ++gridRow, model.dataModel.getTrade().getOffer()); + infoTextField.setContentForInfoPopOver(createInfoPopover()); + } timeLeftTextField = addLabelTextField(gridPane, ++gridRow, Res.getWithCol("portfolio.pending.remainingTime")).second; @@ -212,14 +223,16 @@ private void updateTimeLeft() { if (timeLeftTextField != null) { String remainingTime = model.getRemainingTradeDurationAsWords(); timeLeftProgressBar.setProgress(model.getRemainingTradeDurationAsPercentage()); - if (remainingTime != null) { - timeLeftTextField.setText(remainingTime); + if (!remainingTime.isEmpty()) { + timeLeftTextField.setText(Res.get("portfolio.pending.remainingTimeDetail", + remainingTime, model.getDateForOpenDispute())); if (model.showWarning() || model.showDispute()) { timeLeftTextField.getStyleClass().add("error-text"); timeLeftProgressBar.getStyleClass().add("error"); } } else { - timeLeftTextField.setText("Trade not completed in time (" + model.getDateForOpenDispute() + ")"); + timeLeftTextField.setText(Res.get("portfolio.pending.tradeNotCompleted", + model.getDateForOpenDispute())); timeLeftTextField.getStyleClass().add("error-text"); timeLeftProgressBar.getStyleClass().add("error"); } @@ -421,4 +434,38 @@ private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) { } } } + + /////////////////////////////////////////////////////////////////////////////////////////// + // TradeDurationLimitInfo + /////////////////////////////////////////////////////////////////////////////////////////// + + private GridPane createInfoPopover() { + GridPane infoGridPane = new GridPane(); + int rowIndex = 0; + infoGridPane.setHgap(5); + infoGridPane.setVgap(10); + infoGridPane.setPadding(new Insets(10, 10, 10, 10)); + Label label = addMultilineLabel(infoGridPane, rowIndex++, Res.get("portfolio.pending.tradePeriodInfo")); + label.setMaxWidth(450); + + HBox warningBox = new HBox(); + warningBox.setMinHeight(30); + warningBox.setPadding(new Insets(5)); + warningBox.getStyleClass().add("warning-box"); + GridPane.setRowIndex(warningBox, rowIndex); + GridPane.setColumnSpan(warningBox, 2); + + Label warningIcon = new Label(); + AwesomeDude.setIcon(warningIcon, AwesomeIcon.WARNING_SIGN); + warningIcon.getStyleClass().add("warning"); + + Label warning = new Label(Res.get("portfolio.pending.tradePeriodWarning")); + warning.setWrapText(true); + warning.setMaxWidth(410); + + warningBox.getChildren().addAll(warningIcon, warning); + infoGridPane.getChildren().add(warningBox); + + return infoGridPane; + } } diff --git a/gui/src/main/java/io/bisq/gui/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java b/gui/src/main/java/io/bisq/gui/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java index d33eee9f874..46114670eb6 100644 --- a/gui/src/main/java/io/bisq/gui/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java +++ b/gui/src/main/java/io/bisq/gui/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java @@ -36,10 +36,10 @@ import io.bisq.gui.components.InputTextField; import io.bisq.gui.components.TitledGroupBg; import io.bisq.gui.main.MainView; -import io.bisq.gui.main.funds.FundsView; -import io.bisq.gui.main.funds.transactions.TransactionsView; import io.bisq.gui.main.overlays.notifications.Notification; import io.bisq.gui.main.overlays.popups.Popup; +import io.bisq.gui.main.portfolio.PortfolioView; +import io.bisq.gui.main.portfolio.closedtrades.ClosedTradesView; import io.bisq.gui.main.portfolio.pendingtrades.PendingTradesViewModel; import io.bisq.gui.main.portfolio.pendingtrades.steps.TradeStepView; import io.bisq.gui.util.BSFormatter; @@ -286,8 +286,8 @@ private void handleTradeCompleted() { //noinspection unchecked new Popup<>().headLine(Res.get("portfolio.pending.step5_buyer.withdrawalCompleted.headline")) .feedback(Res.get("portfolio.pending.step5_buyer.withdrawalCompleted.msg")) - .actionButtonTextWithGoTo("navigation.funds.transactions") - .onAction(() -> model.dataModel.navigation.navigateTo(MainView.class, FundsView.class, TransactionsView.class)) + .actionButtonTextWithGoTo("navigation.portfolio.closedTrades") + .onAction(() -> model.dataModel.navigation.navigateTo(MainView.class, PortfolioView.class, ClosedTradesView.class)) .dontShowAgainId(key) .show(); } diff --git a/gui/src/main/java/io/bisq/gui/util/BSFormatter.java b/gui/src/main/java/io/bisq/gui/util/BSFormatter.java index 8d19ca35879..2922ad40cfc 100644 --- a/gui/src/main/java/io/bisq/gui/util/BSFormatter.java +++ b/gui/src/main/java/io/bisq/gui/util/BSFormatter.java @@ -512,7 +512,7 @@ public static String formatDurationAsWords(long durationMillis, boolean showSeco } else format += "H\' " + hours + ", \'m\' " + minutes + "\'"; - String duration = durationMillis > 0 ? DurationFormatUtils.formatDuration(durationMillis, format) : Res.get("formatter.tradePeriodOver"); + String duration = durationMillis > 0 ? DurationFormatUtils.formatDuration(durationMillis, format) : ""; duration = StringUtils.replaceOnce(duration, "1 " + seconds, "1 " + second); duration = StringUtils.replaceOnce(duration, "1 " + minutes, "1 " + minute); diff --git a/gui/src/main/java/io/bisq/gui/util/FormBuilder.java b/gui/src/main/java/io/bisq/gui/util/FormBuilder.java index 5dd2e6a00f0..0196d2eeb91 100644 --- a/gui/src/main/java/io/bisq/gui/util/FormBuilder.java +++ b/gui/src/main/java/io/bisq/gui/util/FormBuilder.java @@ -802,6 +802,28 @@ public static Tuple2 addLabelFundsTextfield(GridPane grid return new Tuple2<>(label, fundsTextField); } + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + InfoTextField + /////////////////////////////////////////////////////////////////////////////////////////// + public static Tuple2 addLabelInfoTextfield(GridPane gridPane, int rowIndex, String labelText, + String fieldText) { + return addLabelInfoTextfield(gridPane, rowIndex, labelText, fieldText, 0); + } + + public static Tuple2 addLabelInfoTextfield(GridPane gridPane, int rowIndex, String labelText, + String fieldText, double top) { + Label label = addLabel(gridPane, rowIndex, labelText, top); + + InfoTextField infoTextField = new InfoTextField(); + infoTextField.setText(fieldText); + GridPane.setRowIndex(infoTextField, rowIndex); + GridPane.setColumnIndex(infoTextField, 1); + GridPane.setMargin(infoTextField, new Insets(top, 0,0,0)); + gridPane.getChildren().add(infoTextField); + + return new Tuple2<>(label, infoTextField); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Label + BsqAddressTextField /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/gui/src/main/java/io/bisq/gui/util/validation/AltCoinAddressValidator.java b/gui/src/main/java/io/bisq/gui/util/validation/AltCoinAddressValidator.java index 686897fd7d9..32c7c4076b5 100644 --- a/gui/src/main/java/io/bisq/gui/util/validation/AltCoinAddressValidator.java +++ b/gui/src/main/java/io/bisq/gui/util/validation/AltCoinAddressValidator.java @@ -434,10 +434,30 @@ public ValidationResult validate(String input) { else return new ValidationResult(true); case "STL": - if(!input.matches("^(Se)\\d[0-9A-Za-z]{94}$")) + if (!input.matches("^(Se)\\d[0-9A-Za-z]{94}$")) return regexTestFailed; else return new ValidationResult(true); + case "DAI": + // https://github.com/ethereum/web3.js/blob/master/lib/utils/utils.js#L403 + if (!input.matches("^(0x)?[0-9a-fA-F]{40}$")) + return regexTestFailed; + else + return new ValidationResult(true); + case "YTN": + return YTNAddressValidator.ValidateAddress(input); + case "DARX": + if (!input.matches("^[R][a-km-zA-HJ-NP-Z1-9]{25,34}$")) + return regexTestFailed; + else + return new ValidationResult(true); + case "ODN": + try { + Address.fromBase58(ODNParams.get(), input); + return new ValidationResult(true); + } catch (AddressFormatException e) { + return new ValidationResult(false, getErrorMessage(e)); + } // Add new coins at the end... default: diff --git a/gui/src/main/java/io/bisq/gui/util/validation/altcoins/YTNAddressValidator.java b/gui/src/main/java/io/bisq/gui/util/validation/altcoins/YTNAddressValidator.java new file mode 100644 index 00000000000..da8d08b0508 --- /dev/null +++ b/gui/src/main/java/io/bisq/gui/util/validation/altcoins/YTNAddressValidator.java @@ -0,0 +1,75 @@ +/* + * 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 io.bisq.gui.util.validation.altcoins; + +import io.bisq.gui.util.validation.InputValidator.ValidationResult; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +public class YTNAddressValidator { + private final static String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + public static ValidationResult ValidateAddress(String addr) { + if (addr.length() != 34) + return new ValidationResult(false, "YTN_Addr_Invalid: Length must be 34!"); + if (!addr.startsWith("Y")) + return new ValidationResult(false, "YTN_Addr_Invalid: must start with 'Y'!"); + byte[] decoded = decodeBase58(addr, 58, 25); + if (decoded == null) + return new ValidationResult(false, "YTN_Addr_Invalid: Base58 decoder error!"); + + byte[] hash = getSha256(decoded, 0, 21, 2); + if (hash == null || !Arrays.equals(Arrays.copyOfRange(hash, 0, 4), Arrays.copyOfRange(decoded, 21, 25))) + return new ValidationResult(false, "YTN_Addr_Invalid: Checksum error!"); + return new ValidationResult(true); + } + + private static byte[] decodeBase58(String input, int base, int len) { + byte[] output = new byte[len]; + for (int i = 0; i < input.length(); i++) { + char t = input.charAt(i); + + int p = ALPHABET.indexOf(t); + if (p == -1) + return null; + for (int j = len - 1; j >= 0; j--, p /= 256) { + p += base * (output[j] & 0xFF); + output[j] = (byte) (p % 256); + } + if (p != 0) + return null; + } + + return output; + } + + private static byte[] getSha256(byte[] data, int start, int len, int recursion) { + if (recursion == 0) + return data; + + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(Arrays.copyOfRange(data, start, start + len)); + return getSha256(md.digest(), 0, 32, recursion - 1); + } catch (NoSuchAlgorithmException e) { + return null; + } + } +} diff --git a/gui/src/main/java/io/bisq/gui/util/validation/params/ODNParams.java b/gui/src/main/java/io/bisq/gui/util/validation/params/ODNParams.java new file mode 100644 index 00000000000..ebc14978f67 --- /dev/null +++ b/gui/src/main/java/io/bisq/gui/util/validation/params/ODNParams.java @@ -0,0 +1,88 @@ +/* + * 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 io.bisq.gui.util.validation.params; + +import org.bitcoinj.core.*; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.utils.MonetaryFormat; + +public class ODNParams extends NetworkParameters { + + private static ODNParams instance; + + public static synchronized ODNParams get() { + if (instance == null) { + instance = new ODNParams(); + } + return instance; + } + + // We only use the properties needed for address validation + public ODNParams() { + super(); + addressHeader = 75; + p2shHeader = 125; + acceptableAddressCodes = new int[]{addressHeader, p2shHeader}; + } + + // default dummy implementations, not used... + @Override + public String getPaymentProtocolId() { + return PAYMENT_PROTOCOL_ID_MAINNET; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return null; + } + + @Override + public Coin getMinNonDustOutput() { + return null; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return null; + } + + @Override + public String getUriScheme() { + return null; + } + + @Override + public boolean hasMaxMoney() { + return false; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return null; + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 0; + } +} diff --git a/gui/src/test/java/io/bisq/gui/util/BSFormatterTest.java b/gui/src/test/java/io/bisq/gui/util/BSFormatterTest.java index c29a9401ce8..42471714dc8 100644 --- a/gui/src/test/java/io/bisq/gui/util/BSFormatterTest.java +++ b/gui/src/test/java/io/bisq/gui/util/BSFormatterTest.java @@ -25,6 +25,7 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class BSFormatterTest { @@ -63,6 +64,7 @@ public void testFormatDurationAsWords() { assertEquals("1 hour, 0 minutes, 0 seconds", formatter.formatDurationAsWords(oneHour, true)); assertEquals("1 hour, 0 minutes, 1 second", formatter.formatDurationAsWords(oneHour + oneSecond, true)); assertEquals("1 hour, 0 minutes, 2 seconds", formatter.formatDurationAsWords(oneHour + oneSecond * 2, true)); - assertEquals("Trade period is over", formatter.formatDurationAsWords(0)); + assertEquals("", formatter.formatDurationAsWords(0)); + assertTrue(formatter.formatDurationAsWords(0).isEmpty()); } } diff --git a/gui/src/test/java/io/bisq/gui/util/validation/AltCoinAddressValidatorTest.java b/gui/src/test/java/io/bisq/gui/util/validation/AltCoinAddressValidatorTest.java index 59b6791ac84..35c5bff1cf3 100644 --- a/gui/src/test/java/io/bisq/gui/util/validation/AltCoinAddressValidatorTest.java +++ b/gui/src/test/java/io/bisq/gui/util/validation/AltCoinAddressValidatorTest.java @@ -569,6 +569,7 @@ public void testREF() { assertFalse(validator.validate("").isValid); } + // Added 0.6.6 @Test public void testSTL() { AltCoinAddressValidator validator = new AltCoinAddressValidator(); @@ -582,4 +583,64 @@ public void testSTL() { assertFalse(validator.validate("Se3F51UzpbVVnQRx2VNbcjfBoQJfeuyFF353i1jLnCZda9yVN3vy8csbYCESBvf38TFkchH1C1tMY6XHkC8L678K2vLsVZVMUII").isValid); //99 Charecters, expected is 97 assertFalse(validator.validate("").isValid); } + + @Test + public void testDAI() { + AltCoinAddressValidator validator = new AltCoinAddressValidator(); + validator.setCurrencyCode("DAI"); + + assertTrue(validator.validate("0x2a65Aca4D5fC5B5C859090a6c34d164135398226").isValid); + assertTrue(validator.validate("2a65Aca4D5fC5B5C859090a6c34d164135398226").isValid); + + assertFalse(validator.validate("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266").isValid); + assertFalse(validator.validate("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g").isValid); + assertFalse(validator.validate("2a65Aca4D5fC5B5C859090a6c34d16413539822g").isValid); + assertFalse(validator.validate("").isValid); + } + + @Test + public void testYTN() { + AltCoinAddressValidator validator = new AltCoinAddressValidator(); + validator.setCurrencyCode("YTN"); + assertTrue(validator.validate("YTgSv7bk5x5p6te3uf3HbUwgnf7zEJM4Jn").isValid); + assertTrue(validator.validate("YVz19KtQUfyTP4AJS8sbRBqi7dkGTL2ovd").isValid); + + assertFalse(validator.validate("YiTwGuv3opowtPF5w8LUWBXFmaxc9S68ha").isValid); + assertFalse(validator.validate("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq").isValid); + assertFalse(validator.validate("YVZNX1SN5NtKa8UQFxwQbFeFc3iqRYheO").isValid); + assertFalse(validator.validate("YiTwGuv3opowtPF5w8LUWBlFmaxc9S68hz").isValid); + assertFalse(validator.validate("YiTwGuv3opowtPF5w8LUWB0Fmaxc9S68hz").isValid); + assertFalse(validator.validate("YiTwGuv3opowtPF5w8LUWBIFmaxc9S68hz").isValid); + assertFalse(validator.validate("").isValid); + } + + @Test + public void testDARX() { + AltCoinAddressValidator validator = new AltCoinAddressValidator(); + validator.setCurrencyCode("DARX"); + + assertTrue(validator.validate("RN8spHmkV6ZtRsquaTJMRZJujRQkkDNh2G").isValid); + assertTrue(validator.validate("RTD9jtKybd7TeM597t5MkNof84GPka34R7").isValid); + + assertFalse(validator.validate("LAPc1FumbYifKpfBpGbRLuPcLAJwHUxeyu").isValid); + assertFalse(validator.validate("ROPc1FumbYifKpfBpGbRLuPcLAJwHUxeyu").isValid); + assertFalse(validator.validate("rN8spHmkV6ZtROquaTJMRZJujRQkkDNh2G").isValid); + assertFalse(validator.validate("1NxrMzHCjG8X9kqTEZBXUNB5PC58DSXAht").isValid); + } + + @Test + public void testODN() { + AltCoinAddressValidator validator = new AltCoinAddressValidator(); + validator.setCurrencyCode("ODN"); + + assertTrue(validator.validate("XEfyuzk8yTp5eA9eVUeCW2PFbCFtNb6Jgv").isValid); + assertTrue(validator.validate("XJegzjV2GK9CeQLNNcc5GVSSqTkv1XMxSF").isValid); + assertTrue(validator.validate("XLfvvLuwjUrz2kf5gghEmUPFE3vFvwfEiL").isValid); + assertTrue(validator.validate("XNC1e9TfUApfBsH9JCubioS5XGuwFLbsP4").isValid); + + assertFalse(validator.validate("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq").isValid); + assertFalse(validator.validate("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266").isValid); + assertFalse(validator.validate("SSnwqFBiyqK1n4BV7kPX86iesev2NobhEo").isValid); + assertFalse(validator.validate("").isValid); + } } diff --git a/network/src/main/java/io/bisq/network/p2p/network/LocalhostNetworkNode.java b/network/src/main/java/io/bisq/network/p2p/network/LocalhostNetworkNode.java index 79057693669..4cec5eaa861 100644 --- a/network/src/main/java/io/bisq/network/p2p/network/LocalhostNetworkNode.java +++ b/network/src/main/java/io/bisq/network/p2p/network/LocalhostNetworkNode.java @@ -67,10 +67,9 @@ public void start(@Nullable SetupListener setupListener) { e.printStackTrace(); log.error("Exception at startServer: " + e.getMessage()); } - - final NodeAddress nodeAddress; - if (null == address) nodeAddress = new NodeAddress("localhost", servicePort); - else nodeAddress = new NodeAddress(address); + final NodeAddress nodeAddress = address == null ? + new NodeAddress("localhost", servicePort) : + new NodeAddress(address); nodeAddressProperty.set(nodeAddress); setupListeners.stream().forEach(SetupListener::onHiddenServicePublished); }, simulateTorDelayTorNode, TimeUnit.MILLISECONDS); diff --git a/network/src/main/java/io/bisq/network/p2p/network/SynchronizedProtoOutputStream.java b/network/src/main/java/io/bisq/network/p2p/network/SynchronizedProtoOutputStream.java index dfc7bbf6028..51a2c65d27b 100644 --- a/network/src/main/java/io/bisq/network/p2p/network/SynchronizedProtoOutputStream.java +++ b/network/src/main/java/io/bisq/network/p2p/network/SynchronizedProtoOutputStream.java @@ -47,11 +47,13 @@ void writeEnvelope(NetworkEnvelope envelope) { } catch (InterruptedException e) { Thread currentThread = Thread.currentThread(); currentThread.interrupt(); - log.error("Thread " + currentThread + " was interrupted", e); - throw new BisqRuntimeException("Failed to write envelope", e); + final String msg = "Thread " + currentThread + " was interrupted. InterruptedException=" + e; + log.error(msg); + throw new BisqRuntimeException(msg, e); } catch (ExecutionException e) { - log.error("Failed to write envelope", e); - throw new BisqRuntimeException("Failed to write envelope", e); + final String msg = "Failed to write envelope. ExecutionException " + e; + log.error(msg); + throw new BisqRuntimeException(msg, e); } } @@ -60,7 +62,7 @@ void onConnectionShutdown() { executorService.shutdownNow(); super.onConnectionShutdown(); } catch (Throwable t) { - log.error("Failed to handle connection shutdown", t); + log.error("Failed to handle connection shutdown. Throwable={}", t); } } }