/* * This file is part of Bitsquare. * * Bitsquare 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. * * Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>. */ package io.bitsquare.user; import io.bitsquare.app.AppOptionKeys; import io.bitsquare.app.BitsquareEnvironment; import io.bitsquare.app.DevFlags; import io.bitsquare.app.Version; import io.bitsquare.btc.BitcoinNetwork; import io.bitsquare.btc.FeePolicy; import io.bitsquare.common.persistance.Persistable; import io.bitsquare.common.util.Utilities; import io.bitsquare.locale.*; import io.bitsquare.storage.Storage; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.utils.MonetaryFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import java.util.*; public final class Preferences implements Persistable { // That object is saved to disc. We need to take care of changes to not break deserialization. private static final long serialVersionUID = Version.LOCAL_DB_VERSION; private static final Logger log = LoggerFactory.getLogger(Preferences.class); public static Preferences INSTANCE; // Deactivate mBit for now as most screens are not supporting it yet private static final List<String> BTC_DENOMINATIONS = Arrays.asList(MonetaryFormat.CODE_BTC/*, MonetaryFormat.CODE_MBTC*/); transient static final private ArrayList<BlockChainExplorer> blockChainExplorersTestNet = new ArrayList<>(Arrays.asList( new BlockChainExplorer("Blocktrail", "https://www.blocktrail.com/tBTC/tx/", "https://www.blocktrail.com/tBTC/address/"), new BlockChainExplorer("Blockexplorer", "https://blockexplorer.com/testnet/tx/", "https://blockexplorer.com/testnet/address/"), new BlockChainExplorer("Blockr.io", "https://tbtc.blockr.io/tx/info/", "https://tbtc.blockr.io/address/info/"), new BlockChainExplorer("Biteasy", "https://www.biteasy.com/testnet/transactions/", "https://www.biteasy.com/testnet/addresses/"), new BlockChainExplorer("Smartbit", "https://testnet.smartbit.com.au/tx/", "https://testnet.smartbit.com.au/address/"), new BlockChainExplorer("SoChain. Wow.", "https://chain.so/tx/BTCTEST/", "https://chain.so/address/BTCTEST/") )); transient static final private ArrayList<BlockChainExplorer> blockChainExplorersMainNet = new ArrayList<>(Arrays.asList( new BlockChainExplorer("Tradeblock.com", "https://tradeblock.com/bitcoin/tx/", "https://tradeblock.com/bitcoin/address/"), new BlockChainExplorer("Blocktrail", "https://www.blocktrail.com/BTC/tx/", "https://www.blocktrail.com/BTC/address/"), new BlockChainExplorer("Insight", "https://insight.bitpay.com/tx/", "https://insight.bitpay.com/address/"), new BlockChainExplorer("Blockchain.info", "https://blockchain.info/tx/", "https://blockchain.info/address/"), new BlockChainExplorer("Blockexplorer", "https://blockexplorer.com/tx/", "https://blockexplorer.com/address/"), new BlockChainExplorer("Blockr.io", "https://btc.blockr.io/tx/info/", "https://btc.blockr.io/address/info/"), new BlockChainExplorer("Biteasy", "https://www.biteasy.com/transactions/", "https://www.biteasy.com/addresses/"), new BlockChainExplorer("Blockonomics", "https://www.blockonomics.co/api/tx?txid=", "https://www.blockonomics.co/#/search?q="), new BlockChainExplorer("Chainflyer", "http://chainflyer.bitflyer.jp/Transaction/", "http://chainflyer.bitflyer.jp/Address/"), new BlockChainExplorer("Smartbit", "https://www.smartbit.com.au/tx/", "https://www.smartbit.com.au/address/"), new BlockChainExplorer("SoChain. Wow.", "https://chain.so/tx/BTC/", "https://chain.so/address/BTC/"), new BlockChainExplorer("Bitaps", "https://bitaps.com/", "https://bitaps.com/") )); public static List<String> getBtcDenominations() { return BTC_DENOMINATIONS; } private static Locale defaultLocale = Locale.getDefault(); //TODO test with other locales // private static Locale defaultLocale = Locale.US; public static Locale getDefaultLocale() { return defaultLocale; } private static TradeCurrency defaultTradeCurrency = new FiatCurrency(CurrencyUtil.getCurrencyByCountryCode(CountryUtil.getDefaultCountryCode()).getCurrency().getCurrencyCode()); public static TradeCurrency getDefaultTradeCurrency() { return defaultTradeCurrency; } private static boolean staticUseAnimations = true; transient private final Storage<Preferences> storage; transient private final BitsquareEnvironment bitsquareEnvironment; transient private BitcoinNetwork bitcoinNetwork; // Persisted fields private String btcDenomination = MonetaryFormat.CODE_BTC; private boolean useAnimations = DevFlags.STRESS_TEST_MODE ? false : true; private final ArrayList<FiatCurrency> fiatCurrencies; private final ArrayList<CryptoCurrency> cryptoCurrencies; private BlockChainExplorer blockChainExplorerMainNet; private BlockChainExplorer blockChainExplorerTestNet; private String backupDirectory; private boolean autoSelectArbitrators = true; private final Map<String, Boolean> dontShowAgainMap; private boolean tacAccepted; private boolean useTorForBitcoinJ = true; private boolean showOwnOffersInOfferBook = true; private Locale preferredLocale; private TradeCurrency preferredTradeCurrency; private long nonTradeTxFeePerKB = FeePolicy.getNonTradeFeePerKb().value; private double maxPriceDistanceInPercent; private String offerBookChartScreenCurrencyCode = CurrencyUtil.getDefaultTradeCurrency().getCode(); private String tradeChartsScreenCurrencyCode = CurrencyUtil.getDefaultTradeCurrency().getCode(); private String buyScreenCurrencyCode = CurrencyUtil.getDefaultTradeCurrency().getCode(); private String sellScreenCurrencyCode = CurrencyUtil.getDefaultTradeCurrency().getCode(); private int tradeStatisticsTickUnitIndex = 3; private boolean useStickyMarketPrice = false; private boolean sortMarketCurrenciesNumerically = true; private boolean usePercentageBasedPrice = false; private Map<String, String> peerTagMap = new HashMap<>(); private String bitcoinNodes = ""; private List<String> ignoreTradersList = new ArrayList<>(); private String defaultPath; // Observable wrappers transient private final StringProperty btcDenominationProperty = new SimpleStringProperty(btcDenomination); transient private final BooleanProperty useAnimationsProperty = new SimpleBooleanProperty(useAnimations); transient private final ObservableList<FiatCurrency> fiatCurrenciesAsObservable = FXCollections.observableArrayList(); transient private final ObservableList<CryptoCurrency> cryptoCurrenciesAsObservable = FXCollections.observableArrayList(); transient private final ObservableList<TradeCurrency> tradeCurrenciesAsObservable = FXCollections.observableArrayList(); // Not used anymore but need to be kept for backward compatibility @Deprecated private boolean useInvertedMarketPrice; @Deprecated private boolean useTorForHttpRequests = true; @Deprecated private List<String> bridgeAddresses; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public Preferences(Storage<Preferences> storage, BitsquareEnvironment bitsquareEnvironment, @Named(AppOptionKeys.BTC_NODES) String btcNodesFromOptions, @Named(AppOptionKeys.USE_TOR_FOR_BTC) String useTorFlagFromOptions) { INSTANCE = this; this.storage = storage; this.bitsquareEnvironment = bitsquareEnvironment; if (Utilities.isWindows()) defaultPath = System.getenv("USERPROFILE"); else defaultPath = System.getProperty("user.home"); fiatCurrencies = new ArrayList<>(fiatCurrenciesAsObservable); cryptoCurrencies = new ArrayList<>(cryptoCurrenciesAsObservable); btcDenominationProperty.addListener((ov) -> { btcDenomination = btcDenominationProperty.get(); storage.queueUpForSave(); }); useAnimationsProperty.addListener((ov) -> { useAnimations = useAnimationsProperty.get(); staticUseAnimations = useAnimations; storage.queueUpForSave(); }); fiatCurrenciesAsObservable.addListener((javafx.beans.Observable ov) -> { fiatCurrencies.clear(); fiatCurrencies.addAll(fiatCurrenciesAsObservable); fiatCurrencies.sort(TradeCurrency::compareTo); storage.queueUpForSave(); }); cryptoCurrenciesAsObservable.addListener((javafx.beans.Observable ov) -> { cryptoCurrencies.clear(); cryptoCurrencies.addAll(cryptoCurrenciesAsObservable); cryptoCurrencies.sort(TradeCurrency::compareTo); storage.queueUpForSave(); }); Preferences persisted = storage.initAndGetPersisted(this); if (persisted != null) { setBtcDenomination(persisted.btcDenomination); setUseAnimations(persisted.useAnimations); setFiatCurrencies(persisted.fiatCurrencies); setCryptoCurrencies(persisted.cryptoCurrencies); setBlockChainExplorerTestNet(persisted.getBlockChainExplorerTestNet()); setBlockChainExplorerMainNet(persisted.getBlockChainExplorerMainNet()); // In case of an older version without that data we set it to defaults if (blockChainExplorerTestNet == null) setBlockChainExplorerTestNet(blockChainExplorersTestNet.get(0)); if (blockChainExplorerMainNet == null) setBlockChainExplorerTestNet(blockChainExplorersMainNet.get(0)); backupDirectory = persisted.getBackupDirectory(); autoSelectArbitrators = persisted.getAutoSelectArbitrators(); dontShowAgainMap = persisted.getDontShowAgainMap(); tacAccepted = persisted.getTacAccepted(); preferredLocale = persisted.getPreferredLocale(); defaultLocale = preferredLocale; preferredTradeCurrency = persisted.getPreferredTradeCurrency(); defaultTradeCurrency = preferredTradeCurrency; useTorForBitcoinJ = persisted.getUseTorForBitcoinJ(); useStickyMarketPrice = persisted.getUseStickyMarketPrice(); sortMarketCurrenciesNumerically = persisted.getSortMarketCurrenciesNumerically(); usePercentageBasedPrice = persisted.getUsePercentageBasedPrice(); showOwnOffersInOfferBook = persisted.getShowOwnOffersInOfferBook(); maxPriceDistanceInPercent = persisted.getMaxPriceDistanceInPercent(); bitcoinNodes = persisted.getBitcoinNodes(); if (bitcoinNodes == null) bitcoinNodes = ""; try { setNonTradeTxFeePerKB(persisted.getNonTradeTxFeePerKB()); } catch (Exception e) { // leave default value } if (persisted.getPeerTagMap() != null) peerTagMap = persisted.getPeerTagMap(); offerBookChartScreenCurrencyCode = persisted.getOfferBookChartScreenCurrencyCode(); buyScreenCurrencyCode = persisted.getBuyScreenCurrencyCode(); sellScreenCurrencyCode = persisted.getSellScreenCurrencyCode(); tradeChartsScreenCurrencyCode = persisted.getTradeChartsScreenCurrencyCode(); tradeStatisticsTickUnitIndex = persisted.getTradeStatisticsTickUnitIndex(); if (persisted.getIgnoreTradersList() != null) ignoreTradersList = persisted.getIgnoreTradersList(); if (persisted.getDefaultPath() != null) defaultPath = persisted.getDefaultPath(); } else { setFiatCurrencies(CurrencyUtil.getAllMainFiatCurrencies()); setCryptoCurrencies(CurrencyUtil.getMainCryptoCurrencies()); setBlockChainExplorerTestNet(blockChainExplorersTestNet.get(0)); setBlockChainExplorerMainNet(blockChainExplorersMainNet.get(0)); dontShowAgainMap = new HashMap<>(); preferredLocale = getDefaultLocale(); preferredTradeCurrency = getDefaultTradeCurrency(); maxPriceDistanceInPercent = 0.1; storage.queueUpForSave(); } this.bitcoinNetwork = bitsquareEnvironment.getBitcoinNetwork(); fiatCurrenciesAsObservable.addListener(this::updateTradeCurrencies); cryptoCurrenciesAsObservable.addListener(this::updateTradeCurrencies); tradeCurrenciesAsObservable.addAll(fiatCurrencies); tradeCurrenciesAsObservable.addAll(cryptoCurrencies); // Override settings with options if set if (useTorFlagFromOptions != null && !useTorFlagFromOptions.isEmpty()) { if (useTorFlagFromOptions.equals("false")) setUseTorForBitcoinJ(false); else if (useTorFlagFromOptions.equals("true")) setUseTorForBitcoinJ(true); } if (btcNodesFromOptions != null && !btcNodesFromOptions.isEmpty()) setBitcoinNodes(btcNodesFromOptions); if (bitcoinNodes.equals("127.0.0.1") || bitcoinNodes.equals("localhost")) setUseTorForBitcoinJ(false); } public void dontShowAgain(String key, boolean dontShowAgain) { dontShowAgainMap.put(key, dontShowAgain); storage.queueUpForSave(); } public void resetDontShowAgainForType() { dontShowAgainMap.clear(); storage.queueUpForSave(); } /////////////////////////////////////////////////////////////////////////////////////////// // Setter /////////////////////////////////////////////////////////////////////////////////////////// public void setBtcDenomination(String btcDenomination) { this.btcDenominationProperty.set(btcDenomination); } public void setUseAnimations(boolean useAnimations) { this.useAnimationsProperty.set(useAnimations); } public void setBitcoinNetwork(BitcoinNetwork bitcoinNetwork) { if (this.bitcoinNetwork != bitcoinNetwork) bitsquareEnvironment.saveBitcoinNetwork(bitcoinNetwork); this.bitcoinNetwork = bitcoinNetwork; // We don't store the bitcoinNetwork locally as BitcoinNetwork is not serializable! } public void addFiatCurrency(FiatCurrency tradeCurrency) { if (!fiatCurrenciesAsObservable.contains(tradeCurrency)) fiatCurrenciesAsObservable.add(tradeCurrency); } public void removeFiatCurrency(FiatCurrency tradeCurrency) { if (tradeCurrenciesAsObservable.size() > 1) { if (fiatCurrenciesAsObservable.contains(tradeCurrency)) fiatCurrenciesAsObservable.remove(tradeCurrency); if (preferredTradeCurrency.equals(tradeCurrency)) setPreferredTradeCurrency(tradeCurrenciesAsObservable.get(0)); } else { log.error("you cannot remove the last currency"); } } public void addCryptoCurrency(CryptoCurrency tradeCurrency) { if (!cryptoCurrenciesAsObservable.contains(tradeCurrency)) cryptoCurrenciesAsObservable.add(tradeCurrency); } public void removeCryptoCurrency(CryptoCurrency tradeCurrency) { if (tradeCurrenciesAsObservable.size() > 1) { if (cryptoCurrenciesAsObservable.contains(tradeCurrency)) cryptoCurrenciesAsObservable.remove(tradeCurrency); if (preferredTradeCurrency.equals(tradeCurrency)) setPreferredTradeCurrency(tradeCurrenciesAsObservable.get(0)); } else { log.error("you cannot remove the last currency"); } } public void setBlockChainExplorer(BlockChainExplorer blockChainExplorer) { if (bitcoinNetwork == BitcoinNetwork.MAINNET) setBlockChainExplorerMainNet(blockChainExplorer); else setBlockChainExplorerTestNet(blockChainExplorer); } public void setTacAccepted(boolean tacAccepted) { this.tacAccepted = tacAccepted; storage.queueUpForSave(); } public void setPreferredLocale(Locale preferredLocale) { this.preferredLocale = preferredLocale; defaultLocale = preferredLocale; storage.queueUpForSave(); } public void setPreferredTradeCurrency(TradeCurrency preferredTradeCurrency) { if (preferredTradeCurrency != null) { this.preferredTradeCurrency = preferredTradeCurrency; defaultTradeCurrency = preferredTradeCurrency; storage.queueUpForSave(); } } public void setNonTradeTxFeePerKB(long nonTradeTxFeePerKB) throws Exception { if (nonTradeTxFeePerKB < Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value) throw new Exception("Transaction fee must be at least 5 satoshi/byte"); if (nonTradeTxFeePerKB > 1_000_000) throw new Exception("Transaction fee is in the range of 10-100 satoshi/byte. Your input is above any reasonable value (>1000 satoshi/byte)."); this.nonTradeTxFeePerKB = nonTradeTxFeePerKB; FeePolicy.setNonTradeFeePerKb(Coin.valueOf(nonTradeTxFeePerKB)); storage.queueUpForSave(); } public void setUseTorForBitcoinJ(boolean useTorForBitcoinJ) { this.useTorForBitcoinJ = useTorForBitcoinJ; storage.queueUpForSave(); } public void setShowOwnOffersInOfferBook(boolean showOwnOffersInOfferBook) { this.showOwnOffersInOfferBook = showOwnOffersInOfferBook; storage.queueUpForSave(); } public void setMaxPriceDistanceInPercent(double maxPriceDistanceInPercent) { this.maxPriceDistanceInPercent = maxPriceDistanceInPercent; storage.queueUpForSave(); } public void setBackupDirectory(String backupDirectory) { this.backupDirectory = backupDirectory; storage.queueUpForSave(); } public void setAutoSelectArbitrators(boolean autoSelectArbitrators) { this.autoSelectArbitrators = autoSelectArbitrators; storage.queueUpForSave(); } public void setUseStickyMarketPrice(boolean useStickyMarketPrice) { this.useStickyMarketPrice = useStickyMarketPrice; storage.queueUpForSave(); } public void setUsePercentageBasedPrice(boolean usePercentageBasedPrice) { this.usePercentageBasedPrice = usePercentageBasedPrice; storage.queueUpForSave(); } public void setTagForPeer(String hostName, String tag) { peerTagMap.put(hostName, tag); storage.queueUpForSave(); } public void setOfferBookChartScreenCurrencyCode(String offerBookChartScreenCurrencyCode) { this.offerBookChartScreenCurrencyCode = offerBookChartScreenCurrencyCode; storage.queueUpForSave(); } public void setBuyScreenCurrencyCode(String buyScreenCurrencyCode) { this.buyScreenCurrencyCode = buyScreenCurrencyCode; storage.queueUpForSave(); } public void setSellScreenCurrencyCode(String sellScreenCurrencyCode) { this.sellScreenCurrencyCode = sellScreenCurrencyCode; storage.queueUpForSave(); } public void setIgnoreTradersList(List<String> ignoreTradersList) { this.ignoreTradersList = ignoreTradersList; storage.queueUpForSave(); } public void setDefaultPath(String defaultPath) { this.defaultPath = defaultPath; storage.queueUpForSave(); } public void setTradeChartsScreenCurrencyCode(String tradeChartsScreenCurrencyCode) { this.tradeChartsScreenCurrencyCode = tradeChartsScreenCurrencyCode; storage.queueUpForSave(); } public void setTradeStatisticsTickUnitIndex(int tradeStatisticsTickUnitIndex) { this.tradeStatisticsTickUnitIndex = tradeStatisticsTickUnitIndex; storage.queueUpForSave(); } public void setSortMarketCurrenciesNumerically(boolean sortMarketCurrenciesNumerically) { this.sortMarketCurrenciesNumerically = sortMarketCurrenciesNumerically; storage.queueUpForSave(); } public void setBitcoinNodes(String bitcoinNodes) { this.bitcoinNodes = bitcoinNodes; storage.queueUpForSave(50); } /////////////////////////////////////////////////////////////////////////////////////////// // Getter /////////////////////////////////////////////////////////////////////////////////////////// public String getBtcDenomination() { return btcDenominationProperty.get(); } public StringProperty btcDenominationProperty() { return btcDenominationProperty; } public boolean getUseAnimations() { return useAnimationsProperty.get(); } public BooleanProperty useAnimationsProperty() { return useAnimationsProperty; } public static boolean useAnimations() { return staticUseAnimations; } public BitcoinNetwork getBitcoinNetwork() { return bitcoinNetwork; } public ObservableList<FiatCurrency> getFiatCurrenciesAsObservable() { return fiatCurrenciesAsObservable; } public ObservableList<CryptoCurrency> getCryptoCurrenciesAsObservable() { return cryptoCurrenciesAsObservable; } public ObservableList<TradeCurrency> getTradeCurrenciesAsObservable() { return tradeCurrenciesAsObservable; } public BlockChainExplorer getBlockChainExplorerTestNet() { return blockChainExplorerTestNet; } public BlockChainExplorer getBlockChainExplorerMainNet() { return blockChainExplorerMainNet; } public BlockChainExplorer getBlockChainExplorer() { if (bitcoinNetwork == BitcoinNetwork.MAINNET) return blockChainExplorerMainNet; else return blockChainExplorerTestNet; } public ArrayList<BlockChainExplorer> getBlockChainExplorers() { if (bitcoinNetwork == BitcoinNetwork.MAINNET) return blockChainExplorersMainNet; else return blockChainExplorersTestNet; } public String getBackupDirectory() { return backupDirectory; } public boolean getAutoSelectArbitrators() { return autoSelectArbitrators; } public Map<String, Boolean> getDontShowAgainMap() { return dontShowAgainMap; } public boolean showAgain(String key) { return !dontShowAgainMap.containsKey(key) || !dontShowAgainMap.get(key); } public boolean getTacAccepted() { return tacAccepted; } public Locale getPreferredLocale() { return preferredLocale; } public TradeCurrency getPreferredTradeCurrency() { return preferredTradeCurrency; } public long getNonTradeTxFeePerKB() { return Math.max(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value, nonTradeTxFeePerKB); } public boolean getUseTorForBitcoinJ() { // We override the useTorForBitcoinJ and set to false if we have bitcoinNodes set // Atm we don't support onion addresses there // This check includes localhost, so we also override useTorForBitcoinJ if (bitcoinNodes != null && !bitcoinNodes.isEmpty()) return false; else return useTorForBitcoinJ; } public boolean getShowOwnOffersInOfferBook() { return showOwnOffersInOfferBook; } public double getMaxPriceDistanceInPercent() { return maxPriceDistanceInPercent; } public boolean getUseStickyMarketPrice() { return useStickyMarketPrice; } public boolean getUsePercentageBasedPrice() { return usePercentageBasedPrice; } public Map<String, String> getPeerTagMap() { return peerTagMap; } public String getOfferBookChartScreenCurrencyCode() { return offerBookChartScreenCurrencyCode; } public String getBuyScreenCurrencyCode() { return buyScreenCurrencyCode; } public String getSellScreenCurrencyCode() { return sellScreenCurrencyCode; } public List<String> getIgnoreTradersList() { return ignoreTradersList; } public String getDefaultPath() { return defaultPath; } public String getTradeChartsScreenCurrencyCode() { return tradeChartsScreenCurrencyCode; } public int getTradeStatisticsTickUnitIndex() { return tradeStatisticsTickUnitIndex; } public boolean getSortMarketCurrenciesNumerically() { return sortMarketCurrenciesNumerically; } public String getBitcoinNodes() { return bitcoinNodes; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void updateTradeCurrencies(ListChangeListener.Change<? extends TradeCurrency> change) { change.next(); if (change.wasAdded() && change.getAddedSize() == 1) tradeCurrenciesAsObservable.add(change.getAddedSubList().get(0)); else if (change.wasRemoved() && change.getRemovedSize() == 1) tradeCurrenciesAsObservable.remove(change.getRemoved().get(0)); } private void setFiatCurrencies(List<FiatCurrency> currencies) { fiatCurrenciesAsObservable.setAll(currencies); } private void setCryptoCurrencies(List<CryptoCurrency> currencies) { cryptoCurrenciesAsObservable.setAll(currencies); } private void setBlockChainExplorerTestNet(BlockChainExplorer blockChainExplorerTestNet) { this.blockChainExplorerTestNet = blockChainExplorerTestNet; storage.queueUpForSave(); } private void setBlockChainExplorerMainNet(BlockChainExplorer blockChainExplorerMainNet) { this.blockChainExplorerMainNet = blockChainExplorerMainNet; storage.queueUpForSave(); } }