Merge pull request #2 from bisq-network/master

Sync branch with master fork.
This commit is contained in:
Stan 2018-02-15 13:56:48 +00:00 committed by GitHub
commit 8daa39e21d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1508 additions and 409 deletions

View File

@ -26,7 +26,7 @@ import java.util.Locale;
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<Locale> localeProperty = new SimpleObjectProperty<>(locale);
private static TradeCurrency defaultTradeCurrency;
private static String btcDenomination;

View File

@ -96,12 +96,14 @@ public class CurrencyUtil {
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 class CurrencyUtil {
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 class CurrencyUtil {
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"));

View File

@ -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

View File

@ -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

View File

@ -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=Όνομα αποδεκτής τράπεζας

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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=Название подтверждённого банка

View File

@ -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

View File

@ -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=接受的银行名称

View File

@ -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})

View File

@ -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 class BsqWalletService extends WalletService implements BsqBlockChainList
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 class BsqWalletService extends WalletService implements BsqBlockChainList
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 class BsqWalletService extends WalletService implements BsqBlockChainList
///////////////////////////////////////////////////////////////////////////////////////////
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 class BsqWalletService extends WalletService implements BsqBlockChainList
// 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;

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<BtcNode> nodes;
PeerAddressesRepository(BtcNodeConverter converter, List<BtcNode> nodes) {
this.converter = converter;
this.nodes = nodes;
}
PeerAddressesRepository(List<BtcNode> nodes) {
this(new BtcNodeConverter(), nodes);
}
List<PeerAddress> getPeerAddresses(@Nullable Socks5Proxy proxy, boolean isUseClearNodesWithProxies) {
List<PeerAddress> 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<PeerAddress> onionHosts = getOnionHosts();
result = new ArrayList<>(onionHosts);
if (isUseClearNodesWithProxies) {
// We also use the clear net nodes (used for monitor)
List<PeerAddress> torAddresses = getClearNodesBehindProxy(proxy);
result.addAll(torAddresses);
}
} else {
result = getClearNodes();
}
return result;
}
private List<PeerAddress> getClearNodes() {
return nodes.stream()
.filter(BtcNode::hasClearNetAddress)
.flatMap(node -> nullableAsStream(converter.convertClearNode(node)))
.collect(Collectors.toList());
}
private List<PeerAddress> getOnionHosts() {
return nodes.stream()
.filter(BtcNode::hasOnionAddress)
.flatMap(node -> nullableAsStream(converter.convertOnionHost(node)))
.collect(Collectors.toList());
}
private List<PeerAddress> getClearNodesBehindProxy(Socks5Proxy proxy) {
return nodes.stream()
.filter(BtcNode::hasClearNetAddress)
.flatMap(node -> nullableAsStream(converter.convertWithTor(node, proxy)))
.collect(Collectors.toList());
}
private static <T> Stream<T> nullableAsStream(@Nullable T item) {
return Optional.ofNullable(item)
.map(Stream::of)
.orElse(Stream.empty());
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<PeerAddress> 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.");
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<BtcNode> selectPreferredNodes(BitcoinNodes nodes) {
List<BtcNode> result;
BitcoinNodesOption nodesOption = BitcoinNodesOption.values()[preferences.getBitcoinNodesOptionOrdinal()];
switch (nodesOption) {
case CUSTOM:
String bitcoinNodes = preferences.getBitcoinNodes();
Set<String> 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<BtcNode> 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;
}
}

View File

@ -28,11 +28,10 @@ import io.bisq.common.app.Log;
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 lombok.extern.slf4j.Slf4j;
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 javax.inject.Named;
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 @@ import static com.google.common.base.Preconditions.checkNotNull;
@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 @@ public class WalletsSetup {
walletConfig.setPeerNodesForLocalHost();
}
private void configPeerNodes(Socks5Proxy socks5Proxy) {
boolean useCustomNodes = false;
List<BitcoinNodes.BtcNode> 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<BtcNode> 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<PeerAddress> peers = repository.getPeerAddresses(proxy, isUseClearNodesWithProxies);
List<PeerAddress> 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);
}

View File

@ -384,8 +384,6 @@ public class BsqBlockChain implements PersistableEnvelope {
public Optional<Tx> 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 class BsqBlockChain implements PersistableEnvelope {
return lock.read(() -> chainHeadHeight);
}
// Only used for Json Exporter
public Map<String, Tx> getTxMap() {
return lock.read(() -> txMap);
}

View File

@ -102,6 +102,7 @@ public class BsqParser {
// Parsing with data requested from bsqBlockchainService
///////////////////////////////////////////////////////////////////////////////////////////
@VisibleForTesting
void parseBlocks(int startBlockHeight,
int chainHeadHeight,
int genesisBlockHeight,
@ -112,6 +113,7 @@ public class BsqParser {
for (int blockHeight = startBlockHeight; blockHeight <= chainHeadHeight; blockHeight++) {
long startTs = System.currentTimeMillis();
Block btcdBlock = rpcService.requestBlock(blockHeight);
List<Tx> bsqTxsInBlock = findBsqTxsInBlock(btcdBlock,
genesisBlockHeight,
genesisTxId);

View File

@ -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<String, String> extraDataMap;
@ -108,4 +122,17 @@ public final class CompensationRequest implements PersistablePayload {
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);
}
}

View File

@ -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.FXCollections;
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<CompensationRequestList> compensationRequestsStorage;
private final PublicKey signaturePubKey;
private final FeeService feeService;
@Getter
private final ObservableList<CompensationRequest> 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<CompensationRequestList> compensationRequestsStorage) {
Storage<CompensationRequestList> 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 class CompensationRequestManager implements PersistedDataHost, BsqBlockCh
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<Transaction> 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<Transaction>() {
@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 @@ public class CompensationRequestManager implements PersistedDataHost, BsqBlockCh
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) ||

View File

@ -91,7 +91,7 @@ public class ProvidersRepository {
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. " +

View File

@ -251,7 +251,12 @@ public class TradeStatisticsManager {
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);

View File

@ -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());
}
}

View File

@ -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<PeerAddress> 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<PeerAddress> 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<PeerAddress> 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<PeerAddress> peers = repository.getPeerAddresses(mock(Socks5Proxy.class), false);
assertEquals(1, peers.size());
}
}

