diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index c6235ef6..ac564160 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -6,6 +6,26 @@ follow [https://changelog.md/](https://changelog.md/) guidelines. ## [Unreleased] +## [51.10] - 2024-05-17 + +### ADDED + +- Background notification processing reliability improvements + +### CHANGED + +- Made outpoints and utxoStatus available to Libwallet's PaymentAnalyzer. Which involved a client +data migration to init utxos' status. +- Enhanced crashes and error reports with extra metadata. +- Include swap_uuid in newop events for better lightning payments metrics. +- Notify logout upon security logout (e.g 3 incorrect pin attempts). + +### FIXED + +- Fixed ANRs happening when trying to send a email error report. +- Adjusted overly verbose logging in release. +- Fixed problems and crashes in devices where VES currency is not supported. + ## [51.9] - 2024-04-30 - Background notification processing reliability improvements diff --git a/android/apollo/src/main/java/io/muun/apollo/data/analytics/AnalyticsProvider.kt b/android/apollo/src/main/java/io/muun/apollo/data/analytics/AnalyticsProvider.kt index 0c42c116..b2c5f5df 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/analytics/AnalyticsProvider.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/analytics/AnalyticsProvider.kt @@ -3,22 +3,36 @@ package io.muun.apollo.data.analytics import android.content.Context import android.content.res.Resources import android.os.Bundle -import android.util.Log import com.google.firebase.analytics.FirebaseAnalytics import io.muun.apollo.domain.analytics.AnalyticsEvent import io.muun.apollo.domain.model.report.CrashReport import io.muun.apollo.domain.model.user.User +import rx.Single +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton -class AnalyticsProvider @Inject constructor(val context: Context) { +class AnalyticsProvider @Inject constructor(context: Context) { private val fba = FirebaseAnalytics.getInstance(context) // Just for enriching error logs. A best effort to add metadata private val inMemoryMapBreadcrumbCollector = sortedMapOf() + fun loadBigQueryPseudoId(): Single = + Single.fromEmitter { emitter -> + fba.appInstanceId + .addOnSuccessListener { id: String? -> + // id can be null on platforms without google play services. + Timber.d("Loaded BigQueryPseudoId: $id") + emitter.onSuccess(id) + } + .addOnFailureListener { error -> + emitter.onError(error) + } + } + /** * Set the user's properties, to be used by Analytics. */ @@ -38,6 +52,12 @@ class AnalyticsProvider @Inject constructor(val context: Context) { fun report(event: AnalyticsEvent) { try { actuallyReport(event) + + // Avoid recursion (Timber.i reports a breadcrumb). TODO proper design and fix this + if (event !is AnalyticsEvent.E_BREADCRUMB) { + Timber.i("AnalyticsProvider", event.toString()) + } + } catch (t: Throwable) { val bundle = Bundle().apply { putString("event", event.eventId) } @@ -60,7 +80,6 @@ class AnalyticsProvider @Inject constructor(val context: Context) { fba.logEvent(event.eventId, bundle) inMemoryMapBreadcrumbCollector[System.currentTimeMillis()] = bundle - Log.i("AnalyticsProvider", event.toString()) } private fun getBreadcrumbMetadata(): String { diff --git a/android/apollo/src/main/java/io/muun/apollo/data/logging/Crashlytics.kt b/android/apollo/src/main/java/io/muun/apollo/data/logging/Crashlytics.kt index b50d731c..467a94c5 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/logging/Crashlytics.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/logging/Crashlytics.kt @@ -1,10 +1,14 @@ package io.muun.apollo.data.logging import android.app.Application +import android.os.Build import com.google.firebase.crashlytics.FirebaseCrashlytics import io.muun.apollo.data.analytics.AnalyticsProvider +import io.muun.apollo.data.os.GooglePlayServicesHelper +import io.muun.apollo.data.os.OS +import io.muun.apollo.data.os.TelephonyInfoProvider +import io.muun.apollo.data.os.getInstallSourceInfo import io.muun.apollo.domain.action.debug.ForceCrashReportAction -import io.muun.apollo.domain.analytics.Analytics import io.muun.apollo.domain.analytics.AnalyticsEvent import io.muun.apollo.domain.errors.fcm.FcmTokenNotAvailableError import io.muun.apollo.domain.errors.newop.CyclicalSwapError @@ -14,8 +18,10 @@ import io.muun.apollo.domain.errors.newop.InvoiceExpiresTooSoonException import io.muun.apollo.domain.errors.newop.InvoiceMissingAmountException import io.muun.apollo.domain.errors.newop.NoPaymentRouteException import io.muun.apollo.domain.errors.newop.UnreachableNodeException +import io.muun.apollo.domain.model.InstallSourceInfo import io.muun.apollo.domain.model.report.CrashReport import io.muun.apollo.domain.utils.isInstanceOrIsCausedByError +import timber.log.Timber object Crashlytics { @@ -25,11 +31,39 @@ object Crashlytics { null } - private var analytics: Analytics? = null + private var analyticsProvider: AnalyticsProvider? = null + + private var bigQueryPseudoId: String? = null + + private var googlePlayServicesAvailable: Boolean? = null + + private var installSource: InstallSourceInfo? = null + + private var region: String? = null + + private var defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null @JvmStatic fun init(application: Application) { - this.analytics = Analytics(AnalyticsProvider(application)) + this.analyticsProvider = AnalyticsProvider(application) + this.analyticsProvider?.loadBigQueryPseudoId() + ?.subscribe({ bigQueryPseudoId = it }, { Timber.e(it) }) + + this.googlePlayServicesAvailable = GooglePlayServicesHelper(application).isAvailable + this.installSource = application.getInstallSourceInfo() + this.region = TelephonyInfoProvider(application).region.orElse("null") + + this.defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler(customUncaughtExceptionHandler) + } + + // enhance crashlytics crashes with custom keys + private val customUncaughtExceptionHandler = Thread.UncaughtExceptionHandler { thread, ex -> + + setStaticCustomKeys() + + //call the default exception handler + this.defaultUncaughtExceptionHandler?.uncaughtException(thread, ex) } /** @@ -48,7 +82,7 @@ object Crashlytics { @Deprecated("Not really but you shouldn't use this directly. Use Timber.i(). See MuunTree.") fun logBreadcrumb(breadcrumb: String) { crashlytics?.log(breadcrumb) - analytics?.report( + analyticsProvider?.report( AnalyticsEvent.E_BREADCRUMB( breadcrumb ) @@ -65,15 +99,17 @@ object Crashlytics { return } + // Note: these custom keys are associated with the non-fatal error being tracked but also + // with the subsequent crash if the error generates one (e.g if error isn't caught/handled). crashlytics?.setCustomKey("tag", report.tag) crashlytics?.setCustomKey("message", report.message) - crashlytics?.setCustomKey("locale", LoggingContext.locale) + setStaticCustomKeys() for (entry in report.metadata.entries) { crashlytics?.setCustomKey(entry.key, entry.value.toString()) } - analytics?.report( + analyticsProvider?.report( AnalyticsEvent.E_CRASHLYTICS_ERROR( report.error.javaClass.simpleName + ":" + report.error.localizedMessage ) @@ -82,6 +118,27 @@ object Crashlytics { crashlytics?.recordException(report.error) } + private fun setStaticCustomKeys() { + crashlytics?.setCustomKey("locale", LoggingContext.locale) + crashlytics?.setCustomKey("region", region ?: "null") + crashlytics?.setCustomKey("bigQueryPseudoId", bigQueryPseudoId ?: "null") + crashlytics?.setCustomKey("abi", getSupportedAbi()) + crashlytics?.setCustomKey("isPlayServicesAvailable", googlePlayServicesAvailable.toString()) + crashlytics?.setCustomKey( + "installSource-installingPackage", installSource?.installingPackageName ?: "null" + ) + crashlytics?.setCustomKey( + "installSource-initiatingPackage", installSource?.initiatingPackageName ?: "null" + ) + } + + private fun getSupportedAbi() = + if (OS.supportsSupportedAbis()) { + Build.SUPPORTED_ABIS[0] + } else { + "api19" + } + /** * Send a "fallback" reporting error to Crashlytics. This means that there was an error while * doing our usual error report processing. Hence we try to report the original error data (tag, diff --git a/android/apollo/src/main/java/io/muun/apollo/data/logging/MuunTree.kt b/android/apollo/src/main/java/io/muun/apollo/data/logging/MuunTree.kt index a2235327..79f7d20b 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/logging/MuunTree.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/logging/MuunTree.kt @@ -1,7 +1,6 @@ package io.muun.apollo.data.logging import android.util.Log -import io.muun.apollo.data.logging.Crashlytics.logBreadcrumb import io.muun.apollo.domain.model.report.CrashReportBuilder import timber.log.Timber @@ -13,20 +12,20 @@ class MuunTree : Timber.DebugTree() { override fun log(priority: Int, tag: String?, message: String?, error: Throwable?) { // For low priority logs, we don't have any special treatment: if (priority < Log.INFO) { - super.log(priority, tag, message, error) + sendToLogcat(priority, tag, message, error) return } when (priority) { Log.INFO -> { - Log.i("Breadcrumb", message!!) + sendToLogcat(Log.INFO, "Breadcrumb", message!!, null) @Suppress("DEPRECATION") // I know. These are the only allowed usages. - logBreadcrumb(message) + Crashlytics.logBreadcrumb(message) } Log.WARN -> { - Log.w(tag, message!!) + sendToLogcat(Log.WARN, tag, message!!, null) @Suppress("DEPRECATION") // I know. These are the only allowed usages. - logBreadcrumb("Warning: $message") + Crashlytics.logBreadcrumb("Warning: $message") } else -> { // Log.ERROR && Log.ASSERT sendCrashReport(tag, message, error) @@ -50,9 +49,7 @@ class MuunTree : Timber.DebugTree() { Crashlytics.reportError(report) } - if (LoggingContext.sendToLogcat) { - Log.e(report.tag, "${report.message} ${report.metadata}", report.error) - } + sendToLogcat(Log.ERROR, report.tag, "${report.message} ${report.metadata}", report.error) } private fun sendFallbackCrashReport( @@ -62,13 +59,17 @@ class MuunTree : Timber.DebugTree() { crashReportingError: Throwable, ) { - if (LoggingContext.sendToLogcat) { - Log.e("CrashReport:$tag", "During error processing", crashReportingError) - Log.e("CrashReport:$tag", message, error) - } + sendToLogcat(Log.ERROR, "CrashReport:$tag", "During error processing", crashReportingError) + sendToLogcat(Log.ERROR, "CrashReport:$tag", message, error) if (LoggingContext.sendToCrashlytics) { Crashlytics.reportReportingError(tag, message, error, crashReportingError) } } + + private fun sendToLogcat(priority: Int, tag: String?, message: String?, error: Throwable?) { + if (LoggingContext.sendToLogcat) { + super.log(priority, tag, message, error) + } + } } \ No newline at end of file diff --git a/android/apollo/src/main/java/io/muun/apollo/data/net/ConnectivityInfoProvider.kt b/android/apollo/src/main/java/io/muun/apollo/data/net/ConnectivityInfoProvider.kt index e77559e1..974420d0 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/net/ConnectivityInfoProvider.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/net/ConnectivityInfoProvider.kt @@ -57,4 +57,19 @@ class ConnectivityInfoProvider @Inject constructor(context: Context) { return if (isVpnNetworkAvailable) 2 else 3 } + + val proxyHttp: String + get() { + return System.getProperty("http.proxyHost") ?: "" + } + + val proxyHttps: String + get() { + return System.getProperty("https.proxyHost") ?: "" + } + + val proxySocks: String + get() { + return System.getProperty("socks.proxyHost") ?: "" + } } \ No newline at end of file diff --git a/android/apollo/src/main/java/io/muun/apollo/data/net/HoustonClient.java b/android/apollo/src/main/java/io/muun/apollo/data/net/HoustonClient.java index c9215bf8..ee8ea71c 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/net/HoustonClient.java +++ b/android/apollo/src/main/java/io/muun/apollo/data/net/HoustonClient.java @@ -6,6 +6,7 @@ import io.muun.apollo.data.os.GooglePlayHelper; import io.muun.apollo.data.os.GooglePlayServicesHelper; import io.muun.apollo.data.os.HardwareCapabilitiesProvider; +import io.muun.apollo.data.os.OS_ExtensionsKt; import io.muun.apollo.domain.errors.newop.CyclicalSwapError; import io.muun.apollo.domain.errors.newop.InvalidInvoiceException; import io.muun.apollo.domain.errors.newop.InvoiceAlreadyUsedException; @@ -95,10 +96,15 @@ public class HoustonClient extends BaseClient { private final ModelObjectsMapper modelMapper; + private final ApiObjectsMapper apiMapper; + private final Context context; + private final HardwareCapabilitiesProvider hardwareCapabilitiesProvider; + private final GooglePlayServicesHelper googlePlayServicesHelper; + private final GooglePlayHelper googlePlayHelper; /** @@ -144,7 +150,7 @@ public Observable createFirstSession( hardwareCapabilitiesProvider.getAndroidId(), hardwareCapabilitiesProvider.getSystemUsersInfo(), hardwareCapabilitiesProvider.getDrmClientIds(), - HoustonClient_ExtensionsKt.getInstallSourceInfo(context), + OS_ExtensionsKt.getInstallSourceInfo(context), hardwareCapabilitiesProvider.getBootCount(), hardwareCapabilitiesProvider.getGlEsVersion(), CpuInfoProvider.INSTANCE.getCpuInfo(), @@ -174,7 +180,7 @@ public Observable createLoginSession( hardwareCapabilitiesProvider.getAndroidId(), hardwareCapabilitiesProvider.getSystemUsersInfo(), hardwareCapabilitiesProvider.getDrmClientIds(), - HoustonClient_ExtensionsKt.getInstallSourceInfo(context), + OS_ExtensionsKt.getInstallSourceInfo(context), hardwareCapabilitiesProvider.getBootCount(), hardwareCapabilitiesProvider.getGlEsVersion(), CpuInfoProvider.INSTANCE.getCpuInfo(), @@ -204,7 +210,7 @@ public Observable createRcLoginSession( hardwareCapabilitiesProvider.getAndroidId(), hardwareCapabilitiesProvider.getSystemUsersInfo(), hardwareCapabilitiesProvider.getDrmClientIds(), - HoustonClient_ExtensionsKt.getInstallSourceInfo(context), + OS_ExtensionsKt.getInstallSourceInfo(context), hardwareCapabilitiesProvider.getBootCount(), hardwareCapabilitiesProvider.getGlEsVersion(), CpuInfoProvider.INSTANCE.getCpuInfo(), @@ -347,6 +353,13 @@ public Observable notifyLogout(String authHeader) { return getService().notifyLogout(authHeader); } + /** + * [Only works for "Multiple sessions" users] Expire all user sessions except the current one. + */ + public Completable expireAllOtherSessions() { + return getService().expireAllOtherSessions(); + } + /** * Updates the GCM token for the current user. */ @@ -359,7 +372,8 @@ public Observable updateFcmToken(@NotNull String fcmToken) { * not return all the existing notifications, it's up to the caller to make subsequent calls. */ public Observable fetchNotificationReportAfter( - @Nullable Long notificationId) { + @Nullable Long notificationId + ) { return getService().fetchNotificationReportAfter(notificationId) .map(modelMapper::mapNotificationReport); @@ -372,7 +386,8 @@ public Observable confirmNotificationsDeliveryUntil( final long notificationId, final String deviceModel, final String osVersion, - final String appStatus) { + final String appStatus + ) { return getService().confirmNotificationsDeliveryUntil( notificationId, deviceModel, osVersion, appStatus @@ -741,8 +756,10 @@ public Single fetchFulfillmentData(final String inc /** * Push the fulfillment TX for an incoming swap. */ - public Completable pushFulfillmentTransaction(final String incomingSwap, - final RawTransaction rawTransaction) { + public Completable pushFulfillmentTransaction( + final String incomingSwap, + final RawTransaction rawTransaction + ) { return getService().pushFulfillmentTransaction(incomingSwap, rawTransaction); } diff --git a/android/apollo/src/main/java/io/muun/apollo/data/os/BackgroundExecutionMetricsProvider.kt b/android/apollo/src/main/java/io/muun/apollo/data/os/BackgroundExecutionMetricsProvider.kt index 7bbf22de..6241e0f1 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/os/BackgroundExecutionMetricsProvider.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/os/BackgroundExecutionMetricsProvider.kt @@ -65,7 +65,10 @@ class BackgroundExecutionMetricsProvider @Inject constructor( systemCapabilitiesProvider.usbPersistConfig, systemCapabilitiesProvider.bridgeEnabled, systemCapabilitiesProvider.bridgeDaemonStatus, - systemCapabilitiesProvider.developerEnabled + systemCapabilitiesProvider.developerEnabled, + connectivityInfoProvider.proxyHttp, + connectivityInfoProvider.proxyHttps, + connectivityInfoProvider.proxySocks ) @Suppress("ArrayInDataClass") @@ -102,6 +105,9 @@ class BackgroundExecutionMetricsProvider @Inject constructor( private val bridgeEnabled: Int, private val bridgeDaemonStatus: String, private val developerEnabled: Int, + private val proxyHttp: String, + private val proxyHttps: String, + private val proxySocks: String, ) /** diff --git a/android/apollo/src/main/java/io/muun/apollo/data/os/GooglePlayServicesHelper.kt b/android/apollo/src/main/java/io/muun/apollo/data/os/GooglePlayServicesHelper.kt index 37576ba7..542cb868 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/os/GooglePlayServicesHelper.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/os/GooglePlayServicesHelper.kt @@ -13,7 +13,7 @@ import javax.inject.Inject class GooglePlayServicesHelper @Inject constructor(private val ctx: Context) { companion object { - const val AVAILABLE = ConnectionResult.SUCCESS + private const val AVAILABLE = ConnectionResult.SUCCESS private const val PLAY_SERVICES_RESOLUTION_REQUEST = 9000 } @@ -28,12 +28,20 @@ class GooglePlayServicesHelper @Inject constructor(private val ctx: Context) { null } + /** + * Check if Google Play Services is installed on the device. + * + * @return true if Google Play Services is available, false otherwise + */ + val isAvailable: Boolean + get() = isAvailableResultCode == AVAILABLE + /** * Check if Google Play Services is installed on the device. * * @return the result code, which will be AVAILABLE if successful. */ - val isAvailable: Int + private val isAvailableResultCode: Int get() = apiAvailability.isGooglePlayServicesAvailable(ctx) /** @@ -67,9 +75,10 @@ class GooglePlayServicesHelper @Inject constructor(private val ctx: Context) { /** * Display a dialog that allows the user to install Google Play Services. */ - fun showDownloadDialog(resultCode: Int): Action1 { + fun showDownloadDialog(): Action1 { + val resultCode = isAvailableResultCode return Action1 { activity: Activity -> - if (apiAvailability.isUserResolvableError(resultCode)) { + if (apiAvailability.isUserResolvableError(isAvailableResultCode)) { apiAvailability .getErrorDialog(activity, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST) ?.show() diff --git a/android/apollo/src/main/java/io/muun/apollo/data/net/HoustonClient+Extensions.kt b/android/apollo/src/main/java/io/muun/apollo/data/os/OS+Extensions.kt similarity index 93% rename from android/apollo/src/main/java/io/muun/apollo/data/net/HoustonClient+Extensions.kt rename to android/apollo/src/main/java/io/muun/apollo/data/os/OS+Extensions.kt index 97a8c103..c521f9a3 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/net/HoustonClient+Extensions.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/os/OS+Extensions.kt @@ -1,7 +1,6 @@ -package io.muun.apollo.data.net +package io.muun.apollo.data.os import android.content.Context -import io.muun.apollo.data.os.OS import io.muun.apollo.domain.model.InstallSourceInfo fun Context.getInstallSourceInfo(): InstallSourceInfo = diff --git a/android/apollo/src/main/java/io/muun/apollo/data/os/OS.kt b/android/apollo/src/main/java/io/muun/apollo/data/os/OS.kt index a2b4f1a2..0840fbd2 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/os/OS.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/os/OS.kt @@ -152,6 +152,11 @@ object OS { fun supportsNetworkCapabilities(): Boolean = isAndroidLOrNewer() + /** + * Whether this OS supports Build.SUPPORTED_ABIS, which was introduced in L-5-21. + */ + fun supportsSupportedAbis(): Boolean = + isAndroidLOrNewer() // PRIVATE STUFF: @@ -186,7 +191,7 @@ object OS { * Whether this OS version is L-5.0-21 or newer. */ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.LOLLIPOP) - private fun isAndroidLOrNewer() = + private fun isAndroidLOrNewer(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP /** @@ -216,4 +221,5 @@ object OS { @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N) private fun isAndroidNOrNewer(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + } \ No newline at end of file diff --git a/android/apollo/src/main/java/io/muun/apollo/data/os/TelephonyInfoProvider.kt b/android/apollo/src/main/java/io/muun/apollo/data/os/TelephonyInfoProvider.kt index 3c0c2d3e..ffc41a1c 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/os/TelephonyInfoProvider.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/os/TelephonyInfoProvider.kt @@ -8,7 +8,7 @@ import javax.inject.Inject private const val UNKNOWN = "UNKNOWN" // TODO open to make tests work with mockito. We should probably move to mockK -open class TelephonyInfoProvider @Inject constructor(private val context: Context) { +open class TelephonyInfoProvider @Inject constructor(context: Context) { private val telephonyManager: TelephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager diff --git a/android/apollo/src/main/java/io/muun/apollo/data/preferences/FirebaseInstallationIdRepository.kt b/android/apollo/src/main/java/io/muun/apollo/data/preferences/FirebaseInstallationIdRepository.kt index cf97dd06..2fdce0e5 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/preferences/FirebaseInstallationIdRepository.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/preferences/FirebaseInstallationIdRepository.kt @@ -40,4 +40,11 @@ class FirebaseInstallationIdRepository @Inject constructor( fun watchFcmToken(): Observable { return fcmTokenPreference.asObservable() } + + /** + * Used solely for error reporting. We should always prioritize using watchFcmToken(). + */ + fun getFcmToken(): String? { + return fcmTokenPreference.get() + } } \ No newline at end of file diff --git a/android/apollo/src/main/java/io/muun/apollo/data/preferences/RepositoryRegistry.kt b/android/apollo/src/main/java/io/muun/apollo/data/preferences/RepositoryRegistry.kt index 45f95fd2..d5b79fc2 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/preferences/RepositoryRegistry.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/preferences/RepositoryRegistry.kt @@ -27,29 +27,29 @@ class RepositoryRegistry { private val lock = this private val registry: Set> = setOf( - ApiMigrationsVersionRepository::class.java, - AuthRepository::class.java, - BlockchainHeightRepository::class.java, - ClientVersionRepository::class.java, - ExchangeRateWindowRepository::class.java, - FeeWindowRepository::class.java, - FirebaseInstallationIdRepository::class.java, - ForwardingPoliciesRepository::class.java, - MinFeeRateRepository::class.java, - KeysRepository::class.java, - NightModeRepository::class.java, - NotificationRepository::class.java, - SchemaVersionRepository::class.java, - TransactionSizeRepository::class.java, - UserPreferencesRepository::class.java, - UserRepository::class.java, - FeaturesRepository::class.java, - AppVersionRepository::class.java, - PlayIntegrityNonceRepository::class.java, - NotificationPermissionStateRepository::class.java, - NotificationPermissionDeniedRepository::class.java, - NotificationPermissionSkippedRepository::class.java, - BackgroundTimesRepository::class.java + ApiMigrationsVersionRepository::class.java, + AuthRepository::class.java, + BlockchainHeightRepository::class.java, + ClientVersionRepository::class.java, + ExchangeRateWindowRepository::class.java, + FeeWindowRepository::class.java, + FirebaseInstallationIdRepository::class.java, + ForwardingPoliciesRepository::class.java, + MinFeeRateRepository::class.java, + KeysRepository::class.java, + NightModeRepository::class.java, + NotificationRepository::class.java, + SchemaVersionRepository::class.java, + TransactionSizeRepository::class.java, + UserPreferencesRepository::class.java, + UserRepository::class.java, + FeaturesRepository::class.java, + AppVersionRepository::class.java, + PlayIntegrityNonceRepository::class.java, + NotificationPermissionStateRepository::class.java, + NotificationPermissionDeniedRepository::class.java, + NotificationPermissionSkippedRepository::class.java, + BackgroundTimesRepository::class.java ) // Notable exceptions: @@ -63,11 +63,11 @@ class RepositoryRegistry { // its more clear and clean to keep it and avoid wiping it (there's no privacy or security // issues). private val logoutSurvivorRepositories: Set> = setOf( - FirebaseInstallationIdRepository::class.java, - NightModeRepository::class.java, - SchemaVersionRepository::class.java, - NotificationPermissionDeniedRepository::class.java, - NotificationPermissionSkippedRepository::class.java + FirebaseInstallationIdRepository::class.java, + NightModeRepository::class.java, + SchemaVersionRepository::class.java, + NotificationPermissionDeniedRepository::class.java, + NotificationPermissionSkippedRepository::class.java ) // Note: the use of a map is critical here for 2 reasons, both of them related to memory @@ -80,7 +80,7 @@ class RepositoryRegistry { // a reference to that "old" repository instance (e.g objects where that repository was injected // as a dependency). private val loadedRepos: MutableMap, BaseRepository> = - mutableMapOf() + mutableMapOf() fun load(repo: BaseRepository) { synchronized(lock) { @@ -90,7 +90,7 @@ class RepositoryRegistry { loadedRepos[repo.javaClass] = repo Timber.d( - "RepositoryRegistry#load(${repo.javaClass.simpleName}). Size: ${loadedRepos.size}" + "RepositoryRegistry#load(${repo.javaClass.simpleName}). Size: ${loadedRepos.size}" ) } } @@ -100,12 +100,12 @@ class RepositoryRegistry { * logoutSurvivorRepositories. */ fun repositoriesToClearOnLogout(): Collection = - synchronized(lock) { - loadedRepos.filterKeys { - !logoutSurvivorRepositories.contains(it) - }.values - } + synchronized(lock) { + loadedRepos.filterKeys { + !logoutSurvivorRepositories.contains(it) + }.values + } private fun isRegistered(repository: BaseRepository) = - registry.contains(repository.javaClass) + registry.contains(repository.javaClass) } \ No newline at end of file diff --git a/android/apollo/src/main/java/io/muun/apollo/data/preferences/TransactionSizeRepository.java b/android/apollo/src/main/java/io/muun/apollo/data/preferences/TransactionSizeRepository.java index 635f6f0e..de0fabe0 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/preferences/TransactionSizeRepository.java +++ b/android/apollo/src/main/java/io/muun/apollo/data/preferences/TransactionSizeRepository.java @@ -93,4 +93,21 @@ public void initNtsOutpoints() { setTransactionSize(nts.initOutpoints()); } + + /** + * Migration to init utxo status for pre-existing NTSs. + */ + public void initNtsUtxoStatus() { + final boolean hasNts = sharedPreferences.contains(KEY_TRANSACTION_SIZE); + + if (!hasNts) { + return; + } + + final NextTransactionSize nts = getNextTransactionSize(); + + Preconditions.checkNotNull(nts); + + setTransactionSize(nts.initUtxoStatus()); + } } diff --git a/android/apollo/src/main/java/io/muun/apollo/data/preferences/UserPreferencesRepository.kt b/android/apollo/src/main/java/io/muun/apollo/data/preferences/UserPreferencesRepository.kt index 382afbb3..035b7a50 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/preferences/UserPreferencesRepository.kt +++ b/android/apollo/src/main/java/io/muun/apollo/data/preferences/UserPreferencesRepository.kt @@ -67,6 +67,7 @@ open class UserPreferencesRepository @Inject constructor( var defaultAddressType: String = "segwit" var skippedEmailSetup: Boolean = false var receivePreference: ReceiveFormatPreference = ReceiveFormatPreference.ONCHAIN + var allowMultiSession: Boolean = false // JSON constructor constructor() @@ -78,6 +79,7 @@ open class UserPreferencesRepository @Inject constructor( defaultAddressType = prefs.defaultAddressType skippedEmailSetup = prefs.skippedEmailSetup receivePreference = prefs.receivePreference + allowMultiSession = prefs.allowMultiSession } fun toModel(): UserPreferences { @@ -87,7 +89,8 @@ open class UserPreferencesRepository @Inject constructor( seenLnurlFirstTime, defaultAddressType, skippedEmailSetup, - receivePreference + receivePreference, + allowMultiSession ) } } diff --git a/android/apollo/src/main/java/io/muun/apollo/data/preferences/migration/PreferencesMigrationManager.java b/android/apollo/src/main/java/io/muun/apollo/data/preferences/migration/PreferencesMigrationManager.java index 7480fb83..c56b91b8 100644 --- a/android/apollo/src/main/java/io/muun/apollo/data/preferences/migration/PreferencesMigrationManager.java +++ b/android/apollo/src/main/java/io/muun/apollo/data/preferences/migration/PreferencesMigrationManager.java @@ -103,7 +103,11 @@ public class PreferencesMigrationManager { this::migrateToRecentEmergencyKitVerificationCodes, // Sept 2021, Apollo 700 introduces EmergencyKit version and its own model - this::addEmergencyKitVersion + this::addEmergencyKitVersion, + + // Feb 2024, Apollo 1108 add local migration that we missed in 2020 (when we + // added UtxoStatus to NTS but never used it). + this::initNtsUtxoStatus }; /** @@ -326,4 +330,8 @@ private void addEmergencyKitVersion() { keysRepositoryPrefs.edit().remove("ek_recent_verification_codes").apply(); } } + + private void initNtsUtxoStatus() { + transactionSizeRepository.initNtsUtxoStatus(); + } } diff --git a/android/apollo/src/main/java/io/muun/apollo/domain/EmailReportManager.kt b/android/apollo/src/main/java/io/muun/apollo/domain/EmailReportManager.kt index bd50510c..228d0ea2 100644 --- a/android/apollo/src/main/java/io/muun/apollo/domain/EmailReportManager.kt +++ b/android/apollo/src/main/java/io/muun/apollo/domain/EmailReportManager.kt @@ -4,7 +4,7 @@ import android.content.Context import io.muun.apollo.data.os.GooglePlayHelper import io.muun.apollo.data.os.GooglePlayServicesHelper import io.muun.apollo.data.os.TelephonyInfoProvider -import io.muun.apollo.domain.action.fcm.GetFcmTokenAction +import io.muun.apollo.data.preferences.FirebaseInstallationIdRepository import io.muun.apollo.domain.action.session.IsRootedDeviceAction import io.muun.apollo.domain.model.report.CrashReport import io.muun.apollo.domain.model.report.EmailReport @@ -13,15 +13,16 @@ import io.muun.apollo.domain.selector.UserSelector import io.muun.apollo.domain.utils.locale import io.muun.common.utils.Encodings import io.muun.common.utils.Hashes +import timber.log.Timber import javax.inject.Inject class EmailReportManager @Inject constructor( private val userSel: UserSelector, - private val getFcmToken: GetFcmTokenAction, private val googlePlayServicesHelper: GooglePlayServicesHelper, private val googlePlayHelper: GooglePlayHelper, private val telephonyInfoProvider: TelephonyInfoProvider, private val isRootedDeviceAction: IsRootedDeviceAction, + private val firebaseInstallationIdRepo: FirebaseInstallationIdRepository, private val context: Context, ) { @@ -31,23 +32,13 @@ class EmailReportManager @Inject constructor( .flatMap { obj: User -> obj.supportId } .orElse(null) - val fcmTokenHash: String = try { - val fcmToken: String = getFcmToken.actionNow() - Encodings.bytesToHex(Hashes.sha256(Encodings.stringToBytes(fcmToken))) - } catch (e: Throwable) { // Avoid crash, we're already processing an error (report). - // GetFcmTokenAction already logs the error - "unavailable" - } - - val googlePlayServicesAvailable = - googlePlayServicesHelper.isAvailable == GooglePlayServicesHelper.AVAILABLE - return EmailReport.Builder() .report(report) .supportId(supportId) - .fcmTokenHash(fcmTokenHash) + .bigQueryPseudoId(firebaseInstallationIdRepo.getBigQueryPseudoId()) + .fcmTokenHash(getFcmTokenHash()) .presenterName(presenterName) - .googlePlayServices(googlePlayServicesAvailable) + .googlePlayServices(googlePlayServicesHelper.isAvailable) .googlePlayServicesVersionCode(googlePlayServicesHelper.versionCode) .googlePlayServicesVersionName(googlePlayServicesHelper.versionName) .googlePlayServicesClientVersionCode(googlePlayServicesHelper.clientVersionCode) @@ -58,4 +49,18 @@ class EmailReportManager @Inject constructor( .locale(context.locale()) .build() } + + private fun getFcmTokenHash() = try { + val fcmToken: String? = firebaseInstallationIdRepo.getFcmToken() + + if (fcmToken != null) { + Encodings.bytesToHex(Hashes.sha256(Encodings.stringToBytes(fcmToken))) + + } else { + "null" + } + } catch (e: Throwable) { // Avoid crash, we're already processing an error (report). + Timber.e(e) + "unavailable" + } } \ No newline at end of file diff --git a/android/apollo/src/main/java/io/muun/apollo/domain/action/fcm/GetFcmTokenAction.kt b/android/apollo/src/main/java/io/muun/apollo/domain/action/fcm/GetFcmTokenAction.kt index 05ebe850..24a32c59 100644 --- a/android/apollo/src/main/java/io/muun/apollo/domain/action/fcm/GetFcmTokenAction.kt +++ b/android/apollo/src/main/java/io/muun/apollo/domain/action/fcm/GetFcmTokenAction.kt @@ -41,7 +41,7 @@ class GetFcmTokenAction @Inject constructor( } private fun getError() = - if (googlePlayServicesHelper.isAvailable != GooglePlayServicesHelper.AVAILABLE) + if (!googlePlayServicesHelper.isAvailable) GooglePlayServicesNotAvailableError() else FcmTokenNotAvailableError() diff --git a/android/apollo/src/main/java/io/muun/apollo/domain/analytics/Analytics.kt b/android/apollo/src/main/java/io/muun/apollo/domain/analytics/Analytics.kt index 5475c0e0..df44bd8d 100644 --- a/android/apollo/src/main/java/io/muun/apollo/domain/analytics/Analytics.kt +++ b/android/apollo/src/main/java/io/muun/apollo/domain/analytics/Analytics.kt @@ -1,14 +1,27 @@ package io.muun.apollo.domain.analytics import io.muun.apollo.data.analytics.AnalyticsProvider +import io.muun.apollo.data.preferences.FirebaseInstallationIdRepository import io.muun.apollo.domain.model.report.CrashReport import io.muun.apollo.domain.model.user.User +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton - @Singleton -class Analytics @Inject constructor(val analyticsProvider: AnalyticsProvider) { +class Analytics @Inject constructor( + private val analyticsProvider: AnalyticsProvider, + private val firebaseInstallationIdRepository: FirebaseInstallationIdRepository, +) { + + fun loadBigQueryPseudoId() { + analyticsProvider.loadBigQueryPseudoId() + .subscribe( + // id can be null on platforms without google play services. + { id -> id?.let { firebaseInstallationIdRepository.storeBigQueryPseudoId(id) } }, + { error -> Timber.e(error) } + ) + } /** * Set the user's properties, to be used by Analytics. diff --git a/android/apollo/src/main/java/io/muun/apollo/domain/debug/DebugExecutable.kt b/android/apollo/src/main/java/io/muun/apollo/domain/debug/DebugExecutable.kt index ee442877..dcfefe20 100644 --- a/android/apollo/src/main/java/io/muun/apollo/domain/debug/DebugExecutable.kt +++ b/android/apollo/src/main/java/io/muun/apollo/domain/debug/DebugExecutable.kt @@ -2,9 +2,11 @@ package io.muun.apollo.domain.debug import io.muun.apollo.data.debug.LappClient import io.muun.apollo.data.debug.LappClientError +import io.muun.apollo.data.net.HoustonClient import io.muun.apollo.data.os.execution.ExecutionTransformerFactory import io.muun.apollo.domain.action.address.CreateAddressAction import io.muun.apollo.domain.action.incoming_swap.GenerateInvoiceAction +import io.muun.apollo.domain.action.user.UpdateUserPreferencesAction import io.muun.apollo.domain.errors.debug.DebugExecutableError import io.muun.apollo.domain.selector.UserPreferencesSelector import io.muun.common.crypto.hd.MuunAddress @@ -18,9 +20,11 @@ import javax.inject.Singleton */ @Singleton class DebugExecutable @Inject constructor( + private val updateUserPreferences: UpdateUserPreferencesAction, private val createAddress: CreateAddressAction, private val generateInvoice: GenerateInvoiceAction, private val userPreferencesSel: UserPreferencesSelector, + private val houstonClient: HoustonClient, private val transformerFactory: ExecutionTransformerFactory, ) { @@ -71,6 +75,23 @@ class DebugExecutable @Inject constructor( }.compose(transformerFactory.getAsyncExecutor()) .compose(errorMapper()) + /** + * Enable/Disable "Multiple sessions" feature for this user. + */ + fun toggleMultiSession() { + updateUserPreferences.run { prefs -> + prefs.copy(allowMultiSession = !prefs.allowMultiSession) + } + } + + /** + * [Only works for "Multiple sessions" users] Expire all user sessions except the current one. + */ + fun expireAllSessions(): Observable = Observable.defer { + houstonClient.expireAllOtherSessions().toObservable() + }.compose(transformerFactory.getAsyncExecutor()) + .compose(errorMapper()) + private fun errorMapper() = { observable: Observable -> observable.onErrorResumeNext { error: Throwable -> if (error is LappClientError) { @@ -80,5 +101,4 @@ class DebugExecutable @Inject constructor( } } } - } diff --git a/android/apollo/src/main/java/io/muun/apollo/domain/errors/MissingPersistentPresenterError.kt b/android/apollo/src/main/java/io/muun/apollo/domain/errors/MissingPersistentPresenterError.kt deleted file mode 100644 index 71553282..00000000 --- a/android/apollo/src/main/java/io/muun/apollo/domain/errors/MissingPersistentPresenterError.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.muun.apollo.domain.errors - -class MissingPersistentPresenterError( - presenterClass: Class, - presentersInCache: List, -) : MuunError() { - - init { - metadata["presenter"] = presenterClass.simpleName - metadata["presenterCacheSize"] = presentersInCache.size - metadata["presenterCacheValues"] = presentersInCache.joinToString(",") - } - -} \ No newline at end of file diff --git a/android/apollo/src/main/java/io/muun/apollo/domain/model/IncomingSwap.kt b/android/apollo/src/main/java/io/muun/apollo/domain/model/IncomingSwap.kt index 6fcfe1d8..d651d384 100644 --- a/android/apollo/src/main/java/io/muun/apollo/domain/model/IncomingSwap.kt +++ b/android/apollo/src/main/java/io/muun/apollo/domain/model/IncomingSwap.kt @@ -7,6 +7,7 @@ import io.muun.common.crypto.hd.PrivateKey import io.muun.common.crypto.hd.PublicKey import io.muun.common.utils.Encodings import org.bitcoinj.core.NetworkParameters +import timber.log.Timber open class IncomingSwap( id: Long?, @@ -16,7 +17,7 @@ open class IncomingSwap( private val sphinxPacket: ByteArray?, val collectInSats: Long, val paymentAmountInSats: Long, - private var preimage: ByteArray? + private var preimage: ByteArray?, ) : HoustonUuidModel(id, houstonUuid) { fun getPaymentHash() = @@ -51,7 +52,7 @@ open class IncomingSwap( data: IncomingSwapFulfillmentData, userKey: PrivateKey, muunKey: PublicKey, - network: NetworkParameters + network: NetworkParameters, ): FulfillmentResult { val libwalletUserKey = userKey.toLibwallet(network) @@ -59,6 +60,8 @@ open class IncomingSwap( val libwalletNetwork = network.toLibwallet() val libwalletFullfillmentData = data.toLibwalletModel() + debugLog(htlc!!, libwalletFullfillmentData, libwalletUserKey, libwalletMuunKey) + try { val result = toLibwalletModel().fulfill( libwalletFullfillmentData, @@ -76,6 +79,46 @@ open class IncomingSwap( } } + /** + * Debugging info for verifying fullfilmentTxs, htlcTxs, etc... + * Note: if you're looking at this you'll probably be interested in some data that's only + * available in libwallet. See: libwallet/incoming_swap.go. + */ + private fun debugLog( + htlc: IncomingSwapHtlc, + fullfillmentData: libwallet.IncomingSwapFulfillmentData, + userKey: libwallet.HDPrivateKey, + muunKey: libwallet.HDPublicKey, + ) { + Timber.d("---IncomingSwap---") + Timber.d("SphinxPacket: $sphinxPacketHex") + Timber.d("PaymentHash: ${Encodings.bytesToHex(paymentHash)}") + Timber.d("HtlcTx: ${Encodings.bytesToHex(htlc.htlcTx)}") + Timber.d("HtlcExpirationHeight: ${htlc.expirationHeight}") + Timber.d("HtlcSwapServerPublicKey: ${Encodings.bytesToHex(htlc.swapServerPublicKey)}") + + val merkleTree = fullfillmentData.merkleTree ?: byteArrayOf() + val htlcBlock = fullfillmentData.htlcBlock ?: byteArrayOf() + Timber.d("---IncomingSwapFulfillmentData---") + Timber.d("FullfilmentTx: ${Encodings.bytesToHex(fullfillmentData.fulfillmentTx)}") + Timber.d("MuunSignature: ${Encodings.bytesToHex(fullfillmentData.muunSignature)}") + Timber.d("OuputVersion: ${fullfillmentData.outputVersion}") + Timber.d("OuputPath: ${fullfillmentData.outputPath}") + Timber.d("MerkleTree: ${Encodings.bytesToHex(merkleTree)}") + Timber.d("HtlcBlock: ${Encodings.bytesToHex(htlcBlock)}") + Timber.d("ConfTarget: ${fullfillmentData.confirmationTarget}") + + Timber.d("---UserKey---") + Timber.d("Base58: ${userKey.string()}") + Timber.d("Path: ${userKey.path}") + Timber.d("Network: ${userKey.network.name()}") + + Timber.d("---muunKey---") + Timber.d("Base58: ${muunKey.string()}") + Timber.d("Path: ${muunKey.path}") + Timber.d("Network: ${muunKey.network.name()}") + } + open fun fulfillFullDebt(): FulfillmentResult { val result = toLibwalletModel().fulfillFullDebt() diff --git a/android/apollo/src/main/java/io/muun/apollo/domain/model/NextTransactionSize.java b/android/apollo/src/main/java/io/muun/apollo/domain/model/NextTransactionSize.java index ad87ac21..d2037020 100644 --- a/android/apollo/src/main/java/io/muun/apollo/domain/model/NextTransactionSize.java +++ b/android/apollo/src/main/java/io/muun/apollo/domain/model/NextTransactionSize.java @@ -121,6 +121,21 @@ public NextTransactionSize initOutpoints() { return this; } + /** + * Migration to init utxo status for pre-existing NTSs. + */ + public NextTransactionSize initUtxoStatus() { + + final List newSizeProgression = new ArrayList<>(); + for (SizeForAmount sizeForAmount : sizeProgression) { + newSizeProgression.add(sizeForAmount.initUtxoStatusForApollo()); + } + + this.sizeProgression = newSizeProgression; + + return this; + } + /** * Extract complete list of outpoints, sorted as used in sizeProgression (aka as we use it for * our fee computations). @@ -173,6 +188,7 @@ private newop.SizeForAmount toLibwallet(SizeForAmount sizeForAmount) { libwalletSizeForAmount.setAmountInSat(sizeForAmount.amountInSatoshis); libwalletSizeForAmount.setSizeInVByte(getSizeInVirtualBytes(sizeForAmount)); libwalletSizeForAmount.setOutpoint(sizeForAmount.outpoint); + libwalletSizeForAmount.setUtxoStatus(sizeForAmount.status.toString()); return libwalletSizeForAmount; } diff --git a/android/apollo/src/main/java/io/muun/apollo/domain/model/report/EmailReport.kt b/android/apollo/src/main/java/io/muun/apollo/domain/model/report/EmailReport.kt index 3212c4d7..ae20b8ac 100644 --- a/android/apollo/src/main/java/io/muun/apollo/domain/model/report/EmailReport.kt +++ b/android/apollo/src/main/java/io/muun/apollo/domain/model/report/EmailReport.kt @@ -11,6 +11,7 @@ import org.threeten.bp.ZoneOffset import org.threeten.bp.ZonedDateTime import timber.log.Timber import java.util.Locale + import javax.annotation.CheckReturnValue class EmailReport private constructor(val body: String) { @@ -18,6 +19,7 @@ class EmailReport private constructor(val body: String) { data class Builder( var report: CrashReport? = null, var supportId: String? = null, + var bigQueryPseudoId: String? = null, var fcmTokenHash: String? = null, var presenterName: String? = null, var googlePlayServicesAvailable: Boolean? = null, @@ -37,6 +39,9 @@ class EmailReport private constructor(val body: String) { @CheckReturnValue fun supportId(supportId: String?) = apply { this.supportId = supportId } + @CheckReturnValue + fun bigQueryPseudoId(pseudoId: String?) = apply { this.bigQueryPseudoId = pseudoId } + @CheckReturnValue fun fcmTokenHash(fcmTokenHash: String) = apply { this.fcmTokenHash = fcmTokenHash } @@ -107,6 +112,7 @@ class EmailReport private constructor(val body: String) { |Date: ${now.format(Dates.ISO_DATE_TIME_WITH_MILLIS)} |Locale: ${locale.toString()} |SupportId: ${if (supportId != null) supportId else "Not logged in"} + |Bid: $bigQueryPseudoId |ScreenPresenter: $presenterName |FcmTokenHash: $fcmTokenHash |GooglePlayServices (GPS): $googlePlayServicesAvailable diff --git a/android/apollo/src/main/java/io/muun/apollo/domain/model/user/UserPreferences.kt b/android/apollo/src/main/java/io/muun/apollo/domain/model/user/UserPreferences.kt index 7f87ca06..5957f03f 100644 --- a/android/apollo/src/main/java/io/muun/apollo/domain/model/user/UserPreferences.kt +++ b/android/apollo/src/main/java/io/muun/apollo/domain/model/user/UserPreferences.kt @@ -9,6 +9,7 @@ data class UserPreferences( val defaultAddressType: String, val skippedEmailSetup: Boolean, val receivePreference: ReceiveFormatPreference, + val allowMultiSession: Boolean, ) { fun toJson(): io.muun.common.model.UserPreferences { return io.muun.common.model.UserPreferences( @@ -18,7 +19,8 @@ data class UserPreferences( defaultAddressType, false, skippedEmailSetup, - receivePreference + receivePreference, + allowMultiSession ) } @@ -32,7 +34,8 @@ data class UserPreferences( prefs.seenLnurlFirstTime, prefs.defaultAddressType, prefs.skippedEmailSetup, - prefs.receiveFormatPreference + prefs.receiveFormatPreference, + prefs.allowMultiSession ) } } diff --git a/android/apollo/src/test/java/io/muun/apollo/domain/action/user/UpdateUserPreferencesActionTest.kt b/android/apollo/src/test/java/io/muun/apollo/domain/action/user/UpdateUserPreferencesActionTest.kt index fef0385b..335eb4c4 100644 --- a/android/apollo/src/test/java/io/muun/apollo/domain/action/user/UpdateUserPreferencesActionTest.kt +++ b/android/apollo/src/test/java/io/muun/apollo/domain/action/user/UpdateUserPreferencesActionTest.kt @@ -40,6 +40,7 @@ class UpdateUserPreferencesActionTest : BaseTest() { defaultAddressType = "segwit", skippedEmailSetup = false, receivePreference = ReceiveFormatPreference.ONCHAIN, + allowMultiSession = false ) ) ).whenever(repository).watch() diff --git a/android/apolloui/build.gradle b/android/apolloui/build.gradle index db7991df..c268f717 100644 --- a/android/apolloui/build.gradle +++ b/android/apolloui/build.gradle @@ -91,8 +91,8 @@ android { applicationId "io.muun.apollo" minSdkVersion 19 targetSdkVersion 33 - versionCode 1109 - versionName "51.9" + versionCode 1110 + versionName "51.10" // Needed to make sure these classes are available in the main DEX file for API 19 // See: https://spin.atomicobject.com/2018/07/16/support-kitkat-multidex/ diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/app/ApolloApplication.java b/android/apolloui/src/main/java/io/muun/apollo/presentation/app/ApolloApplication.java index 8ed9b197..d3008853 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/app/ApolloApplication.java +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/app/ApolloApplication.java @@ -12,7 +12,6 @@ import io.muun.apollo.data.logging.Crashlytics; import io.muun.apollo.data.logging.LoggingContext; import io.muun.apollo.data.logging.MuunTree; -import io.muun.apollo.data.preferences.FirebaseInstallationIdRepository; import io.muun.apollo.data.preferences.UserRepository; import io.muun.apollo.data.preferences.migration.PreferencesMigrationManager; import io.muun.apollo.domain.ApplicationLockManager; @@ -84,9 +83,6 @@ public abstract class ApolloApplication extends Application @Inject DetectAppUpdateAction detectAppUpdate; - @Inject - FirebaseInstallationIdRepository firebaseInstallationIdRepository; - @Inject BackgroundTimesService backgroundTimesService; @@ -147,14 +143,7 @@ public void onCreate() { } private void loadBigQueryPseudoId() { - FirebaseAnalytics.getInstance(this) - .getAppInstanceId() - .addOnSuccessListener(id -> { - // id can be null on platforms without google play services. - if (id != null) { - firebaseInstallationIdRepository.storeBigQueryPseudoId(id); - } - }); + analytics.loadBigQueryPseudoId(); } private void setNightMode() { diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/BasePresenter.java b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/BasePresenter.java index 615d92a1..bcca5f40 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/BasePresenter.java +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/BasePresenter.java @@ -206,9 +206,8 @@ protected AnalyticsEvent getEntryEvent() { } protected void assertGooglePlayServicesPresent() { - final int available = googlePlayServicesHelper.isAvailable(); - if (available != GooglePlayServicesHelper.AVAILABLE) { - view.showPlayServicesDialog(googlePlayServicesHelper.showDownloadDialog(available)); + if (!googlePlayServicesHelper.isAvailable()) { + view.showPlayServicesDialog(googlePlayServicesHelper.showDownloadDialog()); } } diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelActivity.java b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelActivity.java index 80e95e1e..98eb7958 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelActivity.java +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelActivity.java @@ -14,6 +14,7 @@ import androidx.appcompat.app.AlertDialog; import butterknife.BindView; import butterknife.OnClick; +import com.google.android.material.switchmaterial.SwitchMaterial; import rx.functions.Action1; import javax.inject.Inject; @@ -45,6 +46,12 @@ public class DebugPanelActivity extends BaseActivity implem @BindView(R.id.debug_button_undrop_tx) MuunButton undropTx; + @BindView(R.id.debug_switch_allow_multi_session) + SwitchMaterial allowMultiSession; + + @BindView(R.id.debug_button_expire_all_other_sessions) + MuunButton expireAllOtherSessions; + /** * Creates an intent to launch this activity. */ @@ -73,6 +80,11 @@ protected void initializeUi() { dropTx.setOnClickListener(v -> handleTxIdInput(presenter::dropTx)); undropTx.setOnClickListener(v -> handleTxIdInput(presenter::undropTx)); + + allowMultiSession.setOnCheckedChangeListener( + (buttonView, isChecked) -> presenter.toggleMultiSessions() + ); + expireAllOtherSessions.setOnClickListener(v -> presenter.expireAllSessions()); } private void handleTxIdInput(Action1 handler) { diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelPresenter.java b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelPresenter.java index 09c40de6..41e6870a 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelPresenter.java +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelPresenter.java @@ -21,11 +21,15 @@ public class DebugPanelPresenter extends BasePresenter { private final OperationActions operationActions; + private final ContactActions contactActions; + private final IntegrityAction integrityAction; private final FetchRealTimeDataAction fetchRealTimeData; + private final SyncExternalAddressIndexesAction syncExternalAddressIndexes; + private final DebugExecutable debugExecutable; /** @@ -190,6 +194,21 @@ public void undropTx(String txId) { .subscribe(Actions.empty(), this::handleError); } + /** + * Enable/Disable "Multiple sessions" feature for this user. + */ + public void toggleMultiSessions() { + debugExecutable.toggleMultiSession(); + } + + /** + * [Only works for "Multiple sessions" users] Expire all user sessions except the current one. + */ + public void expireAllSessions() { + debugExecutable.expireAllSessions() + .subscribe(Actions.empty(), this::handleError); + } + @Override public void handleError(Throwable error) { if (error instanceof DebugExecutableError) { diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationPresenter.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationPresenter.kt index f1d4743e..603bdde2 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationPresenter.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationPresenter.kt @@ -8,18 +8,7 @@ import io.muun.apollo.domain.action.operation.ResolveOperationUriAction import io.muun.apollo.domain.action.operation.SubmitPaymentAction import io.muun.apollo.domain.action.realtime.FetchRealTimeDataAction import io.muun.apollo.domain.analytics.AnalyticsEvent -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.ABORT -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.BACK -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.CANCEL_ABORT -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.CONFIRM_AMOUNT -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.CONFIRM_DESCRIPTION -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.CONFIRM_FEE -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.CONFIRM_OPERATION -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.CONFIRM_SWAP_OPERATION -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.START_FOR_BITCOIN_URI -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.START_FOR_INVOICE -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.START_FOR_UNIFIED_QR -import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE.USE_ALL_FUNDS +import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_ACTION_TYPE import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_COMPLETED import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_SUBMITTED import io.muun.apollo.domain.analytics.AnalyticsEvent.E_NEW_OP_TYPE.Companion.fromModel @@ -143,10 +132,10 @@ class NewOperationPresenter @Inject constructor( // Since LN payments are handled in startForInvoice() (according to OperationUri#isLn()), if // the operationUri has a ln invoice here it means we are dealing with a "Unified QR" uri. if (operationUri.lnInvoice.isPresent) { - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(START_FOR_UNIFIED_QR)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.START_FOR_UNIFIED_QR) } else { - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(START_FOR_BITCOIN_URI)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.START_FOR_BITCOIN_URI) } stateMachine.withState { state: StartState -> @@ -167,7 +156,7 @@ class NewOperationPresenter @Inject constructor( return } - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(START_FOR_INVOICE)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.START_FOR_INVOICE) stateMachine.withState { state: StartState -> state.resolveInvoice(parseInvoice(networkParams, invoice), networkParams.toLibwallet()) } @@ -234,7 +223,7 @@ class NewOperationPresenter @Inject constructor( override fun confirmFee(selectedFeeRateInVBytes: Double) { view.goToConfirmedFee() - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(CONFIRM_FEE)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.CONFIRM_FEE) if (selectedFeeRateInVBytes >= Rules.toSatsPerVbyte(Rules.OP_MINIMUM_FEE_RATE)) { stateMachine.withState { state: EditFeeState -> state.setFeeRate(selectedFeeRateInVBytes) @@ -345,21 +334,21 @@ class NewOperationPresenter @Inject constructor( } fun confirmAmount(value: MonetaryAmount, takeFreeFromAmount: Boolean) { - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(CONFIRM_AMOUNT)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.CONFIRM_AMOUNT) stateMachine.withState { state: EnterAmountState -> state.enterAmount(value.toLibwallet(), takeFreeFromAmount) } } fun confirmUseAllFunds() { - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(USE_ALL_FUNDS)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.USE_ALL_FUNDS) stateMachine.withState { state: EnterAmountState -> state.enterAmount(state.totalBalance.inInputCurrency, true) } } fun confirmDescription(description: String) { - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(CONFIRM_DESCRIPTION)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.CONFIRM_DESCRIPTION) stateMachine.withState { state: EnterDescriptionState -> state.enterDescription(description) } @@ -385,7 +374,7 @@ class NewOperationPresenter @Inject constructor( fun confirmOperation() { confirmationInProgress = true - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(CONFIRM_OPERATION)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.CONFIRM_OPERATION) stateMachine.withState { state: ConfirmState -> submitOperation(ConfirmStateViewModel.fromConfirmState(state)) } @@ -397,7 +386,7 @@ class NewOperationPresenter @Inject constructor( fun confirmSwapOperation() { confirmationInProgress = true - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(CONFIRM_SWAP_OPERATION)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.CONFIRM_SWAP_OPERATION) stateMachine.withState { state: ConfirmLightningState -> submitOperation( ConfirmStateViewModel.fromConfirmLightningState(state), @@ -417,7 +406,7 @@ class NewOperationPresenter @Inject constructor( return } - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(BACK)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.BACK) stateMachine.withState { state: State -> when (state) { is EnterAmountState -> state.back() @@ -432,20 +421,39 @@ class NewOperationPresenter @Inject constructor( } fun cancelAbort() { - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(CANCEL_ABORT)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.CANCEL_ABORT) stateMachine.withState { state: AbortState -> state.cancel() } } fun finishAndGoHome() { - analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(ABORT)) + reportNewOpAction(E_NEW_OP_ACTION_TYPE.ABORT) view.finishAndGoHome() } + private fun reportNewOpAction(type: E_NEW_OP_ACTION_TYPE) { + var params: Array> = arrayOf() + if (submarineSwap != null) { + params += ("swap_uuid" to submarineSwap!!.houstonUuid) + } + analytics.report(AnalyticsEvent.E_NEW_OP_ACTION(type, *params)) + + } + fun handleNewOpError(errorType: NewOperationErrorType) { + + var params: Array> = arrayOf() + if (submarineSwap != null) { + params += ("swap_uuid" to submarineSwap!!.houstonUuid) + } + analytics.report( - AnalyticsEvent.S_NEW_OP_ERROR(origin.toAnalyticsEvent(), errorType.toAnalyticsEvent()) + AnalyticsEvent.S_NEW_OP_ERROR( + origin.toAnalyticsEvent(), + errorType.toAnalyticsEvent(), + *params + ) ) view.showErrorScreen(errorType) } @@ -666,7 +674,7 @@ class NewOperationPresenter @Inject constructor( ): Array> { val objects = ArrayList>() - objects.add(Pair("operation_id", operationId.toInt())) + objects.add(("operation_id" to operationId.toInt())) // Also add previously known metadata objects.addAll(opSubmittedMetadata(confirmStateViewModel)) @@ -695,22 +703,20 @@ class NewOperationPresenter @Inject constructor( val outputAmountInSat = stateVm.validated.swapInfo?.swapFees?.outputAmountInSat val outputPaddingInSat = stateVm.validated.swapInfo?.swapFees?.outputPaddingInSat - objects.add(Pair("fee_type", type.name.lowercase(Locale.getDefault()))) - objects.add(Pair("sats_per_virtual_byte", feeRateInSatsPerVbyte)) - objects.add(Pair("amount", SerializationUtils.serializeBitcoinAmount(amount))) - objects.add(Pair("fee", SerializationUtils.serializeBitcoinAmount(fee))) - objects.add(Pair("total", SerializationUtils.serializeBitcoinAmount(total))) - objects.add( - Pair("onchainFee", SerializationUtils.serializeBitcoinAmount(onchainFee)) - ) - objects.add(Pair("feeNeedsChange", feeNeedsChange)) - objects.add(Pair("isOneConf", isOneConf.toString())) - objects.add(Pair("routingFeeInSat", routingFeeInSat.toString())) - objects.add(Pair("confirmationsNeeded", confirmationsNeeded.toString())) - objects.add(Pair("debtType", debtType.toString())) - objects.add(Pair("debtAmountInSat", debtAmountInSat.toString())) - objects.add(Pair("outputAmountInSat", outputAmountInSat.toString())) - objects.add(Pair("outputPaddingInSat", outputPaddingInSat.toString())) + objects.add(("fee_type" to type.name.lowercase(Locale.getDefault()))) + objects.add(("sats_per_virtual_byte" to feeRateInSatsPerVbyte)) + objects.add(("amount" to SerializationUtils.serializeBitcoinAmount(amount))) + objects.add(("fee" to SerializationUtils.serializeBitcoinAmount(fee))) + objects.add(("total" to SerializationUtils.serializeBitcoinAmount(total))) + objects.add(("onchainFee" to SerializationUtils.serializeBitcoinAmount(onchainFee))) + objects.add(("feeNeedsChange" to feeNeedsChange)) + objects.add(("isOneConf" to isOneConf.toString())) + objects.add(("routingFeeInSat" to routingFeeInSat.toString())) + objects.add(("confirmationsNeeded" to confirmationsNeeded.toString())) + objects.add(("debtType" to debtType.toString())) + objects.add(("debtAmountInSat" to debtAmountInSat.toString())) + objects.add(("outputAmountInSat" to outputAmountInSat.toString())) + objects.add(("outputPaddingInSat" to outputPaddingInSat.toString())) // Also add previously known metadata objects.addAll(opStartedMetadata(paymentType)) @@ -756,11 +762,12 @@ class NewOperationPresenter @Inject constructor( private fun opStartedMetadata(paymentType: Type): ArrayList> { val objects = ArrayList>() - objects.add(Pair("type", getEventType(paymentType))) - objects.add(Pair("origin", getEventOrigin(origin))) + objects.add(("type" to getEventType(paymentType))) + objects.add(("origin" to getEventOrigin(origin))) if (submarineSwap != null && submarineSwap!!.fundingOutput.debtType != null) { - objects.add(Pair("debt_type", submarineSwap!!.fundingOutput.debtType!!)) + objects.add(("debt_type" to submarineSwap!!.fundingOutput.debtType!!)) + objects.add(("swap_uuid" to submarineSwap!!.houstonUuid)) } return objects diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/security_logout/SecurityLogoutPresenter.java b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/security_logout/SecurityLogoutPresenter.java index 789b19d1..8ff9aa1a 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/security_logout/SecurityLogoutPresenter.java +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/security_logout/SecurityLogoutPresenter.java @@ -1,13 +1,17 @@ package io.muun.apollo.presentation.ui.security_logout; import io.muun.apollo.domain.action.LogoutActions; +import io.muun.apollo.domain.action.UserActions; import io.muun.apollo.domain.analytics.AnalyticsEvent; +import io.muun.apollo.domain.errors.MuunError; import io.muun.apollo.presentation.ui.base.BasePresenter; import io.muun.apollo.presentation.ui.base.BaseView; import io.muun.apollo.presentation.ui.base.di.PerActivity; +import io.muun.common.Optional; import android.os.Bundle; import androidx.annotation.Nullable; +import timber.log.Timber; import javax.inject.Inject; @@ -15,19 +19,26 @@ public class SecurityLogoutPresenter extends BasePresenter { private final LogoutActions logoutActions; + private final UserActions userActions; /** * Constructor. */ @Inject - public SecurityLogoutPresenter(LogoutActions logoutActions) { + public SecurityLogoutPresenter(LogoutActions logoutActions, UserActions userActions) { this.logoutActions = logoutActions; + this.userActions = userActions; } @Override public void setUp(Bundle arguments) { super.setUp(arguments); + + // TODO we should extract this to an action and refactor SettingsPresenter + // We need to "capture" auth header to fire (and forget) notifyLogout request + final String jwt = getJwt(); logoutActions.destroyRecoverableWallet(); + userActions.notifyLogoutAction.run(jwt); } /** @@ -43,4 +54,16 @@ public void goToSignIn() { protected AnalyticsEvent getEntryEvent() { return new AnalyticsEvent.S_LOG_OUT(); } + + private String getJwt() { + final Optional serverJwt = authRepository.getServerJwt(); + if (!serverJwt.isPresent()) { + // Shouldn't happen but we wanna know 'cause probably a bug + Timber.e(new MuunError("Auth token expected to be present")); + return ""; + } + + return serverJwt.get(); + } + } diff --git a/android/apolloui/src/main/res/layout/debug_activity.xml b/android/apolloui/src/main/res/layout/debug_activity.xml index a9407286..c366e3a1 100644 --- a/android/apolloui/src/main/res/layout/debug_activity.xml +++ b/android/apolloui/src/main/res/layout/debug_activity.xml @@ -1,8 +1,7 @@ - + android:text="Muun debug panel" + tools:ignore="HardcodedText" /> + android:text="Clear and fetch operations" + tools:ignore="HardcodedText" /> + android:text="Clear and fetch contacts" + tools:ignore="HardcodedText" /> + android:text="Clear and scan phone contacts" + tools:ignore="HardcodedText" /> + android:text="Sync real-time data" + tools:ignore="HardcodedText" /> + android:text="Sync external addresses indexes" + tools:ignore="HardcodedText" /> + android:text="Run integrity check" + tools:ignore="HardcodedText" /> + android:text="Update FCM token" + tools:ignore="HardcodedText" /> + android:text="Fund Wallet Onchain" + tools:ignore="HardcodedText" /> + android:text="Fund Wallet Offchain" + tools:ignore="HardcodedText" /> + android:text="Generate Block" + tools:ignore="HardcodedText" /> + android:text="Drop Last Tx" + tools:ignore="HardcodedText" /> + android:text="Drop Tx" + tools:ignore="HardcodedText" /> + android:text="Undrop Tx" + tools:ignore="HardcodedText" /> + + + + + android:textStyle="normal|bold" + tools:ignore="HardcodedText" /> notifyLogout(@Header("Authorization") String authHeader); + @POST("sessions/expire-all-others") + Completable expireAllOtherSessions(); + @PUT("sessions/current/gcm-token") Observable updateFcmToken(@Body String gcmToken); diff --git a/common/src/main/java/io/muun/common/model/SizeForAmount.java b/common/src/main/java/io/muun/common/model/SizeForAmount.java index d9352e2c..484a9764 100644 --- a/common/src/main/java/io/muun/common/model/SizeForAmount.java +++ b/common/src/main/java/io/muun/common/model/SizeForAmount.java @@ -54,6 +54,21 @@ public SizeForAmount initOutpoint() { return this; } + /** + * Migration to init utxo status for pre-existing sizeForAmounts. Will be properly initialized + * after first NTS refresh (e.g first newOperation, incoming operation, or any operationUpdate). + * NOTE: we're choosing to init status as CONFIRMED as this field has existed for a while now + * in the codebase, and only users with a REALLY old version (circa mid-2020) should have null + * here. + */ + public SizeForAmount initUtxoStatusForApollo() { + if (status == null) { + status = UtxoStatus.CONFIRMED; + } + return this; + } + + @Override public String toString() { return "[SizeForAmount " + sizeInBytes + " for " + amountInSatoshis + "]"; diff --git a/common/src/main/java/io/muun/common/model/UserPreferences.java b/common/src/main/java/io/muun/common/model/UserPreferences.java index f129ec4e..73413e75 100644 --- a/common/src/main/java/io/muun/common/model/UserPreferences.java +++ b/common/src/main/java/io/muun/common/model/UserPreferences.java @@ -54,6 +54,10 @@ public class UserPreferences { @Since(apolloVersion = 1000) public ReceiveFormatPreference receiveFormatPreference = ReceiveFormatPreference.ONCHAIN; + // TODO: set correct release versions + @Since(apolloVersion = 1001, falconVersion = 1008) + public Boolean allowMultiSession = false; + /** * JSON constructor. */ @@ -70,7 +74,8 @@ public UserPreferences( final String defaultAddressType, final boolean lightningDefaultForReceiving, final boolean skippedEmailSetup, - final ReceiveFormatPreference receiveFormatPreference + final ReceiveFormatPreference receiveFormatPreference, + final boolean allowMultiSession ) { this.receiveStrictMode = receiveStrictMode; this.seenNewHome = seenNewHome; @@ -79,6 +84,7 @@ public UserPreferences( this.lightningDefaultForReceiving = lightningDefaultForReceiving; this.skippedEmailSetup = skippedEmailSetup; this.receiveFormatPreference = receiveFormatPreference; + this.allowMultiSession = allowMultiSession; } /** @@ -122,6 +128,10 @@ public void merge(final UserPreferences other) { if (other.receiveFormatPreference != null) { this.receiveFormatPreference = other.receiveFormatPreference; } + + if (other.allowMultiSession != null) { + this.allowMultiSession = other.allowMultiSession; + } } } diff --git a/common/src/main/java/io/muun/common/utils/VesCurrencyProvider.java b/common/src/main/java/io/muun/common/utils/VesCurrencyProvider.java new file mode 100644 index 00000000..d7e1c9a9 --- /dev/null +++ b/common/src/main/java/io/muun/common/utils/VesCurrencyProvider.java @@ -0,0 +1,66 @@ +package io.muun.common.utils; + +import org.javamoney.moneta.CurrencyUnitBuilder; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Priority; +import javax.money.CurrencyQuery; +import javax.money.CurrencyUnit; +import javax.money.Monetary; +import javax.money.spi.CurrencyProviderSpi; + +@Priority(2000) +public class VesCurrencyProvider implements CurrencyProviderSpi { + + private final Set currencies; + + public VesCurrencyProvider() { + + final CurrencyUnit ves = CurrencyUnitBuilder.of("VES", getClass().getSimpleName()) + .setDefaultFractionDigits(2) + .build(); + + final Set currencies = new HashSet<>(); + currencies.add(ves); + + this.currencies = Collections.unmodifiableSet(currencies); + } + + @Override + public String getProviderName() { + return getClass().getSimpleName(); + } + + @Override + public boolean isCurrencyAvailable(CurrencyQuery query) { + + if (Monetary.isCurrencyAvailable("VES", "default")) { + // If code VES is already defined in the default provider, we should return false + // for this provider in order to avoid an ambiguous CurrencyUnit. + return false; + } + + if (query.isEmpty()) { + return true; + } + + if (query.getCurrencyCodes().contains("VES")) { + return true; + } + + return Boolean.TRUE.equals(query.getBoolean("Venezuelan BolĂ­var")); + } + + @Override + public Set getCurrencies(CurrencyQuery query) { + + if (isCurrencyAvailable(query)) { + return currencies; + } + + return Collections.emptySet(); + } + +} diff --git a/libwallet/hdpath/hdpath.go b/libwallet/hdpath/hdpath.go index 66644772..edf90d4a 100644 --- a/libwallet/hdpath/hdpath.go +++ b/libwallet/hdpath/hdpath.go @@ -13,7 +13,7 @@ type Path string const HardenedSymbol = "'" -var re = regexp.MustCompile("^(m?|\\/|(([a-z]+:)?\\d+'?))(\\/([a-z]+:)?\\d+'?)*$") +var re = regexp.MustCompile(`^(m?|\/|(([a-z]+:)?\d+'?))(\/([a-z]+:)?\d+'?)*$`) func Parse(s string) (Path, error) { if !re.MatchString(s) { diff --git a/libwallet/musig/musig.go b/libwallet/musig/musig.go index 1dc8f100..73641479 100644 --- a/libwallet/musig/musig.go +++ b/libwallet/musig/musig.go @@ -52,8 +52,7 @@ func CombinePubKeysWithTweak(userKey, muunKey *btcec.PublicKey, customTweak []by } var serialized [33]byte - var serializedSize C.size_t - serializedSize = 33 + var serializedSize C.size_t = 33 if C.secp256k1_ec_pubkey_serialize( ctx, toUchar(serialized[:]), diff --git a/libwallet/newop/context_test.go b/libwallet/newop/context_test.go index 6e2eb7c6..4897cec3 100644 --- a/libwallet/newop/context_test.go +++ b/libwallet/newop/context_test.go @@ -22,6 +22,7 @@ func createTestPaymentContext() *PaymentContext { context.NextTransactionSize.AddSizeForAmount(&SizeForAmount{ AmountInSat: 100_000_000, SizeInVByte: 240, + UtxoStatus: "CONFIRMED", }) context.ExchangeRateWindow.AddRate("BTC", 1) diff --git a/libwallet/newop/nts.go b/libwallet/newop/nts.go index cb43dbff..b64b5a2d 100644 --- a/libwallet/newop/nts.go +++ b/libwallet/newop/nts.go @@ -6,10 +6,16 @@ import ( "github.com/muun/libwallet/operation" ) +const ( + UtxosStatusConfirmed = operation.UtxosStatusConfirmed + UtxosStatusUnconfirmed = operation.UtxosStatusUnconfirmed +) + type SizeForAmount struct { SizeInVByte int64 AmountInSat int64 Outpoint string + UtxoStatus string } // NextTransactionSize is a struct used for calculating fees in terms of the @@ -39,9 +45,15 @@ func (w *NextTransactionSize) GetOutpoints() string { func (w *NextTransactionSize) toInternalType() *operation.NextTransactionSize { var sizeProgression []operation.SizeForAmount for _, sizeForAmount := range w.SizeProgression { + status, found := operation.MapUtxoStatus(sizeForAmount.UtxoStatus) + if !found { + panic("unknown utxo status") // TODO(newop): replace panic and bubble up errors? + } sizeProgression = append(sizeProgression, operation.SizeForAmount{ SizeInVByte: sizeForAmount.SizeInVByte, AmountInSat: sizeForAmount.AmountInSat, + Outpoint: sizeForAmount.Outpoint, + UtxoStatus: status, }) } return &operation.NextTransactionSize{ diff --git a/libwallet/newop/state_test.go b/libwallet/newop/state_test.go index d2f5c58b..41e71c86 100644 --- a/libwallet/newop/state_test.go +++ b/libwallet/newop/state_test.go @@ -50,6 +50,7 @@ func createContext() *PaymentContext { context.NextTransactionSize.AddSizeForAmount(&SizeForAmount{ AmountInSat: 100_000_000, SizeInVByte: 240, + UtxoStatus: "CONFIRMED", }) context.ExchangeRateWindow.AddRate("BTC", 1) @@ -1329,6 +1330,7 @@ func TestOnChainTFFAWithDebtFeeNeedsChangeBecauseOutputAmountLowerThanDust(t *te nts.AddSizeForAmount(&SizeForAmount{ AmountInSat: 5338, SizeInVByte: 172, + UtxoStatus: "CONFIRMED", }) nts.ExpectedDebtInSat = 4353 context.NextTransactionSize = nts diff --git a/libwallet/operation/payment_analyzer.go b/libwallet/operation/payment_analyzer.go index a7f92064..142dcef5 100644 --- a/libwallet/operation/payment_analyzer.go +++ b/libwallet/operation/payment_analyzer.go @@ -3,6 +3,7 @@ package operation import ( "errors" "fmt" + "strings" "github.com/btcsuite/btcutil" "github.com/muun/libwallet/fees" @@ -64,9 +65,30 @@ type PaymentAnalyzer struct { feeCalculator *feeCalculator } +type UtxoStatus string + +const ( + UtxosStatusConfirmed UtxoStatus = "CONFIRMED" + UtxosStatusUnconfirmed UtxoStatus = "UNCONFIRMED" +) + +var ( + utxoStatusMap = map[string]UtxoStatus{ + "confirmed": UtxosStatusConfirmed, + "unconfirmed": UtxosStatusUnconfirmed, + } +) + +func MapUtxoStatus(str string) (UtxoStatus, bool) { + val, ok := utxoStatusMap[strings.ToLower(str)] + return val, ok +} + type SizeForAmount struct { AmountInSat int64 SizeInVByte int64 + Outpoint string + UtxoStatus UtxoStatus } type NextTransactionSize struct { @@ -330,7 +352,7 @@ func (a *PaymentAnalyzer) analyzeCollectSwap(payment *PaymentToInvoice, swapFees // determines the fees (on-chain and lightning), and we need both to determine the number of confirmations required for // the swap, which affects the on-chain fee, which affects the amount (since this is TFFA). // For this implementation built from the assumptions that 0-conf on-chain fees are lower than 1-conf fees -//(since we don't have to wait for a block to make the payment). +// (since we don't have to wait for a block to make the payment). // Here we go: // 1. We calculate the on-chain fee for a 0-conf swap spending all funds // - If that fee is greater than our balance -> payment can't be made (VERY low balance scenario) diff --git a/tools/bootstrap-gomobile.sh b/tools/bootstrap-gomobile.sh index f385cd38..c5f8dd16 100755 --- a/tools/bootstrap-gomobile.sh +++ b/tools/bootstrap-gomobile.sh @@ -3,7 +3,7 @@ repo_root=$(git rev-parse --show-toplevel) build_dir="$repo_root/libwallet/.build" -cd "$repo_root/libwallet" +cd "$repo_root/libwallet" || exit mkdir -p "$build_dir/pkg" @@ -11,4 +11,4 @@ mkdir -p "$build_dir/pkg" GOMODCACHE="$build_dir/pkg" \ go install golang.org/x/mobile/cmd/gomobile && \ - go install golang.org/x/mobile/cmd/gobind + go install golang.org/x/mobile/cmd/gobind \ No newline at end of file