From 165a1944c7a31471a2d95c53e3a17d39fdaf6ffa Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 22 Jan 2025 08:52:52 -0800 Subject: [PATCH] refactor: convert BlockchainService to Kotlin to address ANR's (#1336) * feat: show platform version on AboutActivity * chore: update to dashj 21.1.3-SNAPSHOT * Rename .java to .kt * refactor: convert BlockchainServiceImpl to Java * refactor: use serviceScope for onStartCommand, onDestroy, onReceive * fix: various fixes * fix: propogate context * refactor: update based on changes to dash-sdk-kotlin * fix: update copyright year to 2024 * fix: use DashSystem instead of Context * fix: wait for shutdown when starting again after blockchain rescan * refactor: simplify Injected fields * fix: update to v11.0.5 sync fix * Rename .java to .kt * fix: delete old files * fix: more proguard rules * fix: log date format attempted fix * fix: attempting to fix log date format * fix: add logback to proguard * chore: cleanup * fix: proper isComplete check for CrowdNode tx set * fix: change the way topup tx observed for CrowdNode * fix: build error - missing file * fix: update some function calls to include chainLockHeight * fix: make sure that onDestroy doesn't start before onCreate is finished. * chore: update dpp to v1.7.1-SNAPSHOT * fix: use the refactored context dashj --------- Co-authored-by: Andrei Ashikhmin --- build.gradle | 4 +- wallet/res/values-eo/strings.xml | 2 +- wallet/res/values-he/strings.xml | 2 +- wallet/res/values-hr/strings.xml | 2 +- wallet/res/values-iw/strings.xml | 2 +- wallet/res/values-mk/strings.xml | 2 +- wallet/res/values-sw/strings.xml | 2 +- .../schildbach/wallet/WalletApplication.java | 14 +- .../src/de/schildbach/wallet/di/AppModule.kt | 4 + .../wallet/service/BlockchainServiceImpl.java | 1592 ---------------- .../wallet/service/BlockchainServiceImpl.kt | 1616 +++++++++++++++++ .../service/BlockchainServiceImplOld.kt | 0 .../service/BlockchainStateDataProvider.kt | 12 +- .../wallet/service/CoinJoinService.kt | 11 +- .../wallet/service/DashSystemService.kt | 14 + .../platform/PlatformBroadcastService.kt | 4 +- .../wallet/ui/BlockListAdapter.java | 6 +- .../wallet/ui/DashPayUserActivity.kt | 1 + .../wallet/ui/DashPayUserActivityViewModel.kt | 8 +- .../schildbach/wallet/ui/ResetWalletDialog.kt | 5 +- .../wallet/ui/dashpay/NotificationsAdapter.kt | 4 +- .../ui/dashpay/NotificationsFragment.kt | 3 +- .../wallet/ui/dashpay/PlatformRepo.kt | 4 +- .../notification/NotificationsViewModel.kt | 8 +- .../notification/TransactionViewHolder.kt | 7 +- .../wallet/ui/main/MainViewModel.kt | 19 +- .../wallet/ui/more/SecurityFragment.kt | 10 +- .../wallet/ui/more/SecurityViewModel.kt | 4 +- .../transactions/TransactionGroupViewModel.kt | 7 +- .../ui/transactions/TransactionRowView.kt | 10 +- .../ui/transactions/TxResourceMapper.java | 4 +- .../ui/username/UsernameRequestsViewModel.kt | 4 +- 32 files changed, 1742 insertions(+), 1645 deletions(-) delete mode 100644 wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java create mode 100644 wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt create mode 100644 wallet/src/de/schildbach/wallet/service/BlockchainServiceImplOld.kt create mode 100644 wallet/src/de/schildbach/wallet/service/DashSystemService.kt diff --git a/build.gradle b/build.gradle index 99ae263464..c53653ba33 100644 --- a/build.gradle +++ b/build.gradle @@ -3,8 +3,8 @@ buildscript { kotlin_version = '1.9.24' coroutinesVersion = '1.6.4' ok_http_version = '4.9.1' - dashjVersion = '21.1.5-SNAPSHOT' - dppVersion = "1.7.0-SNAPSHOT" + dashjVersion = '21.1.5-RC-SNAPSHOT' + dppVersion = "1.7.1-SNAPSHOT" hiltVersion = '2.51' hiltCompilerVersion = '1.2.0' hiltWorkVersion = '1.0.0' diff --git a/wallet/res/values-eo/strings.xml b/wallet/res/values-eo/strings.xml index ad249cdac9..c85d69d6eb 100644 --- a/wallet/res/values-eo/strings.xml +++ b/wallet/res/values-eo/strings.xml @@ -7,7 +7,7 @@ %1$s, %2$d tagoj malantaŭ %1$s, %2$d semajnoj malantaŭ %1$s, %2$d monatoj malantaŭ - Sinkronigado kun reto + Sinkronigado kun reto (%1$s%%) Sinkronigo stagnis Sinkronigado: Stokante problemo Sinkronigado: Reta problemo diff --git a/wallet/res/values-he/strings.xml b/wallet/res/values-he/strings.xml index 0500bbb524..c959784782 100644 --- a/wallet/res/values-he/strings.xml +++ b/wallet/res/values-he/strings.xml @@ -7,7 +7,7 @@ %1$s, %2$d ימי איחור %1$s, %2$d שבועות איחור %1$s, %2$d חודשי איחור - מסנכרן רשת + (%1$s%%) מסנכרן רשת סינכרון עוקב סינכרון: בעיית אחסון סינכרון:בעיית רשת diff --git a/wallet/res/values-hr/strings.xml b/wallet/res/values-hr/strings.xml index 694c2b144d..38e5366dbb 100644 --- a/wallet/res/values-hr/strings.xml +++ b/wallet/res/values-hr/strings.xml @@ -5,7 +5,7 @@ %1$s, %2$d dani iza %1$s, %2$d tjedana iza %1$s, %2$d mjeseci iza - Sinkroniziranje sa mrežom + Sinkroniziranje sa mrežom (%1$s%%) Sinkronizacija u zastoju Sinkroniziranje: Spremišni problem Sinkroniziranje: Mrežni problem diff --git a/wallet/res/values-iw/strings.xml b/wallet/res/values-iw/strings.xml index 4351a56c8a..d22d28cbee 100644 --- a/wallet/res/values-iw/strings.xml +++ b/wallet/res/values-iw/strings.xml @@ -8,7 +8,7 @@ %1$s, %2$d ימי איחור %1$s, %2$d שבועות איחור %1$s, %2$d חודשי איחור - מסנכרן רשת + מסנכרן רשת (%1$s%%) סינכרון עוקב סינכרון: בעיית אחסון סינכרון:בעיית רשת diff --git a/wallet/res/values-mk/strings.xml b/wallet/res/values-mk/strings.xml index 5cc63ff4f0..c4249a5c49 100644 --- a/wallet/res/values-mk/strings.xml +++ b/wallet/res/values-mk/strings.xml @@ -7,7 +7,7 @@ %1$s, %2$d денови заостанува %1$s, %2$d недели заостанува %1$s, %2$d месеци заостанува - Се синхронизира со мрежата + Се синхронизира со мрежата (%1$s%%) Синхронизацијата застана Се синхронизира: Проблем со простор за складирање Се синхронизира: Проблем со мрежата diff --git a/wallet/res/values-sw/strings.xml b/wallet/res/values-sw/strings.xml index c900e20a31..b33d4b7fc4 100644 --- a/wallet/res/values-sw/strings.xml +++ b/wallet/res/values-sw/strings.xml @@ -7,7 +7,7 @@ siku %1$s, %2$d mwishoni mwa wiki %1$s, %2$d marehemu miezi %1$s, %2$d marehemu - Sambamba na mtandao + Sambamba na mtandao (%1$s%%) Sambamba kusimamishwa Haitoshi disc nafasi Hakuna signal wa mtandao diff --git a/wallet/src/de/schildbach/wallet/WalletApplication.java b/wallet/src/de/schildbach/wallet/WalletApplication.java index 2b2087049f..4a4e2611f3 100644 --- a/wallet/src/de/schildbach/wallet/WalletApplication.java +++ b/wallet/src/de/schildbach/wallet/WalletApplication.java @@ -61,6 +61,7 @@ import org.bitcoinj.core.TransactionBag; import org.bitcoinj.core.VerificationException; import org.bitcoinj.crypto.LinuxSecureRandom; +import org.bitcoinj.manager.DashSystem; import org.bitcoinj.utils.Threading; import org.bitcoinj.core.VersionMessage; import org.bitcoinj.crypto.IKey; @@ -91,6 +92,7 @@ import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy; import ch.qos.logback.core.util.FileSize; import de.schildbach.wallet.service.BlockchainStateDataProvider; +import de.schildbach.wallet.service.DashSystemService; import de.schildbach.wallet.service.PackageInfoProvider; import de.schildbach.wallet.service.WalletFactory; import de.schildbach.wallet.transactions.MasternodeObserver; @@ -173,7 +175,6 @@ public class WalletApplication extends MultiDexApplication private File walletFile; private Wallet wallet; - public static final String ACTION_WALLET_REFERENCE_CHANGED = WalletApplication.class.getPackage().getName() + ".wallet_reference_changed"; @@ -191,6 +192,7 @@ public class WalletApplication extends MultiDexApplication private AutoLogout autoLogout; private AnrSupervisor anrSupervisor; + private Function0 afterWipeFunction; @Inject RestartService restartService; @@ -212,6 +214,8 @@ public class WalletApplication extends MultiDexApplication PackageInfoProvider packageInfoProvider; @Inject WalletFactory walletFactory; + @Inject + DashSystemService dashSystemService; @Override protected void attachBaseContext(Context base) { @@ -433,7 +437,7 @@ public void saveWalletAndFinalizeInitialization() { } public void finalizeInitialization() { - wallet.getContext().initDash(true, true, Constants.SYNC_FLAGS, Constants.VERIFY_FLAGS); + dashSystemService.getSystem().initDash(true, true, Constants.SYNC_FLAGS, Constants.VERIFY_FLAGS); if (config.versionCodeCrossed(packageInfoProvider.getVersionCode(), VERSION_CODE_SHOW_BACKUP_REMINDER) && !wallet.getImportedKeys().isEmpty()) { @@ -992,8 +996,9 @@ else if (lastUsedAgo < Constants.LAST_USAGE_THRESHOLD_RECENTLY_MS) /** * Removes all the data and restarts the app showing onboarding screen. */ - public void triggerWipe() { + public void triggerWipe(Function0 afterWipeFunction) { log.info("Removing all the data and restarting the app."); + this.afterWipeFunction = afterWipeFunction; startService(new Intent(BlockchainService.ACTION_WIPE_WALLET, null, this, BlockchainServiceImpl.class)); } @@ -1036,6 +1041,9 @@ public void finalizeWipe() { // wallet must be null for the OnboardingActivity flow log.info("removing wallet from memory during wipe"); wallet = null; + if (afterWipeFunction != null) + afterWipeFunction.invoke(); + afterWipeFunction = null; } public AnalyticsService getAnalyticsService() { diff --git a/wallet/src/de/schildbach/wallet/di/AppModule.kt b/wallet/src/de/schildbach/wallet/di/AppModule.kt index 104e7abbe7..9c47c4b39c 100644 --- a/wallet/src/de/schildbach/wallet/di/AppModule.kt +++ b/wallet/src/de/schildbach/wallet/di/AppModule.kt @@ -157,4 +157,8 @@ abstract class AppModule { @Binds @Singleton abstract fun bindZenLedgerClient(zenLedgerClient: ZenLedgerClient): ZenLedgerApi + + @Singleton + @Binds + abstract fun provideDashSystemService(dashSystemService: DashSystemServiceImpl): DashSystemService } diff --git a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java deleted file mode 100644 index 1be19b11ab..0000000000 --- a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java +++ /dev/null @@ -1,1592 +0,0 @@ -/* - * Copyright 2011-2015 the original author or authors. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package de.schildbach.wallet.service; - -import android.annotation.SuppressLint; -import android.app.ForegroundServiceStartNotAllowedException; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.ComponentCallbacks2; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.content.pm.ServiceInfo; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; -import android.text.format.DateUtils; - -import androidx.annotation.RequiresApi; -import androidx.core.app.NotificationCompat; -import androidx.core.content.ContextCompat; -import androidx.lifecycle.LifecycleService; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import com.google.common.base.Stopwatch; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Block; -import org.bitcoinj.core.BlockChain; -import org.bitcoinj.core.CheckpointManager; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.FilteredBlock; -import org.bitcoinj.core.Peer; -import org.bitcoinj.core.PeerGroup; -import org.bitcoinj.core.Sha256Hash; -import org.bitcoinj.core.StoredBlock; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionConfidence.ConfidenceType; -import org.bitcoinj.core.Utils; -import org.bitcoinj.core.listeners.DownloadProgressTracker; -import org.bitcoinj.core.listeners.PeerConnectedEventListener; -import org.bitcoinj.core.listeners.PeerDisconnectedEventListener; -import org.bitcoinj.core.listeners.PreBlocksDownloadListener; -import org.bitcoinj.evolution.AssetLockTransaction; -import org.bitcoinj.evolution.SimplifiedMasternodeList; -import org.bitcoinj.evolution.SimplifiedMasternodeListDiff; -import org.bitcoinj.evolution.SimplifiedMasternodeListManager; -import org.bitcoinj.net.discovery.DnsDiscovery; -import org.bitcoinj.net.discovery.MasternodePeerDiscovery; -import org.bitcoinj.net.discovery.MultiplexingDiscovery; -import org.bitcoinj.net.discovery.PeerDiscovery; -import org.bitcoinj.net.discovery.PeerDiscoveryException; -import org.bitcoinj.net.discovery.SeedPeers; -import org.bitcoinj.store.BlockStore; -import org.bitcoinj.store.BlockStoreException; -import org.bitcoinj.store.SPVBlockStore; -import org.bitcoinj.utils.ExchangeRate; -import org.bitcoinj.utils.MonetaryFormat; -import org.bitcoinj.utils.Threading; -import org.bitcoinj.wallet.DefaultRiskAnalysis; -import org.bitcoinj.wallet.Wallet; -import org.bitcoinj.wallet.WalletEx; -import org.bitcoinj.wallet.authentication.AuthenticationGroupExtension; -import org.dash.wallet.common.Configuration; -import org.dash.wallet.common.data.WalletUIConfig; -import org.dash.wallet.common.data.NetworkStatus; -import org.dash.wallet.common.services.TransactionMetadataProvider; -import org.dash.wallet.common.services.NotificationService; -import org.dash.wallet.common.transactions.filters.NotFromAddressTxFilter; -import org.dash.wallet.common.transactions.filters.TransactionFilter; -import org.dash.wallet.common.transactions.TransactionUtils; -import org.dash.wallet.common.util.FlowExtKt; -import org.dash.wallet.common.util.MonetaryExtKt; -import org.dash.wallet.integrations.crowdnode.api.CrowdNodeAPIConfirmationHandler; -import org.dash.wallet.integrations.crowdnode.api.CrowdNodeBlockchainApi; -import org.dash.wallet.integrations.crowdnode.transactions.CrowdNodeDepositReceivedResponse; -import org.dash.wallet.integrations.crowdnode.transactions.CrowdNodeWithdrawalReceivedTx; -import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConfig; -import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.InetSocketAddress; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.EnumSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -import javax.annotation.Nullable; -import javax.inject.Inject; - -import dagger.hilt.android.AndroidEntryPoint; -import de.schildbach.wallet.Constants; -import de.schildbach.wallet.WalletApplication; -import de.schildbach.wallet.WalletApplicationExt; -import de.schildbach.wallet.WalletBalanceWidgetProvider; -import de.schildbach.wallet.data.AddressBookProvider; -import org.dash.wallet.common.data.entity.BlockchainState; -import de.schildbach.wallet.database.dao.BlockchainStateDao; -import de.schildbach.wallet.database.dao.ExchangeRatesDao; -import de.schildbach.wallet.service.platform.PlatformSyncService; -import de.schildbach.wallet.ui.OnboardingActivity; -import de.schildbach.wallet.ui.dashpay.OnPreBlockProgressListener; -import de.schildbach.wallet.ui.dashpay.PlatformRepo; -import de.schildbach.wallet.ui.dashpay.PreBlockStage; -import de.schildbach.wallet.ui.staking.StakingActivity; -import de.schildbach.wallet.util.AllowLockTimeRiskAnalysis; -import de.schildbach.wallet.util.BlockchainStateUtils; -import de.schildbach.wallet.util.CrashReporter; -import de.schildbach.wallet.util.ThrottlingWalletChangeListener; -import de.schildbach.wallet_test.R; - -import static org.dash.wallet.common.util.Constants.PREFIX_ALMOST_EQUAL_TO; - -/** - * @author Andreas Schildbach - */ -@AndroidEntryPoint -public class BlockchainServiceImpl extends LifecycleService implements BlockchainService { - - @Inject WalletApplication application; - @Inject Configuration config; - @Inject WalletUIConfig walletUIConfig; - @Inject NotificationService notificationService; - @Inject CrowdNodeBlockchainApi crowdNodeBlockchainApi; - @Inject CrowdNodeConfig crowdNodeConfig; - @Inject BlockchainStateDao blockchainStateDao; - @Inject ExchangeRatesDao exchangeRatesDao; - @Inject TransactionMetadataProvider transactionMetadataProvider; - @Inject PlatformSyncService platformSyncService; - @Inject PlatformRepo platformRepo; - @Inject PackageInfoProvider packageInfoProvider; - @Inject ConnectivityManager connectivityManager; - @Inject BlockchainStateDataProvider blockchainStateDataProvider; - @Inject CoinJoinService coinJoinService; // not used in this class, but we need to create it - private BlockStore blockStore; - private BlockStore headerStore; - private File blockChainFile; - private File headerChainFile; - private BlockChain blockChain; - private BlockChain headerChain; - private InputStream mnlistinfoBootStrapStream; - private InputStream qrinfoBootStrapStream; - @Nullable - private PeerGroup peerGroup; - - private final Handler handler = new Handler(); - private final Handler delayHandler = new Handler(); - private final Handler metadataHandler = new Handler(); - private WakeLock wakeLock; - - private PeerConnectivityListener peerConnectivityListener; - private NotificationManager nm; - private final Set impediments = EnumSet.noneOf(BlockchainState.Impediment.class); - private BlockchainState blockchainState = new BlockchainState(null, 0, false, impediments, 0, 0, 0); - private int notificationCount = 0; - private Coin notificationAccumulatedAmount = Coin.ZERO; - private final List
notificationAddresses = new LinkedList<>(); - private AtomicInteger transactionsReceived = new AtomicInteger(); - private AtomicInteger mnListDiffsReceived = new AtomicInteger(); - private long serviceCreatedAt; - private boolean resetBlockchainOnShutdown = false; - private boolean deleteWalletFileOnShutdown = false; - - //Settings to bypass dashj default dns seeds - private final SeedPeers seedPeerDiscovery = new SeedPeers(Constants.NETWORK_PARAMETERS); - private final DnsDiscovery dnsDiscovery = new DnsDiscovery(Constants.DNS_SEED, Constants.NETWORK_PARAMETERS); - ArrayList peerDiscoveryList = new ArrayList<>(2); - private final static int MINIMUM_PEER_COUNT = 16; - - private static final int MIN_COLLECT_HISTORY = 2; - private static final int IDLE_HEADER_TIMEOUT_MIN = 2; - private static final int IDLE_MNLIST_TIMEOUT_MIN = 2; - private static final int IDLE_BLOCK_TIMEOUT_MIN = 2; - private static final int IDLE_TRANSACTION_TIMEOUT_MIN = 9; - private static final int MAX_HISTORY_SIZE = Math.max(IDLE_TRANSACTION_TIMEOUT_MIN, IDLE_BLOCK_TIMEOUT_MIN); - private static final long APPWIDGET_THROTTLE_MS = DateUtils.SECOND_IN_MILLIS; - private static final long BLOCKCHAIN_STATE_BROADCAST_THROTTLE_MS = DateUtils.SECOND_IN_MILLIS; - private static final long TX_EXCHANGE_RATE_TIME_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(180); - - private static final Logger log = LoggerFactory.getLogger(BlockchainServiceImpl.class); - - public static final String START_AS_FOREGROUND_EXTRA = "start_as_foreground"; - - private final Executor executor = Executors.newSingleThreadExecutor(); - private int syncPercentage = 0; // 0 to 100% - private MixingStatus mixingStatus = MixingStatus.NOT_STARTED; - private double mixingProgress = 0.0; - private ForegroundService foregroundService = ForegroundService.NONE; - - // Risk Analyser for Transactions that is PeerGroup Aware - AllowLockTimeRiskAnalysis.Analyzer riskAnalyzer; - DefaultRiskAnalysis.Analyzer defaultRiskAnalyzer = DefaultRiskAnalysis.FACTORY; - - private final List crowdnodeFilters = Arrays.asList( - new NotFromAddressTxFilter(CrowdNodeConstants.INSTANCE.getCrowdNodeAddress(Constants.NETWORK_PARAMETERS)), - new CrowdNodeWithdrawalReceivedTx(Constants.NETWORK_PARAMETERS) - ); - - private final CrowdNodeDepositReceivedResponse depositReceivedResponse = - new CrowdNodeDepositReceivedResponse(Constants.NETWORK_PARAMETERS); - - private CrowdNodeAPIConfirmationHandler apiConfirmationHandler; - - void handleMetadata(Transaction tx) { - metadataHandler.post(() -> { - transactionMetadataProvider.syncTransactionBlocking(tx); - }); - } - - private final ThrottlingWalletChangeListener walletEventListener = new ThrottlingWalletChangeListener( - APPWIDGET_THROTTLE_MS) { - - @Override - public void onThrottledWalletChanged() { - updateAppWidget(); - } - - @Override - public void onCoinsReceived(final Wallet wallet, final Transaction tx, final Coin prevBalance, - final Coin newBalance) { - - final int bestChainHeight = blockChain.getBestChainHeight(); - final boolean replaying = bestChainHeight < config.getBestChainHeightEver() || config.isRestoringBackup(); - - long now = new Date().getTime(); - long blockChainHeadTime = blockChain.getChainHead().getHeader().getTime().getTime(); - boolean insideTxExchangeRateTimeThreshold = (now - blockChainHeadTime) < TX_EXCHANGE_RATE_TIME_THRESHOLD_MS; - - log.info("onCoinsReceived: {}; rate: {}; replaying: {}; inside: {}, confid: {}; will update {}", - tx.getTxId(), tx.getExchangeRate(), replaying, insideTxExchangeRateTimeThreshold, tx.getConfidence().getConfidenceType(), - tx.getExchangeRate() == null && ((!replaying || insideTxExchangeRateTimeThreshold) || tx.getConfidence().getConfidenceType() == ConfidenceType.PENDING)); - - // only set an exchange rate if the tx has no exchange rate and: - // 1. the blockchain is not being rescanned nor the wallet is being restored OR - // 2. the transaction is less than three hours old OR - // 3. the transaction is not yet mined - if (tx.getExchangeRate() == null && (!replaying - || insideTxExchangeRateTimeThreshold - || tx.getConfidence().getConfidenceType() == ConfidenceType.PENDING)) { - try { - final org.dash.wallet.common.data.entity.ExchangeRate exchangeRate = - exchangeRatesDao.getRateSync(walletUIConfig.getExchangeCurrencyCodeBlocking()); - if (exchangeRate != null) { - log.info("Setting exchange rate on received transaction. Rate: " + exchangeRate + " tx: " + tx.getTxId().toString()); - tx.setExchangeRate(new ExchangeRate(Coin.COIN, exchangeRate.getFiat())); - application.saveWallet(); - } - } catch (Exception e) { - log.error("Failed to get exchange rate", e); - } - } - - transactionsReceived.incrementAndGet(); - - final Address address = TransactionUtils.INSTANCE.getWalletAddressOfReceived(tx, wallet); - final Coin amount = tx.getValue(wallet); - final ConfidenceType confidenceType = tx.getConfidence().getConfidenceType(); - final boolean isRestoringBackup = application.getConfiguration().isRestoringBackup(); - - handler.post(() -> { - final boolean isReplayedTx = confidenceType == ConfidenceType.BUILDING && (replaying || isRestoringBackup); - - if (!isReplayedTx) { - if (depositReceivedResponse.matches(tx)) { - notificationService.showNotification( - "deposit_received", - getString(R.string.crowdnode_deposit_received), - null, - null, - new Intent(BlockchainServiceImpl.this, StakingActivity.class), - null - ); - } else if (apiConfirmationHandler != null && apiConfirmationHandler.matches(tx)) { - apiConfirmationHandler.handle(tx); - } else if (passFilters(tx, wallet)) { - notifyCoinsReceived(address, amount, tx.getExchangeRate()); - } - } - }); - - handleMetadata(tx); - updateAppWidget(); - } - - @Override - public void onCoinsSent(final Wallet wallet, final Transaction tx, final Coin prevBalance, - final Coin newBalance) { - transactionsReceived.incrementAndGet(); - - log.info("onCoinsSent: {}", tx.getTxId()); - - - if(AssetLockTransaction.isAssetLockTransaction(tx) && tx.getPurpose() == Transaction.Purpose.UNKNOWN) { - // Handle credit function transactions (username creation, topup, invites) - AuthenticationGroupExtension authExtension = - (AuthenticationGroupExtension) wallet.getKeyChainExtension(AuthenticationGroupExtension.EXTENSION_ID); - AssetLockTransaction cftx = authExtension.getAssetLockTransaction(tx); - - long blockChainHeadTime = blockChain.getChainHead().getHeader().getTime().getTime(); - platformRepo.handleSentAssetLockTransaction(cftx, blockChainHeadTime); - - // TODO: if we detect a username creation that we haven't processed, should we? - } - - handleMetadata(tx); - updateAppWidget(); - } - - private Boolean passFilters(final Transaction tx, final Wallet wallet) { - Coin amount = tx.getValue(wallet); - final boolean isReceived = amount.signum() > 0; - - if (!isReceived) { - return false; - } - - boolean passFilters = false; - - for (TransactionFilter filter: crowdnodeFilters) { - if (filter.matches(tx)) { - passFilters = true; - break; - } - } - - return passFilters; - } - }; - - private final OnSharedPreferenceChangeListener sharedPrefsChangeListener = (sharedPreferences, key) -> { - if (key.equals(Configuration.PREFS_KEY_CROWDNODE_PRIMARY_ADDRESS)) { - registerCrowdNodeConfirmedAddressFilter(); - } - }; - - private boolean resetMNListsOnPeerGroupStart = false; - - private void notifyCoinsReceived(@Nullable final Address address, final Coin amount, - @Nullable ExchangeRate exchangeRate) { - if (notificationCount == 1) - nm.cancel(Constants.NOTIFICATION_ID_COINS_RECEIVED); - - notificationCount++; - notificationAccumulatedAmount = notificationAccumulatedAmount.add(amount); - if (address != null && !notificationAddresses.contains(address)) - notificationAddresses.add(address); - - final MonetaryFormat btcFormat = config.getFormat(); - - final String packageFlavor = packageInfoProvider.applicationPackageFlavor(); - String msgSuffix = packageFlavor != null ? " [" + packageFlavor + "]" : ""; - - if (exchangeRate != null) { - MonetaryFormat format = Constants.LOCAL_FORMAT.code(0, - PREFIX_ALMOST_EQUAL_TO + exchangeRate.fiat.getCurrencyCode()); - msgSuffix += " " + format.format(exchangeRate.coinToFiat(notificationAccumulatedAmount)); - } - - final String tickerMsg = getString(R.string.notification_coins_received_msg, btcFormat.format(amount)) - + msgSuffix; - final String msg = getString(R.string.notification_coins_received_msg, - btcFormat.format(notificationAccumulatedAmount)) + msgSuffix; - - final StringBuilder text = new StringBuilder(); - for (final Address notificationAddress : notificationAddresses) { - if (text.length() > 0) - text.append(", "); - - final String addressStr = notificationAddress.toString(); - final String label = AddressBookProvider.resolveLabel(getApplicationContext(), addressStr); - text.append(label != null ? label : addressStr); - } - - final NotificationCompat.Builder notification = new NotificationCompat.Builder(this, - Constants.NOTIFICATION_CHANNEL_ID_TRANSACTIONS); - notification.setSmallIcon(R.drawable.ic_dash_d_white); - notification.setTicker(tickerMsg); - notification.setContentTitle(msg); - if (text.length() > 0) - notification.setContentText(text); - notification.setContentIntent(PendingIntent.getActivity(this, 0, OnboardingActivity.createIntent(this), PendingIntent.FLAG_IMMUTABLE)); - notification.setNumber(notificationCount == 1 ? 0 : notificationCount); - notification.setWhen(System.currentTimeMillis()); - notification.setSound(Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.coins_received)); - nm.notify(Constants.NOTIFICATION_ID_COINS_RECEIVED, notification.build()); - } - - private final class PeerConnectivityListener - implements PeerConnectedEventListener, PeerDisconnectedEventListener, OnSharedPreferenceChangeListener { - private int peerCount; - private AtomicBoolean stopped = new AtomicBoolean(false); - - public PeerConnectivityListener() { - config.registerOnSharedPreferenceChangeListener(this); - } - - public void stop() { - stopped.set(true); - - config.unregisterOnSharedPreferenceChangeListener(this); - - nm.cancel(Constants.NOTIFICATION_ID_CONNECTED); - } - - @Override - public void onPeerConnected(final Peer peer, final int peerCount) { - this.peerCount = peerCount; - changed(peerCount); - } - - @Override - public void onPeerDisconnected(final Peer peer, final int peerCount) { - this.peerCount = peerCount; - changed(peerCount); - } - - @Override - public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) { - if (Configuration.PREFS_KEY_CONNECTIVITY_NOTIFICATION.equals(key)) - changed(peerCount); - } - - private void changed(final int numPeers) { - if (stopped.get()) - return; - NetworkStatus networkStatus = blockchainStateDataProvider.getNetworkStatus(); - if (numPeers > 0 && networkStatus == NetworkStatus.CONNECTING) - blockchainStateDataProvider.setNetworkStatus(NetworkStatus.CONNECTED); - else if (numPeers == 0 && networkStatus == NetworkStatus.DISCONNECTING) - blockchainStateDataProvider.setNetworkStatus(NetworkStatus.DISCONNECTED); - - handler.post(() -> { - final boolean connectivityNotificationEnabled = config.getConnectivityNotificationEnabled(); - - if (!connectivityNotificationEnabled || numPeers == 0) { - nm.cancel(Constants.NOTIFICATION_ID_CONNECTED); - } else { - final Notification.Builder notification = new Notification.Builder(BlockchainServiceImpl.this); - notification.setSmallIcon(R.drawable.stat_sys_peers, numPeers > 4 ? 4 : numPeers); - notification.setContentTitle(getString(R.string.app_name)); - notification.setContentText(getString(R.string.notification_peers_connected_msg, numPeers)); - notification.setContentIntent(PendingIntent.getActivity(BlockchainServiceImpl.this, 0, - OnboardingActivity.createIntent(BlockchainServiceImpl.this), PendingIntent.FLAG_IMMUTABLE)); - notification.setWhen(System.currentTimeMillis()); - notification.setOngoing(true); - nm.notify(Constants.NOTIFICATION_ID_CONNECTED, notification.build()); - } - - // send broadcast - broadcastPeerState(numPeers); - }); - } - } - - private abstract class MyDownloadProgressTracker extends DownloadProgressTracker implements OnPreBlockProgressListener { } - - private final MyDownloadProgressTracker blockchainDownloadListener = new MyDownloadProgressTracker() { - private final AtomicLong lastMessageTime = new AtomicLong(0); - private long throttleDelay = -1; - - @Override - public void onBlocksDownloaded(final Peer peer, final Block block, final FilteredBlock filteredBlock, - final int blocksLeft) { - super.onBlocksDownloaded(peer, block, filteredBlock, blocksLeft); - postOrPostDelayed(); - } - - @Override - public void onHeadersDownloaded(final Peer peer, final Block block, - final int blocksLeft) { - super.onHeadersDownloaded(peer, block, blocksLeft); - postOrPostDelayed(); - } - - private final Runnable runnable = new Runnable() { - @Override - public void run() { - lastMessageTime.set(System.currentTimeMillis()); - log.debug("Runnable % = " + syncPercentage); - - config.maybeIncrementBestChainHeightEver(blockChain.getChainHead().getHeight()); - config.maybeIncrementBestHeaderHeightEver(headerChain.getChainHead().getHeight()); - if(config.isRestoringBackup()) { - long timeAgo = System.currentTimeMillis() - blockChain.getChainHead().getHeader().getTimeSeconds() * 1000; - //if the app was restoring a backup from a file or seed and block chain is nearly synced - //then turn off the restoring indicator - if(timeAgo < DateUtils.DAY_IN_MILLIS) - config.setRestoringBackup(false); - } - // this method is always called after progress or doneDownload - updateBlockchainState(); - } - }; - - /* - This method is called by super.onBlocksDownloaded when the percentage - of the chain downloaded is 0.0, 1.0, 2.0, 3.0 .. 99.0% (whole numbers) - - The pct value is relative to the blocks that need to be downloaded to sync, - rather than the relative to the entire blockchain. - */ - @Override - protected void progress(double pct, int blocksLeft, Date date) { - super.progress(pct, blocksLeft, date); - syncPercentage = pct > 0.0 ? (int)pct : 0; - log.info("progress {}", syncPercentage); - if (syncPercentage > 100) { - syncPercentage = 100; - } - } - - /* - This method is called by super.onBlocksDownloaded when the percentage - of the chain downloaded is 100.0% (completely done) - */ - @Override - protected void doneDownload() { - super.doneDownload(); - log.info("DoneDownload {}", syncPercentage); - // if the chain is already synced from a previous session, then syncPercentage = 0 - // set to 100% so that observers will see that sync is completed - syncPercentage = 100; - updateBlockchainState(); - } - - @Override - public void onMasterNodeListDiffDownloaded(Stage stage, @Nullable SimplifiedMasternodeListDiff mnlistdiff) { - log.info("masternodeListDiffDownloaded:" + stage); - if(peerGroup != null && peerGroup.getSyncStage() == PeerGroup.SyncStage.MNLIST) { - super.onMasterNodeListDiffDownloaded(stage, mnlistdiff); - startPreBlockPercent = syncPercentage; - mnListDiffsReceived.incrementAndGet(); - postOrPostDelayed(); - } - } - - private void postOrPostDelayed() { - delayHandler.removeCallbacksAndMessages(null); - if (throttleDelay == -1) { - throttleDelay = application.isLowRamDevice() ? BLOCKCHAIN_STATE_BROADCAST_THROTTLE_MS : BLOCKCHAIN_STATE_BROADCAST_THROTTLE_MS / 4; - } - final long now = System.currentTimeMillis(); - if (now - lastMessageTime.get() > throttleDelay) { - delayHandler.post(runnable); - } else { - delayHandler.postDelayed(runnable, throttleDelay); - } - } - - int totalPreblockStages = PreBlockStage.UpdateTotal.getValue(); - int startPreBlockPercent = 0; - PreBlockStage lastPreBlockStage = PreBlockStage.None; - - @Override - public void onPreBlockProgressUpdated(PreBlockStage stage) { - if (stage == PreBlockStage.Starting && lastPreBlockStage == PreBlockStage.None) { - startPreBlockPercent = syncPercentage; - } - if (preBlocksWeight > 0.99) { - startPreBlockPercent = 0; - } - if (stage == PreBlockStage.StartRecovery && lastPreBlockStage == PreBlockStage.None) { - startPreBlockPercent = syncPercentage; - if (preBlocksWeight <= 0.10) - setPreBlocksWeight(0.20); - } - double increment = preBlocksWeight * stage.getValue() * 100.0 / PreBlockStage.Complete.getValue(); - if (increment > preBlocksWeight * 100) - increment = preBlocksWeight * 100; - - log.debug("PreBlockDownload: " + increment + "%..." + preBlocksWeight + " " + stage.name() + " " + peerGroup.getSyncStage().name()); - if (peerGroup != null && peerGroup.getSyncStage() == PeerGroup.SyncStage.PREBLOCKS) { - syncPercentage = (int)(startPreBlockPercent + increment); - log.info("PreBlockDownload: " + syncPercentage + "%..." + peerGroup.getSyncStage().name()); - postOrPostDelayed(); - } - lastPreBlockStage = stage; - } - }; - - private final BroadcastReceiver connectivityReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final String action = intent.getAction(); - - if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) { - final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); - final boolean hasConnectivity = networkInfo != null && networkInfo.isConnected(); - - if (log.isInfoEnabled()) { - final StringBuilder s = new StringBuilder("active network is ") - .append(hasConnectivity ? "up" : "down"); - if (networkInfo != null) { - s.append(", type: ").append(networkInfo.getTypeName()); - s.append(", state: ").append(networkInfo.getState()).append('/') - .append(networkInfo.getDetailedState()); - final String extraInfo = networkInfo.getExtraInfo(); - if (extraInfo != null) - s.append(", extraInfo: ").append(extraInfo); - final String reason = networkInfo.getReason(); - if (reason != null) - s.append(", reason: ").append(reason); - } - log.info(s.toString()); - } - - if (hasConnectivity) { - impediments.remove(BlockchainState.Impediment.NETWORK); - } else { - impediments.add(BlockchainState.Impediment.NETWORK); - } - - updateBlockchainStateImpediments(); - check(); - } else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { - log.info("device storage low"); - impediments.add(BlockchainState.Impediment.STORAGE); - updateBlockchainStateImpediments(); - check(); - } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { - log.info("device storage ok"); - - impediments.remove(BlockchainState.Impediment.STORAGE); - updateBlockchainStateImpediments(); - check(); - } - } - - @SuppressLint("Wakelock") - private void check() { - final Wallet wallet = application.getWallet(); - - if (impediments.isEmpty() && peerGroup == null) { - log.debug("acquiring wakelock"); - wakeLock.acquire(); - - // consistency check - final int walletLastBlockSeenHeight = wallet.getLastBlockSeenHeight(); - final int bestChainHeight = blockChain.getBestChainHeight(); - if (walletLastBlockSeenHeight != -1 && walletLastBlockSeenHeight != bestChainHeight) { - final String message = "wallet/blockchain out of sync: " + walletLastBlockSeenHeight + "/" - + bestChainHeight; - log.error(message); - CrashReporter.saveBackgroundTrace(new RuntimeException(message), packageInfoProvider.getPackageInfo()); - } - - wallet.getContext().initDashSync(getDir("masternode", MODE_PRIVATE).getAbsolutePath()); - - log.info("starting peergroup"); - peerGroup = new PeerGroup(Constants.NETWORK_PARAMETERS, blockChain, headerChain); - if (Constants.SUPPORTS_PLATFORM) { - platformRepo.getPlatform().setMasternodeListManager(application.getWallet().getContext().masternodeListManager); - platformSyncService.resume(); - } - if (resetMNListsOnPeerGroupStart) { - resetMNListsOnPeerGroupStart = false; - application.getWallet().getContext().masternodeListManager.setBootstrap(mnlistinfoBootStrapStream, qrinfoBootStrapStream, SimplifiedMasternodeListManager.SMLE_VERSION_FORMAT_VERSION); - resetMNLists(true); - } - - peerGroup.setDownloadTxDependencies(0); // recursive implementation causes StackOverflowError - peerGroup.addWallet(wallet); - peerGroup.setUserAgent(Constants.USER_AGENT, packageInfoProvider.getVersionName()); - peerGroup.addConnectedEventListener(peerConnectivityListener); - peerGroup.addDisconnectedEventListener(peerConnectivityListener); - - final int maxConnectedPeers = application.maxConnectedPeers(); - - final String trustedPeerHost = config.getTrustedPeerHost(); - final boolean hasTrustedPeer = trustedPeerHost != null; - - final boolean connectTrustedPeerOnly = hasTrustedPeer && config.getTrustedPeerOnly(); - peerGroup.setMaxConnections(connectTrustedPeerOnly ? 1 : maxConnectedPeers); - peerGroup.setConnectTimeoutMillis(Constants.PEER_TIMEOUT_MS); - peerGroup.setPeerDiscoveryTimeoutMillis(Constants.PEER_DISCOVERY_TIMEOUT_MS); - - peerGroup.addPeerDiscovery(new PeerDiscovery() { - //Keep Original code here for now - //private final PeerDiscovery normalPeerDiscovery = MultiplexingDiscovery - // .forServices(Constants.NETWORK_PARAMETERS, 0); - private final PeerDiscovery normalPeerDiscovery = new MultiplexingDiscovery(Constants.NETWORK_PARAMETERS, peerDiscoveryList); - - - @Override - public InetSocketAddress[] getPeers(final long services, final long timeoutValue, - final TimeUnit timeoutUnit) throws PeerDiscoveryException { - final List peers = new LinkedList(); - - boolean needsTrimPeersWorkaround = false; - - if (hasTrustedPeer) { - log.info( - "trusted peer '" + trustedPeerHost + "'" + (connectTrustedPeerOnly ? " only" : "")); - - final InetSocketAddress addr = new InetSocketAddress(trustedPeerHost, - Constants.NETWORK_PARAMETERS.getPort()); - if (addr.getAddress() != null) { - peers.add(addr); - needsTrimPeersWorkaround = true; - } - } - - if (!connectTrustedPeerOnly) { - // First use the masternode list that is included - try { - SimplifiedMasternodeList mnlist = org.bitcoinj.core.Context.get().masternodeListManager.getListAtChainTip(); - MasternodePeerDiscovery discovery = new MasternodePeerDiscovery(mnlist); - peers.addAll(Arrays.asList(discovery.getPeers(services, timeoutValue, timeoutUnit))); - } catch (PeerDiscoveryException x) { - //swallow and continue with another method of connection - log.info("DMN List peer discovery failed: "+ x.getMessage()); - } - - // default masternode list - if(peers.size() < MINIMUM_PEER_COUNT) { - String [] defaultMNList = Constants.NETWORK_PARAMETERS.getDefaultMasternodeList(); - if (defaultMNList != null || defaultMNList.length != 0) { - log.info("DMN peer discovery returned less than 16 nodes. Adding default DMN peers to the list to increase connections"); - MasternodePeerDiscovery discovery = new MasternodePeerDiscovery(defaultMNList, Constants.NETWORK_PARAMETERS.getPort()); - peers.addAll(Arrays.asList(discovery.getPeers(services, timeoutValue, timeoutUnit))); - - // use EvoNodes if the network is small - if (peers.size() < MINIMUM_PEER_COUNT) { - String [] defaultEvoNodeList = Constants.NETWORK_PARAMETERS.getDefaultHPMasternodeList(); - MasternodePeerDiscovery discoveryEvo = new MasternodePeerDiscovery(defaultEvoNodeList, Constants.NETWORK_PARAMETERS.getPort()); - peers.addAll(Arrays.asList(discoveryEvo.getPeers(services, timeoutValue, timeoutUnit))); - } - } else { - log.info("DNS peer discovery returned less than 16 nodes. Unable to add seed peers (it is not specified for this network)."); - } - } - - // seed nodes - if(peers.size() < MINIMUM_PEER_COUNT) { - if (Constants.NETWORK_PARAMETERS.getAddrSeeds() != null) { - log.info("Static DMN peer discovery returned less than 16 nodes. Adding seed peers to the list to increase connections"); - peers.addAll(Arrays.asList(seedPeerDiscovery.getPeers(services, timeoutValue, timeoutUnit))); - } else { - log.info("DNS peer discovery returned less than 16 nodes. Unable to add seed peers (it is not specified for this network)."); - } - } - - if(peers.size() < MINIMUM_PEER_COUNT) { - log.info("Masternode peer discovery returned less than 16 nodes. Adding DMN peers to the list to increase connections"); - - try { - peers.addAll( - Arrays.asList(normalPeerDiscovery.getPeers(services, timeoutValue, timeoutUnit))); - } catch (PeerDiscoveryException x) { - //swallow and continue with another method of connection, if one exists. - log.info("DNS peer discovery failed: "+ x.getMessage()); - if(x.getCause() != null) - log.info( "cause: " + x.getCause().getMessage()); - } - } - } - - // workaround because PeerGroup will shuffle peers - if (needsTrimPeersWorkaround) - while (peers.size() >= maxConnectedPeers) - peers.remove(peers.size() - 1); - - return peers.toArray(new InetSocketAddress[0]); - } - - @Override - public void shutdown() { - normalPeerDiscovery.shutdown(); - } - }); - - peerGroup.addPreBlocksDownloadListener(executor, preBlocksDownloadListener); - // Use our custom risk analysis that allows v2 tx with absolute LockTime - riskAnalyzer = new AllowLockTimeRiskAnalysis.Analyzer(peerGroup); - wallet.setRiskAnalyzer(riskAnalyzer); - - // start peergroup - blockchainStateDataProvider.setNetworkStatus(NetworkStatus.CONNECTING); - peerGroup.startAsync(); - peerGroup.startBlockChainDownload(blockchainDownloadListener); - platformSyncService.addPreBlockProgressListener(blockchainDownloadListener); - - } else if (!impediments.isEmpty() && peerGroup != null) { - blockchainStateDataProvider.setNetworkStatus(NetworkStatus.NOT_AVAILABLE); - application.getWallet().getContext().close(); - log.info("stopping peergroup"); - peerGroup.removeDisconnectedEventListener(peerConnectivityListener); - peerGroup.removeConnectedEventListener(peerConnectivityListener); - peerGroup.removePreBlocksDownloadedListener(preBlocksDownloadListener); - peerGroup.removeWallet(wallet); - platformSyncService.removePreBlockProgressListener(blockchainDownloadListener); - peerGroup.stopAsync(); - // use the offline risk analyzer - wallet.setRiskAnalyzer(new AllowLockTimeRiskAnalysis.OfflineAnalyzer(config.getBestHeightEver(), System.currentTimeMillis()/1000)); - riskAnalyzer.shutdown(); - - peerGroup = null; - - log.debug("releasing wakelock"); - wakeLock.release(); - } - } - }; - - private final static class ActivityHistoryEntry { - public final int numTransactionsReceived; - public final int numBlocksDownloaded; - public final int numHeadersDownloaded; - public final int numMnListDiffsDownloaded; - - public ActivityHistoryEntry(final int numTransactionsReceived, final int numBlocksDownloaded, - final int numHeadersDownloaded, final int numMnListDiffsDownloaded) { - this.numTransactionsReceived = numTransactionsReceived; - this.numBlocksDownloaded = numBlocksDownloaded; - this.numHeadersDownloaded = numHeadersDownloaded; - this.numMnListDiffsDownloaded = numMnListDiffsDownloaded; - } - - @Override - public String toString() { - return numTransactionsReceived + "/" + numBlocksDownloaded + "/" + numHeadersDownloaded + "/" + numMnListDiffsDownloaded; - } - } - - private final BroadcastReceiver tickReceiver = new BroadcastReceiver() { - private int lastChainHeight = 0; - private int lastHeaderHeight = 0; - private final List activityHistory = new LinkedList(); - - @Override - public void onReceive(final Context context, final Intent intent) { - final int chainHeight = blockChain.getBestChainHeight(); - final int headerHeight = headerChain.getBestChainHeight(); - - if (lastChainHeight > 0 || lastHeaderHeight > 0) { - final int numBlocksDownloaded = chainHeight - lastChainHeight; - final int numTransactionsReceived = transactionsReceived.getAndSet(0); - // instead of counting headers, count header messages which contain up to 2000 headers - final int numHeadersDownloaded = headerHeight - lastHeaderHeight; - final int numMnListDiffsDownloaded = mnListDiffsReceived.getAndSet(0); - - // push history - activityHistory.add(0, new ActivityHistoryEntry(numTransactionsReceived, numBlocksDownloaded, numHeadersDownloaded, numMnListDiffsDownloaded)); - - // trim - while (activityHistory.size() > MAX_HISTORY_SIZE) - activityHistory.remove(activityHistory.size() - 1); - - // print - final StringBuilder builder = new StringBuilder(); - for (final ActivityHistoryEntry entry : activityHistory) { - if (builder.length() > 0) - builder.append(", "); - builder.append(entry); - } - log.info("History of transactions/blocks/headers/mnlistdiff: " + - (mixingStatus == MixingStatus.MIXING ? "[mixing] " : "") + builder); - - // determine if block and transaction activity is idling - boolean isIdle = false; - if (activityHistory.size() >= MIN_COLLECT_HISTORY) { - isIdle = true; - for (int i = 0; i < activityHistory.size(); i++) { - final ActivityHistoryEntry entry = activityHistory.get(i); - final boolean blocksActive = entry.numBlocksDownloaded > 0 && i <= IDLE_BLOCK_TIMEOUT_MIN; - final boolean transactionsActive = entry.numTransactionsReceived > 0 - && i <= IDLE_TRANSACTION_TIMEOUT_MIN; - final boolean headersActive = entry.numHeadersDownloaded > 0 && i <= IDLE_HEADER_TIMEOUT_MIN; - final boolean mnListDiffsActive = entry.numMnListDiffsDownloaded > 0 && i <= IDLE_MNLIST_TIMEOUT_MIN; - - if (blocksActive || transactionsActive || headersActive || mnListDiffsActive) { - isIdle = false; - break; - } - } - } - - // if idling, shutdown service - if (isIdle && mixingStatus != MixingStatus.MIXING) { - log.info("idling detected, stopping service"); - stopSelf(); - } - } - - lastChainHeight = chainHeight; - lastHeaderHeight = headerHeight; - } - }; - - public class LocalBinder extends Binder { - public BlockchainServiceImpl getService() { - return BlockchainServiceImpl.this; - } - } - - private final IBinder mBinder = new LocalBinder(); - - @Override - public IBinder onBind(final Intent intent) { - super.onBind(intent); - log.debug(".onBind()"); - - return mBinder; - } - - @Override - public boolean onUnbind(final Intent intent) { - log.debug(".onUnbind()"); - - return super.onUnbind(intent); - } - - @Override - public void onCreate() { - serviceCreatedAt = System.currentTimeMillis(); - log.debug(".onCreate()"); - - super.onCreate(); - - nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - - final String lockName = getPackageName() + " blockchain sync"; - - final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, lockName); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundAndCatch(); - } - - final Wallet wallet = application.getWallet(); - - peerConnectivityListener = new PeerConnectivityListener(); - - broadcastPeerState(0); - - blockChainFile = new File(getDir("blockstore", Context.MODE_PRIVATE), Constants.Files.BLOCKCHAIN_FILENAME); - final boolean blockChainFileExists = blockChainFile.exists(); - - headerChainFile = new File(getDir("blockstore", Context.MODE_PRIVATE), Constants.Files.HEADERS_FILENAME); - - mnlistinfoBootStrapStream = loadStream(Constants.Files.MNLIST_BOOTSTRAP_FILENAME); - qrinfoBootStrapStream = loadStream(Constants.Files.QRINFO_BOOTSTRAP_FILENAME); - - if (!blockChainFileExists) { - log.info("blockchain does not exist, resetting wallet"); - wallet.reset(); - resetMNLists(false); - resetMNListsOnPeerGroupStart = true; - } - - try { - log.info("loading blockchain file"); - blockStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile); - blockStore.getChainHead(); // detect corruptions as early as possible - log.info("loading header file"); - headerStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, headerChainFile); - headerStore.getChainHead(); // detect corruptions as early as possible - verifyBlockStores(); - - final long earliestKeyCreationTime = wallet.getEarliestKeyCreationTime(); - - if (!blockChainFileExists && earliestKeyCreationTime > 0) { - try { - final Stopwatch watch = Stopwatch.createStarted(); - InputStream checkpointsInputStream = getAssets().open(Constants.Files.CHECKPOINTS_FILENAME); - CheckpointManager.checkpoint(Constants.NETWORK_PARAMETERS, checkpointsInputStream, blockStore, - earliestKeyCreationTime); - //the headerStore should be set to the most recent checkpoint - checkpointsInputStream = getAssets().open(Constants.Files.CHECKPOINTS_FILENAME); - CheckpointManager.checkpoint(Constants.NETWORK_PARAMETERS, checkpointsInputStream, headerStore, - System.currentTimeMillis() / 1000); - watch.stop(); - log.info("checkpoints loaded from '{}', took {}", Constants.Files.CHECKPOINTS_FILENAME, watch); - } catch (final IOException x) { - log.error("problem reading checkpoints, continuing without", x); - } - } - } catch (final BlockStoreException x) { - blockChainFile.delete(); - headerChainFile.delete(); - resetMNLists(false); - - final String msg = "blockstore cannot be created"; - log.error(msg, x); - throw new Error(msg, x); - } - - try { - blockChain = new BlockChain(Constants.NETWORK_PARAMETERS, wallet, blockStore); - headerChain = new BlockChain(Constants.NETWORK_PARAMETERS, headerStore); - blockchainStateDataProvider.setBlockChain(blockChain); - } catch (final BlockStoreException x) { - throw new Error("blockchain cannot be created", x); - } - - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); - intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW); - intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); - registerReceiver(connectivityReceiver, intentFilter); // implicitly start PeerGroup - - application.getWallet().addCoinsReceivedEventListener(Threading.SAME_THREAD, walletEventListener); - application.getWallet().addCoinsSentEventListener(Threading.SAME_THREAD, walletEventListener); - application.getWallet().addChangeEventListener(Threading.SAME_THREAD, walletEventListener); - config.registerOnSharedPreferenceChangeListener(sharedPrefsChangeListener); - - registerReceiver(tickReceiver, new IntentFilter(Intent.ACTION_TIME_TICK)); - - peerDiscoveryList.add(dnsDiscovery); - - updateAppWidget(); - FlowExtKt.observe(blockchainStateDao.observeState(), this, (blockchainState, continuation) -> { - handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress); - return null; - }); - registerCrowdNodeConfirmedAddressFilter(); - - FlowExtKt.observe(coinJoinService.observeMixingState(), this, (mixingStatus, continuation) -> { - handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress); - return null; - }); - - FlowExtKt.observe(coinJoinService.observeMixingProgress(), this, (mixingProgress, continuation) -> { - handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress); - return null; - }); - } - - private Notification createCoinJoinNotification() { - Coin mixedBalance = ((WalletEx)application.getWallet()).getCoinJoinBalance(); - Coin totalBalance = application.getWallet().getBalance(); - Intent notificationIntent = OnboardingActivity.createIntent(this); - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, - notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - - DecimalFormat decimalFormat = new DecimalFormat("0.000"); - int statusStringId = R.string.error; - switch(mixingStatus) { - case MIXING: - statusStringId = R.string.coinjoin_mixing; - break; - case PAUSED: - statusStringId = R.string.coinjoin_paused; - break; - case FINISHED: - statusStringId = R.string.coinjoin_progress_finished; - break; - } - final String message = getString( - R.string.coinjoin_progress, - getString(statusStringId), - mixingProgress, - decimalFormat.format(MonetaryExtKt.toBigDecimal(mixedBalance)), - decimalFormat.format(MonetaryExtKt.toBigDecimal(totalBalance)) - ); - - return new NotificationCompat.Builder(this, - Constants.NOTIFICATION_CHANNEL_ID_ONGOING) - .setSmallIcon(R.drawable.ic_dash_d_white) - .setContentTitle(getString(R.string.app_name)) - .setContentText(message) - .setContentIntent(pendingIntent).build(); - } - - private void resetMNLists(boolean requestFreshList) { - try { - SimplifiedMasternodeListManager manager = application.getWallet().getContext().masternodeListManager; - if (manager != null) - manager.resetMNList(true, requestFreshList); - } catch (RuntimeException x) { - // swallow this exception. It is thrown when there is not a bootstrap file - // there is not a bootstrap mnlist file for testnet - log.info("error resetting masternode list with bootstrap files", x); - } - } - - @Override - public int onStartCommand(final Intent intent, final int flags, final int startId) { - super.onStartCommand(intent, flags, startId); - - if (intent != null) { - //Restart service as a Foreground Service if it's synchronizing the blockchain - Bundle extras = intent.getExtras(); - if (extras != null && extras.containsKey(START_AS_FOREGROUND_EXTRA)) { - startForegroundAndCatch(); - } - - log.info("service start command: " + intent + (intent.hasExtra(Intent.EXTRA_ALARM_COUNT) - ? " (alarm count: " + intent.getIntExtra(Intent.EXTRA_ALARM_COUNT, 0) + ")" : "")); - - final String action = intent.getAction(); - - if (BlockchainService.ACTION_CANCEL_COINS_RECEIVED.equals(action)) { - notificationCount = 0; - notificationAccumulatedAmount = Coin.ZERO; - notificationAddresses.clear(); - - nm.cancel(Constants.NOTIFICATION_ID_COINS_RECEIVED); - } else if (BlockchainService.ACTION_RESET_BLOCKCHAIN.equals(action)) { - log.info("will remove blockchain on service shutdown"); - - resetBlockchainOnShutdown = true; - config.setResetBlockchainPending(); - stopSelf(); - } else if (BlockchainService.ACTION_WIPE_WALLET.equals(action)) { - log.info("will remove blockchain and delete walletFile on service shutdown"); - - deleteWalletFileOnShutdown = true; - stopSelf(); - } else if (BlockchainService.ACTION_BROADCAST_TRANSACTION.equals(action)) { - final Sha256Hash hash = Sha256Hash - .wrap(intent.getByteArrayExtra(BlockchainService.ACTION_BROADCAST_TRANSACTION_HASH)); - final Transaction tx = application.getWallet().getTransaction(hash); - - if (peerGroup != null) { - log.info("broadcasting transaction " + tx.getHashAsString()); - int count = peerGroup.numConnectedPeers(); - int minimum = peerGroup.getMinBroadcastConnections(); - //if the number of peers is <= 3, then only require that number of peers to send - //if the number of peers is 0, then require 3 peers (default min connections) - if(count > 0 && count <= 3) - minimum = count; - - peerGroup.broadcastTransaction(tx, minimum, true); - } else { - log.info("peergroup not available, not broadcasting transaction {}", tx.getTxId()); - tx.getConfidence().setPeerInfo(0, 1); - } - } else if(BlockchainService.ACTION_RESET_BLOOMFILTERS.equals(action)) { - if (peerGroup != null) { - log.info("recalculating bloom filters"); - peerGroup.recalculateFastCatchupAndFilter(PeerGroup.FilterRecalculateMode.FORCE_SEND_FOR_REFRESH); - } else { - log.info("peergroup not available, not recalculating bloom filers"); - } - } - } else { - log.warn("service restart, although it was started as non-sticky"); - } - - return START_NOT_STICKY; - } - - private void startForeground() { - //Shows ongoing notification promoting service to foreground service and - //preventing it from being killed in Android 26 or later - Notification notification = createNetworkSyncNotification(null); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - startForeground(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC); - } else { - startForeground(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification); - } - foregroundService = ForegroundService.BLOCKCHAIN_SYNC; - } - - private void startForegroundCoinJoin() { - // Shows ongoing notification promoting service to foreground service and - // preventing it from being killed in Android 26 or later - Notification notification = createCoinJoinNotification(); - startForeground(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification); - foregroundService = ForegroundService.COINJOIN_MIXING; - } - - private void startForegroundAndCatch() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - try { - startForeground(); - } catch (ForegroundServiceStartNotAllowedException e) { - log.info("failed to start in foreground", e); - } - } else { - startForeground(); - } - } - - private void startForegroundCoinJoinAndCatch() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - try { - startForegroundCoinJoin(); - } catch (ForegroundServiceStartNotAllowedException e) { - log.info("failed to start in foreground", e); - } - } else { - startForegroundCoinJoin(); - } - } - - @Override - public void onDestroy() { - log.info(".onDestroy()"); - - WalletApplication.scheduleStartBlockchainService(this); //disconnect feature - - unregisterReceiver(tickReceiver); - - application.getWallet().removeChangeEventListener(walletEventListener); - application.getWallet().removeCoinsSentEventListener(walletEventListener); - application.getWallet().removeCoinsReceivedEventListener(walletEventListener); - config.unregisterOnSharedPreferenceChangeListener(sharedPrefsChangeListener); - - unregisterReceiver(connectivityReceiver); - - platformSyncService.shutdown(); - - if (peerGroup != null) { - application.getWallet().getContext().close(); - peerGroup.removeDisconnectedEventListener(peerConnectivityListener); - peerGroup.removeConnectedEventListener(peerConnectivityListener); - peerGroup.removeWallet(application.getWallet()); - platformSyncService.removePreBlockProgressListener(blockchainDownloadListener); - blockchainStateDataProvider.setNetworkStatus(NetworkStatus.DISCONNECTING); - peerGroup.stop(); - blockchainStateDataProvider.setNetworkStatus(NetworkStatus.STOPPED); - application.getWallet().setRiskAnalyzer(defaultRiskAnalyzer); - riskAnalyzer.shutdown(); - - log.info("peergroup stopped"); - } - - peerConnectivityListener.stop(); - - delayHandler.removeCallbacksAndMessages(null); - - try { - blockStore.close(); - headerStore.close(); - blockchainStateDataProvider.setBlockChain(null); - } catch (final BlockStoreException x) { - throw new RuntimeException(x); - } - - if (!deleteWalletFileOnShutdown) { - application.saveWallet(); - } - - if (wakeLock.isHeld()) { - log.debug("wakelock still held, releasing"); - wakeLock.release(); - } - - if (resetBlockchainOnShutdown || deleteWalletFileOnShutdown) { - log.info("removing blockchain"); - //noinspection ResultOfMethodCallIgnored - blockChainFile.delete(); - //noinspection ResultOfMethodCallIgnored - headerChainFile.delete(); - resetMNLists(false); - if (deleteWalletFileOnShutdown) { - log.info("removing wallet file and app data"); - application.finalizeWipe(); - } - //Clear the blockchain identity - WalletApplicationExt.INSTANCE.clearDatabases(application, false); - if (resetBlockchainOnShutdown) { - config.clearResetBlockchainPending(); - } - } - - closeStream(mnlistinfoBootStrapStream); - closeStream(qrinfoBootStrapStream); - - super.onDestroy(); - - log.info("service was up for " + ((System.currentTimeMillis() - serviceCreatedAt) / 1000 / 60) + " minutes"); - } - - @Override - public void onTrimMemory(final int level) { - log.info("onTrimMemory({}) called", level); - - if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { - log.warn("low memory detected, stopping service"); - stopSelf(); - } - } - - private Notification createNetworkSyncNotification(BlockchainState blockchainState) { - Intent notificationIntent = OnboardingActivity.createIntent(this); - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, - notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - - final String message = (blockchainState != null) - ? BlockchainStateUtils.getSyncStateString(blockchainState, this) - : getString(R.string.blockchain_state_progress_downloading, 0); - - return new NotificationCompat.Builder(this, - Constants.NOTIFICATION_CHANNEL_ID_ONGOING) - .setSmallIcon(R.drawable.ic_dash_d_white) - .setContentTitle(getString(R.string.app_name)) - .setContentText(message) - .setContentIntent(pendingIntent).build(); - } - - private void updateBlockchainStateImpediments() { - blockchainStateDataProvider.updateImpediments(impediments); - } - - private void updateBlockchainState() { - blockchainStateDataProvider.updateBlockchainState( - blockChain, impediments, percentageSync(), - peerGroup != null ? peerGroup.getSyncStage() : null - ); - } - - @Override - public List getConnectedPeers() { - if (peerGroup != null) - return peerGroup.getConnectedPeers(); - else - return null; - } - - @Override - public List getRecentBlocks(final int maxBlocks) { - final List blocks = new ArrayList(maxBlocks); - - try { - StoredBlock block = blockChain.getChainHead(); - - while (block != null) { - blocks.add(block); - - if (blocks.size() >= maxBlocks) - break; - - block = block.getPrev(blockStore); - } - } catch (final BlockStoreException x) { - // swallow - } - - return blocks; - } - - private void broadcastPeerState(final int numPeers) { - final Intent broadcast = new Intent(ACTION_PEER_STATE); - broadcast.setPackage(getPackageName()); - broadcast.putExtra(ACTION_PEER_STATE_NUM_PEERS, numPeers); - - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - private void handleBlockchainStateNotification(BlockchainState blockchainState, MixingStatus mixingStatus, double mixingProgress) { - // send this out for the Network Monitor, other activities observe the database - final Intent broadcast = new Intent(ACTION_BLOCKCHAIN_STATE); - broadcast.setPackage(getPackageName()); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - // log.info("handle blockchain state notification: {}, {}", foregroundService, mixingStatus); - this.mixingProgress = mixingProgress; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && blockchainState != null - && blockchainState.getBestChainDate() != null) { - //Handle Ongoing notification state - boolean syncing = blockchainState.getBestChainDate().getTime() < (Utils.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS); //1 hour - if (!syncing && blockchainState.getBestChainHeight() == config.getBestChainHeightEver() && mixingStatus != MixingStatus.MIXING) { - //Remove ongoing notification if blockchain sync finished - stopForeground(true); - foregroundService = ForegroundService.NONE; - nm.cancel(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC); - } else if (blockchainState.getReplaying() || syncing) { - //Shows ongoing notification when synchronizing the blockchain - Notification notification = createNetworkSyncNotification(blockchainState); - nm.notify(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification); - } else if (mixingStatus == MixingStatus.MIXING || mixingStatus == MixingStatus.PAUSED) { - log.info("foreground service: {}", foregroundService); - if (foregroundService == ForegroundService.NONE) { - log.info("foreground service not active, create notification"); - startForegroundCoinJoinAndCatch(); - foregroundService = ForegroundService.COINJOIN_MIXING; - } else { - log.info("foreground service active, update notification"); - Notification notification = createCoinJoinNotification(); - nm.notify(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification); - } - } - } - this.blockchainState = blockchainState; - this.mixingStatus = mixingStatus; - } - - private int percentageSync() { - return syncPercentage; - } - - private void updateAppWidget() { - Coin balance = application.getWallet().getBalance(Wallet.BalanceType.ESTIMATED); - WalletBalanceWidgetProvider.updateWidgets(BlockchainServiceImpl.this, balance); - } - - public void forceForeground() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Intent intent = new Intent(this, BlockchainServiceImpl.class); - ContextCompat.startForegroundService(this, intent); - // call startForeground just after startForegroundService. - startForegroundAndCatch(); - } - } - - private PreBlocksDownloadListener preBlocksDownloadListener = new PreBlocksDownloadListener() { - @Override - public void onPreBlocksDownload(Peer peer) { - log.info("onPreBlocksDownload using peer {}", peer); - platformSyncService.preBlockDownload(peerGroup.getPreBlockDownloadFuture()); - } - }; - - private void registerCrowdNodeConfirmedAddressFilter() { - String apiAddressStr = config.getCrowdNodeAccountAddress(); - String primaryAddressStr = config.getCrowdNodePrimaryAddress(); - - if (!apiAddressStr.isEmpty() && !primaryAddressStr.isEmpty()) { - Address apiAddress = Address.fromBase58(Constants.NETWORK_PARAMETERS, apiAddressStr); - Address primaryAddress = Address.fromBase58(Constants.NETWORK_PARAMETERS, primaryAddressStr); - - apiConfirmationHandler = new CrowdNodeAPIConfirmationHandler( - apiAddress, - primaryAddress, - crowdNodeBlockchainApi, - notificationService, - crowdNodeConfig, - getResources(), - new Intent(this, StakingActivity.class) - ); - } else { - apiConfirmationHandler = null; - } - } - - InputStream loadStream(String filename) { - InputStream stream = null; - try { - stream = getAssets().open(filename); - } catch (IOException x) { - log.warn("cannot load the bootstrap stream: {}", x.getMessage()); - } - return stream; - } - - - private void closeStream(InputStream mnlistinfoBootStrapStream) { - if (mnlistinfoBootStrapStream != null) { - try { - mnlistinfoBootStrapStream.close(); - } catch (IOException x) { - //do nothing - } - } - } - - // TODO: should we have a backup blockchain file? -// private NewBestBlockListener newBestBlockListener = block -> { -// try { -// backupBlockStore.put(block); -// } catch (BlockStoreException x) { -// throw new RuntimeException(x); -// } -// }; - - private boolean verifyBlockStore(BlockStore store) throws BlockStoreException { - StoredBlock cursor = store.getChainHead(); - for (int i = 0; i < 10; ++i) { - cursor = cursor.getPrev(store); - if (cursor == null || cursor.getHeader().equals(Constants.NETWORK_PARAMETERS.getGenesisBlock())) { - break; - } - } - return true; - } - - private boolean verifyBlockStore(BlockStore store, ScheduledExecutorService scheduledExecutorService) { - try { - ScheduledFuture future = scheduledExecutorService.schedule(() -> verifyBlockStore(store), 100, TimeUnit.MILLISECONDS); - return future.get(1, TimeUnit.SECONDS); - } catch (Exception e) { - log.warn("verification of blockstore failed:", e); - return false; - } - } - - // TODO: should we have a backup blockchain file? -// public static void copyFile(File source, File destination) throws IOException { -// try (FileChannel sourceChannel = new FileInputStream(source).getChannel(); -// FileChannel destChannel = new FileOutputStream(destination).getChannel()) { -// sourceChannel.transferTo(0, sourceChannel.size(), destChannel); -// } -// } -// -// -// private void replaceBlockStore(BlockStore a, File aFile, BlockStore b, File bFile) throws BlockStoreException { -// try { -// a.close(); -// b.close(); -// copyFile(bFile, aFile); -// } catch (IOException e) { -// throw new RuntimeException(e); -// } -// } - - private void verifyBlockStores() throws BlockStoreException { - ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1); - log.info("verifying backupBlockStore"); -// boolean verifiedBackupBlockStore = false; - boolean verifiedHeaderStore = false; - boolean verifiedBlockStore = false; -// if (!(verifiedBackupBlockStore = verifyBlockStore(backupBlockStore, scheduledExecutorService))) { -// log.info("backupBlockStore verification failed"); -// } - - log.info("verifying headerStore"); - if (!(verifiedHeaderStore = verifyBlockStore(headerStore, scheduledExecutorService))) { - log.info("headerStore verification failed"); - } - - log.info("verifying blockStore"); - if (!(verifiedBlockStore = verifyBlockStore(blockStore, scheduledExecutorService))) { - log.info("blockStore verification failed"); - } - // TODO: should we have a backup blockchain file? -// if (!verifiedBlockStore) { -// if (verifiedBackupBlockStore && -// !backupBlockStore.getChainHead().getHeader().getHash().equals(Constants.NETWORK_PARAMETERS.getGenesisBlock().getHash())) { -// log.info("replacing blockStore with backup"); -// replaceBlockStore(blockStore, blockChainFile, backupBlockStore, backupBlockChainFile); -// log.info("reloading blockStore"); -// blockStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile); -// blockStore.getChainHead(); // detect corruptions as early as possible -// log.info("reloading backup blockchain file"); -// backupBlockStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile); -// backupBlockStore.getChainHead(); // detect corruptions as early as possible -// verifyBlockStores(); -// } /*else if (verifiedHeaderStore) { -// log.info("replacing blockStore with header"); -// replaceBlockStore(blockStore, blockChainFile, headerStore, headerChainFile); -// log.info("reloading blockStore"); -// blockStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile); -// blockStore.getChainHead(); // detect corruptions as early as possible -// log.info("reloading header file"); -// headerStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, headerChainFile); -// headerStore.getChainHead(); // detect corruptions as early as possible -// verifyBlockStores(); -// } else*/ { -// // get blocks from platform here... -// throw new BlockStoreException("can't verify and recover"); -// } -// } - log.info("blockstore files verified: {}, {}", verifiedBlockStore, verifiedHeaderStore); - } -} diff --git a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt new file mode 100644 index 0000000000..d974252caf --- /dev/null +++ b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt @@ -0,0 +1,1616 @@ +/* + * Copyright 2011-2024 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package de.schildbach.wallet.service + +import android.annotation.SuppressLint +import android.app.ForegroundServiceStartNotAllowedException +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.SharedPreferences +import android.content.pm.ServiceInfo +import android.net.ConnectivityManager +import android.net.Uri +import android.os.Binder +import android.os.Build +import android.os.Handler +import android.os.IBinder +import android.os.PowerManager +import android.text.format.DateUtils +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleService +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.google.common.base.Stopwatch +import dagger.hilt.android.AndroidEntryPoint +import de.schildbach.wallet.Constants +import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.WalletApplicationExt.clearDatabases +import de.schildbach.wallet.WalletBalanceWidgetProvider +import de.schildbach.wallet.data.AddressBookProvider +import de.schildbach.wallet.database.dao.BlockchainStateDao +import de.schildbach.wallet.database.dao.ExchangeRatesDao +import de.schildbach.wallet.service.platform.PlatformSyncService +import de.schildbach.wallet.ui.OnboardingActivity.Companion.createIntent +import de.schildbach.wallet.ui.dashpay.OnPreBlockProgressListener +import de.schildbach.wallet.ui.dashpay.PlatformRepo +import de.schildbach.wallet.ui.dashpay.PreBlockStage +import de.schildbach.wallet.ui.staking.StakingActivity +import de.schildbach.wallet.util.AllowLockTimeRiskAnalysis +import de.schildbach.wallet.util.AllowLockTimeRiskAnalysis.OfflineAnalyzer +import de.schildbach.wallet.util.BlockchainStateUtils +import de.schildbach.wallet.util.CrashReporter +import de.schildbach.wallet.util.ThrottlingWalletChangeListener +import de.schildbach.wallet_test.R +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.bitcoinj.core.Address +import org.bitcoinj.core.Block +import org.bitcoinj.core.BlockChain +import org.bitcoinj.core.CheckpointManager +import org.bitcoinj.core.Coin +import org.bitcoinj.core.FilteredBlock +import org.bitcoinj.core.MasternodeSync +import org.bitcoinj.core.Peer +import org.bitcoinj.core.PeerGroup +import org.bitcoinj.core.Sha256Hash +import org.bitcoinj.core.StoredBlock +import org.bitcoinj.core.Transaction +import org.bitcoinj.core.TransactionConfidence +import org.bitcoinj.core.Utils +import org.bitcoinj.core.listeners.DownloadProgressTracker +import org.bitcoinj.core.listeners.PeerConnectedEventListener +import org.bitcoinj.core.listeners.PeerDisconnectedEventListener +import org.bitcoinj.core.listeners.PreBlocksDownloadListener +import org.bitcoinj.evolution.AssetLockTransaction +import org.bitcoinj.evolution.SimplifiedMasternodeListDiff +import org.bitcoinj.evolution.SimplifiedMasternodeListManager +import org.bitcoinj.evolution.listeners.MasternodeListDownloadedListener +import org.bitcoinj.net.discovery.DnsDiscovery +import org.bitcoinj.net.discovery.MasternodePeerDiscovery +import org.bitcoinj.net.discovery.MultiplexingDiscovery +import org.bitcoinj.net.discovery.PeerDiscovery +import org.bitcoinj.net.discovery.PeerDiscoveryException +import org.bitcoinj.net.discovery.SeedPeers +import org.bitcoinj.store.BlockStore +import org.bitcoinj.store.BlockStoreException +import org.bitcoinj.store.SPVBlockStore +import org.bitcoinj.utils.ExchangeRate +import org.bitcoinj.utils.Threading +import org.bitcoinj.wallet.DefaultRiskAnalysis +import org.bitcoinj.wallet.Wallet +import org.bitcoinj.wallet.WalletEx +import org.bitcoinj.wallet.authentication.AuthenticationGroupExtension +import org.dash.wallet.common.Configuration +import org.dash.wallet.common.data.NetworkStatus +import org.dash.wallet.common.data.WalletUIConfig +import org.dash.wallet.common.data.entity.BlockchainState +import org.dash.wallet.common.data.entity.BlockchainState.Impediment +import org.dash.wallet.common.services.NotificationService +import org.dash.wallet.common.services.TransactionMetadataProvider +import org.dash.wallet.common.transactions.TransactionUtils.getWalletAddressOfReceived +import org.dash.wallet.common.transactions.filters.NotFromAddressTxFilter +import org.dash.wallet.common.util.Constants.PREFIX_ALMOST_EQUAL_TO +import org.dash.wallet.common.util.observe +import org.dash.wallet.common.util.toBigDecimal +import org.dash.wallet.integrations.crowdnode.api.CrowdNodeAPIConfirmationHandler +import org.dash.wallet.integrations.crowdnode.api.CrowdNodeBlockchainApi +import org.dash.wallet.integrations.crowdnode.transactions.CrowdNodeDepositReceivedResponse +import org.dash.wallet.integrations.crowdnode.transactions.CrowdNodeWithdrawalReceivedTx +import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConfig +import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants.getCrowdNodeAddress +import org.slf4j.LoggerFactory +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.net.InetSocketAddress +import java.text.DecimalFormat +import java.util.Date +import java.util.EnumSet +import java.util.LinkedList +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong +import javax.inject.Inject +import kotlin.math.max + +/** + * @author Andreas Schildbach + * @author Eric Britten + */ +@AndroidEntryPoint +class BlockchainServiceImpl : LifecycleService(), BlockchainService { + + companion object { + private const val MINIMUM_PEER_COUNT = 16 + private const val MIN_COLLECT_HISTORY = 2 + private const val IDLE_HEADER_TIMEOUT_MIN = 2 + private const val IDLE_MNLIST_TIMEOUT_MIN = 2 + private const val IDLE_BLOCK_TIMEOUT_MIN = 2 + private const val IDLE_TRANSACTION_TIMEOUT_MIN = 9 + private val MAX_HISTORY_SIZE = max( + IDLE_TRANSACTION_TIMEOUT_MIN.toDouble(), + IDLE_BLOCK_TIMEOUT_MIN.toDouble() + ).toInt() + private const val APPWIDGET_THROTTLE_MS = DateUtils.SECOND_IN_MILLIS + private const val BLOCKCHAIN_STATE_BROADCAST_THROTTLE_MS = DateUtils.SECOND_IN_MILLIS + private val TX_EXCHANGE_RATE_TIME_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(180) + private val log = LoggerFactory.getLogger(BlockchainServiceImpl::class.java) + const val START_AS_FOREGROUND_EXTRA = "start_as_foreground" + var cleanupDeferred: CompletableDeferred? = null + } + private val serviceJob = SupervisorJob() + private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) + private val onCreateCompleted = CompletableDeferred() + + @Inject lateinit var application: WalletApplication + + @Inject lateinit var config: Configuration + + @Inject lateinit var walletUIConfig: WalletUIConfig + + @Inject lateinit var notificationService: NotificationService + + @Inject lateinit var crowdNodeBlockchainApi: CrowdNodeBlockchainApi + + @Inject lateinit var crowdNodeConfig: CrowdNodeConfig + + @Inject lateinit var blockchainStateDao: BlockchainStateDao + + @Inject lateinit var exchangeRatesDao: ExchangeRatesDao + + @Inject lateinit var transactionMetadataProvider: TransactionMetadataProvider + + @Inject lateinit var platformSyncService: PlatformSyncService + + @Inject lateinit var platformRepo: PlatformRepo + + @Inject lateinit var packageInfoProvider: PackageInfoProvider + + @Inject lateinit var connectivityManager: ConnectivityManager + + @Inject + lateinit var blockchainStateDataProvider: BlockchainStateDataProvider + + @Inject + lateinit var dashSystemService: DashSystemService + + @Inject lateinit var coinJoinService: CoinJoinService + + private var blockStore: BlockStore? = null + private var headerStore: BlockStore? = null + private var blockChainFile: File? = null + private var headerChainFile: File? = null + private var blockChain: BlockChain? = null + private var headerChain: BlockChain? = null + private var mnlistinfoBootStrapStream: InputStream? = null + private var qrinfoBootStrapStream: InputStream? = null + private var peerGroup: PeerGroup? = null + private val handler = Handler() + private val delayHandler = Handler() + private val metadataHandler = Handler() + private var wakeLock: PowerManager.WakeLock? = null + private var peerConnectivityListener: PeerConnectivityListener? = null + private var nm: NotificationManager? = null + private val impediments: MutableSet = EnumSet.noneOf( + Impediment::class.java + ) + private var blockchainState: BlockchainState? = + BlockchainState(null, 0, false, impediments, 0, 0, 0) + private var notificationCount = 0 + private var notificationAccumulatedAmount = Coin.ZERO + private val notificationAddresses: MutableList
= LinkedList() + private val transactionsReceived = AtomicInteger() + private val mnListDiffsReceived = AtomicInteger() + private var serviceCreatedAt: Long = 0 + private var resetBlockchainOnShutdown = false + private var deleteWalletFileOnShutdown = false + + //Settings to bypass dashj default dns seeds + private val seedPeerDiscovery = SeedPeers(Constants.NETWORK_PARAMETERS) + private val dnsDiscovery = DnsDiscovery(Constants.DNS_SEED, Constants.NETWORK_PARAMETERS) + var peerDiscoveryList = ArrayList(2) + private val executor: Executor = Executors.newSingleThreadExecutor() + private var syncPercentage = 0 // 0 to 100% + private var mixingStatus = MixingStatus.NOT_STARTED + private var mixingProgress = 0.0 + private var foregroundService = ForegroundService.NONE + + // Risk Analyser for Transactions that is PeerGroup Aware + private var riskAnalyzer: AllowLockTimeRiskAnalysis.Analyzer? = null + private var defaultRiskAnalyzer = DefaultRiskAnalysis.FACTORY + private val crowdnodeFilters = listOf( + NotFromAddressTxFilter(getCrowdNodeAddress(Constants.NETWORK_PARAMETERS)), + CrowdNodeWithdrawalReceivedTx(Constants.NETWORK_PARAMETERS) + ) + private val depositReceivedResponse = + CrowdNodeDepositReceivedResponse(Constants.NETWORK_PARAMETERS) + private var apiConfirmationHandler: CrowdNodeAPIConfirmationHandler? = null + private fun handleMetadata(tx: Transaction) { + metadataHandler.post { + transactionMetadataProvider.syncTransactionBlocking( + tx + ) + } + } + + private val walletEventListener: ThrottlingWalletChangeListener = + object : ThrottlingWalletChangeListener( + APPWIDGET_THROTTLE_MS + ) { + override fun onThrottledWalletChanged() { + updateAppWidget() + } + + override fun onCoinsReceived( + wallet: Wallet, tx: Transaction, prevBalance: Coin, + newBalance: Coin + ) { + val bestChainHeight = blockChain!!.bestChainHeight + val replaying = + bestChainHeight < config.bestChainHeightEver || config.isRestoringBackup + val now = Date().time + val blockChainHeadTime = blockChain!!.chainHead.header.time.time + val insideTxExchangeRateTimeThreshold = + now - blockChainHeadTime < TX_EXCHANGE_RATE_TIME_THRESHOLD_MS + log.info( + "onCoinsReceived: {}; rate: {}; replaying: {}; inside: {}, config: {}; will update {}", + tx.txId, + tx.exchangeRate, + replaying, + insideTxExchangeRateTimeThreshold, + tx.confidence.confidenceType, + tx.exchangeRate == null && (!replaying || insideTxExchangeRateTimeThreshold || tx.confidence.confidenceType == TransactionConfidence.ConfidenceType.PENDING) + ) + + // only set an exchange rate if the tx has no exchange rate and: + // 1. the blockchain is not being rescanned nor the wallet is being restored OR + // 2. the transaction is less than three hours old OR + // 3. the transaction is not yet mined + if (tx.exchangeRate == null && (!replaying + || insideTxExchangeRateTimeThreshold || tx.confidence.confidenceType == TransactionConfidence.ConfidenceType.PENDING) + ) { + try { + val exchangeRate = exchangeRatesDao.getRateSync( + walletUIConfig.getExchangeCurrencyCodeBlocking() + ) + if (exchangeRate != null) { + log.info("Setting exchange rate on received transaction. Rate: " + exchangeRate + " tx: " + tx.txId.toString()) + tx.exchangeRate = ExchangeRate(Coin.COIN, exchangeRate.fiat) + application.saveWallet() + } + } catch (e: Exception) { + log.error("Failed to get exchange rate", e) + } + } + transactionsReceived.incrementAndGet() + val address = getWalletAddressOfReceived(tx, wallet) + val amount = tx.getValue(wallet) + val confidenceType = tx.confidence.confidenceType + val isRestoringBackup = application.configuration.isRestoringBackup + handler.post { + val isReplayedTx = + confidenceType == TransactionConfidence.ConfidenceType.BUILDING && (replaying || isRestoringBackup) + if (!isReplayedTx) { + if (depositReceivedResponse.matches(tx)) { + notificationService.showNotification( + "deposit_received", + getString(R.string.crowdnode_deposit_received), + null, + null, + Intent(this@BlockchainServiceImpl, StakingActivity::class.java), + null + ) + } else if (apiConfirmationHandler != null && apiConfirmationHandler!!.matches( + tx + ) + ) { + apiConfirmationHandler!!.handle(tx) + } else if (passFilters(tx, wallet)) { + notifyCoinsReceived(address, amount, tx.exchangeRate) + } + } + } + handleMetadata(tx) + updateAppWidget() + } + + override fun onCoinsSent( + wallet: Wallet, tx: Transaction, prevBalance: Coin, + newBalance: Coin + ) { + transactionsReceived.incrementAndGet() + log.info("onCoinsSent: {}", tx.txId) + if (AssetLockTransaction.isAssetLockTransaction(tx) && tx.purpose == Transaction.Purpose.UNKNOWN) { + // Handle credit function transactions (username creation, topup, invites) + val authExtension = + wallet.getKeyChainExtension(AuthenticationGroupExtension.EXTENSION_ID) as AuthenticationGroupExtension + val cftx = authExtension.getAssetLockTransaction(tx) + val blockChainHeadTime = blockChain!!.chainHead.header.time.time + platformRepo.handleSentAssetLockTransaction(cftx, blockChainHeadTime) + + // TODO: if we detect a username creation that we haven't processed, should we? + } + handleMetadata(tx) + updateAppWidget() + } + + private fun passFilters(tx: Transaction, wallet: Wallet): Boolean { + val amount = tx.getValue(wallet) + val isReceived = amount.signum() > 0 + if (!isReceived) { + return false + } + var passFilters = false + for (filter in crowdnodeFilters) { + if (filter.matches(tx)) { + passFilters = true + break + } + } + return passFilters + } + } + private val sharedPrefsChangeListener = + SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences: SharedPreferences?, key: String? -> + if (key == Configuration.PREFS_KEY_CROWDNODE_PRIMARY_ADDRESS) { + registerCrowdNodeConfirmedAddressFilter() + } + } + private var resetMNListsOnPeerGroupStart = false + private fun notifyCoinsReceived( + address: Address?, amount: Coin, + exchangeRate: ExchangeRate? + ) { + if (notificationCount == 1) nm!!.cancel(Constants.NOTIFICATION_ID_COINS_RECEIVED) + notificationCount++ + notificationAccumulatedAmount = notificationAccumulatedAmount.add(amount) + if (address != null && !notificationAddresses.contains(address)) notificationAddresses.add( + address + ) + val btcFormat = config.getFormat() + val packageFlavor = packageInfoProvider.applicationPackageFlavor() + var msgSuffix = if (packageFlavor != null) " [$packageFlavor]" else "" + if (exchangeRate != null) { + val format = Constants.LOCAL_FORMAT.code( + 0, + PREFIX_ALMOST_EQUAL_TO + exchangeRate.fiat.getCurrencyCode() + ) + msgSuffix += " " + format.format(exchangeRate.coinToFiat(notificationAccumulatedAmount)) + } + val tickerMsg = + (getString(R.string.notification_coins_received_msg, btcFormat.format(amount)) + + msgSuffix) + val msg = getString( + R.string.notification_coins_received_msg, + btcFormat.format(notificationAccumulatedAmount) + ) + msgSuffix + val text = StringBuilder() + for (notificationAddress in notificationAddresses) { + if (text.isNotEmpty()) text.append(", ") + val addressStr = notificationAddress.toString() + val label = AddressBookProvider.resolveLabel(applicationContext, addressStr) + text.append(label ?: addressStr) + } + val notification = NotificationCompat.Builder( + this, + Constants.NOTIFICATION_CHANNEL_ID_TRANSACTIONS + ) + notification.setSmallIcon(R.drawable.ic_dash_d_white) + notification.setTicker(tickerMsg) + notification.setContentTitle(msg) + if (text.isNotEmpty()) { + notification.setContentText(text) + } + notification.setContentIntent( + PendingIntent.getActivity( + this, + 0, + createIntent(this), + PendingIntent.FLAG_IMMUTABLE + ) + ) + notification.setNumber(if (notificationCount == 1) 0 else notificationCount) + notification.setWhen(System.currentTimeMillis()) + notification.setSound(Uri.parse("android.resource://" + packageName + "/" + R.raw.coins_received)) + nm!!.notify(Constants.NOTIFICATION_ID_COINS_RECEIVED, notification.build()) + } + + private inner class PeerConnectivityListener : PeerConnectedEventListener, + PeerDisconnectedEventListener, SharedPreferences.OnSharedPreferenceChangeListener { + private var peerCount = 0 + private val stopped = AtomicBoolean(false) + + init { + config.registerOnSharedPreferenceChangeListener(this) + } + + fun stop() { + stopped.set(true) + config.unregisterOnSharedPreferenceChangeListener(this) + nm!!.cancel(Constants.NOTIFICATION_ID_CONNECTED) + } + + override fun onPeerConnected(peer: Peer, peerCount: Int) { + this.peerCount = peerCount + changed(peerCount) + } + + override fun onPeerDisconnected(peer: Peer, peerCount: Int) { + this.peerCount = peerCount + changed(peerCount) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { + if (Configuration.PREFS_KEY_CONNECTIVITY_NOTIFICATION == key) changed(peerCount) + } + + private fun changed(numPeers: Int) { + if (stopped.get()) return + val networkStatus = blockchainStateDataProvider.getNetworkStatus() + if (numPeers > 0 && networkStatus == NetworkStatus.CONNECTING) blockchainStateDataProvider.setNetworkStatus( + NetworkStatus.CONNECTED + ) else if (numPeers == 0 && networkStatus == NetworkStatus.DISCONNECTING) blockchainStateDataProvider.setNetworkStatus( + NetworkStatus.DISCONNECTED + ) + handler.post { + val connectivityNotificationEnabled = config.connectivityNotificationEnabled + if (!connectivityNotificationEnabled || numPeers == 0) { + nm!!.cancel(Constants.NOTIFICATION_ID_CONNECTED) + } else { + val notification = Notification.Builder(this@BlockchainServiceImpl) + notification.setSmallIcon( + R.drawable.stat_sys_peers, + if (numPeers > 4) 4 else numPeers + ) + notification.setContentTitle(getString(R.string.app_name)) + notification.setContentText( + getString( + R.string.notification_peers_connected_msg, + numPeers + ) + ) + notification.setContentIntent( + PendingIntent.getActivity( + this@BlockchainServiceImpl, 0, + createIntent(this@BlockchainServiceImpl), PendingIntent.FLAG_IMMUTABLE + ) + ) + notification.setWhen(System.currentTimeMillis()) + notification.setOngoing(true) + nm!!.notify(Constants.NOTIFICATION_ID_CONNECTED, notification.build()) + } + + // send broadcast + broadcastPeerState(numPeers) + } + } + } + + private abstract inner class MyDownloadProgressTracker + : DownloadProgressTracker(Constants.SYNC_FLAGS.contains(MasternodeSync.SYNC_FLAGS.SYNC_BLOCKS_AFTER_PREPROCESSING)), OnPreBlockProgressListener + + private val blockchainDownloadListener: MyDownloadProgressTracker = + object : MyDownloadProgressTracker() { + private val lastMessageTime = AtomicLong(0) + private var throttleDelay: Long = -1 + override fun onBlocksDownloaded( + peer: Peer, block: Block, filteredBlock: FilteredBlock?, + blocksLeft: Int + ) { + super.onBlocksDownloaded(peer, block, filteredBlock, blocksLeft) + postOrPostDelayed() + } + + override fun onHeadersDownloaded( + peer: Peer, block: Block, + blocksLeft: Int + ) { + super.onHeadersDownloaded(peer, block, blocksLeft) + postOrPostDelayed() + } + + private val runnable = Runnable { + lastMessageTime.set(System.currentTimeMillis()) + log.debug("Runnable % = $syncPercentage") + config.maybeIncrementBestChainHeightEver(blockChain!!.chainHead.height) + config.maybeIncrementBestHeaderHeightEver(headerChain!!.chainHead.height) + if (config.isRestoringBackup) { + val timeAgo = + System.currentTimeMillis() - blockChain!!.chainHead.header.timeSeconds * 1000 + //if the app was restoring a backup from a file or seed and block chain is nearly synced + //then turn off the restoring indicator + if (timeAgo < DateUtils.DAY_IN_MILLIS) { + config.isRestoringBackup = false + } + } + // this method is always called after progress or doneDownload + updateBlockchainState() + } + + /* + This method is called by super.onBlocksDownloaded when the percentage + of the chain downloaded is 0.0, 1.0, 2.0, 3.0 .. 99.0% (whole numbers) + + The pct value is relative to the blocks that need to be downloaded to sync, + rather than the relative to the entire blockchain. + */ + override fun progress(pct: Double, blocksLeft: Int, date: Date) { + super.progress(pct, blocksLeft, date) + syncPercentage = if (pct > 0.0) pct.toInt() else 0 + log.info("progress {}", syncPercentage) + if (syncPercentage > 100) { + syncPercentage = 100 + } + } + + /* + This method is called by super.onBlocksDownloaded when the percentage + of the chain downloaded is 100.0% (completely done) + */ + override fun doneDownload() { + super.doneDownload() + log.info("DoneDownload {}", syncPercentage) + // if the chain is already synced from a previous session, then syncPercentage = 0 + // set to 100% so that observers will see that sync is completed + syncPercentage = 100 + updateBlockchainState() + } + + override fun onMasterNodeListDiffDownloaded( + stage: MasternodeListDownloadedListener.Stage, + mnlistdiff: SimplifiedMasternodeListDiff? + ) { + log.info("masternodeListDiffDownloaded:$stage") + if (peerGroup != null && peerGroup!!.syncStage == PeerGroup.SyncStage.MNLIST) { + super.onMasterNodeListDiffDownloaded(stage, mnlistdiff) + startPreBlockPercent = syncPercentage + mnListDiffsReceived.incrementAndGet() + postOrPostDelayed() + } + } + + private fun postOrPostDelayed() { + delayHandler.removeCallbacksAndMessages(null) + if (throttleDelay == -1L) { + throttleDelay = + if (application.isLowRamDevice()) BLOCKCHAIN_STATE_BROADCAST_THROTTLE_MS else BLOCKCHAIN_STATE_BROADCAST_THROTTLE_MS / 4 + } + val now = System.currentTimeMillis() + if (now - lastMessageTime.get() > throttleDelay) { + delayHandler.post(runnable) + } else { + delayHandler.postDelayed(runnable, throttleDelay) + } + } + + var totalPreblockStages = PreBlockStage.UpdateTotal.value + var startPreBlockPercent = 0 + var lastPreBlockStage = PreBlockStage.None + override fun onPreBlockProgressUpdated(stage: PreBlockStage) { + if (stage == PreBlockStage.Starting && lastPreBlockStage == PreBlockStage.None) { + startPreBlockPercent = syncPercentage + } + if (preBlocksWeight > 0.99) { + startPreBlockPercent = 0 + } + if (stage == PreBlockStage.StartRecovery && lastPreBlockStage == PreBlockStage.None) { + startPreBlockPercent = syncPercentage + if (preBlocksWeight <= 0.10) setPreBlocksWeight(0.20) + } + var increment = preBlocksWeight * stage.value * 100.0 / PreBlockStage.Complete.value + if (increment > preBlocksWeight * 100) increment = preBlocksWeight * 100 + log.debug("PreBlockDownload: " + increment + "%..." + preBlocksWeight + " " + stage.name + " " + peerGroup!!.syncStage.name) + if (peerGroup != null && peerGroup!!.syncStage == PeerGroup.SyncStage.PREBLOCKS) { + syncPercentage = (startPreBlockPercent + increment).toInt() + log.info("PreBlockDownload: " + syncPercentage + "%..." + peerGroup!!.syncStage.name) + postOrPostDelayed() + } + lastPreBlockStage = stage + } + } + private val connectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + serviceScope.launch { + val action = intent.action + if (ConnectivityManager.CONNECTIVITY_ACTION == action) { + val networkInfo = connectivityManager.activeNetworkInfo + val hasConnectivity = networkInfo != null && networkInfo.isConnected + if (log.isInfoEnabled) { + val s = StringBuilder("active network is ") + .append(if (hasConnectivity) "up" else "down") + if (networkInfo != null) { + s.append(", type: ").append(networkInfo.typeName) + s.append(", state: ").append(networkInfo.state).append('/') + .append(networkInfo.detailedState) + val extraInfo = networkInfo.extraInfo + if (extraInfo != null) s.append(", extraInfo: ").append(extraInfo) + val reason = networkInfo.reason + if (reason != null) s.append(", reason: ").append(reason) + } + log.info(s.toString()) + } + if (hasConnectivity) { + impediments.remove(Impediment.NETWORK) + } else { + impediments.add(Impediment.NETWORK) + } + updateBlockchainStateImpediments() + check() + } else if (Intent.ACTION_DEVICE_STORAGE_LOW == action) { + log.info("device storage low") + impediments.add(Impediment.STORAGE) + updateBlockchainStateImpediments() + check() + } else if (Intent.ACTION_DEVICE_STORAGE_OK == action) { + log.info("device storage ok") + impediments.remove(Impediment.STORAGE) + updateBlockchainStateImpediments() + check() + } + } + } + + @SuppressLint("Wakelock") + private fun check() { + log.info("check()") + val wallet = application.wallet + if (impediments.isEmpty() && peerGroup == null) { + log.debug("acquiring wakelock") + wakeLock!!.acquire() + + // consistency check + val walletLastBlockSeenHeight = wallet!!.lastBlockSeenHeight + val bestChainHeight = blockChain!!.bestChainHeight + if (walletLastBlockSeenHeight != -1 && walletLastBlockSeenHeight != bestChainHeight) { + val message = + ("wallet/blockchain out of sync: " + walletLastBlockSeenHeight + "/" + + bestChainHeight) + log.error(message) + CrashReporter.saveBackgroundTrace( + RuntimeException(message), + packageInfoProvider.packageInfo + ) + } + org.bitcoinj.core.Context.propagate(wallet.context) + dashSystemService.system.initDashSync(getDir("masternode", MODE_PRIVATE).absolutePath) + log.info("starting peergroup") + peerGroup = PeerGroup(Constants.NETWORK_PARAMETERS, blockChain, headerChain) + if (Constants.SUPPORTS_PLATFORM) { + platformRepo.platform.setMasternodeListManager(dashSystemService.system.masternodeListManager) + platformSyncService.resume() + } + if (resetMNListsOnPeerGroupStart) { + resetMNListsOnPeerGroupStart = false + dashSystemService.system.masternodeListManager.setBootstrap( + mnlistinfoBootStrapStream, + qrinfoBootStrapStream, + SimplifiedMasternodeListManager.SMLE_VERSION_FORMAT_VERSION + ) + resetMNLists(true) + } + peerGroup!!.setDownloadTxDependencies(0) // recursive implementation causes StackOverflowError + peerGroup!!.addWallet(wallet) + peerGroup!!.setUserAgent(Constants.USER_AGENT, packageInfoProvider.versionName) + peerGroup!!.addConnectedEventListener(peerConnectivityListener) + peerGroup!!.addDisconnectedEventListener(peerConnectivityListener) + val maxConnectedPeers = application.maxConnectedPeers() + val trustedPeerHost = config.trustedPeerHost + val hasTrustedPeer = trustedPeerHost != null + val connectTrustedPeerOnly = hasTrustedPeer && config.trustedPeerOnly + peerGroup!!.maxConnections = if (connectTrustedPeerOnly) 1 else maxConnectedPeers + peerGroup!!.setConnectTimeoutMillis(Constants.PEER_TIMEOUT_MS) + peerGroup!!.setPeerDiscoveryTimeoutMillis(Constants.PEER_DISCOVERY_TIMEOUT_MS.toLong()) + peerGroup!!.addPeerDiscovery(object : PeerDiscovery { + //Keep Original code here for now + //private final PeerDiscovery normalPeerDiscovery = MultiplexingDiscovery + // .forServices(Constants.NETWORK_PARAMETERS, 0); + private val normalPeerDiscovery: PeerDiscovery = + MultiplexingDiscovery(Constants.NETWORK_PARAMETERS, peerDiscoveryList) + + @Throws(PeerDiscoveryException::class) + override fun getPeers( + services: Long, timeoutValue: Long, + timeoutUnit: TimeUnit + ): Array { + val peers: MutableList = LinkedList() + var needsTrimPeersWorkaround = false + if (hasTrustedPeer) { + log.info( + "trusted peer '" + trustedPeerHost + "'" + if (connectTrustedPeerOnly) " only" else "" + ) + val addr = InetSocketAddress( + trustedPeerHost, + Constants.NETWORK_PARAMETERS.port + ) + if (addr.address != null) { + peers.add(addr) + needsTrimPeersWorkaround = true + } + } + if (!connectTrustedPeerOnly) { + // First use the masternode list that is included + try { + val mnlist = dashSystemService.system.masternodeListManager.listAtChainTip + val discovery = MasternodePeerDiscovery(mnlist) + peers.addAll( + listOf( + *discovery.getPeers( + services, + timeoutValue, + timeoutUnit + ) + ) + ) + } catch (x: PeerDiscoveryException) { + //swallow and continue with another method of connection + log.info("DMN List peer discovery failed: " + x.message) + } + + // default masternode list + if (peers.size < MINIMUM_PEER_COUNT) { + val defaultMNList = + Constants.NETWORK_PARAMETERS.defaultMasternodeList + if (defaultMNList != null && defaultMNList.isNotEmpty()) { + log.info("DMN peer discovery returned less than 16 nodes. Adding default DMN peers to the list to increase connections") + val discovery = MasternodePeerDiscovery( + defaultMNList, + Constants.NETWORK_PARAMETERS.port + ) + peers.addAll( + listOf( + *discovery.getPeers( + services, + timeoutValue, + timeoutUnit + ) + ) + ) + + // use EvoNodes if the network is small + if (peers.size < MINIMUM_PEER_COUNT) { + val defaultEvoNodeList = + Constants.NETWORK_PARAMETERS.defaultHPMasternodeList + val discoveryEvo = MasternodePeerDiscovery( + defaultEvoNodeList, + Constants.NETWORK_PARAMETERS.port + ) + peers.addAll( + listOf( + *discoveryEvo.getPeers( + services, + timeoutValue, + timeoutUnit + ) + ) + ) + } + } else { + log.info("DNS peer discovery returned less than 16 nodes. Unable to add seed peers (it is not specified for this network).") + } + } + + // seed nodes + if (peers.size < MINIMUM_PEER_COUNT) { + if (Constants.NETWORK_PARAMETERS.addrSeeds != null) { + log.info("Static DMN peer discovery returned less than 16 nodes. Adding seed peers to the list to increase connections") + peers.addAll( + listOf( + *seedPeerDiscovery.getPeers( + services, + timeoutValue, + timeoutUnit + ) + ) + ) + } else { + log.info("DNS peer discovery returned less than 16 nodes. Unable to add seed peers (it is not specified for this network).") + } + } + if (peers.size < MINIMUM_PEER_COUNT) { + log.info("Masternode peer discovery returned less than 16 nodes. Adding DMN peers to the list to increase connections") + try { + peers.addAll( + listOf( + *normalPeerDiscovery.getPeers( + services, + timeoutValue, + timeoutUnit + ) + ) + ) + } catch (x: PeerDiscoveryException) { + //swallow and continue with another method of connection, if one exists. + log.info("DNS peer discovery failed: " + x.message) + if (x.cause != null) log.info("cause: " + x.cause!!.message) + } + } + } + + // workaround because PeerGroup will shuffle peers + if (needsTrimPeersWorkaround) while (peers.size >= maxConnectedPeers) peers.removeAt( + peers.size - 1 + ) + return peers.toTypedArray() + } + + override fun shutdown() { + normalPeerDiscovery.shutdown() + } + }) + peerGroup!!.addPreBlocksDownloadListener(executor, preBlocksDownloadListener) + // Use our custom risk analysis that allows v2 tx with absolute LockTime + riskAnalyzer = AllowLockTimeRiskAnalysis.Analyzer( + peerGroup!! + ) + wallet.riskAnalyzer = riskAnalyzer + + // start peergroup + blockchainStateDataProvider.setNetworkStatus(NetworkStatus.CONNECTING) + peerGroup!!.startAsync() + peerGroup!!.startBlockChainDownload(blockchainDownloadListener) + platformSyncService.addPreBlockProgressListener(blockchainDownloadListener) + } else if (impediments.isNotEmpty() && peerGroup != null) { + blockchainStateDataProvider.setNetworkStatus(NetworkStatus.NOT_AVAILABLE) + dashSystemService.system.close() + log.info("stopping peergroup") + peerGroup!!.removeDisconnectedEventListener(peerConnectivityListener) + peerGroup!!.removeConnectedEventListener(peerConnectivityListener) + peerGroup!!.removePreBlocksDownloadedListener(preBlocksDownloadListener) + peerGroup!!.removeWallet(wallet) + platformSyncService.removePreBlockProgressListener(blockchainDownloadListener) + peerGroup!!.stopAsync() + // use the offline risk analyzer + wallet!!.riskAnalyzer = + OfflineAnalyzer(config.bestHeightEver, System.currentTimeMillis() / 1000) + riskAnalyzer!!.shutdown() + peerGroup = null + log.debug("releasing wakelock") + wakeLock!!.release() + } + } + } + + private class ActivityHistoryEntry( + val numTransactionsReceived: Int, val numBlocksDownloaded: Int, + val numHeadersDownloaded: Int, val numMnListDiffsDownloaded: Int + ) { + override fun toString(): String { + return "$numTransactionsReceived/$numBlocksDownloaded/$numHeadersDownloaded/$numMnListDiffsDownloaded" + } + } + + private val tickReceiver: BroadcastReceiver = object : BroadcastReceiver() { + private var lastChainHeight = 0 + private var lastHeaderHeight = 0 + private val activityHistory = arrayListOf () + override fun onReceive(context: Context, intent: Intent) { + val chainHeight = blockChain!!.bestChainHeight + val headerHeight = headerChain!!.bestChainHeight + if (lastChainHeight > 0 || lastHeaderHeight > 0) { + val numBlocksDownloaded = chainHeight - lastChainHeight + val numTransactionsReceived = transactionsReceived.getAndSet(0) + // instead of counting headers, count header messages which contain up to 2000 headers + val numHeadersDownloaded = headerHeight - lastHeaderHeight + val numMnListDiffsDownloaded = mnListDiffsReceived.getAndSet(0) + + // push history + activityHistory.add( + 0, + ActivityHistoryEntry( + numTransactionsReceived, + numBlocksDownloaded, + numHeadersDownloaded, + numMnListDiffsDownloaded + ) + ) + + // trim + while (activityHistory.size > MAX_HISTORY_SIZE) { + activityHistory.removeAt( + activityHistory.size - 1 + ) + } + + // print + val builder = StringBuilder() + for (entry in activityHistory) { + if (builder.isNotEmpty()) { + builder.append(", ") + } + builder.append(entry) + } + log.info( + "History of transactions/blocks/headers/mnlistdiff: " + + (if (mixingStatus == MixingStatus.MIXING) "[mixing] " else "") + builder + ) + + // determine if block and transaction activity is idling + var isIdle = false + if (activityHistory.size >= MIN_COLLECT_HISTORY) { + isIdle = true + for (i in activityHistory.indices) { + val entry = activityHistory[i] + val blocksActive = + entry.numBlocksDownloaded > 0 && i <= IDLE_BLOCK_TIMEOUT_MIN + val transactionsActive = (entry.numTransactionsReceived > 0 + && i <= IDLE_TRANSACTION_TIMEOUT_MIN) + val headersActive = + entry.numHeadersDownloaded > 0 && i <= IDLE_HEADER_TIMEOUT_MIN + val mnListDiffsActive = + entry.numMnListDiffsDownloaded > 0 && i <= IDLE_MNLIST_TIMEOUT_MIN + if (blocksActive || transactionsActive || headersActive || mnListDiffsActive) { + isIdle = false + break + } + } + } + + // if idling, shutdown service + if (isIdle && mixingStatus != MixingStatus.MIXING) { + log.info("idling detected, stopping service") + stopSelf() + } + } + lastChainHeight = chainHeight + lastHeaderHeight = headerHeight + } + } + + inner class LocalBinder : Binder() { + val service: BlockchainServiceImpl + get() = this@BlockchainServiceImpl + } + + private val mBinder: IBinder = LocalBinder() + override fun onBind(intent: Intent): IBinder { + super.onBind(intent) + log.debug(".onBind()") + return mBinder + } + + override fun onUnbind(intent: Intent): Boolean { + log.debug(".onUnbind()") + return super.onUnbind(intent) + } + + private fun propagateContext() { + org.bitcoinj.core.Context.propagate(Constants.CONTEXT) + } + + override fun onCreate() { + serviceCreatedAt = System.currentTimeMillis() + log.info(".onCreate()") + super.onCreate() + nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val lockName = "$packageName blockchain sync" + val pm = getSystemService(POWER_SERVICE) as PowerManager + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, lockName) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundAndCatch() + } + val wallet = application.wallet + serviceScope.launch { + cleanupDeferred?.await() + propagateContext() + peerConnectivityListener = PeerConnectivityListener() + broadcastPeerState(0) + blockChainFile = + File(getDir("blockstore", MODE_PRIVATE), Constants.Files.BLOCKCHAIN_FILENAME) + val blockChainFileExists = blockChainFile!!.exists() + headerChainFile = File(getDir("blockstore", MODE_PRIVATE), Constants.Files.HEADERS_FILENAME) + mnlistinfoBootStrapStream = loadStream(Constants.Files.MNLIST_BOOTSTRAP_FILENAME) + qrinfoBootStrapStream = loadStream(Constants.Files.QRINFO_BOOTSTRAP_FILENAME) + if (!blockChainFileExists) { + log.info("blockchain does not exist, resetting wallet") + propagateContext() + wallet!!.reset() + resetMNLists(false) + resetMNListsOnPeerGroupStart = true + } + try { + blockStore = SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile) + blockStore?.chainHead // detect corruptions as early as possible + headerStore = SPVBlockStore(Constants.NETWORK_PARAMETERS, headerChainFile) + headerStore?.chainHead // detect corruptions as early as possible + withContext(Dispatchers.Main) { verifyBlockStores() } + val earliestKeyCreationTime = wallet!!.earliestKeyCreationTime + if (!blockChainFileExists && earliestKeyCreationTime > 0) { + try { + val watch = Stopwatch.createStarted() + var checkpointsInputStream = assets.open(Constants.Files.CHECKPOINTS_FILENAME) + CheckpointManager.checkpoint( + Constants.NETWORK_PARAMETERS, checkpointsInputStream, blockStore, + earliestKeyCreationTime + ) + //the headerStore should be set to the most recent checkpoint + checkpointsInputStream = assets.open(Constants.Files.CHECKPOINTS_FILENAME) + CheckpointManager.checkpoint( + Constants.NETWORK_PARAMETERS, checkpointsInputStream, headerStore, + System.currentTimeMillis() / 1000 + ) + watch.stop() + log.info( + "checkpoints loaded from '{}', took {}", + Constants.Files.CHECKPOINTS_FILENAME, + watch + ) + } catch (x: IOException) { + log.error("problem reading checkpoints, continuing without", x) + } + } + } catch (x: BlockStoreException) { + blockChainFile!!.delete() + headerChainFile!!.delete() + resetMNLists(false) + val msg = "blockstore cannot be created" + log.error(msg, x) + throw Error(msg, x) + } + try { + blockChain = BlockChain(Constants.NETWORK_PARAMETERS, wallet, blockStore) + headerChain = BlockChain(Constants.NETWORK_PARAMETERS, headerStore) + blockchainStateDataProvider.setBlockChain(blockChain) + } catch (x: BlockStoreException) { + throw Error("blockchain cannot be created", x) + } + val intentFilter = IntentFilter() + intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION) + intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW) + intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK) + registerReceiver(connectivityReceiver, intentFilter) // implicitly start PeerGroup + application.wallet!!.addCoinsReceivedEventListener( + Threading.SAME_THREAD, + walletEventListener + ) + application.wallet!!.addCoinsSentEventListener(Threading.SAME_THREAD, walletEventListener) + application.wallet!!.addChangeEventListener(Threading.SAME_THREAD, walletEventListener) + config.registerOnSharedPreferenceChangeListener(sharedPrefsChangeListener) + registerReceiver(tickReceiver, IntentFilter(Intent.ACTION_TIME_TICK)) + peerDiscoveryList.add(dnsDiscovery) + updateAppWidget() + blockchainStateDao.observeState().observe(this@BlockchainServiceImpl) { blockchainState -> + handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) + } + registerCrowdNodeConfirmedAddressFilter() + coinJoinService.observeMixingState().observe(this@BlockchainServiceImpl) { mixingStatus -> + handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) + } + coinJoinService.observeMixingProgress().observe(this@BlockchainServiceImpl) { mixingProgress -> + handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) + } + onCreateCompleted.complete(Unit) // Signal completion of onCreate + log.info(".onCreate() finished") + } + } + + private fun createCoinJoinNotification(): Notification { + val mixedBalance = (application.wallet as WalletEx?)!!.coinJoinBalance + val totalBalance = application.wallet!!.balance + val notificationIntent = createIntent(this) + val pendingIntent = PendingIntent.getActivity( + this, 0, + notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val decimalFormat = DecimalFormat("0.000") + val statusStringId = when (mixingStatus) { + MixingStatus.MIXING -> R.string.coinjoin_mixing + MixingStatus.PAUSED -> R.string.coinjoin_paused + MixingStatus.FINISHED -> R.string.coinjoin_progress_finished + else -> R.string.error + } + val message = getString( + R.string.coinjoin_progress, + getString(statusStringId), + mixingProgress, + decimalFormat.format(mixedBalance.toBigDecimal()), + decimalFormat.format(totalBalance.toBigDecimal()) + ) + return NotificationCompat.Builder( + this, + Constants.NOTIFICATION_CHANNEL_ID_ONGOING + ) + .setSmallIcon(R.drawable.ic_dash_d_white) + .setContentTitle(getString(R.string.app_name)) + .setContentText(message) + .setContentIntent(pendingIntent).build() + } + + private fun resetMNLists(requestFreshList: Boolean) { + try { + val manager = dashSystemService.system.masternodeListManager + manager?.resetMNList(true, requestFreshList) + } catch (x: RuntimeException) { + // swallow this exception. It is thrown when there is not a bootstrap file + // there is not a bootstrap mnlist file for testnet + log.info("error resetting masternode list with bootstrap files", x) + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + log.info(".onStartCommand($intent)") + super.onStartCommand(intent, flags, startId) + serviceScope.launch { + onCreateCompleted.await() // wait until onCreate is finished + if (intent != null) { + propagateContext() + //Restart service as a Foreground Service if it's synchronizing the blockchain + val extras = intent.extras + if (extras != null && extras.containsKey(START_AS_FOREGROUND_EXTRA)) { + startForegroundAndCatch() + } + log.info( + "service start command: $intent" + if (intent.hasExtra(Intent.EXTRA_ALARM_COUNT)) " (alarm count: " + intent.getIntExtra( + Intent.EXTRA_ALARM_COUNT, 0 + ) + ")" else "" + ) + val action = intent.action + if (BlockchainService.ACTION_CANCEL_COINS_RECEIVED == action) { + notificationCount = 0 + notificationAccumulatedAmount = Coin.ZERO + notificationAddresses.clear() + nm!!.cancel(Constants.NOTIFICATION_ID_COINS_RECEIVED) + } else if (BlockchainService.ACTION_RESET_BLOCKCHAIN == action) { + log.info("will remove blockchain on service shutdown") + resetBlockchainOnShutdown = true + stopSelf() + } else if (BlockchainService.ACTION_WIPE_WALLET == action) { + log.info("will remove blockchain and delete walletFile on service shutdown") + deleteWalletFileOnShutdown = true + stopSelf() + } else if (BlockchainService.ACTION_BROADCAST_TRANSACTION == action) { + val hash = Sha256Hash + .wrap(intent.getByteArrayExtra(BlockchainService.ACTION_BROADCAST_TRANSACTION_HASH)) + val tx = application.wallet!!.getTransaction(hash) + if (peerGroup != null) { + log.info("broadcasting transaction " + tx!!.hashAsString) + val count = peerGroup!!.numConnectedPeers() + var minimum = peerGroup!!.minBroadcastConnections + // if the number of peers is <= 3, then only require that number of peers to send + // if the number of peers is 0, then require 3 peers (default min connections) + if (count in 1..3) minimum = count + peerGroup!!.broadcastTransaction(tx, minimum, true) + } else { + log.info("peergroup not available, not broadcasting transaction {}", tx!!.txId) + tx.confidence.setPeerInfo(0, 1) + } + } else if (BlockchainService.ACTION_RESET_BLOOMFILTERS == action) { + if (peerGroup != null) { + log.info("recalculating bloom filters") + peerGroup!!.recalculateFastCatchupAndFilter(PeerGroup.FilterRecalculateMode.FORCE_SEND_FOR_REFRESH) + } else { + log.info("peergroup not available, not recalculating bloom filers") + } + } + } else { + log.warn("service restart, although it was started as non-sticky") + } + log.info(".onStartCommand($intent) finished") + } + return START_NOT_STICKY + } + + private fun startForeground() { + //Shows ongoing notification promoting service to foreground service and + //preventing it from being killed in Android 26 or later + val notification = createNetworkSyncNotification(null) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } else { + startForeground(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification) + } + foregroundService = ForegroundService.BLOCKCHAIN_SYNC + } + + private fun startForegroundCoinJoin() { + // Shows ongoing notification promoting service to foreground service and + // preventing it from being killed in Android 26 or later + val notification = createCoinJoinNotification() + startForeground(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification) + foregroundService = ForegroundService.COINJOIN_MIXING + } + + private fun startForegroundAndCatch() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + startForeground() + } catch (e: ForegroundServiceStartNotAllowedException) { + log.info("failed to start in foreground", e) + } + } else { + startForeground() + } + } + + private fun startForegroundCoinJoinAndCatch() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + startForegroundCoinJoin() + } catch (e: ForegroundServiceStartNotAllowedException) { + log.info("failed to start in foreground", e) + } + } else { + startForegroundCoinJoin() + } + } + + override fun onDestroy() { + log.info(".onDestroy()") + super.onDestroy() + cleanupDeferred = CompletableDeferred() + serviceScope.launch { + try { + onCreateCompleted.await() // wait until onCreate is finished + WalletApplication.scheduleStartBlockchainService(this@BlockchainServiceImpl) //disconnect feature + unregisterReceiver(tickReceiver) + application.wallet!!.removeChangeEventListener(walletEventListener) + application.wallet!!.removeCoinsSentEventListener(walletEventListener) + application.wallet!!.removeCoinsReceivedEventListener(walletEventListener) + config.unregisterOnSharedPreferenceChangeListener(sharedPrefsChangeListener) + unregisterReceiver(connectivityReceiver) + platformSyncService.shutdown() + if (peerGroup != null) { + propagateContext() + dashSystemService.system.close() + peerGroup!!.removeDisconnectedEventListener(peerConnectivityListener) + peerGroup!!.removeConnectedEventListener(peerConnectivityListener) + peerGroup!!.removeWallet(application.wallet) + platformSyncService.removePreBlockProgressListener(blockchainDownloadListener) + blockchainStateDataProvider.setNetworkStatus(NetworkStatus.DISCONNECTING) + peerGroup!!.stop() + blockchainStateDataProvider.setNetworkStatus(NetworkStatus.STOPPED) + application.wallet!!.riskAnalyzer = defaultRiskAnalyzer + riskAnalyzer!!.shutdown() + log.info("peergroup stopped") + } + peerConnectivityListener!!.stop() + delayHandler.removeCallbacksAndMessages(null) + try { + blockStore!!.close() + headerStore!!.close() + blockchainStateDataProvider.setBlockChain(null) + } catch (x: BlockStoreException) { + throw RuntimeException(x) + } + if (!deleteWalletFileOnShutdown) { + application.saveWallet() + } + if (wakeLock!!.isHeld) { + log.debug("wakelock still held, releasing") + wakeLock!!.release() + } + if (resetBlockchainOnShutdown || deleteWalletFileOnShutdown) { + log.info("removing blockchain") + blockChainFile!!.delete() + headerChainFile!!.delete() + resetMNLists(false) + if (deleteWalletFileOnShutdown) { + log.info("removing wallet file and app data") + application.finalizeWipe() + } + //Clear the blockchain identity + application.clearDatabases(false) + } + closeStream(mnlistinfoBootStrapStream) + closeStream(qrinfoBootStrapStream) + } finally { + log.info("serviceJob cancelled after " + (System.currentTimeMillis() - serviceCreatedAt) / 1000 / 60 + " minutes") + serviceJob.cancel() + cleanupDeferred?.complete(Unit) + } + } + log.info("service was up for " + (System.currentTimeMillis() - serviceCreatedAt) / 1000 / 60 + " minutes") + } + + override fun onTrimMemory(level: Int) { + log.info("onTrimMemory({}) called", level) + if (level >= TRIM_MEMORY_BACKGROUND) { + log.warn("low memory detected, stopping service") + stopSelf() + } + } + + private fun createNetworkSyncNotification(blockchainState: BlockchainState?): Notification { + val notificationIntent = createIntent(this) + val pendingIntent = PendingIntent.getActivity( + this, 0, + notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val message = if (blockchainState != null) BlockchainStateUtils.getSyncStateString( + blockchainState, + this + ) else getString( + R.string.blockchain_state_progress_downloading, "0" + ) + return NotificationCompat.Builder( + this, + Constants.NOTIFICATION_CHANNEL_ID_ONGOING + ) + .setSmallIcon(R.drawable.ic_dash_d_white) + .setContentTitle(getString(R.string.app_name)) + .setContentText(message) + .setContentIntent(pendingIntent).build() + } + + private fun updateBlockchainStateImpediments() { + blockchainStateDataProvider.updateImpediments(impediments) + } + + private fun updateBlockchainState() { + blockchainStateDataProvider.updateBlockchainState( + blockChain!!, impediments, percentageSync(), + if (peerGroup != null) peerGroup!!.syncStage else null + ) + } + + override fun getConnectedPeers(): List? { + return if (peerGroup != null) peerGroup!!.connectedPeers else null + } + + override fun getRecentBlocks(maxBlocks: Int): List { + val blocks: MutableList = ArrayList(maxBlocks) + try { + var block = blockChain!!.chainHead + while (block != null) { + blocks.add(block) + if (blocks.size >= maxBlocks) break + block = block.getPrev(blockStore) + } + } catch (x: BlockStoreException) { + // swallow + } + return blocks + } + + private fun broadcastPeerState(numPeers: Int) { + val broadcast = Intent(BlockchainService.ACTION_PEER_STATE) + broadcast.setPackage(packageName) + broadcast.putExtra(BlockchainService.ACTION_PEER_STATE_NUM_PEERS, numPeers) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + } + + private fun handleBlockchainStateNotification( + blockchainState: BlockchainState?, + mixingStatus: MixingStatus, + mixingProgress: Double + ) { + // send this out for the Network Monitor, other activities observe the database + val broadcast = Intent(BlockchainService.ACTION_BLOCKCHAIN_STATE) + broadcast.setPackage(packageName) + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) + // log.info("handle blockchain state notification: {}, {}", foregroundService, mixingStatus); + this.mixingProgress = mixingProgress + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && blockchainState != null && blockchainState.bestChainDate != null) { + //Handle Ongoing notification state + val syncing = + blockchainState.bestChainDate!!.time < Utils.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS //1 hour + if (!syncing && blockchainState.bestChainHeight == config.bestChainHeightEver && mixingStatus != MixingStatus.MIXING) { + //Remove ongoing notification if blockchain sync finished + stopForeground(true) + foregroundService = ForegroundService.NONE + nm!!.cancel(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC) + } else if (blockchainState.replaying || syncing) { + //Shows ongoing notification when synchronizing the blockchain + val notification = createNetworkSyncNotification(blockchainState) + nm!!.notify(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification) + } else if (mixingStatus == MixingStatus.MIXING || mixingStatus == MixingStatus.PAUSED) { + log.info("foreground service: {}", foregroundService) + if (foregroundService == ForegroundService.NONE) { + log.info("foreground service not active, create notification") + startForegroundCoinJoinAndCatch() + foregroundService = ForegroundService.COINJOIN_MIXING + } else { + log.info("foreground service active, update notification") + val notification = createCoinJoinNotification() + nm!!.notify(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification) + } + } + } + this.blockchainState = blockchainState + this.mixingStatus = mixingStatus + } + + private fun percentageSync(): Int { + return syncPercentage + } + + private fun updateAppWidget() { + val balance = application.wallet!!.getBalance(Wallet.BalanceType.ESTIMATED) + WalletBalanceWidgetProvider.updateWidgets(this@BlockchainServiceImpl, balance) + } + + fun forceForeground() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val intent = Intent(this, BlockchainServiceImpl::class.java) + ContextCompat.startForegroundService(this, intent) + // call startForeground just after startForegroundService. + startForegroundAndCatch() + } + } + + private val preBlocksDownloadListener = PreBlocksDownloadListener { peer -> + log.info("onPreBlocksDownload using peer {}", peer) + platformSyncService.preBlockDownload(peerGroup!!.preBlockDownloadFuture) + } + + private fun registerCrowdNodeConfirmedAddressFilter() { + val apiAddressStr = config.crowdNodeAccountAddress + val primaryAddressStr = config.crowdNodePrimaryAddress + apiConfirmationHandler = if (apiAddressStr.isNotEmpty() && primaryAddressStr.isNotEmpty()) { + val apiAddress = Address.fromBase58( + Constants.NETWORK_PARAMETERS, + apiAddressStr + ) + val primaryAddress = Address.fromBase58( + Constants.NETWORK_PARAMETERS, + primaryAddressStr + ) + CrowdNodeAPIConfirmationHandler( + apiAddress, + primaryAddress, + crowdNodeBlockchainApi, + notificationService, + crowdNodeConfig, + resources, + Intent(this, StakingActivity::class.java) + ) + } else { + null + } + } + + private fun loadStream(filename: String): InputStream? { + return try { + assets.open(filename) + } catch (x: IOException) { + log.warn("cannot load the bootstrap stream: {}", x.message) + null + } + } + + private fun closeStream(mnlistinfoBootStrapStream: InputStream?) { + if (mnlistinfoBootStrapStream != null) { + try { + mnlistinfoBootStrapStream.close() + } catch (x: IOException) { + //do nothing + } + } + } + + // TODO: should we have a backup blockchain file? + // private NewBestBlockListener newBestBlockListener = block -> { + // try { + // backupBlockStore.put(block); + // } catch (BlockStoreException x) { + // throw new RuntimeException(x); + // } + // }; + @Throws(BlockStoreException::class) + private fun verifyBlockStore(store: BlockStore?): Boolean { + var cursor = store!!.chainHead + for (i in 0..9) { + cursor = cursor!!.getPrev(store) + if (cursor == null || cursor.header == Constants.NETWORK_PARAMETERS.genesisBlock) { + break + } + } + return true + } + + private fun verifyBlockStore( + store: BlockStore?, + scheduledExecutorService: ScheduledExecutorService + ): Boolean { + return try { + val future = scheduledExecutorService.schedule( + { verifyBlockStore(store) }, 100, TimeUnit.MILLISECONDS + ) + future[1, TimeUnit.SECONDS] + } catch (e: Exception) { + log.warn("verification of blockstore failed:", e) + false + } + } + + // TODO: should we have a backup blockchain file? + // public static void copyFile(File source, File destination) throws IOException { + // try (FileChannel sourceChannel = new FileInputStream(source).getChannel(); + // FileChannel destChannel = new FileOutputStream(destination).getChannel()) { + // sourceChannel.transferTo(0, sourceChannel.size(), destChannel); + // } + // } + // + // + // private void replaceBlockStore(BlockStore a, File aFile, BlockStore b, File bFile) throws BlockStoreException { + // try { + // a.close(); + // b.close(); + // copyFile(bFile, aFile); + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // } + @Throws(BlockStoreException::class) + private fun verifyBlockStores() { + val scheduledExecutorService: ScheduledExecutorService = ScheduledThreadPoolExecutor(1) + log.info("verifying backupBlockStore") + // boolean verifiedBackupBlockStore = false; + var verifiedHeaderStore = false + var verifiedBlockStore = false + // if (!(verifiedBackupBlockStore = verifyBlockStore(backupBlockStore, scheduledExecutorService))) { +// log.info("backupBlockStore verification failed"); +// } + log.info("verifying headerStore") + if (!verifyBlockStore(headerStore, scheduledExecutorService).also { + verifiedHeaderStore = it + }) { + log.info("headerStore verification failed") + } + log.info("verifying blockStore") + if (!verifyBlockStore(blockStore, scheduledExecutorService).also { + verifiedBlockStore = it + }) { + log.info("blockStore verification failed") + } + // TODO: should we have a backup blockchain file? +// if (!verifiedBlockStore) { +// if (verifiedBackupBlockStore && +// !backupBlockStore.getChainHead().getHeader().getHash().equals(Constants.NETWORK_PARAMETERS.getGenesisBlock().getHash())) { +// log.info("replacing blockStore with backup"); +// replaceBlockStore(blockStore, blockChainFile, backupBlockStore, backupBlockChainFile); +// log.info("reloading blockStore"); +// blockStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile); +// blockStore.getChainHead(); // detect corruptions as early as possible +// log.info("reloading backup blockchain file"); +// backupBlockStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile); +// backupBlockStore.getChainHead(); // detect corruptions as early as possible +// verifyBlockStores(); +// } /*else if (verifiedHeaderStore) { +// log.info("replacing blockStore with header"); +// replaceBlockStore(blockStore, blockChainFile, headerStore, headerChainFile); +// log.info("reloading blockStore"); +// blockStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile); +// blockStore.getChainHead(); // detect corruptions as early as possible +// log.info("reloading header file"); +// headerStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, headerChainFile); +// headerStore.getChainHead(); // detect corruptions as early as possible +// verifyBlockStores(); +// } else*/ { +// // get blocks from platform here... +// throw new BlockStoreException("can't verify and recover"); +// } +// } + log.info("blockstore files verified: {}, {}", verifiedBlockStore, verifiedHeaderStore) + } +} diff --git a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImplOld.kt b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImplOld.kt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wallet/src/de/schildbach/wallet/service/BlockchainStateDataProvider.kt b/wallet/src/de/schildbach/wallet/service/BlockchainStateDataProvider.kt index d20cfdf68f..2c9d872808 100644 --- a/wallet/src/de/schildbach/wallet/service/BlockchainStateDataProvider.kt +++ b/wallet/src/de/schildbach/wallet/service/BlockchainStateDataProvider.kt @@ -21,6 +21,7 @@ import android.content.Context import android.database.sqlite.SQLiteException import dagger.hilt.android.qualifiers.ApplicationContext import de.schildbach.wallet.Constants +import de.schildbach.wallet.WalletApplication import de.schildbach.wallet.database.dao.BlockchainStateDao import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher @@ -65,6 +66,7 @@ import kotlin.math.min class BlockchainStateDataProvider @Inject constructor( @ApplicationContext private val context: Context, + private val dashSystemService: DashSystemService, private val blockchainStateDao: BlockchainStateDao, private val walletDataProvider: WalletDataProvider, private val configuration: Configuration @@ -114,9 +116,9 @@ class BlockchainStateDataProvider @Inject constructor( blockchainState = BlockchainState() } val chainHead: StoredBlock = blockChain.chainHead - val chainLockHeight = walletDataProvider.wallet!!.context.chainLockHandler.bestChainLockBlockHeight + val chainLockHeight = dashSystemService.system.chainLockHandler.bestChainLockBlockHeight val mnListHeight: Int = - walletDataProvider.wallet!!.context.masternodeListManager.listAtChainTip.height.toInt() + dashSystemService.system.masternodeListManager.listAtChainTip.height.toInt() blockchainState.bestChainDate = chainHead.header.time blockchainState.bestChainHeight = chainHead.height blockchainState.impediments = EnumSet.copyOf(impediments) @@ -180,7 +182,7 @@ class BlockchainStateDataProvider @Inject constructor( } override fun getBlockChain(): AbstractBlockChain? { - return walletDataProvider.wallet?.context?.blockChain + return dashSystemService.system.blockChain } override fun observeBlockChain(): Flow { @@ -192,8 +194,8 @@ class BlockchainStateDataProvider @Inject constructor( } override fun getMasternodeAPY(): Double { - val masternodeListManager = walletDataProvider.wallet?.context?.masternodeListManager - val blockChain = walletDataProvider.wallet?.context?.blockChain + val masternodeListManager = dashSystemService.system.masternodeListManager + val blockChain = dashSystemService.system.blockChain if (masternodeListManager != null && blockChain != null) { val mnlist = masternodeListManager.listAtChainTip if (mnlist.height != 0L) { diff --git a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt index cd80ceb1a2..7a23391b56 100644 --- a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt +++ b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt @@ -111,6 +111,7 @@ const val MAX_ALLOWED_BEHIND_TIMESKEW = 20000L // 20 seconds @Singleton class CoinJoinMixingService @Inject constructor( @ApplicationContext private val context: Context, + val dashSystemService: DashSystemService, val walletDataProvider: WalletDataProvider, private val blockchainStateProvider: BlockchainStateProvider, private val config: CoinJoinConfig, @@ -138,7 +139,7 @@ class CoinJoinMixingService @Inject constructor( } private val coinJoinManager: CoinJoinManager? - get() = walletDataProvider.wallet?.context?.coinJoinManager + get() = dashSystemService.system.coinJoinManager private lateinit var clientManager: CoinJoinClientManager private var mixingCompleteListeners: ArrayList = arrayListOf() @@ -489,7 +490,13 @@ class CoinJoinMixingService @Inject constructor( clear() val wallet = walletDataProvider.wallet!! coinJoinManager?.run { - clientManager = CoinJoinClientManager(wallet) + clientManager = CoinJoinClientManager( + wallet, + dashSystemService.system.masternodeSync, + this, + dashSystemService.system.masternodeListManager, + dashSystemService.system.masternodeMetaDataManager, + ) coinJoinClientManagers[wallet.description] = clientManager // this allows mixing to wait for the last transaction to be confirmed // clientManager.addContinueMixingOnError(PoolStatus.ERR_NO_INPUTS) diff --git a/wallet/src/de/schildbach/wallet/service/DashSystemService.kt b/wallet/src/de/schildbach/wallet/service/DashSystemService.kt new file mode 100644 index 0000000000..9206a7f68b --- /dev/null +++ b/wallet/src/de/schildbach/wallet/service/DashSystemService.kt @@ -0,0 +1,14 @@ +package de.schildbach.wallet.service + +import de.schildbach.wallet.Constants +import org.bitcoinj.manager.DashSystem +import javax.inject.Inject + + +interface DashSystemService { + val system: DashSystem +} + +class DashSystemServiceImpl @Inject constructor() : DashSystemService { + override val system = DashSystem(Constants.CONTEXT) +} \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt b/wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt index 02961d5c87..768dedb504 100644 --- a/wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt +++ b/wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt @@ -21,6 +21,7 @@ import com.google.common.base.Preconditions import de.schildbach.wallet.database.entity.DashPayContactRequest import de.schildbach.wallet.database.entity.DashPayProfile import de.schildbach.wallet.security.SecurityGuard +import de.schildbach.wallet.service.DashSystemService import de.schildbach.wallet.ui.dashpay.PlatformRepo import org.bitcoinj.core.Context import org.bitcoinj.core.ECKey @@ -58,6 +59,7 @@ interface PlatformBroadcastService { } class PlatformDocumentBroadcastService @Inject constructor( + val dashSystemService: DashSystemService, val platform: PlatformService, val platformRepo: PlatformRepo, val analytics: AnalyticsService, @@ -144,7 +146,7 @@ class PlatformDocumentBroadcastService @Inject constructor( val masternodeKey = ECKey.fromPrivate(masternodeKeyBytes) val votingKeyId = KeyId.fromBytes(masternodeKey.pubKeyHash) val boas = ByteArrayOutputStream(32 + 20) - val masternodes = walletDataProvider.wallet!!.context.masternodeListManager.masternodeList.getMasternodesByVotingKey(votingKeyId) + val masternodes = dashSystemService.system.masternodeListManager.masternodeList.getMasternodesByVotingKey(votingKeyId) masternodes.forEach { masternode -> try { boas.write(masternode.proTxHash.bytes) diff --git a/wallet/src/de/schildbach/wallet/ui/BlockListAdapter.java b/wallet/src/de/schildbach/wallet/ui/BlockListAdapter.java index b0ef639622..30451d902d 100644 --- a/wallet/src/de/schildbach/wallet/ui/BlockListAdapter.java +++ b/wallet/src/de/schildbach/wallet/ui/BlockListAdapter.java @@ -200,9 +200,9 @@ public void onClick(final View v) { } */ - final StoredBlock block = wallet.getContext().chainLockHandler.getBestChainLockBlock(); - final int chainLockHeight = block != null ? block.getHeight() : 0; - final int mnListHeight = (int) wallet.getContext().masternodeListManager.getListAtChainTip().getHeight(); +// final StoredBlock block = wallet.getContext().chainLockHandler.getBestChainLockBlock(); +// final int chainLockHeight = block != null ? block.getHeight() : 0; +// final int mnListHeight = (int) wallet.getContext().masternodeListManager.getListAtChainTip().getHeight(); /* if(chainLockHeight == storedBlock.getHeight() || mnListHeight == storedBlock.getHeight()) { diff --git a/wallet/src/de/schildbach/wallet/ui/DashPayUserActivity.kt b/wallet/src/de/schildbach/wallet/ui/DashPayUserActivity.kt index a2da17fe3f..be2846b560 100644 --- a/wallet/src/de/schildbach/wallet/ui/DashPayUserActivity.kt +++ b/wallet/src/de/schildbach/wallet/ui/DashPayUserActivity.kt @@ -71,6 +71,7 @@ class DashPayUserActivity : LockScreenActivity() { { searchResult, position -> onIgnoreRequest(searchResult, position) }, { onUserAlertDismiss(it) }, { onItemClicked(it) }, + viewModel.getChainLockBlockHeight(), true, showContactHistoryDisclaimer ) diff --git a/wallet/src/de/schildbach/wallet/ui/DashPayUserActivityViewModel.kt b/wallet/src/de/schildbach/wallet/ui/DashPayUserActivityViewModel.kt index 657b24d378..a1df3f9517 100644 --- a/wallet/src/de/schildbach/wallet/ui/DashPayUserActivityViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/DashPayUserActivityViewModel.kt @@ -28,6 +28,7 @@ import de.schildbach.wallet.data.CreditBalanceInfo import de.schildbach.wallet.data.UsernameSearchResult import de.schildbach.wallet.database.entity.DashPayProfile import de.schildbach.wallet.livedata.Resource +import de.schildbach.wallet.service.DashSystemService import de.schildbach.wallet.service.platform.PlatformSyncService import de.schildbach.wallet.ui.dashpay.NotificationsForUserLiveData import de.schildbach.wallet.ui.dashpay.PlatformRepo @@ -45,7 +46,8 @@ class DashPayUserActivityViewModel @Inject constructor( application: Application, val platformSyncService: PlatformSyncService, private val analytics: AnalyticsService, - val platformRepo: PlatformRepo + val platformRepo: PlatformRepo, + private val dashSystemService: DashSystemService ) : AndroidViewModel(application) { companion object { @@ -105,4 +107,8 @@ class DashPayUserActivityViewModel @Inject constructor( suspend fun hasEnoughCredits(): CreditBalanceInfo { return platformRepo.getIdentityBalance() } + + fun getChainLockBlockHeight(): Int { + return dashSystemService.system.chainLockHandler.bestChainLockBlockHeight + } } \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/ui/ResetWalletDialog.kt b/wallet/src/de/schildbach/wallet/ui/ResetWalletDialog.kt index ee2bd55949..5065b1bd66 100644 --- a/wallet/src/de/schildbach/wallet/ui/ResetWalletDialog.kt +++ b/wallet/src/de/schildbach/wallet/ui/ResetWalletDialog.kt @@ -52,8 +52,9 @@ class ResetWalletDialog : DialogFragment() { // 1. wipe the wallet // 2. start OnboardingActivity // 3. close the backstack (Home->More->Security) - WalletApplication.getInstance().triggerWipe() - restartService.performRestart(requireActivity(), true) + WalletApplication.getInstance().triggerWipe() { + restartService.performRestart(requireActivity(), true) + } } positiveText = getString(android.R.string.no) cancelable = false diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsAdapter.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsAdapter.kt index 5c9175a41c..3b3b439a06 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsAdapter.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsAdapter.kt @@ -42,6 +42,7 @@ class NotificationsAdapter(val context: Context, val wallet: Wallet, private val private val ignoreRequest: (usernameSearchResult: UsernameSearchResult, position: Int) -> Unit, private val onUserAlertDismissListener: (Int) -> Unit, private val onItemClicked: ((NotificationItem) -> Unit)?, + private val chainLockBlockHeight: Int, private val fromProfile: Boolean = false, private val fromStrangerQr: Boolean = false) @@ -74,7 +75,6 @@ class NotificationsAdapter(val context: Context, val wallet: Wallet, private val notifyDataSetChanged() } var filteredResults: MutableList = arrayListOf() - var sendContactRequestWorkStateMap: Map> = mapOf() set(value) { field = value @@ -126,7 +126,7 @@ class NotificationsAdapter(val context: Context, val wallet: Wallet, private val notificationViewItem.isNew, position == 0, recentlyModified, showAvatars, acceptRequest, ignoreRequest) } NOTIFICATION_PAYMENT -> { - holder.bind(notificationItem, transactionCache, wallet) + holder.bind(notificationItem, transactionCache, wallet, chainLockBlockHeight) } NOTIFICATION_ALERT -> { val userAlertItem = notificationItem as NotificationItemUserAlert diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsFragment.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsFragment.kt index febe898e90..2ee6de5195 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsFragment.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsFragment.kt @@ -98,7 +98,8 @@ class NotificationsFragment : Fragment(R.layout.fragment_notifications) { { u, position -> onAcceptRequest(u, position) }, { u, position -> onIgnoreRequest(u, position) }, { onUserAlertDismiss(it) }, - { onItemClicked(it) } + { onItemClicked(it) }, + viewModel.getChainLockBlockHeight() ) if (arguments != null && requireArguments().containsKey(EXTRA_MODE)) { diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt index 4657d30951..aa1a26bdd6 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt @@ -414,7 +414,7 @@ class PlatformRepo @Inject constructor( .flatMapLatest { _ -> init() - if (!hasIdentity || blockchainIdentity.identity == null) { + if (!hasIdentity) { return@flatMapLatest flowOf(emptyList()) } @@ -1238,7 +1238,7 @@ class PlatformRepo @Inject constructor( fun getIdentityBalance(identifier: Identifier): CreditBalanceInfo { return CreditBalanceInfo(platform.client.getIdentityBalance(identifier)) } - + suspend fun addInviteUserAlert() { // this alert will be shown or not based on the current balance and will be // managed by NotificationsLiveData diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/notification/NotificationsViewModel.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/notification/NotificationsViewModel.kt index d6ee8e3971..df8123931d 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/notification/NotificationsViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/notification/NotificationsViewModel.kt @@ -24,6 +24,7 @@ import de.schildbach.wallet.WalletApplication import de.schildbach.wallet.database.dao.DashPayProfileDao import de.schildbach.wallet.database.dao.UserAlertDao import de.schildbach.wallet.database.entity.BlockchainIdentityConfig +import de.schildbach.wallet.service.DashSystemService import de.schildbach.wallet.service.platform.PlatformSyncService import de.schildbach.wallet.ui.dashpay.BaseProfileViewModel import de.schildbach.wallet.ui.dashpay.NotificationsLiveData @@ -43,7 +44,8 @@ class NotificationsViewModel @Inject constructor( private val userAlert: UserAlertDao, platformSyncService: PlatformSyncService, private val userAlertDao: UserAlertDao, - private val dashPayConfig: DashPayConfig + private val dashPayConfig: DashPayConfig, + private val dashSystemService: DashSystemService, ) : BaseProfileViewModel(blockchainIdentityDataDao, dashPayProfileDao) { val notificationsLiveData = NotificationsLiveData(walletApplication, platformRepo, platformSyncService, viewModelScope, userAlertDao) @@ -67,4 +69,8 @@ class NotificationsViewModel @Inject constructor( dashPayConfig.set(DashPayConfig.LAST_SEEN_NOTIFICATION_TIME, updatedTime) } } + + fun getChainLockBlockHeight(): Int { + return dashSystemService.system.chainLockHandler.bestChainLockBlockHeight + } } \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/notification/TransactionViewHolder.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/notification/TransactionViewHolder.kt index 69a245044c..e382761c0b 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/notification/TransactionViewHolder.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/notification/TransactionViewHolder.kt @@ -75,10 +75,11 @@ class TransactionViewHolder(val binding: NotificationTransactionRowBinding) : @Suppress("UNCHECKED_CAST") val transactionCache = (args[0] as HashMap) val wallet = (args[1] as Wallet) - bind(tx, transactionCache, wallet) + val chainLockBlockHeight = (args[2] as Int) + bind(tx, transactionCache, wallet, chainLockBlockHeight) } - private fun bind(tx: Transaction, transactionCache: HashMap, wallet: Wallet) { + private fun bind(tx: Transaction, transactionCache: HashMap, wallet: Wallet, chainLockBlockHeight: Int) { if (itemView is CardView) { (itemView as CardView).setCardBackgroundColor(if (itemView.isActivated()) colorBackgroundSelected else colorBackground) } @@ -186,7 +187,7 @@ class TransactionViewHolder(val binding: NotificationTransactionRowBinding) : // Show the secondary status: // var secondaryStatusId = -1 - if (confidence.hasErrors()) secondaryStatusId = txResourceMapper.getErrorName(tx) else if (!txCache.sent) secondaryStatusId = txResourceMapper.getReceivedStatusString(tx, wallet.context) + if (confidence.hasErrors()) secondaryStatusId = txResourceMapper.getErrorName(tx) else if (!txCache.sent) secondaryStatusId = txResourceMapper.getReceivedStatusString(tx, wallet.context, chainLockBlockHeight) if (secondaryStatusId != -1) secondaryStatusView.setText(secondaryStatusId) else secondaryStatusView.text = null secondaryStatusView.setTextColor(secondaryStatusColor) } diff --git a/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt b/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt index 5a63bf5c98..99722b54b6 100644 --- a/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt @@ -58,6 +58,7 @@ import de.schildbach.wallet.ui.dashpay.PlatformRepo import de.schildbach.wallet.ui.dashpay.utils.DashPayConfig import de.schildbach.wallet.ui.dashpay.work.SendContactRequestOperation import de.schildbach.wallet.ui.transactions.TransactionRowView +import de.schildbach.wallet.ui.transactions.TxResourceMapper import de.schildbach.wallet.util.getTimeSkew import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -182,6 +183,7 @@ class MainViewModel @Inject constructor( private val _blockchainSyncPercentage = MutableLiveData() val blockchainSyncPercentage: LiveData get() = _blockchainSyncPercentage + private var chainLockBlockHeight: Int = 0 private val _exchangeRate = MutableLiveData() val exchangeRate: LiveData @@ -341,6 +343,7 @@ class MainViewModel @Inject constructor( .onEach { state -> updateSyncStatus(state) updatePercentage(state) + chainLockBlockHeight = state.chainlockHeight } .launchIn(viewModelWorkerScope) @@ -529,7 +532,8 @@ class MainViewModel @Inject constructor( walletData.transactionBag, Constants.CONTEXT, null, - metadata[tx.txId] + metadata[tx.txId], + chainLockBlockHeight ) txByHash[rowView.id] = rowView rowView @@ -603,7 +607,9 @@ class MainViewModel @Inject constructor( walletData.transactionBag, Constants.CONTEXT, metadata[tx.txId], - contacts[tx.txId] ?: rowView?.contact + contacts[tx.txId] ?: rowView?.contact, + TxResourceMapper(), + chainLockBlockHeight ) } else { TransactionRowView.fromTransactionWrapper( @@ -611,7 +617,8 @@ class MainViewModel @Inject constructor( walletData.transactionBag, Constants.CONTEXT, null, - metadata[tx.txId] + metadata[tx.txId], + chainLockBlockHeight ) } txByHash[transactionRow.id] = transactionRow @@ -685,7 +692,7 @@ class MainViewModel @Inject constructor( val txByHash = this.txByHash.toMutableMap() // Process both old and new metadata in case if some metadata was cleared val allTxIds = (oldMetadata.keys + metadata.keys + contacts.keys).toSet() - + for (txId in allTxIds) { val rowView = txByHash[txId.toString()] @@ -698,7 +705,9 @@ class MainViewModel @Inject constructor( walletData.transactionBag, Constants.CONTEXT, txMetadata, - contacts[txId] ?: rowView.contact + contacts[txId] ?: rowView.contact, + TxResourceMapper(), + chainLockBlockHeight ) txByHash[txId.toString()] = updatedRowView val dateKey = tx.updateTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() diff --git a/wallet/src/de/schildbach/wallet/ui/more/SecurityFragment.kt b/wallet/src/de/schildbach/wallet/ui/more/SecurityFragment.kt index 1b7dc84cc8..7f12c15cab 100644 --- a/wallet/src/de/schildbach/wallet/ui/more/SecurityFragment.kt +++ b/wallet/src/de/schildbach/wallet/ui/more/SecurityFragment.kt @@ -196,9 +196,13 @@ class SecurityFragment : Fragment(R.layout.fragment_security) { private fun doReset() { viewModel.logEvent(AnalyticsConstants.Security.RESET_WALLET) - viewModel.triggerWipe() - startActivity(OnboardingActivity.createIntent(requireContext())) - requireActivity().finishAffinity() + val dialog = AdaptiveDialog.progress(getString(R.string.perm_lock_wipe_wallet)) + dialog.show(requireActivity()) + viewModel.triggerWipe() { + dialog.dismissAllowingStateLoss() + startActivity(OnboardingActivity.createIntent(requireContext())) + requireActivity().finishAffinity() + } } private fun showSeed(seed: Array) { diff --git a/wallet/src/de/schildbach/wallet/ui/more/SecurityViewModel.kt b/wallet/src/de/schildbach/wallet/ui/more/SecurityViewModel.kt index a2275ec289..4a56f0ba91 100644 --- a/wallet/src/de/schildbach/wallet/ui/more/SecurityViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/more/SecurityViewModel.kt @@ -93,8 +93,8 @@ class SecurityViewModel @Inject constructor( analytics.logEvent(event, mapOf()) } - fun triggerWipe() { - walletApplication.triggerWipe() + fun triggerWipe(afterWipeFunction: () -> Unit) { + walletApplication.triggerWipe(afterWipeFunction) } fun setEnableFingerprint(enable: Boolean) { diff --git a/wallet/src/de/schildbach/wallet/ui/transactions/TransactionGroupViewModel.kt b/wallet/src/de/schildbach/wallet/ui/transactions/TransactionGroupViewModel.kt index 95028d3c57..a49f9977d6 100644 --- a/wallet/src/de/schildbach/wallet/ui/transactions/TransactionGroupViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/transactions/TransactionGroupViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import de.schildbach.wallet.service.DashSystemService import de.schildbach.wallet.transactions.coinjoin.CoinJoinMixingTxSet import de.schildbach.wallet.transactions.coinjoin.CoinJoinTxResourceMapper import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -46,13 +47,14 @@ import javax.inject.Inject @HiltViewModel class TransactionGroupViewModel @Inject constructor( val walletData: WalletDataProvider, + val dashSystemService: DashSystemService, val config: Configuration, private val metadataProvider: TransactionMetadataProvider ) : ViewModel() { companion object { private const val THROTTLE_DURATION = 500L } - + private var chainLockBlockHeight: Int = 0 val dashFormat: MonetaryFormat = config.format.noCode() private val _dashValue = MutableLiveData() @@ -76,6 +78,7 @@ class TransactionGroupViewModel @Inject constructor( walletData.observeTransactions(true) .debounce(THROTTLE_DURATION) .onEach { tx -> + chainLockBlockHeight = dashSystemService.system.chainLockHandler.getBestChainLockBlockHeight() if (transactionWrapper.tryInclude(tx)) { refreshTransactions(transactionWrapper, memos) } @@ -97,7 +100,7 @@ class TransactionGroupViewModel @Inject constructor( _transactions.value = transactionWrapper.transactions.values.map { val txMetadata = metadata.getOrDefault(it.txId, null) TransactionRowView.fromTransaction( - it, walletData.wallet!!, walletData.wallet!!.context, txMetadata, null, resourceMapper + it, walletData.wallet!!, walletData.wallet!!.context, txMetadata, null, resourceMapper, chainLockBlockHeight ) } _dashValue.value = transactionWrapper.getValue(walletData.transactionBag) diff --git a/wallet/src/de/schildbach/wallet/ui/transactions/TransactionRowView.kt b/wallet/src/de/schildbach/wallet/ui/transactions/TransactionRowView.kt index d3f3313d88..06a239d5ed 100644 --- a/wallet/src/de/schildbach/wallet/ui/transactions/TransactionRowView.kt +++ b/wallet/src/de/schildbach/wallet/ui/transactions/TransactionRowView.kt @@ -60,7 +60,8 @@ data class TransactionRowView( bag: TransactionBag, context: Context, contact: DashPayProfile?, - metadata: PresentableTxMetadata? = null + metadata: PresentableTxMetadata? = null, + chainLockBlockHeight: Int ): TransactionRowView { val firstTx = txWrapper.transactions.values.first() @@ -102,7 +103,7 @@ data class TransactionRowView( ServiceName.Unknown, txWrapper ) - else -> fromTransaction(firstTx, bag, context, metadata, contact) + else -> fromTransaction(firstTx, bag, context, metadata, contact, chainLockBlockHeight = chainLockBlockHeight) } } @@ -112,7 +113,8 @@ data class TransactionRowView( context: Context, metadata: PresentableTxMetadata? = null, contact: DashPayProfile? = null, - resourceMapper: TxResourceMapper = TxResourceMapper() + resourceMapper: TxResourceMapper = TxResourceMapper(), + chainLockBlockHeight: Int ): TransactionRowView { val value = tx.getValue(bag) val isInternal = tx.isEntirelySelf(bag) @@ -150,7 +152,7 @@ data class TransactionRowView( } val status = if (!hasErrors && !isSent) { - resourceMapper.getReceivedStatusString(tx, context) + resourceMapper.getReceivedStatusString(tx, context, chainLockBlockHeight) } else { -1 } diff --git a/wallet/src/de/schildbach/wallet/ui/transactions/TxResourceMapper.java b/wallet/src/de/schildbach/wallet/ui/transactions/TxResourceMapper.java index 3e2434618b..b125bf584e 100644 --- a/wallet/src/de/schildbach/wallet/ui/transactions/TxResourceMapper.java +++ b/wallet/src/de/schildbach/wallet/ui/transactions/TxResourceMapper.java @@ -172,12 +172,12 @@ public int getErrorName(@NonNull TxError error) { * @return the secondary status or -1 if there is none */ @StringRes - public int getReceivedStatusString(Transaction tx, @NonNull Context context) { + public int getReceivedStatusString(Transaction tx, @NonNull Context context, int bestChainLockBlockHeight) { TransactionConfidence confidence = tx.getConfidence(context); int statusId = -1; if (confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) { int confirmations = confidence.getDepthInBlocks(); - boolean isChainLocked = context.chainLockHandler.getBestChainLockBlockHeight() >= confidence.getDepthInBlocks(); + boolean isChainLocked = bestChainLockBlockHeight >= confidence.getDepthInBlocks(); // process coinbase transactions (Mining Rewards) before other BUILDING transactions if (tx.isCoinBase()) { diff --git a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt index 0e114c1c01..0c82a0bea7 100644 --- a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt @@ -28,6 +28,7 @@ import de.schildbach.wallet.database.dao.UsernameVoteDao import de.schildbach.wallet.database.entity.ImportedMasternodeKey import de.schildbach.wallet.database.entity.UsernameRequest import de.schildbach.wallet.database.entity.UsernameVote +import de.schildbach.wallet.service.DashSystemService import de.schildbach.wallet.service.platform.PlatformSyncService import de.schildbach.wallet.ui.dashpay.utils.DashPayConfig import de.schildbach.wallet.ui.dashpay.work.BroadcastUsernameVotesOperation @@ -122,6 +123,7 @@ class UsernameRequestsViewModel @Inject constructor( private val platformSyncService: PlatformSyncService, private val walletDataProvider: WalletDataProvider, private val walletApplication: WalletApplication, + private val dashSystemService: DashSystemService, private val analytics: AnalyticsService ): ViewModel() { companion object { @@ -151,7 +153,7 @@ class UsernameRequestsViewModel @Inject constructor( } private val masternodeListManager: SimplifiedMasternodeListManager - get() = walletDataProvider.wallet!!.context.masternodeListManager + get() = dashSystemService.system.masternodeListManager private val _addedKeys = MutableStateFlow(listOf()) private val _masternodes = MutableStateFlow>(listOf())