View File

@ -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());
}
}

View File

@ -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<BtcNode> 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<BtcNode> nodes = preferences.selectPreferredNodes(mock(BitcoinNodes.class));
assertEquals(2, nodes.size());
}
}

View File

@ -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 class BsqParserTest {
int height = 200;
String hash = "abc123";
long time = new Date().getTime();
List<TxInput> inputs = new ArrayList<>();
inputs.add(new TxInput("tx1", 0));
inputs.add(new TxInput("tx1", 1));
List<TxOutput> 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 class BsqParserTest {
// 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<TxInput>(),
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<TxInput>(),
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<TxInput>(),
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));
}
}

View File

@ -25,17 +25,12 @@ import joptsimple.OptionException;
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();
}

View File

@ -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;
}

View File

@ -87,12 +87,12 @@ public class AddressTextField extends AnchorPane {
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() {

View File

@ -19,6 +19,7 @@ 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 io.bisq.common.util.Utilities;
import javafx.beans.binding.Bindings;
@ -33,13 +34,12 @@ import org.controlsfx.control.PopOver;
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 class FundsTextField extends AnchorPane {
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 class FundsTextField extends AnchorPane {
}
});
AnchorPane.setRightAnchor(copyIcon, 5.0);
AnchorPane.setRightAnchor(infoIcon, 37.0);
AnchorPane.setRightAnchor(textField, 30.0);
AnchorPane.setLeftAnchor(textField, 0.0);
AnchorPane.setRightAnchor(copyIcon, 30.0);
AnchorPane.setRightAnchor(infoIcon, 62.0);
AnchorPane.setRightAnchor(textField, 55.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);
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);
}

View File

@ -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;
}
}

View File

@ -26,6 +26,7 @@ import io.bisq.core.offer.Offer;
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 @@ public abstract class PaymentMethodForm {
});
}
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) {

View File

@ -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.NodeAddress;
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 @@ public class CreateCompensationRequestView extends ActivatableView<GridPane, Voi
);
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<Transaction>() {
compensationRequestManager.commitCompensationRequest(compensationRequest, new FutureCallback<Transaction>() {
@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 class CreateCompensationRequestView extends ActivatableView<GridPane, Voi
.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());

View File

@ -207,10 +207,10 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
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 class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
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 class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
}
});
*/
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")));

View File

@ -410,7 +410,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
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 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
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 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
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);

View File

@ -831,9 +831,8 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
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() {

View File

@ -420,8 +420,8 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
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 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
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 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
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();

View File

@ -584,9 +584,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
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() {

View File

@ -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.PendingTradesViewModel;
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 org.slf4j.LoggerFactory;
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 @@ public abstract class TradeStepView extends AnchorPane {
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 @@ public abstract class TradeStepView extends AnchorPane {
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 @@ public abstract class TradeStepView extends AnchorPane {
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// 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;
}
}

View File

@ -36,10 +36,10 @@ import io.bisq.gui.components.AutoTooltipLabel;
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 @@ public class BuyerStep4View extends TradeStepView {
//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();
}

View File

@ -512,7 +512,7 @@ public class BSFormatter {
} 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);

View File

@ -802,6 +802,28 @@ public class FormBuilder {
return new Tuple2<>(label, fundsTextField);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Label + InfoTextField
///////////////////////////////////////////////////////////////////////////////////////////
public static Tuple2<Label, InfoTextField> addLabelInfoTextfield(GridPane gridPane, int rowIndex, String labelText,
String fieldText) {
return addLabelInfoTextfield(gridPane, rowIndex, labelText, fieldText, 0);
}
public static Tuple2<Label, InfoTextField> 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
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -434,10 +434,30 @@ public final class AltCoinAddressValidator extends InputValidator {
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:

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -25,6 +25,7 @@ import java.util.Locale;
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 class BSFormatterTest {
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());
}
}

View File

@ -569,6 +569,7 @@ public class AltCoinAddressValidatorTest {
assertFalse(validator.validate("").isValid);
}
// Added 0.6.6
@Test
public void testSTL() {
AltCoinAddressValidator validator = new AltCoinAddressValidator();
@ -582,4 +583,64 @@ public class AltCoinAddressValidatorTest {
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);
}
}

View File

@ -67,10 +67,9 @@ public class LocalhostNetworkNode extends NetworkNode {
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);

View File

@ -47,11 +47,13 @@ class SynchronizedProtoOutputStream extends ProtoOutputStream {
} 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 @@ class SynchronizedProtoOutputStream extends ProtoOutputStream {
executorService.shutdownNow();
super.onConnectionShutdown();
} catch (Throwable t) {
log.error("Failed to handle connection shutdown", t);
log.error("Failed to handle connection shutdown. Throwable={}", t);
}
}
}