mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 18:03:12 +01:00
Merge pull request #2 from bisq-network/master
Sync branch with master fork.
This commit is contained in:
commit
8daa39e21d
@ -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;
|
||||
|
@ -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"));
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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=Όνομα αποδεκτής τράπεζας
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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=Название подтверждённого банка
|
||||
|
@ -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
|
||||
|
@ -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=接受的银行名称
|
||||
|
@ -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})
|
||||
|
@ -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;
|
||||
|
132
core/src/main/java/io/bisq/core/btc/wallet/BtcNodeConverter.java
Normal file
132
core/src/main/java/io/bisq/core/btc/wallet/BtcNodeConverter.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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) ||
|
||||
|
@ -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. " +
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
102
gui/src/main/java/io/bisq/gui/components/InfoTextField.java
Normal file
102
gui/src/main/java/io/bisq/gui/components/InfoTextField.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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());
|
||||
|
@ -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")));
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user