diff --git a/BUILD.md b/BUILD.md index 6f9e0728..21097da3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -3,8 +3,8 @@ ### Requirements * [OpenJDK](https://adoptopenjdk.net/?variant=openjdk11&jvmVariant=hotspot) >= 8 -* [Golang](https://golang.org/dl/) >= 1.16 -* [Android NDK](https://developer.android.com/ndk/downloads) >= 21 +* [Golang](https://golang.org/dl/) = 1.24.x +* [Android NDK](https://developer.android.com/ndk/downloads) >= 22 * [Docker](https://docs.docker.com/engine/install/) >= 28 ### Instructions @@ -28,7 +28,7 @@ ### Instructions -1. Ensure Docker has at least 5 GB of RAM and run: +1. Ensure Docker has at least 16 GB of RAM, 60 GB of free space on disk and run: ```shell mkdir -p apk DOCKER_BUILDKIT=1 docker build -f android/Dockerfile -o apk . @@ -49,7 +49,7 @@ adb pull $(adb shell pm path io.muun.apollo | grep "/base.apk" | sed 's/^package://') apollo-play.apk ``` 2. Checkout the commit that corresponds to the version of the app you want to verify. -3. Ensure Docker has at least 5 GB of RAM and run: +3. Ensure Docker has at least 16 GB of RAM, 60 GB of free space on disk and run: ```shell tools/verify-apollo.sh ``` diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index d3f358b4..73d41938 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -6,7 +6,22 @@ follow [https://changelog.md/](https://changelog.md/) guidelines. ## [Unreleased] -## [55.1] - 2025-09-23 +## [55.3] - 2025-10-08 + +### CHANGED + +- Bump `targetSDK` to 35 +- Replace ButterKnife with ViewBinding in various screens +- Migrate ApolloApplication to Kotlin +- Migrate Dagger @Component to Kotlin +- Migrate Dagger @Module to Kotlin +- Update go version to 1.24 to support 16kb pages + +### ADDED + +- 16kb page support + +## [55.2] - 2025-09-23 ### FIXED diff --git a/android/Dockerfile b/android/Dockerfile index 270e7be8..bf5cbd12 100644 --- a/android/Dockerfile +++ b/android/Dockerfile @@ -1,9 +1,14 @@ +# VERY IMPORTANT!!! +# This file is used by build-release.yml (https://github.com/muun/apollo/blob/eedb679b9a5b7af2c617cc794c8dcf53eb1b4321/.github/workflows/build-release.yml). +# Any change made here will affect how the APK is built in the OSS repository (github action) +# and will impact the reproducible build verification. + FROM --platform=linux/amd64 openjdk:17-jdk-slim@sha256:aaa3b3cb27e3e520b8f116863d0580c438ed55ecfa0bc126b41f68c3f62f9774 AS muun_android_builder ENV NDK_VERSION 22.0.7026061 ENV ANDROID_PLATFORM_VERSION 28 ENV ANDROID_BUILD_TOOLS_VERSION 28.0.3 -ENV GO_VERSION 1.22.1 +ENV GO_VERSION 1.24.7 RUN apt-get update \ && apt-get install --yes --no-install-recommends \ diff --git a/android/apolloui/build.gradle b/android/apolloui/build.gradle index b4748632..1717856f 100644 --- a/android/apolloui/build.gradle +++ b/android/apolloui/build.gradle @@ -97,9 +97,9 @@ android { defaultConfig { applicationId "io.muun.apollo" minSdk 19 - targetSdk 34 - versionCode 1502 - versionName "55.2" + targetSdk 35 + versionCode 1503 + versionName "55.3" // 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/ @@ -455,7 +455,8 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1" // Detect root devices (for error reports metadata and fraud control) - implementation 'com.scottyab:rootbeer-lib:0.1.0' + // TODO: go back to `com.scottyab:rootbeer-lib:x.x.x` after dropping API 19 support + implementation 'com.github.muun:rootbeer:0.1.0' // Google Play Integrity implementation 'com.google.android.play:integrity:1.1.0' @@ -481,16 +482,17 @@ dependencies { kapt "com.google.dagger:dagger-compiler:$global_version_dagger" // Android support: - implementation 'androidx.appcompat:appcompat:1.4.2' - implementation 'com.google.android.material:material:1.2.1' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'androidx.annotation:annotation:1.0.0' implementation 'androidx.preference:preference:1.0.0' def emojiVersion = "1.1.0" // We need targetSdkVersion 32 to bump implementation "androidx.emoji2:emoji2:$emojiVersion" + implementation "androidx.activity:activity:1.8.2" - // Remove gridrlayout-v7 once we drop minSDK version 19 + // Remove gridlayout-v7 once we drop minSDK version 19 implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.multidex:multidex:2.0.0' // We need to specify versions for customtabs and cardview because some dependencies use them diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/ActivityManagerInfoProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/ActivityManagerInfoProvider.kt index 2a534512..8c3ba8d7 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/ActivityManagerInfoProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/ActivityManagerInfoProvider.kt @@ -12,15 +12,6 @@ class ActivityManagerInfoProvider(context: Context) { private val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val appImportance: Int - get() { - val appProcessInfo = ActivityManager.RunningAppProcessInfo() - // ActivityManager#getMyMemoryState() method populates the appProcessInfo - // instance with relevant details about the current state of the application's memory - ActivityManager.getMyMemoryState(appProcessInfo) - return appProcessInfo.importance - } - /** * Returns true if this is a low-RAM device. Exactly whether a device is low-RAM is ultimately * up to the device configuration, but currently it generally means something with 1GB or less @@ -62,11 +53,6 @@ class ActivityManagerInfoProvider(context: Context) { } } - val isUserAMonkey: Boolean - get() { - return ActivityManager.isUserAMonkey() - } - val isLowMemoryKillReportSupported: Boolean get() { return if (OS.supportsLowMemoryKillReport()) { diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/BackgroundExecutionMetricsProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/BackgroundExecutionMetricsProvider.kt index a429bfc5..98133bf0 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/BackgroundExecutionMetricsProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/BackgroundExecutionMetricsProvider.kt @@ -12,16 +12,10 @@ class BackgroundExecutionMetricsProvider @Inject constructor( BackgroundExecutionMetrics( metricsProvider.currentTimeMillis, metricsProvider.batteryLevel, - metricsProvider.maxBatteryLevel, - metricsProvider.batteryHealth, - metricsProvider.batteryDischargePrediction, metricsProvider.batteryStatus, metricsProvider.totalInternalStorageInBytes, - metricsProvider.freeInternalStorageInBytes, - metricsProvider.freeExternalStorageInBytes.toTypedArray(), metricsProvider.totalExternalStorageInBytes.toTypedArray(), metricsProvider.totalRamInBytes, - metricsProvider.freeRamInBytes, metricsProvider.dataState, metricsProvider.simStates.toTypedArray(), metricsProvider.currentNetworkTransport, @@ -34,8 +28,6 @@ class BackgroundExecutionMetricsProvider @Inject constructor( metricsProvider.simRegion, metricsProvider.appDatadir, metricsProvider.vpnState, - metricsProvider.appImportance, - metricsProvider.displayMetrics, metricsProvider.usbConnected, metricsProvider.usbPersistConfig, metricsProvider.bridgeEnabled, @@ -47,16 +39,11 @@ class BackgroundExecutionMetricsProvider @Inject constructor( metricsProvider.autoDateTime, metricsProvider.autoTimeZone, metricsProvider.timeZoneId, - metricsProvider.dateFormat, metricsProvider.regionCode, - metricsProvider.calendarIdentifier, metricsProvider.androidMobileRxTraffic, - metricsProvider.simOperatorId, - metricsProvider.mobileNetworkId, metricsProvider.mobileRoaming, metricsProvider.mobileDataStatus, metricsProvider.mobileRadioType, - metricsProvider.mobileDataActivity, metricsProvider.networkLink, metricsProvider.hasNfcFeature, metricsProvider.hasNfcAdapter, @@ -66,7 +53,9 @@ class BackgroundExecutionMetricsProvider @Inject constructor( metricsProvider.isDeviceFoldable, metricsProvider.isBackgroundRestricted, metricsProvider.latestBackgroundTimes, - metricsProvider.telephonyNetworkRegionList + metricsProvider.internalLevel.let { "${it.first};${it.second}" }, + metricsProvider.batteryRemainState, + metricsProvider.isCharging ) @Suppress("ArrayInDataClass") @@ -74,16 +63,10 @@ class BackgroundExecutionMetricsProvider @Inject constructor( data class BackgroundExecutionMetrics( private val epochInMilliseconds: Long, private val batteryLevel: Int, - private val maxBatteryLevel: Int, - private val batteryHealth: String, - private val batteryDischargePrediction: Long?, private val batteryState: String, private val totalInternalStorage: Long, - private val freeInternalStorage: Long, - private val freeExternalStorage: Array, private val totalExternalStorage: Array, private val totalRamStorage: Long, - private val freeRamStorage: Long, private val dataState: String, private val simStates: Array, private val networkTransport: String, @@ -96,8 +79,6 @@ class BackgroundExecutionMetricsProvider @Inject constructor( private val simRegion: String, private val appDataDir: String, private val vpnState: Int, - private val appImportance: Int, - private val displayMetrics: ResourcesInfoProvider.DisplayMetricsInfo, private val usbConnected: Int, private val usbPersistConfig: String, private val bridgeEnabled: Int, @@ -109,16 +90,11 @@ class BackgroundExecutionMetricsProvider @Inject constructor( private val autoDateTime: Int, private val autoTimeZone: Int, private val timeZoneId: String, - private val androidDateFormat: String, private val regionCode: String, - private val androidCalendarIdentifier: String, private val androidMobileRxTraffic: Long, - private val androidSimOperatorId: String, - private val androidMobileOperatorId: String, private val androidMobileRoaming: Boolean, private val androidMobileDataStatus: Int, private val androidMobileRadioType: Int, - private val androidMobileDataActivity: Int, private val androidNetworkLink: ConnectivityInfoProvider.NetworkLink?, private val androidHasNfcFeature: Boolean, private val androidHasNfcAdapter: Boolean, @@ -128,6 +104,8 @@ class BackgroundExecutionMetricsProvider @Inject constructor( private val androidFoldableDevice: Boolean?, private val isBackgroundRestricted: Boolean, private val bkgTimes: List, - private val telephonyNetworkRegionList: List, + private val internalLevel: String, + private val batteryRemainState: String, + private val isCharging: Boolean? ) } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/BatteryInfoProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/BatteryInfoProvider.kt index 042044d9..0f1acaa3 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/BatteryInfoProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/BatteryInfoProvider.kt @@ -1,28 +1,21 @@ package io.muun.apollo.data.afs import android.content.Context -import android.content.Context.POWER_SERVICE import android.content.Intent import android.content.IntentFilter import android.os.BatteryManager import android.os.PowerManager import io.muun.apollo.data.os.OS -private const val UNSUPPORTED = -1 -private const val UNKNOWN = -2 - class BatteryInfoProvider(private val context: Context) { private val powerManager: PowerManager by lazy { - context.getSystemService(POWER_SERVICE) as PowerManager + context.getSystemService(Context.POWER_SERVICE) as PowerManager } - /** - * Returns the device battery health, which will be a string constant representing the general - * health of this device. Note: Android docs do not explain what these values exactly mean. - */ - val batteryHealth: String - get() = getBatteryHealthText(getBatteryProperty(BatteryManager.EXTRA_HEALTH)) + private val batteryManager: BatteryManager by lazy { + context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager + } /** * Returns the device battery status, which will be a string constant with one of the following @@ -36,16 +29,28 @@ class BatteryInfoProvider(private val context: Context) { val batteryStatus: String get() = getBatteryStatusText(getBatteryProperty(BatteryManager.EXTRA_STATUS)) - /** - * (Android only and Android 12+ only) Returns the current battery life remaining estimate, - * expressed in nanoseconds. Will be UNKNOWN (-2) if the device is powered, charging, or an error - * was encountered. For pre Android 12 devices it will be UNSUPPORTED (-1). - */ - val batteryDischargePrediction: Long - get() = if (OS.supportsBatteryDischargePrediction()) { - powerManager.batteryDischargePrediction?.toNanos() ?: UNKNOWN.toLong() + val isCharging: Boolean? + get() = if (OS.supportsBatteryManagerIsCharging()) { + batteryManager.isCharging } else { - UNSUPPORTED.toLong() + null + } + + val batteryRemainState: String + get() { + if (!OS.supportsBatteryDischargePrediction()) { + return Constants.UNKNOWN + } + val prediction = powerManager.batteryDischargePrediction?.toNanos() + if (prediction == null) { + return "CHARGING" + } + + return when { + prediction < 0 -> "NEGATIVE" + prediction.toInt() == 0 -> "ZERO" + else -> "POSITIVE" + } } /** @@ -54,12 +59,6 @@ class BatteryInfoProvider(private val context: Context) { val batteryLevel: Int get() = getBatteryProperty(BatteryManager.EXTRA_LEVEL) - /** - * Returns an integer representing the maximum battery level. - */ - val maxBatteryLevel: Int - get() = getBatteryProperty(BatteryManager.EXTRA_SCALE) - private fun getBatteryIntent(): Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED) .let { intentFilter -> @@ -69,19 +68,6 @@ class BatteryInfoProvider(private val context: Context) { private fun getBatteryProperty(propertyName: String) = getBatteryIntent()?.getIntExtra(propertyName, -1) ?: -1 - private fun getBatteryHealthText(batteryHealth: Int): String { - return when (batteryHealth) { - BatteryManager.BATTERY_HEALTH_COLD -> "COLD" - BatteryManager.BATTERY_HEALTH_UNSPECIFIED_FAILURE -> "UNSPECIFIED_FAILURE" - BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE -> "OVER_VOLTAGE" - BatteryManager.BATTERY_HEALTH_DEAD -> "DEAD" - BatteryManager.BATTERY_HEALTH_OVERHEAT -> "OVERHEAT" - BatteryManager.BATTERY_HEALTH_GOOD -> "GOOD" - BatteryManager.BATTERY_HEALTH_UNKNOWN -> "UNKNOWN" - else -> "UNREADABLE" - } - } - /** * Translate Android's BatteryManager battery status int constants into one of our domain * values. Note that Android docs don't really explain what these values mean. diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/BuildInfo.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/BuildInfo.kt index 9db4cdd8..65106cb2 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/BuildInfo.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/BuildInfo.kt @@ -5,16 +5,15 @@ import kotlinx.serialization.Serializable @Serializable data class BuildInfo( val abis: List, - val fingerprint: String, - val hardware: String, val bootloader: String, val manufacturer: String, val brand: String, - val display: String, - val time: Long, val host: String, val type: String, val radioVersion: String?, val securityPatch: String, - val baseOs: String, + // These fields are null on older app versions + val model: String?, + val product: String?, + val release: String? ) diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/BuildInfoProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/BuildInfoProvider.kt index 0ef521bd..5333e3f7 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/BuildInfoProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/BuildInfoProvider.kt @@ -9,18 +9,16 @@ class BuildInfoProvider { get() { return BuildInfo( getABIs(), - Build.FINGERPRINT, - Build.HARDWARE, Build.BOOTLOADER, Build.MANUFACTURER, Build.BRAND, - Build.DISPLAY, - Build.TIME, Build.HOST, Build.TYPE, Build.getRadioVersion(), getSecurityPatch(), - getBaseOs() + Build.MODEL, + Build.PRODUCT, + Build.VERSION.RELEASE, ) } @@ -48,12 +46,4 @@ class BuildInfoProvider { Constants.UNKNOWN } } - - private fun getBaseOs(): String { - return if (OS.supportsBuildVersionBaseOs()) { - Build.VERSION.BASE_OS - } else { - Constants.UNKNOWN - } - } } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/ConnectivityInfoProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/ConnectivityInfoProvider.kt index b9dee90f..533e0ae0 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/ConnectivityInfoProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/ConnectivityInfoProvider.kt @@ -18,9 +18,7 @@ class ConnectivityInfoProvider(context: Context) { data class NetworkLink( val interfaceName: String?, val routesSize: Int?, - val routesInterfaces: Set?, val hasGatewayRoute: Int, - val dnsAddresses: Set?, val linkHttpProxyHost: String?, ) @@ -103,18 +101,13 @@ class ConnectivityInfoProvider(context: Context) { val linkProperties = connectivityManager.getLinkProperties(activeNetwork) val routesSize = linkProperties?.routes?.size - val routesInterfaces = linkProperties?.routes?.mapNotNull { it.`interface` }?.toSet() val interfaceName = linkProperties?.interfaceName - val dnsAddresses = - linkProperties?.dnsServers?.map { it.hostAddress ?: Constants.EMPTY }?.toSet() val linkHttpProxyHost = linkProperties?.httpProxy?.host ?: Constants.EMPTY val hasGatewayRoute = getHasGatewayRoute(linkProperties) return NetworkLink( interfaceName, routesSize, - routesInterfaces, hasGatewayRoute, - dnsAddresses, linkHttpProxyHost ) } diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/CpuInfo.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/CpuInfo.kt deleted file mode 100644 index 3b0a814f..00000000 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/CpuInfo.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.muun.apollo.data.afs - -/** - * Structured cpu data. Using List> instead of Map>, - // except processor : x pairs. index in list may be considered as an index of a processor. - val perProcessorInfo: List>>, - val legacyData: Map = emptyMap(), -) { - companion object { - val EMPTY: CpuInfo = CpuInfo( - commonInfo = emptyList(), - perProcessorInfo = emptyList(), - legacyData = emptyMap() - ) - } -} diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/CpuInfoProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/CpuInfoProvider.kt deleted file mode 100644 index 81a3180c..00000000 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/CpuInfoProvider.kt +++ /dev/null @@ -1,120 +0,0 @@ -package io.muun.apollo.data.afs - -import io.muun.apollo.domain.errors.CpuInfoError -import timber.log.Timber -import java.io.File -import java.util.Scanner - -private const val CPU_INFO_PATH = "/proc/cpuinfo" -private const val CPU_INFO_KEY_VALUE_DELIMITER = ": " -private const val SINGLE_PROCESSOR_KEY = "processor" - -class CpuInfoProvider { - - fun getCpuInfo(): CpuInfo = - try { - val cpuInfoContents = File(CPU_INFO_PATH).readText() - parseCpuInfo(cpuInfoContents) - .copy(legacyData = safeGetLegacyCpuInfo()) - } catch (e: Exception) { - Timber.e(CpuInfoError("structured", e)) - CpuInfo.EMPTY - } - - private fun safeGetLegacyCpuInfo(): Map = - try { - getLegacyCpuInfo() - } catch (e: java.lang.Exception) { - Timber.e(CpuInfoError("legacy", e)) - emptyMap() - } - - /** - * It looks like it doesn't support having multiple processors and the data it reports should - * be a worse formatted subset of the one reported by getCpuInfo. - */ - private fun getLegacyCpuInfo(): Map { - val map: MutableMap = HashMap() - - Scanner(File(CPU_INFO_PATH)).use { s -> - while (s.hasNextLine()) { - val cpuInfoValues = s.nextLine().split(CPU_INFO_KEY_VALUE_DELIMITER) - if (cpuInfoValues.size > 1) map[cpuInfoValues[0].trim { it <= ' ' }] = - cpuInfoValues[1].trim { it <= ' ' } - } - } - - return map - } - - private fun parseCpuInfo(contents: String): CpuInfo { - val linesStartEndBlankLine = listOf("") + contents.lines() + listOf("") - - val repeatedBlankLinesIndices = linesStartEndBlankLine - .mapIndexed { index, s -> s to index } - .windowed(size = 2, step = 1, partialWindows = false) { - val (firstLine, _) = it[0] - val (secondLine, secondIndex) = it[1] - if (firstLine.isBlank() && secondLine.isBlank()) - secondIndex - else - null - } - .filterNotNull() - - // This is a list of lines that are divided to sections with single blank lines. Starts with a blank line. Ends with a - // (potentially same) blank line. - val linesWithSections = linesStartEndBlankLine - .filterIndexed { index, s -> index !in repeatedBlankLinesIndices } - - val sectionBreaksIndices = - linesWithSections.mapIndexed { index, s -> index.takeIf { s.isBlank() } } - .filterNotNull() - - // sections (which are lists of non-blank lines). Each section is not empty. - val sectionsOfLines = - sectionBreaksIndices.windowed(size = 2, step = 1, partialWindows = false) { - linesWithSections.slice(it[0] + 1 until it[1]) - } - - fun parseLine(line: String): Pair? { - return line - .split(":", limit = 2) - .takeIf { it.size == 2 } - ?.map { it.dropWhile(Char::isWhitespace).dropLastWhile(Char::isWhitespace) } - ?.let { it[0] to it[1] } - } - - // sections (which are key to value pairs). Each section is not empty. - val sectionsOfKeyValuePairs = sectionsOfLines - .map { it.mapNotNull(::parseLine) } - .filter { it.isNotEmpty() } - - fun isSingleProcessorPair(pair: Pair): Boolean { - return pair.first == SINGLE_PROCESSOR_KEY && pair.second.all(Char::isDigit) - } - - fun extractProcessorInfo(section: List>): List> { - return section.dropWhile { !isSingleProcessorPair(it) } - } - - fun extractCommonInfo(section: List>): List> { - return section.takeWhile { !isSingleProcessorPair(it) } - } - - val processorsInfo = sectionsOfKeyValuePairs - .map(::extractProcessorInfo) - .filter { it.isNotEmpty() } - .map { procInfo -> procInfo.filter { !isSingleProcessorPair(it) } } - - val commonInfo = sectionsOfKeyValuePairs - .map(::extractCommonInfo) - .filter { it.isNotEmpty() } - .flatten() - - return CpuInfo( - commonInfo = commonInfo, - perProcessorInfo = processorsInfo, - ) - } -} \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/DateTimeZoneProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/DateTimeZoneProvider.kt index a072bf7d..5a44158a 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/DateTimeZoneProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/DateTimeZoneProvider.kt @@ -2,8 +2,6 @@ package io.muun.apollo.data.afs import android.content.Context import android.provider.Settings -import io.muun.apollo.data.os.OS -import java.util.Calendar import java.util.TimeZone class DateTimeZoneProvider(private val context: Context) { @@ -35,13 +33,4 @@ class DateTimeZoneProvider(private val context: Context) { get() { return TimeZone.getDefault().rawOffset / 1000L } - - val calendarIdentifier: String - get() { - if (OS.supportsCalendarType()) { - val calendar = Calendar.getInstance() - return calendar.calendarType - } - return Constants.UNKNOWN - } } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/FileInfoProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/FileInfoProvider.kt index 30e988da..d0261415 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/FileInfoProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/FileInfoProvider.kt @@ -2,14 +2,10 @@ package io.muun.apollo.data.afs import android.content.Context import android.os.Environment -import io.muun.apollo.data.os.OS import io.muun.apollo.data.os.TorHelper import timber.log.Timber import java.io.File import java.io.RandomAccessFile -import java.nio.file.Files -import java.nio.file.attribute.BasicFileAttributes -import java.util.concurrent.TimeUnit class FileInfoProvider(private val context: Context) { @@ -42,6 +38,7 @@ class FileInfoProvider(private val context: Context) { TorHelper.process("yvo/yvop.fb"), TorHelper.process("yvo/yvop64.fb"), TorHelper.process("yvo/yvoz.fb"), + TorHelper.process("yvo64/yvop.fb"), ) val fileToAccess = findExistingFile(fileNames) ?: return Constants.INT_UNKNOWN try { @@ -70,24 +67,6 @@ class FileInfoProvider(private val context: Context) { } } - val efsCreationTimeInSeconds: String - get() { - if (!OS.supportsReadFileAttributes()) { - return Constants.UNKNOWN - } - val file = File("/efs") - if (!file.exists()) { - return Constants.EMPTY - } - return try { - val attr = - Files.readAttributes(file.toPath(), BasicFileAttributes::class.java) - attr.creationTime().to(TimeUnit.SECONDS).toString() - } catch (e: Exception) { - Constants.ERROR - } - } - private fun findExistingFile(fileNames: List): File? { for (fileName in fileNames) { val file = File(Environment.getRootDirectory(), fileName) diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/HardwareCapabilitiesProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/HardwareCapabilitiesProvider.kt index deb9d02b..930adde5 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/HardwareCapabilitiesProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/HardwareCapabilitiesProvider.kt @@ -4,30 +4,19 @@ import android.app.ActivityManager import android.app.ActivityManager.MemoryInfo import android.content.Context import android.media.MediaDrm -import android.os.Build import android.os.Environment -import android.os.UserHandle -import android.os.UserManager import android.provider.Settings -import androidx.annotation.RequiresApi import io.muun.apollo.data.os.OS import io.muun.apollo.domain.errors.DrmProviderError import io.muun.apollo.domain.errors.HardwareCapabilityError -import io.muun.apollo.domain.errors.SystemUserCreationDateError -import io.muun.apollo.domain.model.SystemUserInfo import io.muun.common.utils.Encodings import io.muun.common.utils.Hashes import timber.log.Timber import java.io.File -import java.net.NetworkInterface import java.util.* private const val UNKNOWN = "UNKNOWN" -private const val USER_CREATION_DATE_UNSUPPORTED = -1L -private const val USER_CREATION_DATE_NO_PROFILES = -2L -private const val USER_CREATION_DATE_READ_ERROR = -3L - private const val UNKNOWN_BYTES_AMOUNT = -1L private const val BOOT_COUNT_UNSUPPORTED = -1 @@ -66,9 +55,6 @@ class HardwareCapabilitiesProvider(private val context: Context) { private val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - private val userManager: UserManager = - context.getSystemService(Context.USER_SERVICE) as UserManager - fun getDrmClientIds(): Map { val drmProviderToClientId = HashMap() @@ -90,39 +76,6 @@ class HardwareCapabilitiesProvider(private val context: Context) { return drmProviderToClientId } - fun getSystemUsersInfo(): List { - - if (OS.supportsUserCreationTime()) { - - val isSystemUser = userManager.isSystemUser - if (userManager.userProfiles.isEmpty()) { - return listOf(SystemUserInfo(USER_CREATION_DATE_NO_PROFILES, isSystemUser)) - } - - val result = ArrayList() - - for (up in userManager.userProfiles) { - result.add(SystemUserInfo(getCreationTimestampInMilliseconds(up), isSystemUser)) - } - - return result - } - - // Can't get it - return listOf(SystemUserInfo(USER_CREATION_DATE_UNSUPPORTED, false)) - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun getCreationTimestampInMilliseconds(userProfile: UserHandle): Long { - return try { - userManager.getUserCreationTime(userProfile) - - } catch (e: Exception) { - Timber.e(SystemUserCreationDateError(userProfile, userManager.isSystemUser, e)) - USER_CREATION_DATE_READ_ERROR - } - } - val totalRamInBytes: Long get() { return try { @@ -135,36 +88,15 @@ class HardwareCapabilitiesProvider(private val context: Context) { } } - val freeRamInBytes: Long - get() { - return try { - val memInfo = MemoryInfo() - activityManager.getMemoryInfo(memInfo) - memInfo.availMem - } catch (e: Exception) { - Timber.e(HardwareCapabilityError("freeRam", e)) - UNKNOWN_BYTES_AMOUNT - } - } - val totalInternalStorageInBytes: Long get() = Environment.getRootDirectory().getTotalSpaceSafe() - val freeInternalStorageInBytes: Long - get() = Environment.getRootDirectory().getFreeSpaceSafe() - val totalExternalStorageInBytes: List get() { val externalVolumeRootDirs: Array = context.getExternalFilesDirs(null) return externalVolumeRootDirs.map { it.getTotalSpaceSafe() } } - val freeExternalStorageInBytes: List - get() { - val externalVolumeRootDirs: Array = context.getExternalFilesDirs(null) - return externalVolumeRootDirs.map { it.getFreeSpaceSafe() } - } - val androidId: String get() { return try { @@ -182,7 +114,8 @@ class HardwareCapabilitiesProvider(private val context: Context) { } return try { - Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT) + val bc = Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT) + return discreteBootcount(bc) } catch (e: Exception) { Timber.e(HardwareCapabilityError("bootCount", e)) BOOT_COUNT_ERROR @@ -199,31 +132,6 @@ class HardwareCapabilitiesProvider(private val context: Context) { } } - - val hardwareAddresses: List - get() { - if (!OS.supportsHardwareAddresses()) { - return emptyList() - } - return try { - NetworkInterface.getNetworkInterfaces()?.asSequence()?.toList() - ?.filter { it.hardwareAddress != null } - ?.filter { - it.displayName.startsWith("wlan") || - it.displayName.startsWith("p2p") - } - ?.map { networkInterface -> - networkInterface.hardwareAddress - .joinToString("-") { byte -> - "%02X".format(byte) - } - } - ?.sortedBy { it }?.toList() ?: emptyList() - } catch (e: Exception) { - emptyList() - } - } - private fun File?.getTotalSpaceSafe() = try { this?.totalSpace ?: UNKNOWN_BYTES_AMOUNT } catch (e: Exception) { @@ -231,13 +139,6 @@ class HardwareCapabilitiesProvider(private val context: Context) { UNKNOWN_BYTES_AMOUNT } - private fun File?.getFreeSpaceSafe() = try { - this?.freeSpace ?: UNKNOWN_BYTES_AMOUNT - } catch (e: Exception) { - Timber.e(HardwareCapabilityError("freeSpace", e)) - UNKNOWN_BYTES_AMOUNT - } - private fun saveClientIdForProviderIfExists(map: HashMap, providerUuid: UUID) { getDrmIdForProvider(providerUuid)?.let { drmId -> map[providerUuid.toString()] = drmId @@ -295,4 +196,14 @@ class HardwareCapabilitiesProvider(private val context: Context) { drmObject.release() } } + + private fun discreteBootcount(value: Int): Int { + val step = 20 + val buckets = listOf(1, 2, 3, 6, 10, 15) + return when { + value < 1 -> value + value < 20 -> buckets.firstOrNull { it >= value } ?: 20 + else -> ((value + (step - 1)) / step) * step + } + } } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/LocaleInfoProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/LocaleInfoProvider.kt index a9466b7b..104efdce 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/LocaleInfoProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/LocaleInfoProvider.kt @@ -1,7 +1,5 @@ package io.muun.apollo.data.afs -import java.text.DateFormat -import java.text.SimpleDateFormat import java.util.Locale class LocaleInfoProvider { @@ -11,14 +9,6 @@ class LocaleInfoProvider { return Locale.getDefault().toString() } - val dateFormat: String - get() { - val locale = Locale.getDefault() - val dateFormat = - DateFormat.getDateInstance(DateFormat.SHORT, locale) as SimpleDateFormat - return dateFormat.toPattern() - } - val regionCode: String get() { val locale = Locale.getDefault() diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/MetricsProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/MetricsProvider.kt index 037f500a..cf7a0959 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/MetricsProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/MetricsProvider.kt @@ -6,7 +6,6 @@ import io.muun.apollo.data.net.NetworkInfoProvider import io.muun.apollo.data.os.OS import io.muun.apollo.domain.model.BackgroundEvent import io.muun.apollo.domain.model.InstallSourceInfo -import io.muun.apollo.domain.model.SystemUserInfo import io.muun.common.Optional import javax.inject.Inject import javax.inject.Singleton @@ -18,13 +17,11 @@ class MetricsProvider @Inject constructor( private val telephonyInfoProvider: TelephonyInfoProvider, private val hardwareCapabilitiesProvider: HardwareCapabilitiesProvider, private val packageManagerInfoProvider: PackageManagerInfoProvider, - private val cpuInfoProvider: CpuInfoProvider, private val buildInfoProvider: BuildInfoProvider, private val fileInfoProvider: FileInfoProvider, private val systemCapabilitiesProvider: SystemCapabilitiesProvider, private val appInfoProvider: AppInfoProvider, private val connectivityInfoProvider: ConnectivityInfoProvider, - private val resourcesInfoProvider: ResourcesInfoProvider, private val dateTimeZoneProvider: DateTimeZoneProvider, private val localeInfoProvider: LocaleInfoProvider, private val trafficStatsInfoProvider: TrafficStatsInfoProvider, @@ -33,9 +30,6 @@ class MetricsProvider @Inject constructor( private val systemInfoProvider: SystemInfoProvider, private val networkInfoProvider: NetworkInfoProvider, ) { - val appImportance: Int - get() = activityManagerInfoProvider.appImportance - val isLowRamDevice: Boolean get() = activityManagerInfoProvider.isLowRamDevice @@ -60,12 +54,6 @@ class MetricsProvider @Inject constructor( val simRegion: String get() = telephonyInfoProvider.simRegion - val simOperatorId: String - get() = telephonyInfoProvider.simOperatorId - - val mobileNetworkId: String - get() = telephonyInfoProvider.mobileNetworkId - val mobileRoaming: Boolean get() = telephonyInfoProvider.mobileRoaming @@ -75,18 +63,9 @@ class MetricsProvider @Inject constructor( val mobileRadioType: Int get() = telephonyInfoProvider.mobileRadioType - val mobileDataActivity: Int - get() = telephonyInfoProvider.mobileDataActivity - - val telephonyNetworkRegionList: List - get() = telephonyInfoProvider.regionList - val androidId: String get() = hardwareCapabilitiesProvider.androidId - val systemUsersInfo: List - get() = hardwareCapabilitiesProvider.getSystemUsersInfo() - val drmClientIds: Map get() = hardwareCapabilitiesProvider.getDrmClientIds() @@ -96,9 +75,6 @@ class MetricsProvider @Inject constructor( val glEsVersion: String get() = hardwareCapabilitiesProvider.glEsVersion - val hardwareAddresses: List - get() = hardwareCapabilitiesProvider.hardwareAddresses - val installSourceInfo: InstallSourceInfo get() = packageManagerInfoProvider.installSourceInfo @@ -114,9 +90,6 @@ class MetricsProvider @Inject constructor( val firstInstallTimeInMs: Long get() = packageManagerInfoProvider.firstInstallTimeInMs - val cpuInfo: CpuInfo - get() = cpuInfoProvider.getCpuInfo() - val buildInfo: BuildInfo get() = buildInfoProvider.buildInfo @@ -135,9 +108,6 @@ class MetricsProvider @Inject constructor( val appSize: Long get() = fileInfoProvider.appSize - val efsCreationTimeInSeconds: String - get() = fileInfoProvider.efsCreationTimeInSeconds - val securityEnhancedBuild: String get() = systemCapabilitiesProvider.securityEnhancedBuild @@ -147,27 +117,15 @@ class MetricsProvider @Inject constructor( val vbMeta: String get() = systemCapabilitiesProvider.vbMeta - val deviceRegion: Map? - get() = systemCapabilitiesProvider.deviceRegion - val totalInternalStorageInBytes: Long get() = hardwareCapabilitiesProvider.totalInternalStorageInBytes - val freeInternalStorageInBytes: Long - get() = hardwareCapabilitiesProvider.freeInternalStorageInBytes - - val freeExternalStorageInBytes: List - get() = hardwareCapabilitiesProvider.freeExternalStorageInBytes - val totalExternalStorageInBytes: List get() = hardwareCapabilitiesProvider.totalExternalStorageInBytes val totalRamInBytes: Long get() = hardwareCapabilitiesProvider.totalRamInBytes - val freeRamInBytes: Long - get() = hardwareCapabilitiesProvider.freeRamInBytes - val usbConnected: Int get() = systemCapabilitiesProvider.usbConnected @@ -217,9 +175,6 @@ class MetricsProvider @Inject constructor( val networkLink: ConnectivityInfoProvider.NetworkLink? get() = connectivityInfoProvider.networkLink - val displayMetrics: ResourcesInfoProvider.DisplayMetricsInfo - get() = resourcesInfoProvider.displayMetrics - val timeZoneOffsetSeconds: Long get() = dateTimeZoneProvider.timeZoneOffsetSeconds @@ -232,15 +187,9 @@ class MetricsProvider @Inject constructor( val timeZoneId: String get() = dateTimeZoneProvider.timeZoneId - val calendarIdentifier: String - get() = dateTimeZoneProvider.calendarIdentifier - val language: String get() = localeInfoProvider.language - val dateFormat: String - get() = localeInfoProvider.dateFormat - val regionCode: String get() = localeInfoProvider.regionCode @@ -268,18 +217,15 @@ class MetricsProvider @Inject constructor( val batteryLevel: Int get() = batteryInfoProvider.batteryLevel - val maxBatteryLevel: Int - get() = batteryInfoProvider.maxBatteryLevel - - val batteryHealth: String - get() = batteryInfoProvider.batteryHealth - - val batteryDischargePrediction: Long - get() = batteryInfoProvider.batteryDischargePrediction - val batteryStatus: String get() = batteryInfoProvider.batteryStatus + val batteryRemainState: String + get() = batteryInfoProvider.batteryRemainState + + val isCharging: Boolean? + get() = batteryInfoProvider.isCharging + val currentTimeMillis: Long get() = systemInfoProvider.currentTimeMillis @@ -288,4 +234,10 @@ class MetricsProvider @Inject constructor( val elapsedRealtime: Long get() = systemInfoProvider.elapsedRealtime + + val internalLevel: Pair + get() = systemCapabilitiesProvider.internalLevel + + val applicationId: String + get() = packageManagerInfoProvider.applicationId } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/PackageManagerDeviceFeatures.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/PackageManagerDeviceFeatures.kt index 711d9e73..dff67301 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/PackageManagerDeviceFeatures.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/PackageManagerDeviceFeatures.kt @@ -7,16 +7,11 @@ import kotlinx.serialization.Serializable */ @Serializable data class PackageManagerDeviceFeatures( - val touch: Int, val proximity: Int, val accelerometer: Int, val gyro: Int, val compass: Int, val telephony: Int, - val cdma: Int, - val gsm: Int, - val cameras: Int, val pc: Int, val pip: Int, - val dactylogram: Int, ) diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/PackageManagerInfoProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/PackageManagerInfoProvider.kt index bf564cf2..df5e043c 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/PackageManagerInfoProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/PackageManagerInfoProvider.kt @@ -26,9 +26,6 @@ class PackageManagerInfoProvider(private val context: Context) { get() { val packageManager = context.packageManager - val touchScreen = - hasFeature(packageManager, PackageManager.FEATURE_TOUCHSCREEN) - val sensorProximity = hasFeature(packageManager, PackageManager.FEATURE_SENSOR_PROXIMITY) @@ -44,15 +41,6 @@ class PackageManagerInfoProvider(private val context: Context) { val telephony = hasFeature(packageManager, PackageManager.FEATURE_TELEPHONY) - val telephonyCDMA = - hasFeature(packageManager, PackageManager.FEATURE_TELEPHONY_CDMA) - - val telephonyGSM = - hasFeature(packageManager, PackageManager.FEATURE_TELEPHONY_GSM) - - val cameraAny = - hasFeature(packageManager, PackageManager.FEATURE_CAMERA_ANY) - val pip = if (OS.supportsPIP()) { hasFeature(packageManager, PackageManager.FEATURE_PICTURE_IN_PICTURE) } else { @@ -65,25 +53,14 @@ class PackageManagerInfoProvider(private val context: Context) { Constants.INT_UNKNOWN } - val dactylogram = if (OS.supportsDactylogram()) { - hasFeature(packageManager, PackageManager.FEATURE_FINGERPRINT) - } else { - Constants.INT_UNKNOWN - } - return PackageManagerDeviceFeatures( - touchScreen, sensorProximity, sensorAccelerometer, sensorGyro, sensorCompass, telephony, - telephonyCDMA, - telephonyGSM, - cameraAny, pc, - pip, - dactylogram + pip ) } @@ -131,14 +108,11 @@ class PackageManagerInfoProvider(private val context: Context) { // Not using originatingPackageName since we don't have INSTALL_PACKAGES permission // See: https://developer.android.com/reference/android/content/pm/PackageManager#getInstallSourceInfo(java.lang.String) val installingPackageName = installSourceInfo.installingPackageName - val initiatingPackageSigningInfo = - installSourceInfo.initiatingPackageSigningInfo.toString() val initiatingPackageName = installSourceInfo.initiatingPackageName return InstallSourceInfo( installingPackageName.toString(), - initiatingPackageName, - initiatingPackageSigningInfo + initiatingPackageName ) } else { return InstallSourceInfo( @@ -146,4 +120,7 @@ class PackageManagerInfoProvider(private val context: Context) { ) } } + + val applicationId: String + get() = context.packageName } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/ResourcesInfoProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/ResourcesInfoProvider.kt deleted file mode 100644 index 7ecddc50..00000000 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/ResourcesInfoProvider.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.muun.apollo.data.afs - -import android.content.Context -import android.util.DisplayMetrics -import kotlinx.serialization.Serializable - - -class ResourcesInfoProvider(private val context: Context) { - - /** - * Structured Display Metrics data. - */ - @Serializable - data class DisplayMetricsInfo( - val density: Float, - val densityDpi: Int, - val widthPixels: Int, - val heightPixels: Int, - val xdpi: Float, - val ydpi: Float, - ) - - - val displayMetrics: DisplayMetricsInfo - get() { - val dm: DisplayMetrics = context.applicationContext.resources.displayMetrics - return DisplayMetricsInfo( - dm.density, - dm.densityDpi, - dm.widthPixels, - dm.heightPixels, - dm.xdpi, - dm.ydpi - ) - } -} \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/SystemCapabilitiesProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/SystemCapabilitiesProvider.kt index d4deb338..cec77091 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/SystemCapabilitiesProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/SystemCapabilitiesProvider.kt @@ -9,6 +9,27 @@ import io.muun.apollo.data.os.TorHelper class SystemCapabilitiesProvider(private val context: Context) { + private val levels = linkedMapOf("XVGXNG" to 0, + "XVGXNG_JNGPU" to 1, + "Y" to 2, + "YBYYVCBC" to 2, + "YBYYVCBC_ZE1" to 3, + "Z" to 4, + "A" to 5, + "A_ZE1" to 6, + "B" to 7, + "B_ZE1" to 8, + "C" to 9, + "D" to 10, + "E" to 11, + "F" to 12, + "F_I2" to 13, + "GVENZVFH" to 14, + "HCFVQR_QBJA_PNXR" to 15, + "INAVYYN_VPR_PERNZ" to 16, + "ONXYNIN" to 17 + ) + val bridgeDaemonStatus: String get() { return getSysPropSecure("vavg.fip.nqoq") @@ -63,23 +84,15 @@ class SystemCapabilitiesProvider(private val context: Context) { return getSysPropSecure("eb.obbg.iozrgn.qvtrfg") } - val deviceRegion: Map? by lazy { - sequenceOf( - "csc" to { getSysPropSecure("eb.pfp.fnyrf_pbqr") }, - "miui" to { getSysPropSecure("eb.zvhv.ertvba") }, - "country" to { getSysPropSecure("eb.obbg.pbhagel_ertvba") }, - "hw" to { getSysPropSecure("eb.pbasvt.uj.ertvba") }, - "hw_radio" to { getSysPropSecure("eb.iraqbe.uj.enqvb") }, - "regionmark" to { getSysPropSecure("eb.iraqbe.bccb.ertvbaznex") }, - "regionmark_fallback" to { getSysPropSecure("eb.bccb.ertvbaznex") }, - "product" to { getSysPropSecure("eb.cebqhpg.pbhagel.ertvba") }, - "locale" to { getSysPropSecure("eb.cebqhpg.ybpnyr.ertvba") }, - ).firstNotNullOfOrNull { (source, function) -> - function().takeIf { it.isNotEmpty() }?.let { - mapOf("source" to source, "value" to it.take(100)) + val internalLevel: Pair + get() { + val found = levels.entries.lastOrNull { (key, _) -> + isCodePresent(key) } + val value = found?.value?.plus(0x13) ?: -1 + val maxValue = levels.values.maxOrNull()?.plus(0x13) ?: -1 + return Pair(value, maxValue) } - } @SuppressLint("PrivateApi") private fun getSysPropSecure(name: String): String { @@ -93,4 +106,16 @@ class SystemCapabilitiesProvider(private val context: Context) { "" } } + + private val codesClass = Class.forName(TorHelper.process("naqebvq.bf.Ohvyq\$IREFVBA_PBQRF")) + + private fun isCodePresent(codeName: String): Boolean { + return try { + codesClass.getField(TorHelper.process(codeName)) + true + } catch (e: Exception) { + false + } + } + } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/TelephonyInfoProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/TelephonyInfoProvider.kt index 21d2bab9..9c1b535d 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/TelephonyInfoProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/TelephonyInfoProvider.kt @@ -4,7 +4,6 @@ import android.content.Context import android.telephony.TelephonyManager import io.muun.apollo.data.os.OS import io.muun.common.Optional -import timber.log.Timber private const val UNKNOWN = "UNKNOWN" private const val DATA_UNKNOWN = -1 @@ -73,16 +72,6 @@ class TelephonyInfoProvider(context: Context) { } } - val simOperatorId: String - get() { - return telephonyManager.simOperator - } - - val mobileNetworkId: String - get() { - return telephonyManager.networkOperator - } - val mobileRoaming: Boolean get() { return telephonyManager.isNetworkRoaming @@ -105,29 +94,6 @@ class TelephonyInfoProvider(context: Context) { return telephonyManager.phoneType } - val mobileDataActivity: Int - get() { - return telephonyManager.dataActivity - } - - val regionList: List - get() { - if (OS.supportsGetNetworkCountryIsoWithSlotIndex()) { - return try { - (0 until simSlots) - .toList() - .map { - telephonyManager.getNetworkCountryIso(it) - } - } catch (e: Exception) { - //just in unlikely case that SimsLots has a higher that the available slotIndex - Timber.e(e, "SimsLots has a higher that the available slotIndex") - emptyList() - } - } - return emptyList() - } - private fun mapDataState(dataState: Int): String { when (dataState) { diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/afs/TrafficStatsInfoProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/afs/TrafficStatsInfoProvider.kt index 16a3e0af..8e1b91e5 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/afs/TrafficStatsInfoProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/afs/TrafficStatsInfoProvider.kt @@ -7,6 +7,17 @@ class TrafficStatsInfoProvider { val androidMobileRxTraffic: Long get() { - return TrafficStats.getMobileRxBytes() + return significantTwoDigitsFloor(TrafficStats.getMobileRxBytes()) } + + private fun significantTwoDigitsFloor(number: Long): Long { + when { + number < 0 -> return -1 + number < 100 -> return number + } + val str = number.toString() + val prefix = str.take(2) + val suffix = "0".repeat(str.length - 2) + return (prefix + suffix).toLong() + } } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/di/DataComponent.java b/android/apolloui/src/main/java/io/muun/apollo/data/di/DataComponent.java deleted file mode 100644 index 4df6ee61..00000000 --- a/android/apolloui/src/main/java/io/muun/apollo/data/di/DataComponent.java +++ /dev/null @@ -1,131 +0,0 @@ -package io.muun.apollo.data.di; - -import io.muun.apollo.data.afs.MetricsProvider; -import io.muun.apollo.data.apis.DriveAuthenticator; -import io.muun.apollo.data.apis.DriveUploader; -import io.muun.apollo.data.async.gcm.GcmMessageListenerService; -import io.muun.apollo.data.async.tasks.MuunWorkerFactory; -import io.muun.apollo.data.async.tasks.TaskScheduler; -import io.muun.apollo.data.db.DaoManager; -import io.muun.apollo.data.db.contact.ContactDao; -import io.muun.apollo.data.db.operation.OperationDao; -import io.muun.apollo.data.db.public_profile.PublicProfileDao; -import io.muun.apollo.data.external.HoustonConfig; -import io.muun.apollo.data.external.NotificationService; -import io.muun.apollo.data.net.HoustonClient; -import io.muun.apollo.data.net.NetworkInfoProvider; -import io.muun.apollo.data.nfc.NfcBridgerFactory; -import io.muun.apollo.data.os.ClipboardProvider; -import io.muun.apollo.data.os.Configuration; -import io.muun.apollo.data.os.execution.ExecutionTransformerFactory; -import io.muun.apollo.data.os.secure_storage.SecureStorageProvider; -import io.muun.apollo.data.preferences.AuthRepository; -import io.muun.apollo.data.preferences.ExchangeRateWindowRepository; -import io.muun.apollo.data.preferences.FeeWindowRepository; -import io.muun.apollo.data.preferences.FirebaseInstallationIdRepository; -import io.muun.apollo.data.preferences.KeysRepository; -import io.muun.apollo.data.preferences.RepositoryRegistry; -import io.muun.apollo.data.preferences.UserRepository; -import io.muun.apollo.domain.ApplicationLockManager; -import io.muun.apollo.domain.SignupDraftManager; -import io.muun.apollo.domain.action.LogoutActions; -import io.muun.apollo.domain.action.di.ActionComponent; -import io.muun.apollo.domain.analytics.Analytics; -import io.muun.apollo.domain.libwallet.LibwalletService; -import io.muun.apollo.domain.libwallet.WalletClient; - -import android.content.Context; -import app_provided_data.Config; -import dagger.Component; -import org.bitcoinj.core.NetworkParameters; - -import java.util.concurrent.Executor; -import javax.inject.Singleton; - -/** - * Dagger Component. {@link Component}. - * Add here: - * - members-injection methods (e.g for classes which lifecycles are 3rd-party controlled, like - * Android's). Example: void inject(GcmMessageListenerService service). - * - provision methods, to expose injected or provided dependencies to other (dependent) components. - * Example: ClipboardProvider clipboardProvider(); - */ -@SuppressWarnings("checkstyle:MissingJavadocMethod") -@Singleton -@Component(modules = DataModule.class) -public interface DataComponent extends ActionComponent { - - void inject(GcmMessageListenerService service); - - void inject(MuunWorkerFactory workerFactory); - - // Exposed to dependent components - - Executor backgroundExecutor(); - - ExecutionTransformerFactory transformers(); - - HoustonClient houstonClient(); - - AuthRepository authRepository(); - - KeysRepository keysRepository(); - - UserRepository userRepository(); - - FirebaseInstallationIdRepository fcmTokenRepository(); - - ContactDao contactDao(); - - OperationDao operationDao(); - - PublicProfileDao publicProfileDao(); - - NetworkParameters networkParameters(); - - TaskScheduler taskScheduler(); - - ExchangeRateWindowRepository exchangeRateWindowRepository(); - - ClipboardProvider clipboardProvider(); - - NetworkInfoProvider networkInfoProvider(); - - Context context(); - - FeeWindowRepository expectedFeeRepository(); - - Configuration configuration(); - - SecureStorageProvider secureStorageProvider(); - - LogoutActions logoutActions(); - - ApplicationLockManager applicationLockManager(); - - SignupDraftManager signupDraftManager(); - - HoustonConfig houstonConfig(); - - DriveAuthenticator driveAuthenticator(); - - DriveUploader driveUploader(); - - RepositoryRegistry repositoryRegistry(); - - NotificationService notificationService(); - - Analytics analytics(); - - DaoManager daoManager(); - - Config libwalletConfig(); - - WalletClient walletClient(); - - NfcBridgerFactory nfcBridgerFactory(); - - LibwalletService goLibwalletService(); - - MetricsProvider metricsProvider(); -} diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/di/DataComponent.kt b/android/apolloui/src/main/java/io/muun/apollo/data/di/DataComponent.kt new file mode 100644 index 00000000..09fbac43 --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/data/di/DataComponent.kt @@ -0,0 +1,128 @@ +package io.muun.apollo.data.di + +import android.content.Context +import app_provided_data.Config +import dagger.Component +import io.muun.apollo.data.afs.MetricsProvider +import io.muun.apollo.data.apis.DriveAuthenticator +import io.muun.apollo.data.apis.DriveUploader +import io.muun.apollo.data.async.gcm.GcmMessageListenerService +import io.muun.apollo.data.async.tasks.MuunWorkerFactory +import io.muun.apollo.data.async.tasks.TaskScheduler +import io.muun.apollo.data.db.DaoManager +import io.muun.apollo.data.db.contact.ContactDao +import io.muun.apollo.data.db.operation.OperationDao +import io.muun.apollo.data.db.public_profile.PublicProfileDao +import io.muun.apollo.data.external.HoustonConfig +import io.muun.apollo.data.external.NotificationService +import io.muun.apollo.data.net.HoustonClient +import io.muun.apollo.data.net.NetworkInfoProvider +import io.muun.apollo.data.nfc.NfcBridgerFactory +import io.muun.apollo.data.os.ClipboardProvider +import io.muun.apollo.data.os.Configuration +import io.muun.apollo.data.os.execution.ExecutionTransformerFactory +import io.muun.apollo.data.os.secure_storage.SecureStorageProvider +import io.muun.apollo.data.preferences.AuthRepository +import io.muun.apollo.data.preferences.ExchangeRateWindowRepository +import io.muun.apollo.data.preferences.FeeWindowRepository +import io.muun.apollo.data.preferences.FirebaseInstallationIdRepository +import io.muun.apollo.data.preferences.KeysRepository +import io.muun.apollo.data.preferences.RepositoryRegistry +import io.muun.apollo.data.preferences.UserRepository +import io.muun.apollo.domain.ApplicationLockManager +import io.muun.apollo.domain.SignupDraftManager +import io.muun.apollo.domain.action.LogoutActions +import io.muun.apollo.domain.action.di.ActionComponent +import io.muun.apollo.domain.analytics.Analytics +import io.muun.apollo.domain.libwallet.LibwalletService +import io.muun.apollo.domain.libwallet.WalletClient +import org.bitcoinj.core.NetworkParameters +import java.util.concurrent.Executor +import javax.inject.Singleton + +/** + * Dagger Component. {@link Component}. + * Add here: + * - members-injection methods (e.g for classes which lifecycles are 3rd-party controlled, like + * Android's). Example: void inject(GcmMessageListenerService service). + * - provision methods, to expose injected or provided dependencies to other (dependent) components. + * Example: ClipboardProvider clipboardProvider(); + */ +@Singleton +@Component(modules = [DataModule::class]) +interface DataComponent : ActionComponent { + + fun inject(service: GcmMessageListenerService) + + fun inject(workerFactory: MuunWorkerFactory) + + // Exposed to dependent components + + fun backgroundExecutor(): Executor + + fun transformers(): ExecutionTransformerFactory + + fun houstonClient(): HoustonClient + + fun authRepository(): AuthRepository + + fun keysRepository(): KeysRepository + + fun userRepository(): UserRepository + + fun fcmTokenRepository(): FirebaseInstallationIdRepository + + fun contactDao(): ContactDao + + fun operationDao(): OperationDao + + fun publicProfileDao(): PublicProfileDao + + fun networkParameters(): NetworkParameters + + fun taskScheduler(): TaskScheduler + + fun exchangeRateWindowRepository(): ExchangeRateWindowRepository + + fun clipboardProvider(): ClipboardProvider + + fun networkInfoProvider(): NetworkInfoProvider + + fun context(): Context + + fun expectedFeeRepository(): FeeWindowRepository + + fun configuration(): Configuration + + fun secureStorageProvider(): SecureStorageProvider + + fun logoutActions(): LogoutActions + + fun applicationLockManager(): ApplicationLockManager + + fun signupDraftManager(): SignupDraftManager + + fun houstonConfig(): HoustonConfig + + fun driveAuthenticator(): DriveAuthenticator + + fun driveUploader(): DriveUploader + + fun repositoryRegistry(): RepositoryRegistry + + fun notificationService(): NotificationService + + fun analytics(): Analytics + + fun daoManager(): DaoManager + + fun libwalletConfig(): Config + + fun walletClient(): WalletClient + + fun nfcBridgerFactory(): NfcBridgerFactory + + fun goLibwalletService(): LibwalletService + + fun metricsProvider(): MetricsProvider +} diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/di/DataModule.java b/android/apolloui/src/main/java/io/muun/apollo/data/di/DataModule.java deleted file mode 100644 index 3214263a..00000000 --- a/android/apolloui/src/main/java/io/muun/apollo/data/di/DataModule.java +++ /dev/null @@ -1,361 +0,0 @@ -package io.muun.apollo.data.di; - -import io.muun.apollo.data.afs.ActivityManagerInfoProvider; -import io.muun.apollo.data.afs.AppInfoProvider; -import io.muun.apollo.data.afs.BatteryInfoProvider; -import io.muun.apollo.data.afs.BuildInfoProvider; -import io.muun.apollo.data.afs.ConnectivityInfoProvider; -import io.muun.apollo.data.afs.CpuInfoProvider; -import io.muun.apollo.data.afs.DateTimeZoneProvider; -import io.muun.apollo.data.afs.FileInfoProvider; -import io.muun.apollo.data.afs.HardwareCapabilitiesProvider; -import io.muun.apollo.data.afs.LocaleInfoProvider; -import io.muun.apollo.data.afs.MetricsProvider; -import io.muun.apollo.data.afs.NfcProvider; -import io.muun.apollo.data.afs.PackageManagerInfoProvider; -import io.muun.apollo.data.afs.ResourcesInfoProvider; -import io.muun.apollo.data.afs.SystemCapabilitiesProvider; -import io.muun.apollo.data.afs.SystemInfoProvider; -import io.muun.apollo.data.afs.TelephonyInfoProvider; -import io.muun.apollo.data.afs.TrafficStatsInfoProvider; -import io.muun.apollo.data.apis.DriveAuthenticator; -import io.muun.apollo.data.apis.DriveImpl; -import io.muun.apollo.data.apis.DriveUploader; -import io.muun.apollo.data.db.DaoManager; -import io.muun.apollo.data.db.contact.ContactDao; -import io.muun.apollo.data.db.incoming_swap.IncomingSwapDao; -import io.muun.apollo.data.db.incoming_swap.IncomingSwapHtlcDao; -import io.muun.apollo.data.db.operation.OperationDao; -import io.muun.apollo.data.db.phone_contact.PhoneContactDao; -import io.muun.apollo.data.db.public_profile.PublicProfileDao; -import io.muun.apollo.data.db.submarine_swap.SubmarineSwapDao; -import io.muun.apollo.data.external.AppStandbyBucketProvider; -import io.muun.apollo.data.external.Globals; -import io.muun.apollo.data.external.HoustonConfig; -import io.muun.apollo.data.external.NotificationService; -import io.muun.apollo.data.fs.LibwalletDataDirectory; -import io.muun.apollo.data.libwallet.HttpClientSessionProvider; -import io.muun.apollo.data.libwallet.KeyProvider; -import io.muun.apollo.data.libwallet.grpc.GrpcChannelFactory; -import io.muun.apollo.data.net.NetworkInfoProvider; -import io.muun.apollo.data.nfc.AndroidNfcBridge; -import io.muun.apollo.data.os.Configuration; -import io.muun.apollo.data.os.execution.ExecutionTransformerFactory; -import io.muun.apollo.data.os.execution.JobExecutor; -import io.muun.apollo.data.preferences.BackgroundTimesRepository; -import io.muun.apollo.data.preferences.FeaturesRepository; -import io.muun.apollo.data.preferences.RepositoryRegistry; -import io.muun.apollo.data.preferences.UserRepository; -import io.muun.apollo.domain.action.NotificationActions; -import io.muun.apollo.domain.action.NotificationPoller; -import io.muun.apollo.domain.libwallet.GoLibwalletService; -import io.muun.apollo.domain.libwallet.LibwalletLogAdapter; -import io.muun.apollo.domain.libwallet.LibwalletService; -import io.muun.apollo.domain.libwallet.WalletClient; - -import android.content.Context; -import app_provided_data.Config; -import com.fasterxml.jackson.databind.ObjectMapper; -import dagger.Module; -import dagger.Provides; -import io.grpc.ManagedChannel; -import io.reactivex.schedulers.Schedulers; -import org.bitcoinj.core.NetworkParameters; -import rx.Scheduler; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Func1; -import rx.functions.Func3; - -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import javax.inject.Named; -import javax.inject.Singleton; - -@Module -public class DataModule { - - private final Context applicationContext; - - private final Func3< - Context, - ExecutionTransformerFactory, - UserRepository, - NotificationService - > - notificationServiceFactory; - - private final Func1 appStandbyBucketProviderFactory; - - private final HoustonConfig houstonConfig; - - /** - * Creates a data module. - */ - public DataModule( - Context applicationContext, - Func3< - Context, - ExecutionTransformerFactory, - UserRepository, - NotificationService - > notificationServiceFactory, - Func1 appStandbyBucketProviderFactory, - HoustonConfig houstonConfig - ) { - - this.applicationContext = applicationContext; - this.notificationServiceFactory = notificationServiceFactory; - this.appStandbyBucketProviderFactory = appStandbyBucketProviderFactory; - this.houstonConfig = houstonConfig; - } - - @Provides - @Singleton - Context provideContext() { - return applicationContext; - } - - @Provides - @Singleton - DataModule provideDataModule() { - return this; - } - - /** - * Provide a dao manager with logging enabled. - */ - @Provides - @Singleton - DaoManager provideDaoManager( - Context context, - ContactDao contactDao, - OperationDao operationDao, - PhoneContactDao phoneContactDao, - PublicProfileDao publicProfileDao, - SubmarineSwapDao submarineSwapDao, - IncomingSwapDao incomingSwapDao, - IncomingSwapHtlcDao incomingSwapHtlcDao, - Configuration config, - Executor executor - ) { - - return new DaoManager( - context, - config.getString("database.filename"), - config.getInt("database.version"), - Schedulers.from(executor), - contactDao, - operationDao, - phoneContactDao, - publicProfileDao, - submarineSwapDao, - incomingSwapHtlcDao, - incomingSwapDao - ); - } - - @Provides - @Singleton - NotificationService provideNotificationService( - Context context, - ExecutionTransformerFactory executionTransformerFactory, - UserRepository userRepository - ) { - return notificationServiceFactory.call( - context, - executionTransformerFactory, - userRepository - ); - } - - @Provides - @Singleton - NetworkParameters provideNetworkParameters() { - return houstonConfig.getNetwork(); - } - - @Provides - @Singleton - Executor provideExecutor(JobExecutor jobExecutor) { - return jobExecutor; - } - - @Provides - @Singleton - @Named("mainThreadScheduler") - Scheduler provideScheduler() { - return AndroidSchedulers.mainThread(); - } - - @Provides - @Singleton - @Named("notificationScheduler") - Scheduler provideNotificationScheduler() { - return rx.schedulers.Schedulers.from(Executors.newSingleThreadScheduledExecutor(r -> - new Thread(r, "notifications") - )); - } - - @Provides - @Singleton - ObjectMapper provideObjectMapper() { - return new ObjectMapper(); - } - - @Provides - @Singleton - HoustonConfig provideHoustonConfig() { - return houstonConfig; - } - - @Provides - @Singleton - DriveAuthenticator provideGoogleDriveAuthenticator(DriveImpl singletonImpl) { - return singletonImpl; - } - - @Provides - @Singleton - DriveUploader provideGoogleDriveUploader(DriveImpl singletonImpl) { - return singletonImpl; - } - - @Provides - @Singleton - AppStandbyBucketProvider provideAppStandbyBucketProvider(final Context context) { - return appStandbyBucketProviderFactory.call(context); - } - - @Provides - @Singleton - RepositoryRegistry provideRepositoryRegistry() { - return new RepositoryRegistry(); - } - - // TODO: this should be extracted to a DomainModule or similar - @Provides - @Singleton - NotificationPoller provideNotificationPoller(final NotificationActions notificationActions) { - return notificationActions; - } - - @Provides - @Singleton - Config provideLibwalletConfig( - final LibwalletDataDirectory libwalletDataDirectory, - final FeaturesRepository featuresRepository, - final HttpClientSessionProvider httpClientSessionProvider, - final AndroidNfcBridge nfcBridge, - final KeyProvider keyProvider - ) { - libwalletDataDirectory.ensureExists(); - final String dataDir = libwalletDataDirectory.getPath().getAbsolutePath(); - final String socketName = libwalletDataDirectory.getSocket().getAbsolutePath(); - - final Config config = new Config(); - config.setDataDir(dataDir); - config.setSocketPath(socketName); - config.setFeatureStatusProvider(featuresRepository); - config.setAppLogSink(new LibwalletLogAdapter()); - config.setHttpClientSessionProvider(httpClientSessionProvider); - config.setNfcBridge(nfcBridge); - config.setKeyProvider(keyProvider); - - final String network; - switch (Globals.INSTANCE.getNetwork().getId()) { - case NetworkParameters.ID_MAINNET: - network = "mainnet"; - break; - case NetworkParameters.ID_REGTEST: - network = "regtest"; - break; - case NetworkParameters.ID_TESTNET: - network = "testnet3"; - break; - default: - throw new IllegalArgumentException( - "Unknown network id " + Globals.INSTANCE.getNetwork().getId() - ); - } - - config.setNetwork(network); - - return config; - } - - @Provides - @Singleton - ManagedChannel provideGrpcChannel(Config libwalletConfig) { - final String socketPath = libwalletConfig.getSocketPath(); - return GrpcChannelFactory.create(socketPath); - } - - @Provides - @Singleton - WalletClient provideWalletClient(ManagedChannel channel) { - return new WalletClient(channel); - } - - @Provides - @Singleton - AndroidNfcBridge provideNfcBridge() { - return new AndroidNfcBridge(); - } - - @Provides - LibwalletService provideLibwalletService() { - return new GoLibwalletService(); - } - - @Provides - @Singleton - MetricsProvider provideMetricsProvider(Context context) { - final ActivityManagerInfoProvider activityManagerInfoProvider = - new ActivityManagerInfoProvider(context); - final TelephonyInfoProvider telephonyInfoProvider = new TelephonyInfoProvider(context); - final HardwareCapabilitiesProvider hardwareCapabilitiesProvider = - new HardwareCapabilitiesProvider(context); - final PackageManagerInfoProvider packageManagerInfoProvider = - new PackageManagerInfoProvider(context); - final CpuInfoProvider cpuInfoProvider = new CpuInfoProvider(); - final BuildInfoProvider buildInfoProvider = new BuildInfoProvider(); - final FileInfoProvider fileInfoProvider = new FileInfoProvider(context); - final SystemCapabilitiesProvider systemCapabilitiesProvider = - new SystemCapabilitiesProvider(context); - final BackgroundTimesRepository backgroundTimesRepository = - new BackgroundTimesRepository(context, provideRepositoryRegistry()); - final AppInfoProvider appInfoProvider = - new AppInfoProvider(context, backgroundTimesRepository); - final ConnectivityInfoProvider connectivityInfoProvider = - new ConnectivityInfoProvider(context); - final ResourcesInfoProvider resourcesInfoProvider = new ResourcesInfoProvider(context); - final DateTimeZoneProvider dateTimeZoneProvider = new DateTimeZoneProvider(context); - final LocaleInfoProvider localeInfoProvider = new LocaleInfoProvider(); - final TrafficStatsInfoProvider trafficStatsInfoProvider = new TrafficStatsInfoProvider(); - final NfcProvider nfcProvider = new NfcProvider(context); - final BatteryInfoProvider batteryInfoProvider = new BatteryInfoProvider(context); - final SystemInfoProvider systemInfoProvider = new SystemInfoProvider(); - final NetworkInfoProvider networkInfoProvider = new NetworkInfoProvider(context); - - return new MetricsProvider( - activityManagerInfoProvider, - telephonyInfoProvider, - hardwareCapabilitiesProvider, - packageManagerInfoProvider, - cpuInfoProvider, - buildInfoProvider, - fileInfoProvider, - systemCapabilitiesProvider, - appInfoProvider, - connectivityInfoProvider, - resourcesInfoProvider, - dateTimeZoneProvider, - localeInfoProvider, - trafficStatsInfoProvider, - nfcProvider, - batteryInfoProvider, - systemInfoProvider, - networkInfoProvider - ); - } -} diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/di/DataModule.kt b/android/apolloui/src/main/java/io/muun/apollo/data/di/DataModule.kt new file mode 100644 index 00000000..27e32b97 --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/data/di/DataModule.kt @@ -0,0 +1,276 @@ +package io.muun.apollo.data.di + +import android.content.Context +import app_provided_data.Config +import com.fasterxml.jackson.databind.ObjectMapper +import dagger.Module +import dagger.Provides +import io.grpc.ManagedChannel +import io.muun.apollo.data.afs.ActivityManagerInfoProvider +import io.muun.apollo.data.afs.AppInfoProvider +import io.muun.apollo.data.afs.BatteryInfoProvider +import io.muun.apollo.data.afs.BuildInfoProvider +import io.muun.apollo.data.afs.ConnectivityInfoProvider +import io.muun.apollo.data.afs.DateTimeZoneProvider +import io.muun.apollo.data.afs.FileInfoProvider +import io.muun.apollo.data.afs.HardwareCapabilitiesProvider +import io.muun.apollo.data.afs.LocaleInfoProvider +import io.muun.apollo.data.afs.MetricsProvider +import io.muun.apollo.data.afs.NfcProvider +import io.muun.apollo.data.afs.PackageManagerInfoProvider +import io.muun.apollo.data.afs.SystemCapabilitiesProvider +import io.muun.apollo.data.afs.SystemInfoProvider +import io.muun.apollo.data.afs.TelephonyInfoProvider +import io.muun.apollo.data.afs.TrafficStatsInfoProvider +import io.muun.apollo.data.apis.DriveAuthenticator +import io.muun.apollo.data.apis.DriveImpl +import io.muun.apollo.data.apis.DriveUploader +import io.muun.apollo.data.db.DaoManager +import io.muun.apollo.data.db.contact.ContactDao +import io.muun.apollo.data.db.incoming_swap.IncomingSwapDao +import io.muun.apollo.data.db.incoming_swap.IncomingSwapHtlcDao +import io.muun.apollo.data.db.operation.OperationDao +import io.muun.apollo.data.db.phone_contact.PhoneContactDao +import io.muun.apollo.data.db.public_profile.PublicProfileDao +import io.muun.apollo.data.db.submarine_swap.SubmarineSwapDao +import io.muun.apollo.data.external.AppStandbyBucketProvider +import io.muun.apollo.data.external.Globals +import io.muun.apollo.data.external.HoustonConfig +import io.muun.apollo.data.external.NotificationService +import io.muun.apollo.data.fs.LibwalletDataDirectory +import io.muun.apollo.data.libwallet.HttpClientSessionProvider +import io.muun.apollo.data.libwallet.KeyProvider +import io.muun.apollo.data.libwallet.grpc.GrpcChannelFactory +import io.muun.apollo.data.net.NetworkInfoProvider +import io.muun.apollo.data.nfc.AndroidNfcBridge +import io.muun.apollo.data.os.Configuration +import io.muun.apollo.data.os.execution.ExecutionTransformerFactory +import io.muun.apollo.data.os.execution.JobExecutor +import io.muun.apollo.data.preferences.BackgroundTimesRepository +import io.muun.apollo.data.preferences.FeaturesRepository +import io.muun.apollo.data.preferences.RepositoryRegistry +import io.muun.apollo.domain.action.NotificationActions +import io.muun.apollo.domain.action.NotificationPoller +import io.muun.apollo.domain.libwallet.GoLibwalletService +import io.muun.apollo.domain.libwallet.LibwalletLogAdapter +import io.muun.apollo.domain.libwallet.LibwalletService +import io.muun.apollo.domain.libwallet.WalletClient +import io.muun.apollo.domain.selector.BitcoinUnitSelector +import io.muun.apollo.presentation.app.AppStandbyBucketProviderImpl +import io.muun.apollo.presentation.app.NotificationServiceImpl +import io.reactivex.schedulers.Schedulers +import org.bitcoinj.core.NetworkParameters +import rx.Scheduler +import rx.android.schedulers.AndroidSchedulers +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import javax.inject.Named +import javax.inject.Singleton + +@Module +class DataModule( + private val applicationContext: Context, + private val houstonConfig: HoustonConfig, +) { + + @Provides + @Singleton + fun provideContext(): Context = applicationContext + + @Provides + @Singleton + fun provideDataModule(): DataModule = this + + /** + * Provide a dao manager with logging enabled. + */ + @Provides + @Singleton + fun provideDaoManager( + context: Context, + contactDao: ContactDao, + operationDao: OperationDao, + phoneContactDao: PhoneContactDao, + publicProfileDao: PublicProfileDao, + submarineSwapDao: SubmarineSwapDao, + incomingSwapDao: IncomingSwapDao, + incomingSwapHtlcDao: IncomingSwapHtlcDao, + config: Configuration, + executor: Executor, + ): DaoManager = DaoManager( + context, + config.getString("database.filename"), + config.getInt("database.version"), + Schedulers.from(executor), + contactDao, + operationDao, + phoneContactDao, + publicProfileDao, + submarineSwapDao, + incomingSwapHtlcDao, + incomingSwapDao + ) + + @Provides + @Singleton + fun provideNotificationService( + context: Context, + executionTransformerFactory: ExecutionTransformerFactory, + bitcoinUnitSelector: BitcoinUnitSelector, + ): NotificationService = NotificationServiceImpl( + context, + executionTransformerFactory, + bitcoinUnitSelector + ) + + @Provides + @Singleton + fun provideNetworkParameters(): NetworkParameters = houstonConfig.getNetwork() + + @Provides + @Singleton + fun provideExecutor(jobExecutor: JobExecutor): Executor = jobExecutor + + @Provides + @Singleton + @Named("mainThreadScheduler") + fun provideScheduler(): Scheduler = AndroidSchedulers.mainThread() + + @Provides + @Singleton + @Named("notificationScheduler") + fun provideNotificationScheduler( + ): Scheduler = rx.schedulers.Schedulers.from(Executors.newSingleThreadScheduledExecutor { r -> + Thread(r, "notifications") + }) + + @Provides + @Singleton + fun provideObjectMapper(): ObjectMapper = ObjectMapper() + + @Provides + @Singleton + fun provideHoustonConfig(): HoustonConfig = houstonConfig + + @Provides + @Singleton + fun provideGoogleDriveAuthenticator( + singletonImpl: DriveImpl, + ): DriveAuthenticator = singletonImpl + + @Provides + @Singleton + fun provideGoogleDriveUploader(singletonImpl: DriveImpl): DriveUploader = singletonImpl + + @Provides + @Singleton + fun provideAppStandbyBucketProvider( + context: Context, + ): AppStandbyBucketProvider = AppStandbyBucketProviderImpl(context) + + @Provides + @Singleton + fun provideRepositoryRegistry(): RepositoryRegistry = RepositoryRegistry() + + // TODO: this should be extracted to a DomainModule or similar + @Provides + @Singleton + fun provideNotificationPoller( + notificationActions: NotificationActions, + ): NotificationPoller = notificationActions + + @Provides + @Singleton + fun provideLibwalletConfig( + libwalletDataDirectory: LibwalletDataDirectory, + featuresRepository: FeaturesRepository, + httpClientSessionProvider: HttpClientSessionProvider, + nfcBridge: AndroidNfcBridge, + keyProvider: KeyProvider, + ): Config { + libwalletDataDirectory.ensureExists() + val dataDir = libwalletDataDirectory.path.absolutePath + val socketName = libwalletDataDirectory.socket.absolutePath + + val config = Config() + config.dataDir = dataDir + config.socketPath = socketName + config.featureStatusProvider = featuresRepository + config.appLogSink = LibwalletLogAdapter() + config.httpClientSessionProvider = httpClientSessionProvider + config.nfcBridge = nfcBridge + config.keyProvider = keyProvider + + val network = when (Globals.INSTANCE.network.getId()) { + NetworkParameters.ID_MAINNET -> "mainnet" + NetworkParameters.ID_REGTEST -> "regtest" + NetworkParameters.ID_TESTNET -> "testnet3" + else -> throw IllegalArgumentException( + "Unknown network id " + Globals.INSTANCE.network.getId() + ) + } + + config.network = network + + return config + } + + @Provides + @Singleton + fun provideGrpcChannel( + libwalletConfig: Config, + ): ManagedChannel = GrpcChannelFactory.create(libwalletConfig.socketPath) + + @Provides + @Singleton + fun provideWalletClient(channel: ManagedChannel): WalletClient = WalletClient(channel) + + @Provides + @Singleton + fun provideNfcBridge(): AndroidNfcBridge = AndroidNfcBridge() + + @Provides + fun provideLibwalletService(): LibwalletService = GoLibwalletService() + + @Provides + @Singleton + fun provideMetricsProvider(context: Context): MetricsProvider { + val activityManagerInfoProvider = ActivityManagerInfoProvider(context) + val telephonyInfoProvider = TelephonyInfoProvider(context) + val hardwareCapabilitiesProvider = HardwareCapabilitiesProvider(context) + val packageManagerInfoProvider = PackageManagerInfoProvider(context) + val buildInfoProvider = BuildInfoProvider() + val fileInfoProvider = FileInfoProvider(context) + val systemCapabilitiesProvider = SystemCapabilitiesProvider(context) + val backgroundTimesRepository = + BackgroundTimesRepository(context, provideRepositoryRegistry()) + val appInfoProvider = AppInfoProvider(context, backgroundTimesRepository) + val connectivityInfoProvider = ConnectivityInfoProvider(context) + val dateTimeZoneProvider = DateTimeZoneProvider(context) + val localeInfoProvider = LocaleInfoProvider() + val trafficStatsInfoProvider = TrafficStatsInfoProvider() + val nfcProvider = NfcProvider(context) + val batteryInfoProvider = BatteryInfoProvider(context) + val systemInfoProvider = SystemInfoProvider() + val networkInfoProvider = NetworkInfoProvider(context) + + return MetricsProvider( + activityManagerInfoProvider, + telephonyInfoProvider, + hardwareCapabilitiesProvider, + packageManagerInfoProvider, + buildInfoProvider, + fileInfoProvider, + systemCapabilitiesProvider, + appInfoProvider, + connectivityInfoProvider, + dateTimeZoneProvider, + localeInfoProvider, + trafficStatsInfoProvider, + nfcProvider, + batteryInfoProvider, + systemInfoProvider, + networkInfoProvider + ) + } +} \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/libwallet/HttpClientSessionProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/libwallet/HttpClientSessionProvider.kt index ca1f5410..5883a6da 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/libwallet/HttpClientSessionProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/libwallet/HttpClientSessionProvider.kt @@ -3,9 +3,9 @@ package io.muun.apollo.data.libwallet import android.content.Context import android.os.Build import app_provided_data.Session +import io.muun.apollo.data.afs.BackgroundExecutionMetricsProvider import io.muun.apollo.data.external.Globals import io.muun.apollo.data.external.HoustonConfig -import io.muun.apollo.data.afs.BackgroundExecutionMetricsProvider import io.muun.apollo.data.preferences.AuthRepository import io.muun.apollo.data.preferences.ClientVersionRepository import io.muun.apollo.data.toSafeAscii @@ -27,10 +27,10 @@ class HttpClientSessionProvider @Inject constructor( ): app_provided_data.HttpClientSessionProvider { override fun session(): Session { - var language = applicationContext.locale().language + val language = applicationContext.locale().language val session = Session() - var authToken = authRepository.serverJwt.orElse("") + val authToken = authRepository.serverJwt.orElse("") session.authToken = authToken session.clientType = ClientTypeJson.APOLLO.toString() session.clientVersion = Globals.INSTANCE.versionCode.toString() diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/libwallet/KeyProvider.kt b/android/apolloui/src/main/java/io/muun/apollo/data/libwallet/KeyProvider.kt index 9de4fe26..d8244ed6 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/libwallet/KeyProvider.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/libwallet/KeyProvider.kt @@ -26,7 +26,11 @@ class KeyProvider @Inject constructor( return keyData } + override fun fetchEncryptedMuunPrivateKey(): String { + return keysRepository.encryptedMuunPrivateKey.toBlocking().first() + } + override fun fetchMaxDerivedIndex(): Long { return keysRepository.maxWatchingExternalAddressIndex.toLong() } -} \ No newline at end of file +} diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/logging/Crashlytics.kt b/android/apolloui/src/main/java/io/muun/apollo/data/logging/Crashlytics.kt index eaee6efb..3351c754 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/logging/Crashlytics.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/logging/Crashlytics.kt @@ -3,8 +3,8 @@ 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.afs.EarlyMetricsProvider +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.domain.action.debug.ForceErrorReportAction @@ -102,6 +102,7 @@ object Crashlytics { // 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("errorId", report.uniqueId) crashlytics?.setCustomKey("tag", report.tag) crashlytics?.setCustomKey("message", report.message) setStaticCustomKeys() @@ -110,6 +111,7 @@ object Crashlytics { crashlytics?.setCustomKey(entry.key, entry.value.toString()) } + analyticsProvider?.report( AnalyticsEvent.E_CRASHLYTICS_ERROR(report) ) diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/logging/MuunTree.kt b/android/apolloui/src/main/java/io/muun/apollo/data/logging/MuunTree.kt index 9dbf2101..8506d160 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/logging/MuunTree.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/logging/MuunTree.kt @@ -53,7 +53,7 @@ class MuunTree : Timber.DebugTree() { } // Report.message gets the stacktrace removed so we need original message - sendToLogcat(Log.ERROR, tag, "$message ${report.metadata}", report.error) + sendToLogcat(Log.ERROR, tag, "$message\n${report.printMetadata()}", report.error) } private fun sendFallbackErrorReport( diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/net/ApiObjectsMapper.java b/android/apolloui/src/main/java/io/muun/apollo/data/net/ApiObjectsMapper.java index ff63f321..7b4068ef 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/net/ApiObjectsMapper.java +++ b/android/apolloui/src/main/java/io/muun/apollo/data/net/ApiObjectsMapper.java @@ -1,7 +1,6 @@ package io.muun.apollo.data.net; import io.muun.apollo.data.afs.BuildInfo; -import io.muun.apollo.data.afs.CpuInfo; import io.muun.apollo.data.afs.PackageManagerAppInfo; import io.muun.apollo.data.afs.PackageManagerDeviceFeatures; import io.muun.apollo.data.external.Globals; @@ -17,14 +16,14 @@ import io.muun.apollo.domain.model.InstallSourceInfo; import io.muun.apollo.domain.model.OperationWithMetadata; import io.muun.apollo.domain.model.PublicProfile; +import io.muun.apollo.domain.model.SensorEvent; +import io.muun.apollo.domain.model.SensorEventBatch; import io.muun.apollo.domain.model.SubmarineSwap; import io.muun.apollo.domain.model.SubmarineSwapRequest; -import io.muun.apollo.domain.model.SystemUserInfo; import io.muun.apollo.domain.model.user.UserProfile; import io.muun.common.api.AndroidAppInfoJson; import io.muun.common.api.AndroidBuildInfoJson; import io.muun.common.api.AndroidDeviceFeaturesJson; -import io.muun.common.api.AndroidSystemUserInfoJson; import io.muun.common.api.BackgroundEventJson; import io.muun.common.api.BitcoinAmountJson; import io.muun.common.api.ChallengeKeyJson; @@ -47,6 +46,8 @@ import io.muun.common.api.PublicKeyJson; import io.muun.common.api.PublicProfileJson; import io.muun.common.api.RealTimeFeesRequestJson; +import io.muun.common.api.SensorEventBatchJson; +import io.muun.common.api.SensorEventJson; import io.muun.common.api.StartEmailSetupJson; import io.muun.common.api.SubmarineSwapRequestJson; import io.muun.common.api.UserInvoiceJson; @@ -63,7 +64,6 @@ import io.muun.common.model.challenge.ChallengeSignature; import io.muun.common.utils.BitcoinUtils; import io.muun.common.utils.Encodings; -import io.muun.common.utils.Pair; import io.muun.common.utils.Preconditions; import androidx.annotation.NonNull; @@ -223,12 +223,10 @@ public ClientJson mapClient( final String bigQueryPseudoId, final boolean isRootHint, @NonNull final String androidId, - @NonNull final List systemUsersInfo, @NonNull final Map drmProviderToClientId, @NonNull final InstallSourceInfo installSourceInfo, final int bootCount, @NonNull final String glEsVersion, - @NonNull final CpuInfo cpuInfo, @NonNull GooglePlayServicesHelper.PlayServicesInfo playServicesInfo, @NonNull GooglePlayHelper.PlayInfo playInfo, final BuildInfo buildInfo, @@ -240,17 +238,15 @@ public ClientJson mapClient( final String securityEnhancedBuild, final String bridgeRootService, final long appSize, - final List hardwareAddresses, final String vbMeta, - final String efsCreationTimeInSeconds, final Boolean isLowRamDevice, final Long firstInstallTimeInMs, - final Map deviceRegion, final String deviceName, final Long timeZoneOffsetSeconds, final String language, final Long uptimeMillis, - final Long elapsedRealtime + final Long elapsedRealtime, + final String applicationId ) { return new ClientJson( ClientTypeJson.APOLLO, @@ -264,22 +260,14 @@ public ClientJson mapClient( isRootHint, androidId, 0, - mapSystemUsersInfo(systemUsersInfo), drmProviderToClientId, uptimeMillis, elapsedRealtime, installSourceInfo.getInstallingPackageName(), installSourceInfo.getInitiatingPackageName(), - installSourceInfo.getInitiatingPackageSigningInfo(), - //@TODO: Redundancy removal: Next 3 and also at Houston (data present in buildInfo) - buildInfo.getFingerprint(), - buildInfo.getHardware(), buildInfo.getBootloader(), bootCount, glEsVersion, - cpuInfo.getLegacyData(), - mapListOfPairs(cpuInfo.getCommonInfo()), - mapCpuPerProcessorInfo(cpuInfo.getPerProcessorInfo()), playServicesInfo.getVersionCode(), playServicesInfo.getVersionName(), playServicesInfo.getClientVersionCode(), @@ -294,61 +282,27 @@ public ClientJson mapClient( mapSeLinux(securityEnhancedBuild), mapAdbRootService(bridgeRootService), appSize, - hardwareAddresses, vbMeta, - efsCreationTimeInSeconds, isLowRamDevice, firstInstallTimeInMs, - deviceRegion + applicationId ); } - private List> mapListOfPairs(List> info) { - final List> result = new ArrayList<>(); - - for (kotlin.Pair kotlinPair : info) { - result.add(new Pair<>(kotlinPair.getFirst(), kotlinPair.getSecond())); - } - return result; - } - - private List>> mapCpuPerProcessorInfo( - List>> perProcessorInfo - ) { - final List>> result = new ArrayList<>(); - - for (List> listOfPairs : perProcessorInfo) { - result.add(mapListOfPairs(listOfPairs)); - } - return result; - } - - private List mapSystemUsersInfo(List usersInfo) { - final List result = new ArrayList<>(); - for (final SystemUserInfo model : usersInfo) { - result.add(new AndroidSystemUserInfoJson( - model.getCreationTimestampInMillis(), - model.isSystemUser() - )); - } - return result; - } - private AndroidBuildInfoJson mapBuildInfo(BuildInfo buildInfo) { return new AndroidBuildInfoJson( buildInfo.getAbis(), - buildInfo.getFingerprint(), - buildInfo.getHardware(), + null, buildInfo.getBootloader(), buildInfo.getManufacturer(), buildInfo.getBrand(), - buildInfo.getDisplay(), - buildInfo.getTime(), buildInfo.getHost(), buildInfo.getType(), buildInfo.getRadioVersion(), buildInfo.getSecurityPatch(), - buildInfo.getBaseOs() + buildInfo.getModel(), + buildInfo.getProduct(), + buildInfo.getRelease() ); } @@ -367,18 +321,13 @@ private AndroidDeviceFeaturesJson mapDeviceFeatures( PackageManagerDeviceFeatures deviceFeatures ) { return new AndroidDeviceFeaturesJson( - deviceFeatures.getTouch(), deviceFeatures.getProximity(), deviceFeatures.getAccelerometer(), deviceFeatures.getGyro(), deviceFeatures.getCompass(), deviceFeatures.getTelephony(), - deviceFeatures.getCdma(), - deviceFeatures.getGsm(), - deviceFeatures.getCameras(), deviceFeatures.getPc(), - deviceFeatures.getPip(), - deviceFeatures.getDactylogram() + deviceFeatures.getPip() ); } @@ -417,12 +366,10 @@ public CreateFirstSessionJson mapCreateFirstSession( String bigQueryPseudoId, boolean isRootHint, @NonNull String androidId, - @NonNull List systemUsersInfo, @NonNull Map drmProviderToClientId, @NonNull InstallSourceInfo installSourceInfo, int bootCount, @NonNull String glEsVersion, - @NonNull CpuInfo cpuInfo, @NonNull GooglePlayServicesHelper.PlayServicesInfo playServicesInfo, @NonNull GooglePlayHelper.PlayInfo playInfo, final BuildInfo buildInfo, @@ -434,17 +381,15 @@ public CreateFirstSessionJson mapCreateFirstSession( final String securityEnhancedBuild, final String bridgeRootService, final Long appSize, - final List hardwareAddresses, final String vbMeta, - final String efsCreationTimeInSeconds, final Boolean isLowRamDevice, final Long firstInstallTimeInMs, - final Map deviceRegion, final String deviceName, final Long timeZoneOffsetSeconds, final String language, final Long uptimeMillis, - final Long elapsedRealtime + final Long elapsedRealtime, + final String applicationId ) { return new CreateFirstSessionJson( @@ -452,12 +397,10 @@ public CreateFirstSessionJson mapCreateFirstSession( bigQueryPseudoId, isRootHint, androidId, - systemUsersInfo, drmProviderToClientId, installSourceInfo, bootCount, glEsVersion, - cpuInfo, playServicesInfo, playInfo, buildInfo, @@ -469,17 +412,15 @@ public CreateFirstSessionJson mapCreateFirstSession( securityEnhancedBuild, bridgeRootService, appSize, - hardwareAddresses, vbMeta, - efsCreationTimeInSeconds, isLowRamDevice, firstInstallTimeInMs, - deviceRegion, deviceName, timeZoneOffsetSeconds, language, uptimeMillis, - elapsedRealtime + elapsedRealtime, + applicationId ), gcmToken, primaryCurrency, @@ -497,12 +438,10 @@ public CreateLoginSessionJson mapCreateLoginSession( String bigQueryPseudoId, boolean isRootHint, @NonNull String androidId, - @NonNull List systemUsersInfo, @NonNull Map drmProviderToClientId, @NonNull InstallSourceInfo installSourceInfo, int bootCount, @NonNull String glEsVersion, - @NonNull CpuInfo cpuInfo, @NonNull GooglePlayServicesHelper.PlayServicesInfo playServicesInfo, @NonNull GooglePlayHelper.PlayInfo playInfo, final BuildInfo buildInfo, @@ -514,17 +453,15 @@ public CreateLoginSessionJson mapCreateLoginSession( final String securityEnhancedBuild, final String bridgeRootService, final Long appSize, - final List hardwareAddresses, final String vbMeta, - final String efsCreationTimeInSeconds, final Boolean isLowRamDevice, final Long firstInstallTimeInMs, - final Map deviceRegion, final String deviceName, final Long timeZoneOffsetSeconds, final String language, final Long uptimeMillis, - final Long elapsedRealtime + final Long elapsedRealtime, + final String applicationId ) { return new CreateLoginSessionJson( @@ -532,12 +469,10 @@ public CreateLoginSessionJson mapCreateLoginSession( bigQueryPseudoId, isRootHint, androidId, - systemUsersInfo, drmProviderToClientId, installSourceInfo, bootCount, glEsVersion, - cpuInfo, playServicesInfo, playInfo, buildInfo, @@ -549,17 +484,15 @@ public CreateLoginSessionJson mapCreateLoginSession( securityEnhancedBuild, bridgeRootService, appSize, - hardwareAddresses, vbMeta, - efsCreationTimeInSeconds, isLowRamDevice, firstInstallTimeInMs, - deviceRegion, deviceName, timeZoneOffsetSeconds, language, uptimeMillis, - elapsedRealtime + elapsedRealtime, + applicationId ), gcmToken, email @@ -575,12 +508,10 @@ public CreateRcLoginSessionJson mapCreateRcLoginSession( String bigQueryPseudoId, boolean isRootHint, @NonNull String androidId, - @NonNull List systemUsersInfo, @NonNull Map drmProviderToClientId, @NonNull InstallSourceInfo installSourceInfo, int bootCount, @NonNull String glEsVersion, - @NonNull CpuInfo cpuInfo, @NonNull GooglePlayServicesHelper.PlayServicesInfo playServicesInfo, @NonNull GooglePlayHelper.PlayInfo playInfo, final BuildInfo buildInfo, @@ -592,17 +523,15 @@ public CreateRcLoginSessionJson mapCreateRcLoginSession( final String securityEnhancedBuild, final String bridgeRootService, final Long appSize, - final List hardwareAddresses, final String vbMeta, - final String efsCreationTimeInSeconds, final Boolean isLowRamDevice, final Long firstInstallTimeInMs, - final Map deviceRegion, final String deviceName, final Long timeZoneOffsetSeconds, final String language, final Long uptimeMillis, - final Long elapsedRealtime + final Long elapsedRealtime, + final String applicationId ) { return new CreateRcLoginSessionJson( @@ -610,12 +539,10 @@ public CreateRcLoginSessionJson mapCreateRcLoginSession( bigQueryPseudoId, isRootHint, androidId, - systemUsersInfo, drmProviderToClientId, installSourceInfo, bootCount, glEsVersion, - cpuInfo, playServicesInfo, playInfo, buildInfo, @@ -627,17 +554,15 @@ public CreateRcLoginSessionJson mapCreateRcLoginSession( securityEnhancedBuild, bridgeRootService, appSize, - hardwareAddresses, vbMeta, - efsCreationTimeInSeconds, isLowRamDevice, firstInstallTimeInMs, - deviceRegion, deviceName, timeZoneOffsetSeconds, language, uptimeMillis, - elapsedRealtime + elapsedRealtime, + applicationId ), gcmToken, new ChallengeKeyJson( @@ -878,4 +803,46 @@ private RealTimeFeesRequestJson.FeeBumpRefreshPolicy mapFeeBumpRefreshPolicy( throw new MissingCaseError(feeBumpRefreshPolicy); } } + + /** + * Map a SensorEventBatch. + */ + @NonNull + public SensorEventBatchJson mapSensorEventBatch( + @NonNull SensorEventBatch sensorEventBatch + ) { + return new SensorEventBatchJson( + mapSensorEvents(sensorEventBatch.getEvents()) + ); + } + + /** + * Map a list of SensorEvent objects to a list of SensorEventJson representations. + * + * @param sensorEvent The list of domain model sensor event. + * @return A list of SensorEventJson for JSON serialization. + */ + private List mapSensorEvents(List sensorEvent) { + final List mappedList = new ArrayList<>(); + for (SensorEvent event : sensorEvent) { + mappedList.add(mapSensorEvent(event)); + } + + return mappedList; + } + + /** + * Map a single SensorEvent to its SensorEventJson representation. + * + * @param event The SensorEvent to map. + * @return The mapped SensorEventJson containing the event's details. + */ + private SensorEventJson mapSensorEvent(SensorEvent event) { + return new SensorEventJson( + event.getEventId(), + ApolloZonedDateTime.of(event.getEventTimestamp()), + event.getEventType(), + event.getEventData() + ); + } } diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/net/HoustonClient.java b/android/apolloui/src/main/java/io/muun/apollo/data/net/HoustonClient.java index b10bc9b4..ec904185 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/net/HoustonClient.java +++ b/android/apolloui/src/main/java/io/muun/apollo/data/net/HoustonClient.java @@ -35,6 +35,7 @@ import io.muun.apollo.domain.model.PublicKeySet; import io.muun.apollo.domain.model.RealTimeData; import io.muun.apollo.domain.model.RealTimeFees; +import io.muun.apollo.domain.model.SensorEventBatch; import io.muun.apollo.domain.model.Sha256Hash; import io.muun.apollo.domain.model.SubmarineSwap; import io.muun.apollo.domain.model.SubmarineSwapRequest; @@ -63,6 +64,7 @@ import io.muun.common.api.PublicKeySetJson; import io.muun.common.api.PushTransactionsJson; import io.muun.common.api.RawTransaction; +import io.muun.common.api.SensorEventBatchJson; import io.muun.common.api.SetupChallengeResponse; import io.muun.common.api.UpdateOperationMetadataJson; import io.muun.common.api.UserJson; @@ -158,12 +160,10 @@ public Observable createFirstSession( bigQueryPseudoId, isRootHint, metricsProvider.getAndroidId(), - metricsProvider.getSystemUsersInfo(), metricsProvider.getDrmClientIds(), metricsProvider.getInstallSourceInfo(), metricsProvider.getBootCount(), metricsProvider.getGlEsVersion(), - metricsProvider.getCpuInfo(), googlePlayServicesHelper.getPlayServicesInfo(), googlePlayHelper.getPlayInfo(), metricsProvider.getBuildInfo(), @@ -175,17 +175,15 @@ public Observable createFirstSession( metricsProvider.getSecurityEnhancedBuild(), metricsProvider.getBridgeRootService(), metricsProvider.getAppSize(), - metricsProvider.getHardwareAddresses(), metricsProvider.getVbMeta(), - metricsProvider.getEfsCreationTimeInSeconds(), metricsProvider.isLowRamDevice(), metricsProvider.getFirstInstallTimeInMs(), - metricsProvider.getDeviceRegion(), metricsProvider.getDeviceName(), metricsProvider.getTimeZoneOffsetSeconds(), metricsProvider.getLanguage(), metricsProvider.getUptimeMillis(), - metricsProvider.getElapsedRealtime() + metricsProvider.getElapsedRealtime(), + metricsProvider.getApplicationId() ); return getService().createFirstSession(params) @@ -208,12 +206,10 @@ public Observable createLoginSession( bigQueryPseudoId, isRootHint, metricsProvider.getAndroidId(), - metricsProvider.getSystemUsersInfo(), metricsProvider.getDrmClientIds(), metricsProvider.getInstallSourceInfo(), metricsProvider.getBootCount(), metricsProvider.getGlEsVersion(), - metricsProvider.getCpuInfo(), googlePlayServicesHelper.getPlayServicesInfo(), googlePlayHelper.getPlayInfo(), metricsProvider.getBuildInfo(), @@ -225,17 +221,15 @@ public Observable createLoginSession( metricsProvider.getSecurityEnhancedBuild(), metricsProvider.getBridgeRootService(), metricsProvider.getAppSize(), - metricsProvider.getHardwareAddresses(), metricsProvider.getVbMeta(), - metricsProvider.getEfsCreationTimeInSeconds(), metricsProvider.isLowRamDevice(), metricsProvider.getFirstInstallTimeInMs(), - metricsProvider.getDeviceRegion(), metricsProvider.getDeviceName(), metricsProvider.getTimeZoneOffsetSeconds(), metricsProvider.getLanguage(), metricsProvider.getUptimeMillis(), - metricsProvider.getElapsedRealtime() + metricsProvider.getElapsedRealtime(), + metricsProvider.getApplicationId() ); return getService().createLoginSession(params) @@ -258,12 +252,10 @@ public Observable createRcLoginSession( bigQueryPseudoId, isRootHint, metricsProvider.getAndroidId(), - metricsProvider.getSystemUsersInfo(), metricsProvider.getDrmClientIds(), metricsProvider.getInstallSourceInfo(), metricsProvider.getBootCount(), metricsProvider.getGlEsVersion(), - metricsProvider.getCpuInfo(), googlePlayServicesHelper.getPlayServicesInfo(), googlePlayHelper.getPlayInfo(), metricsProvider.getBuildInfo(), @@ -275,17 +267,15 @@ public Observable createRcLoginSession( metricsProvider.getSecurityEnhancedBuild(), metricsProvider.getBridgeRootService(), metricsProvider.getAppSize(), - metricsProvider.getHardwareAddresses(), metricsProvider.getVbMeta(), - metricsProvider.getEfsCreationTimeInSeconds(), metricsProvider.isLowRamDevice(), metricsProvider.getFirstInstallTimeInMs(), - metricsProvider.getDeviceRegion(), metricsProvider.getDeviceName(), metricsProvider.getTimeZoneOffsetSeconds(), metricsProvider.getLanguage(), metricsProvider.getUptimeMillis(), - metricsProvider.getElapsedRealtime() + metricsProvider.getElapsedRealtime(), + metricsProvider.getApplicationId() ); return getService().createRecoveryCodeLoginSession(session) @@ -918,4 +908,17 @@ public Completable deleteWallet(@NonNull final ChallengeSignature challengeSigna public Observable confirmAccountDeletion(String uuid) { return getService().confirmAccountDeletion(new LinkActionJson(uuid)); } + + /** + * Map the provided SensorEventBatch to its JSON representation and sends + * it to the backend service. + * + * @param sensorEventBatch The batch of sensor events to be mapped and saved. + * @return An Observable indicating the completion of the save operation. + */ + public Observable saveSensorEventBatch(final SensorEventBatch sensorEventBatch) { + final SensorEventBatchJson mappedBatch = apiMapper.mapSensorEventBatch(sensorEventBatch); + + return getService().saveSensorEventBatch(mappedBatch); + } } diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/net/ModelObjectsMapper.java b/android/apolloui/src/main/java/io/muun/apollo/data/net/ModelObjectsMapper.java index 19ddcbd7..d455e5e9 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/net/ModelObjectsMapper.java +++ b/android/apolloui/src/main/java/io/muun/apollo/data/net/ModelObjectsMapper.java @@ -507,7 +507,7 @@ private static SortedMap mapConfTargetToTargetFeeRateInSatPerVb ) { final SortedMap targetedFeeRates = new TreeMap<>(); - for (final var entry: confTargetToTargetFeeRateInSatPerVbyte.entrySet()) { + for (final var entry : confTargetToTargetFeeRateInSatPerVbyte.entrySet()) { final var target = entry.getKey(); final var feeRateInSatPerVbyte = entry.getValue(); @@ -537,8 +537,10 @@ private List mapForwadingPolicies( } private List mapMuunFeatures(List features) { - features.removeAll(Collections.singletonList(MuunFeatureJson.UNSUPPORTED_FEATURE)); - return CollectionUtils.mapList(features, MuunFeature.Companion::fromJson); + final List mappedFeatures = + CollectionUtils.mapList(features, MuunFeature.Companion::fromJson); + mappedFeatures.removeAll(Collections.singletonList(MuunFeature.UNSUPPORTED_FEATURE)); + return mappedFeatures; } /** diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/net/base/RxCallAdapterWrapper.java b/android/apolloui/src/main/java/io/muun/apollo/data/net/base/RxCallAdapterWrapper.java index 2c789da3..63436541 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/net/base/RxCallAdapterWrapper.java +++ b/android/apolloui/src/main/java/io/muun/apollo/data/net/base/RxCallAdapterWrapper.java @@ -2,30 +2,8 @@ import io.muun.apollo.data.logging.LoggingRequestTracker; import io.muun.apollo.data.serialization.SerializationUtils; -import io.muun.apollo.domain.errors.DeprecatedClientVersionError; -import io.muun.apollo.domain.errors.ExpiredActionLinkError; -import io.muun.apollo.domain.errors.ExpiredSessionError; -import io.muun.apollo.domain.errors.InvalidActionLinkError; -import io.muun.apollo.domain.errors.InvalidChallengeSignatureError; import io.muun.apollo.domain.errors.InvalidJsonError; -import io.muun.apollo.domain.errors.TooManyRequestsError; -import io.muun.apollo.domain.errors.newop.AmountTooSmallError; -import io.muun.apollo.domain.errors.newop.ExchangeRateWindowTooOldError; -import io.muun.apollo.domain.errors.newop.InsufficientFundsError; -import io.muun.apollo.domain.errors.newop.InvalidAddressError; -import io.muun.apollo.domain.errors.p2p.CountryNotSupportedError; -import io.muun.apollo.domain.errors.p2p.ExpiredVerificationCodeError; -import io.muun.apollo.domain.errors.p2p.InvalidPhoneNumberError; -import io.muun.apollo.domain.errors.p2p.InvalidVerificationCodeError; -import io.muun.apollo.domain.errors.p2p.PhoneNumberAlreadyUsedError; -import io.muun.apollo.domain.errors.p2p.RevokedVerificationCodeError; -import io.muun.apollo.domain.errors.p2p.TooManyWrongVerificationCodesError; -import io.muun.apollo.domain.errors.passwd.EmailAlreadyUsedError; -import io.muun.apollo.domain.errors.passwd.EmailNotRegisteredError; -import io.muun.apollo.domain.errors.passwd.IncorrectPasswordError; -import io.muun.apollo.domain.errors.rc.CredentialsDontMatchError; -import io.muun.apollo.domain.errors.rc.InvalidRecoveryCodeV2Error; -import io.muun.apollo.domain.errors.rc.StaleChallengeKeyError; +import io.muun.apollo.domain.errors.MuunErrorMapper; import io.muun.common.Optional; import io.muun.common.api.error.Error; import io.muun.common.api.error.ErrorCode; @@ -112,79 +90,8 @@ public class RxCallAdapterWrapper implements CallAdapter { SPECIAL_HTTP_EXCEPTIONS_TRANSFORMER = ObservableFn.replaceTypedError( HttpException.class, error -> { - switch ((ErrorCode) error.getErrorCode()) { - case DEPRECATED_CLIENT_VERSION: - return new DeprecatedClientVersionError(); - - case EXPIRED_SESSION: - return new ExpiredSessionError(); - - case INVALID_PHONE_NUMBER: - return new InvalidPhoneNumberError(); - - case COUNTRY_NOT_SUPPORTED: - return new CountryNotSupportedError(); - - case INVALID_VERIFICATION_CODE: - return new InvalidVerificationCodeError(); - - case REVOKED_VERIFICATION_CODE: - return new RevokedVerificationCodeError(); - - case EXPIRED_VERIFICATION_CODE: - return new ExpiredVerificationCodeError(); - - case TOO_MANY_WRONG_VERIFICATION_CODES: - return new TooManyWrongVerificationCodesError(); // that's a mouthful :) - - case PHONE_NUMBER_ALREADY_USED: - return new PhoneNumberAlreadyUsedError(); - - case EMAIL_ALREADY_USED: - return new EmailAlreadyUsedError(); - - case EMAIL_NOT_REGISTERED: - return new EmailNotRegisteredError(); - - case INSUFFICIENT_CLIENT_FUNDS: - return new InsufficientFundsError(); - - case INVALID_ADDRESS: - return new InvalidAddressError(); - - case INVALID_PASSWORD: - return new IncorrectPasswordError(); - - case INVALID_CHALLENGE_SIGNATURE: - return new InvalidChallengeSignatureError(); - - case AMOUNT_SMALLER_THAN_DUST: - return new AmountTooSmallError(-1); // symbolic, this shouldn't happen - - case EXCHANGE_RATE_WINDOW_TOO_OLD: - return new ExchangeRateWindowTooOldError(); - - case EMAIL_LINK_EXPIRED: - return new ExpiredActionLinkError(); - - case EMAIL_LINK_INVALID: - return new InvalidActionLinkError(); - - case RECOVERY_CODE_V2_NOT_SET_UP: - return new InvalidRecoveryCodeV2Error(); - - case HTTP_TOO_MANY_REQUESTS: - return new TooManyRequestsError(); - - case STALE_CHALLENGE_KEY: - return new StaleChallengeKeyError(); - - case CREDENTIALS_DONT_MATCH: - return new CredentialsDontMatchError(); - - default: - return error; - } + final var muunError = MuunErrorMapper.map(error.getErrorCode().getCode()); + return muunError != null ? muunError : error; } ); } diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/net/base/interceptor/BackgroundExecutionMetricsInterceptor.kt b/android/apolloui/src/main/java/io/muun/apollo/data/net/base/interceptor/BackgroundExecutionMetricsInterceptor.kt index 014fb97b..9a928068 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/net/base/interceptor/BackgroundExecutionMetricsInterceptor.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/net/base/interceptor/BackgroundExecutionMetricsInterceptor.kt @@ -1,7 +1,7 @@ package io.muun.apollo.data.net.base.interceptor -import io.muun.apollo.data.net.base.BaseInterceptor import io.muun.apollo.data.afs.BackgroundExecutionMetricsProvider +import io.muun.apollo.data.net.base.BaseInterceptor import io.muun.apollo.data.toSafeAscii import io.muun.apollo.domain.errors.data.MuunSerializationError import io.muun.apollo.domain.model.user.User diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/nfc/api/FakeNfcSession.kt b/android/apolloui/src/main/java/io/muun/apollo/data/nfc/api/FakeNfcSession.kt index 5e83089c..8e3a90c5 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/nfc/api/FakeNfcSession.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/nfc/api/FakeNfcSession.kt @@ -2,6 +2,10 @@ package io.muun.apollo.data.nfc.api import io.muun.apollo.data.nfc.CardResponse +/** + * Dummy implementation of NfcSession. Meant to enable using Libwallet's MockNfcBridge to use + * a mock security card to develop and test in emulators and have security cards ui tests. + */ class FakeNfcSession : NfcSession { override fun connect() { // Do Nothing diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/os/OS.kt b/android/apolloui/src/main/java/io/muun/apollo/data/os/OS.kt index 0107c8f2..81d4e721 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/os/OS.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/os/OS.kt @@ -65,13 +65,6 @@ object OS { fun supportsGetActiveModemCount(): Boolean = isAndroidROrNewer() - /** - * Whether this OS supports telephonyManager#getNetworkCountryIso(int slotIndex), which was introduced in - * R-11-30. - */ - fun supportsGetNetworkCountryIsoWithSlotIndex(): Boolean = - isAndroidROrNewer() - /** * Whether this OS supports Pending Intent mutability flags, which where introduced in M-6-23 * and are required starting in S-12-31. @@ -131,13 +124,6 @@ object OS { fun supportsBatteryDischargePrediction(): Boolean = isAndroidSOrNewer() - /** - * Whether this OS supports UserManager#getUserCreationTime(UserHandle), which was added - * in M-6-23. - */ - fun supportsUserCreationTime(): Boolean = - isAndroidMOrNewer() - /** * Whether this OS supports MediaDrm#getSupportedCryptoSchemes(), which was introduced in * R-11-30. @@ -152,9 +138,10 @@ object OS { isAndroidMOrNewer() /** - * Whether this OS supports Build.VERSION.BASE_OS, which was introduced in M-6-23. + * Whether this OS supports BatteryManager.isCharging, which was introduced in M-6-23. */ - fun supportsBuildVersionBaseOs(): Boolean = + + fun supportsBatteryManagerIsCharging(): Boolean = isAndroidMOrNewer() /** @@ -169,13 +156,6 @@ object OS { fun supportsPIP(): Boolean = isAndroidNOrNewer() - /** - * Whether this OS device has biometric hardware to detect a fingerprint, which was introduced - * in M-6-23. - */ - fun supportsDactylogram(): Boolean = - isAndroidMOrNewer() - /** * Whether this OS supports Feature PC, which was introduced in O-8.1-27. */ @@ -223,12 +203,6 @@ object OS { fun supportsSupportedAbis(): Boolean = isAndroidLOrNewer() - /** - * Whether this OS supports Calendar#calendarType, which was introduced in O-8-26. - */ - fun supportsCalendarType(): Boolean = - isAndroidOOrNewer() - /** * Whether this OS supports {@link android.security.KeyStoreException} public methods, which * were introduced in T-13-33. @@ -271,16 +245,6 @@ object OS { fun supportsgetHistoricalProcessExitReasons(): Boolean = isAndroidROrNewer() - /** - * Whether this OS supports File#readAttributes, which was added in O-8-26. - */ - fun supportsReadFileAttributes(): Boolean = - isAndroidOOrNewer() - - - fun supportsHardwareAddresses(): Boolean = - isAndroidQOrOlder() - // PRIVATE STUFF: @@ -305,12 +269,6 @@ object OS { private fun isAndroidSOrNewer() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - /** - * Whether this OS version is Q-10-29 or older. - */ - private fun isAndroidQOrOlder() = - Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q - /** * Whether this OS version is EXACTLY Q-10-29. */ diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/preferences/RepositoryRegistry.kt b/android/apolloui/src/main/java/io/muun/apollo/data/preferences/RepositoryRegistry.kt index afe405f7..ee4a9b92 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/preferences/RepositoryRegistry.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/preferences/RepositoryRegistry.kt @@ -89,7 +89,7 @@ class RepositoryRegistry { loadedRepos[repo.javaClass] = repo Timber.d( - "RepositoryRegistry#load(${repo.javaClass.simpleName}). Size: ${loadedRepos.size}" + "RepositoryRegistry#load(${repo.javaClass}). Size: ${loadedRepos.size}" ) } } diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/action/LogoutActions.java b/android/apolloui/src/main/java/io/muun/apollo/domain/action/LogoutActions.java index d4dfd6b3..e7f95b16 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/action/LogoutActions.java +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/action/LogoutActions.java @@ -10,7 +10,6 @@ import io.muun.apollo.domain.action.base.AsyncActionStore; import io.muun.apollo.domain.action.session.ClearRepositoriesAction; import io.muun.apollo.domain.errors.UnrecoverableUserLogoutError; -import io.muun.apollo.domain.libwallet.WalletClient; import io.muun.apollo.domain.model.SignupDraft; import io.muun.apollo.domain.selector.LogoutOptionsSelector; import io.muun.apollo.domain.selector.LogoutOptionsSelector.LogoutOptions; @@ -40,8 +39,6 @@ public class LogoutActions { private final ClearRepositoriesAction clearRepositories; - private final WalletClient walletClient; - // Data private final TaskScheduler taskScheduler; @@ -63,7 +60,6 @@ public LogoutActions( LogoutOptionsSelector logoutOptionsSel, SignupDraftManager signupDraftManager, ClearRepositoriesAction clearRepositories, - WalletClient walletClient, TaskScheduler taskScheduler, SecureStorageProvider secureStorageProvider, NotificationService notificationService, @@ -77,7 +73,6 @@ public LogoutActions( this.logoutOptionsSel = logoutOptionsSel; this.signupDraftManager = signupDraftManager; this.clearRepositories = clearRepositories; - this.walletClient = walletClient; this.taskScheduler = taskScheduler; this.secureStorageProvider = secureStorageProvider; @@ -158,8 +153,6 @@ private void destroyWallet() { Timber.i("destroyWallet"); taskScheduler.unscheduleAllTasks(); - walletClient.deleteWalletBlocking(); - asyncActionStore.resetAllExceptLogout(); secureStorageProvider.wipe(); diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/action/sensor/StoreSensorsDataAction.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/action/sensor/StoreSensorsDataAction.kt new file mode 100644 index 00000000..86964719 --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/action/sensor/StoreSensorsDataAction.kt @@ -0,0 +1,81 @@ +package io.muun.apollo.domain.action.sensor + +import io.muun.apollo.data.net.HoustonClient +import io.muun.apollo.domain.action.base.BaseAsyncAction1 +import io.muun.apollo.domain.model.SensorEventBatch +import io.muun.apollo.domain.model.SensorEvent +import io.muun.apollo.presentation.ui.nfc.events.ISensorEvent +import rx.Observable +import rx.schedulers.Schedulers +import rx.subjects.ReplaySubject +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +private const val BATCHING_TIME_INTERVAL_SECONDS = 3L +private const val MAX_BATCH_SIZE = 500 +private const val MAX_BATCH_QUEUE_SIZE = MAX_BATCH_SIZE * 20 + +/** + * Collects and batches sensor event data over time, sending it to the backend via [HoustonClient]. + * + * Extends [BaseAsyncAction1] to asynchronously handle individual [ISensorEvent] inputs. + * Batches events every [BATCHING_TIME_INTERVAL_SECONDS] or after [MAX_BATCH_SIZE] events, whichever comes first. + * In case of network failure, events are requeued for future retries. + */ +class StoreSensorsDataAction @Inject constructor( + private val houstonClient: HoustonClient, +) : BaseAsyncAction1() { + + /** + * Subject used to queue and buffer incoming sensor event data before batch transmission. + * If the queue is full we'll drop the event surplus. + */ + private val sensorDataSubject = ReplaySubject + .createWithSize(MAX_BATCH_QUEUE_SIZE).toSerialized() + + /** + * Initializes the batching mechanism, which observes incoming events and sends them to the backend + * in batches at regular intervals or when the batch size limit is reached. + * + * Failed transmissions are logged and requeued for retry. If sensorDataSubject queue gets full + * we'll drop the event surplus. + */ + init { + sensorDataSubject + .buffer(BATCHING_TIME_INTERVAL_SECONDS, TimeUnit.SECONDS, MAX_BATCH_SIZE) + .filter { it.isNotEmpty() } + .observeOn(Schedulers.io()) + .subscribe({ eventList -> + val batch = SensorEventBatch( + events = eventList, + ) + houstonClient.saveSensorEventBatch(batch) + .subscribe( + { /* success, nothing to do */ }, + { error -> + Timber.e(error, "Permanent failure saving sensor data, requeueing") + eventList.forEach { sensorDataSubject.onNext(it) } + } + ) + }, { error -> + Timber.e(error, "Unexpected error in batching stream") + }) + } + + /** + * Handles an incoming [ISensorEvent] by converting it to [SensorEvent] and queuing it for batching. + * + * @param sensorEvent The sensor event to process and queue. + * @return An [Observable] that completes immediately after queuing the event. + */ + override fun action( + sensorEvent: ISensorEvent, + ): Observable { + sensorDataSubject.onNext( + sensorEvent.handle() + ) + + return Observable.create { it.onCompleted() } + } +} \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/analytics/AnalyticsEvent.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/analytics/AnalyticsEvent.kt index ed7aaeda..01ebcc40 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/analytics/AnalyticsEvent.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/analytics/AnalyticsEvent.kt @@ -7,6 +7,7 @@ import io.muun.apollo.domain.model.PaymentRequest import io.muun.apollo.domain.model.report.ErrorReport import io.muun.common.model.OperationDirection import java.util.Locale +import kotlin.math.min /** * AnalyticsEvent list, shared with Falcon. Names are lower-cased into FBA event IDs, do not rename. @@ -14,6 +15,32 @@ import java.util.Locale @Suppress("ClassName") sealed class AnalyticsEvent(metadataKeyValues: List> = listOf()) { + companion object { + private const val ANALYTICS_EVENT_PARAM_VALUE_MAX_LENGTH = 100 + + fun buildParamsFor(report: ErrorReport) = listOf( + "id" to report.uniqueId, + "tag" to report.tag, + "title" to report.getTrackingTitle(), + "message" to report.message.safelyTrimParamValue(), + // Trim error stacktraces to avoid problems (not ideal but should be more than enough) + "error" to report.printError(false).safelyTrimParamValue(), + "metadata" to report.printMetadata().safelyTrimParamValue() + ) + + fun buildParamsFor(error: Throwable) = listOf( + "errorSimpleName" to error.javaClass.getSimpleName(), + "errorLocalizedMessage" to (error.localizedMessage ?: ""), + ) + + /** + * Does Substring while ignoring error if endIndex is out of bounds. + */ + private fun String.safelyTrimParamValue(): String { + return substring(0, min(ANALYTICS_EVENT_PARAM_VALUE_MAX_LENGTH, this.length)) + } + } + val eventId = javaClass.simpleName.lowercase(Locale.ROOT) val metadata = metadataKeyValues.toMap() @@ -459,18 +486,29 @@ sealed class AnalyticsEvent(metadataKeyValues: List> = listOf( NFC_2FA_FAILED, } - class E_ERROR(val type: ERROR_TYPE, vararg extras: Any) : AnalyticsEvent( - listOf( - "type" to type.name.lowercase(Locale.getDefault()), - *extras.mapIndexed { index: Int, extra: Any -> Pair("extra$index", extra) } - .toTypedArray() + class E_ERROR : AnalyticsEvent { + + constructor(type: ERROR_TYPE, vararg extras: Any) : super( + listOf( + "type" to type.name.lowercase(Locale.getDefault()), + *extras.mapIndexed { index: Int, extra: Any -> Pair("extra$index", extra) } + .toTypedArray() + ) ) - ) - class E_ERROR_REPORT_DIALOG(vararg extras: Any) : AnalyticsEvent( + constructor(type: ERROR_TYPE, error: Throwable, report: ErrorReport) : super( + listOf( + "type" to type.name.lowercase(Locale.getDefault()), + *buildParamsFor(error).toTypedArray(), + *buildParamsFor(report).toTypedArray() + ) + ) + } + + class E_ERROR_REPORT_DIALOG(error: Throwable, report: ErrorReport) : AnalyticsEvent( listOf( - *extras.mapIndexed { index: Int, extra: Any -> Pair("extra$index", extra) } - .toTypedArray() + *buildParamsFor(error).toTypedArray(), + *buildParamsFor(report).toTypedArray() ) ) @@ -513,23 +551,16 @@ sealed class AnalyticsEvent(metadataKeyValues: List> = listOf( } class E_CRASHLYTICS_ERROR(report: ErrorReport) : AnalyticsEvent( - listOf( - "title" to report.getTrackingTitle(), - "message" to report.message, - // Trim error stacktraces to avoid problems (not ideal but should be more than enough) - "error" to report.printErrorForAnalytics(), - "metadata" to report.printMetadata() - ) + buildParamsFor(report) ) class E_BREADCRUMB(value: String) : AnalyticsEvent( - listOf("crumb" to value) + listOf("crumb" to value.safelyTrimParamValue()) ) - class E_PUSH_NOTIFICATIONS_PERMISSION_ASKED : AnalyticsEvent() - class E_PUSH_NOTIFICATIONS_PERMISSION_SKIPPED : AnalyticsEvent() - class E_PUSH_NOTIFICATIONS_PERMISSION_GRANTED : AnalyticsEvent() - class E_PUSH_NOTIFICATIONS_PERMISSION_DECLINED : AnalyticsEvent() - class E_PUSH_NOTIFICATIONS_PERMISSION_DECLINED_PERMANENTLY : AnalyticsEvent() - -} \ No newline at end of file + class E_PUSH_NOTI_PERMISSION_ASKED : AnalyticsEvent() + class E_PUSH_NOTI_PERMISSION_SKIPPED : AnalyticsEvent() + class E_PUSH_NOTI_PERMISSION_GRANTED : AnalyticsEvent() + class E_PUSH_NOTI_PERMISSION_DECLINED : AnalyticsEvent() + class E_PUSH_NOTI_PERMISSION_PERMA_DECLINED : AnalyticsEvent() +} diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/debug/DebugExecutable.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/debug/DebugExecutable.kt index 4eb7ae97..6d6ee6f6 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/debug/DebugExecutable.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/debug/DebugExecutable.kt @@ -7,6 +7,7 @@ import io.muun.apollo.data.nfc.NfcBridgerFactory import io.muun.apollo.data.nfc.api.NfcSession import io.muun.apollo.data.os.execution.ExecutionTransformerFactory import io.muun.apollo.domain.action.address.CreateAddressAction +import io.muun.apollo.domain.action.debug.ForceErrorReportAction 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 @@ -23,6 +24,7 @@ import javax.inject.Singleton */ @Singleton class DebugExecutable @Inject constructor( + private val forceErrorReportAction: ForceErrorReportAction, private val updateUserPreferences: UpdateUserPreferencesAction, private val createAddress: CreateAddressAction, private val generateInvoice: GenerateInvoiceAction, @@ -115,6 +117,13 @@ class DebugExecutable @Inject constructor( }.compose(transformerFactory.getAsyncExecutor()) .compose(errorMapper()) + /** + * Trigger a background, non fatal error tracking. Making it reach Crashlytics and Bigquery. + */ + fun forceErrorReport() { + forceErrorReportAction.run("Debug Panel") + } + private fun errorMapper() = { observable: Observable -> observable.onErrorResumeNext { error: Throwable -> if (error is LappClientError) { diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/errors/CpuInfoError.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/errors/CpuInfoError.kt deleted file mode 100644 index effa15ba..00000000 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/errors/CpuInfoError.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.muun.apollo.domain.errors - -open class CpuInfoError(tag: String, cause: Throwable) : MuunError( - "Error reading cpu info", - cause -) { - - init { - metadata["tag"] = tag - } -} diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/errors/MuunError.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/errors/MuunError.kt index 0b0c00d8..8df65af8 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/errors/MuunError.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/errors/MuunError.kt @@ -19,7 +19,7 @@ open class MuunError : RuntimeException { * could be another MuunError with its own metadata). */ fun extractMetadata(): MutableMap { - val mapKeys = metadata.mapKeys { entry -> "${javaClass.canonicalName}.${entry.key}" } + val mapKeys = metadata.mapKeys { entry -> "${javaClass.simpleName}.${entry.key}" } return mapKeys as MutableMap } diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/errors/MuunErrorMapper.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/errors/MuunErrorMapper.kt new file mode 100644 index 00000000..d170a36d --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/errors/MuunErrorMapper.kt @@ -0,0 +1,63 @@ +package io.muun.apollo.domain.errors + +import io.muun.apollo.domain.errors.newop.AmountTooSmallError +import io.muun.apollo.domain.errors.newop.ExchangeRateWindowTooOldError +import io.muun.apollo.domain.errors.newop.InsufficientFundsError +import io.muun.apollo.domain.errors.newop.InvalidAddressError +import io.muun.apollo.domain.errors.p2p.CountryNotSupportedError +import io.muun.apollo.domain.errors.p2p.ExpiredVerificationCodeError +import io.muun.apollo.domain.errors.p2p.InvalidPhoneNumberError +import io.muun.apollo.domain.errors.p2p.InvalidVerificationCodeError +import io.muun.apollo.domain.errors.p2p.PhoneNumberAlreadyUsedError +import io.muun.apollo.domain.errors.p2p.RevokedVerificationCodeError +import io.muun.apollo.domain.errors.p2p.TooManyWrongVerificationCodesError +import io.muun.apollo.domain.errors.passwd.EmailAlreadyUsedError +import io.muun.apollo.domain.errors.passwd.EmailNotRegisteredError +import io.muun.apollo.domain.errors.passwd.IncorrectPasswordError +import io.muun.apollo.domain.errors.rc.CredentialsDontMatchError +import io.muun.apollo.domain.errors.rc.InvalidRecoveryCodeV2Error +import io.muun.apollo.domain.errors.rc.StaleChallengeKeyError +import io.muun.common.api.error.ErrorCode + +object MuunErrorMapper { + + private val newMuunErrorByErrorCode: Map MuunError> = mapOf( + ErrorCode.DEPRECATED_CLIENT_VERSION to { DeprecatedClientVersionError() }, + ErrorCode.EXPIRED_SESSION to { ExpiredSessionError() }, + ErrorCode.INVALID_PHONE_NUMBER to { InvalidPhoneNumberError() }, + ErrorCode.COUNTRY_NOT_SUPPORTED to { CountryNotSupportedError() }, + ErrorCode.INVALID_VERIFICATION_CODE to { InvalidVerificationCodeError() }, + ErrorCode.REVOKED_VERIFICATION_CODE to { RevokedVerificationCodeError() }, + ErrorCode.EXPIRED_VERIFICATION_CODE to { ExpiredVerificationCodeError() }, + ErrorCode.TOO_MANY_WRONG_VERIFICATION_CODES to { TooManyWrongVerificationCodesError() }, // that's a mouthful :) + ErrorCode.PHONE_NUMBER_ALREADY_USED to { PhoneNumberAlreadyUsedError() }, + ErrorCode.EMAIL_ALREADY_USED to { EmailAlreadyUsedError() }, + ErrorCode.EMAIL_NOT_REGISTERED to { EmailNotRegisteredError() }, + ErrorCode.INSUFFICIENT_CLIENT_FUNDS to { InsufficientFundsError() }, + ErrorCode.INVALID_ADDRESS to { InvalidAddressError() }, + ErrorCode.INVALID_PASSWORD to { IncorrectPasswordError() }, + ErrorCode.INVALID_CHALLENGE_SIGNATURE to { InvalidChallengeSignatureError() }, + ErrorCode.AMOUNT_SMALLER_THAN_DUST to { AmountTooSmallError(-1) }, // symbolic, this shouldn't happen + ErrorCode.EXCHANGE_RATE_WINDOW_TOO_OLD to { ExchangeRateWindowTooOldError() }, + ErrorCode.EMAIL_LINK_EXPIRED to { ExpiredActionLinkError() }, + ErrorCode.EMAIL_LINK_INVALID to { InvalidActionLinkError() }, + ErrorCode.RECOVERY_CODE_V2_NOT_SET_UP to { InvalidRecoveryCodeV2Error() }, + ErrorCode.HTTP_TOO_MANY_REQUESTS to { TooManyRequestsError() }, + ErrorCode.STALE_CHALLENGE_KEY to { StaleChallengeKeyError() }, + ErrorCode.CREDENTIALS_DONT_MATCH to { CredentialsDontMatchError() }, + ) + + private val errorCodeByNumericCode: Map = + ErrorCode.values().associateBy { it.code.toLong() } + + /** + * Returns a MuunError given a houston error code, + * or null if no mapping exists. + */ + @JvmStatic + fun map(numericCode: Long): MuunError? = + errorCodeByNumericCode[numericCode]?.let { errorCode -> + return newMuunErrorByErrorCode[errorCode]?.invoke() + } + +} diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/errors/SystemUserCreationDateError.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/errors/SystemUserCreationDateError.kt deleted file mode 100644 index b145f711..00000000 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/errors/SystemUserCreationDateError.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.muun.apollo.domain.errors - -import android.os.UserHandle - -class SystemUserCreationDateError( - userProfile: UserHandle, - isSystemUser: Boolean, - cause: Throwable, -) : HardwareCapabilityError("user creation date", cause) { - - init { - metadata["userProfile"] = userProfile.toString() - metadata["isSystemUser"] = isSystemUser - } -} diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/LibwalletClient.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/LibwalletClient.kt index 52083f35..fb1955c1 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/LibwalletClient.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/LibwalletClient.kt @@ -2,17 +2,21 @@ package io.muun.apollo.domain.libwallet import com.google.protobuf.Empty import io.grpc.ManagedChannel +import io.grpc.StatusRuntimeException import io.grpc.stub.StreamObserver import io.muun.apollo.data.libwallet.UtxoScanUpdate import io.muun.apollo.data.nfc.NfcBridger +import io.muun.apollo.domain.errors.MuunErrorMapper +import io.muun.apollo.domain.libwallet.errors.ErrorDetailType +import io.muun.apollo.domain.libwallet.errors.LibwalletGrpcError import rpc.WalletServiceGrpc -import rpc.WalletServiceOuterClass +import rpc.WalletServiceGrpc.WalletServiceBlockingStub +import rpc.WalletServiceGrpc.WalletServiceStub import rpc.WalletServiceOuterClass.DiagnosticSessionDescriptor import rpc.WalletServiceOuterClass.GetRequest import rpc.WalletServiceOuterClass.NullValue -import rpc.WalletServiceOuterClass.OperationStatus -import rpc.WalletServiceOuterClass.SignMessageSecurityCardRequest import rpc.WalletServiceOuterClass.SaveRequest +import rpc.WalletServiceOuterClass.SignMessageSecurityCardRequest import rpc.WalletServiceOuterClass.Value import rx.Emitter import rx.Observable @@ -24,32 +28,24 @@ class WalletClient(private val channel: ManagedChannel) { private val emptyMessage = Empty.getDefaultInstance() - fun deleteWallet(): Observable? { - val handler = { emitter: Emitter -> - val observer = EventForwardingStreamObserver(emitter) - asyncStub.deleteWallet(emptyMessage, observer) - } - - return Observable.create(handler, Emitter.BackpressureMode.BUFFER) - } - - fun deleteWalletBlocking(): OperationStatus? { - return blockingStub.deleteWallet(emptyMessage) - } - fun setUpSecurityCard(nfcBridger: NfcBridger) { nfcBridger.setupBridge() - val xpubResponse = blockingStub.setupSecurityCard(emptyMessage) + val response = blockingStub.performSyncRequest { + setupSecurityCardV2(emptyMessage) + } nfcBridger.tearDownBridge() - Timber.d("Paired Security Card: ${xpubResponse.base58Xpub}") + Timber.d("Paired Security Card - isKnownProvider: ${response.isKnownProvider}") + Timber.d("Paired Security Card - isCardAlreadyUsed: ${response.isCardAlreadyUsed}") } fun resetSecurityCard(nfcBridger: NfcBridger) { nfcBridger.setupBridge() - blockingStub.resetSecurityCard(emptyMessage) + blockingStub.performSyncRequest { + resetSecurityCard(emptyMessage) + } nfcBridger.tearDownBridge() Timber.d("Reset Security Card") @@ -62,40 +58,37 @@ class WalletClient(private val channel: ManagedChannel) { .build() nfcBridger.setupBridge() - val signMessageNfcCardResponse = blockingStub.signMessageSecurityCard(request) + val signMessageNfcCardResponse = blockingStub.performSyncRequest { + signMessageSecurityCard(request) + } nfcBridger.tearDownBridge() return signMessageNfcCardResponse.signedMessageHex.toByteArray() } fun startDiagnosticSession(): Observable { - val handler = { emitter: Emitter -> - val observer = EventForwardingStreamObserver(emitter) - asyncStub.startDiagnosticSession(emptyMessage, observer) + val observable = asyncStub.performAsyncRequest { streamObserver -> + startDiagnosticSession(emptyMessage, streamObserver) } - - return Observable.create(handler, Emitter.BackpressureMode.BUFFER) - .map { it.sessionId } + return observable.map { it.sessionId } } fun scanForUtxos(sessionId: String): Observable { val scanParams = DiagnosticSessionDescriptor.newBuilder() .setSessionId(sessionId) .build() - val handler = { emitter: Emitter -> - val observer = EventForwardingStreamObserver(emitter) - asyncStub.performDiagnosticScanForUtxos(scanParams, observer) - } - return Observable.create(handler, Emitter.BackpressureMode.BUFFER) - .map { - UtxoScanUpdate( - scanComplete = it.hasScanComplete(), - scanStatus = it.scanComplete?.status, - address = it.foundUtxoReport?.address, - amount = it.foundUtxoReport?.amount - ) - } + val observable = asyncStub.performAsyncRequest { streamObserver -> + performDiagnosticScanForUtxos(scanParams, streamObserver) + } + return observable.map { + UtxoScanUpdate( + scanComplete = it.hasScanComplete(), + scanStatus = it.scanComplete?.status, + address = it.foundUtxoReport?.address, + amount = it.foundUtxoReport?.amount + ) + } } fun submitDiagnosticLog(sessionId: String): Observable { @@ -103,15 +96,12 @@ class WalletClient(private val channel: ManagedChannel) { .setSessionId(sessionId) .build() - val handler = { emitter: Emitter -> - val observer = EventForwardingStreamObserver(emitter) - asyncStub.submitDiagnosticLog(scanParams, observer) + val observable = asyncStub.performAsyncRequest { streamObserver -> + submitDiagnosticLog(scanParams, streamObserver) + } + return observable.map { + it.statusMessage } - - return Observable.create(handler, Emitter.BackpressureMode.BUFFER) - .map { - it.statusMessage - } } fun shutdown() { @@ -119,16 +109,13 @@ class WalletClient(private val channel: ManagedChannel) { } private fun save(key: String, rpcValue: Value) { - try { - val saveRequest = SaveRequest.newBuilder() - .setKey(key) - .setValue(rpcValue) - .build() - - blockingStub.save(saveRequest) - } catch(e: Exception) { - Timber.e(e) - throw e + val saveRequest = SaveRequest.newBuilder() + .setKey(key) + .setValue(rpcValue) + .build() + + blockingStub.performSyncRequest { + save(saveRequest) } } @@ -137,13 +124,10 @@ class WalletClient(private val channel: ManagedChannel) { .setKey(key) .build() - try { - val getResponse = blockingStub.get(getRequest) - return getResponse.value - } catch (e: Exception) { - Timber.e(e) - throw e + val getResponse = blockingStub.performSyncRequest { + get(getRequest) } + return getResponse.value } fun saveString(key: String, value: String?) { @@ -170,7 +154,7 @@ class WalletClient(private val channel: ManagedChannel) { } } - fun getString(key: String, defaultValue: String) : String = + fun getString(key: String, defaultValue: String): String = getString(key) ?: defaultValue fun > saveEnum(key: String, value: T?) { @@ -178,11 +162,52 @@ class WalletClient(private val channel: ManagedChannel) { } inline fun > getEnum(key: String): T? = - getString(key) ?.let { enumValueOf(it) } + getString(key)?.let { enumValueOf(it) } inline fun > getEnum(key: String, defaultValue: T): T = getEnum(key) ?: defaultValue + private inline fun WalletServiceBlockingStub.performSyncRequest( + crossinline rpcCall: WalletServiceBlockingStub.() -> T, + ): T { + try { + return rpcCall() + } catch (e: StatusRuntimeException) { + throw mapToMuunError(e) + } catch (t: Throwable) { + Timber.e(t) + throw t + } + } + + private inline fun WalletServiceStub.performAsyncRequest( + crossinline rpcCall: WalletServiceStub.(StreamObserver) -> Unit, + ): Observable { + val handler = { emitter: Emitter -> + val observer = EventForwardingStreamObserver(emitter) + try { + rpcCall(observer) + } catch (e: StatusRuntimeException) { + val rpcError = mapToMuunError(e) + emitter.onError(rpcError) + } catch (t: Throwable) { + Timber.e(t) + emitter.onError(t) + } + } + return Observable.create(handler, Emitter.BackpressureMode.BUFFER) + } + + private fun mapToMuunError(statusException: StatusRuntimeException): RuntimeException { + val grpcError = LibwalletGrpcError(statusException) + Timber.e(grpcError) + return grpcError.errorDetail?.code + ?.takeIf { grpcError.errorDetail.type == ErrorDetailType.HOUSTON } + ?.let(MuunErrorMapper::map) + ?: grpcError + } + + } class EventForwardingStreamObserver(private val emitter: Emitter) : StreamObserver { diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/LibwalletError.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/LibwalletError.kt deleted file mode 100644 index f17e48e7..00000000 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/LibwalletError.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.muun.apollo.domain.libwallet - -import io.muun.apollo.domain.errors.MuunError -import libwallet.Libwallet - -class LibwalletError(val kind: Kind, msg: String, cause: Throwable) : MuunError(msg, cause) { - - enum class Kind(val code: Long) { - UNKNOWN(1L), - INVALID_URI(2L), - NETWORK(3L), - INVALID_PRIVATE_KEY(4L), - INVALID_DERIVATION_PATH(5L); - - companion object { - - fun fromCode(code: Long): Kind? { - for (value in values()) { - if (code == value.code) { - return value - } - } - return null - } - } - } - - companion object { - - fun from(e: Exception): LibwalletError { - val code = Libwallet.errorCode(e) - val kind = Kind.fromCode(code) - var message = e.message ?: "" - if (kind == null) { - message += " (code ${code})" - } - return LibwalletError(kind ?: Kind.UNKNOWN, message, e) - } - } -} \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/LibwalletService.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/LibwalletService.kt index e156eb13..772ad183 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/LibwalletService.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/LibwalletService.kt @@ -14,7 +14,8 @@ interface LibwalletService { // Fee Bump Functions section fun persistFeeBumpFunctions( feeBumpFunctions: FeeBumpFunctions, - refreshPolicy: FeeBumpRefreshPolicy + refreshPolicy: FeeBumpRefreshPolicy, ) + fun areFeeBumpFunctionsInvalidated(): Boolean } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/errors/ErrorDetail.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/errors/ErrorDetail.kt new file mode 100644 index 00000000..bdd97180 --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/errors/ErrorDetail.kt @@ -0,0 +1,8 @@ +package io.muun.apollo.domain.libwallet.errors + +data class ErrorDetail( + val type: ErrorDetailType, + val code: Long, + val message: String, + val developerMessage: String, +) diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/errors/ErrorDetailType.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/errors/ErrorDetailType.kt new file mode 100644 index 00000000..763f16d8 --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/errors/ErrorDetailType.kt @@ -0,0 +1,10 @@ +package io.muun.apollo.domain.libwallet.errors + +enum class ErrorDetailType(val code: Int) { + UNKNOWN(-1), + CLIENT(0), + LIBWALLET(1), + HOUSTON(2) +} + + diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/errors/LibwalletGrpcError.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/errors/LibwalletGrpcError.kt new file mode 100644 index 00000000..85cf3781 --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/libwallet/errors/LibwalletGrpcError.kt @@ -0,0 +1,77 @@ +package io.muun.apollo.domain.libwallet.errors + +import com.google.rpc.Status +import io.grpc.Metadata +import io.grpc.StatusRuntimeException +import io.grpc.protobuf.lite.ProtoLiteUtils +import io.muun.apollo.domain.errors.MuunError +import rpc.WalletServiceOuterClass +import rpc.WalletServiceOuterClass.ErrorType +import timber.log.Timber + +class LibwalletGrpcError(cause: StatusRuntimeException) : MuunError(cause) { + + val errorDetail = mapToErrorDetail(parseErrorDetail(cause)) + + init { + metadata["status.code"] = cause.status.code.name + metadata["status.description"] = cause.status.description ?: "null" + metadata["type"] = errorDetail?.type ?: "null" + metadata["code"] = errorDetail?.code ?: "null" + metadata["message"] = errorDetail?.message ?: "null" + metadata["developerMessage"] = errorDetail?.developerMessage ?: "null" + } + + override fun toString(): String { + return "${errorDetail.toString()}\n${super.toString()}" + } + + private fun parseErrorDetail(e: StatusRuntimeException): WalletServiceOuterClass.ErrorDetail? { + + try { + val statusDetailsKey = Metadata.Key.of( + "grpc-status-details-bin", + ProtoLiteUtils.metadataMarshaller(Status.getDefaultInstance()) + ) + + e.trailers + ?.get(statusDetailsKey) + ?.detailsList + ?.firstOrNull { + // typeUrl follows the format "type.googleapis.com/[package].[MessageName]" + // where [package] and [MessageName] are defined in the .proto file of the msg + it.typeUrl == "type.googleapis.com/rpc.ErrorDetail" + } + ?.let { + return WalletServiceOuterClass.ErrorDetail.parseFrom(it.value.toByteArray()) + } + + return null + } catch (e: Exception) { + Timber.e("cannot extract ErrorDetail from StatusRuntimeException") + return null + } + } + + private fun mapToErrorDetail(grpcErrorDetail: WalletServiceOuterClass.ErrorDetail?): ErrorDetail? { + if (grpcErrorDetail == null) { + return null + } + + return ErrorDetail( + mapToErrorDetailType(grpcErrorDetail.type), + grpcErrorDetail.code, + grpcErrorDetail.message, + grpcErrorDetail.developerMessage + ) + } + + private fun mapToErrorDetailType(grpcErrorType: ErrorType): ErrorDetailType { + return when (grpcErrorType) { + ErrorType.CLIENT -> ErrorDetailType.CLIENT + ErrorType.LIBWALLET -> ErrorDetailType.LIBWALLET + ErrorType.HOUSTON -> ErrorDetailType.HOUSTON + ErrorType.UNRECOGNIZED -> ErrorDetailType.UNKNOWN + } + } +} \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/model/InstallSourceInfo.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/model/InstallSourceInfo.kt index a20951db..c6d46155 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/model/InstallSourceInfo.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/model/InstallSourceInfo.kt @@ -7,5 +7,4 @@ package io.muun.apollo.domain.model data class InstallSourceInfo( val installingPackageName: String, val initiatingPackageName: String? = null, //If !OS.supportsInstallSourceInfo() - val initiatingPackageSigningInfo: String? = null, // If !OS.supportsInstallSourceInfo() ) \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/model/MuunFeature.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/model/MuunFeature.kt index 0748e04c..5c978d56 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/model/MuunFeature.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/model/MuunFeature.kt @@ -1,7 +1,6 @@ package io.muun.apollo.domain.model import io.muun.common.api.MuunFeatureJson -import io.muun.common.exception.MissingCaseError import libwallet.Libwallet enum class MuunFeature { @@ -13,7 +12,10 @@ enum class MuunFeature { HIGH_FEES_RECEIVE_FLOW, EFFECTIVE_FEES_CALCULATION, NFC_CARD, - NFC_SENSORS; + NFC_SENSORS, + DIAGNOSTIC_MODE, + + UNSUPPORTED_FEATURE; companion object { @@ -28,7 +30,9 @@ enum class MuunFeature { MuunFeatureJson.OS_VERSION_DEPRECATED_FLOW -> OS_VERSION_DEPRECATED_FLOW MuunFeatureJson.NFC_CARD -> NFC_CARD MuunFeatureJson.NFC_SENSORS -> NFC_SENSORS - else -> throw MissingCaseError(json) + MuunFeatureJson.DIAGNOSTIC_MODE -> DIAGNOSTIC_MODE + + else -> UNSUPPORTED_FEATURE } fun fromLibwalletModel(name: String): MuunFeature = @@ -42,8 +46,9 @@ enum class MuunFeature { Libwallet.BackendFeatureOsVersionDeprecatedFlow -> OS_VERSION_DEPRECATED_FLOW Libwallet.BackendFeatureNfcCard -> NFC_CARD Libwallet.BackendFeatureNfcSensors -> NFC_SENSORS + Libwallet.BackendFeatureDiagnosticMode -> DIAGNOSTIC_MODE - else -> throw MissingCaseError(name, "MuunFeature conversion from libwallet") + else -> UNSUPPORTED_FEATURE } } @@ -58,6 +63,9 @@ enum class MuunFeature { OS_VERSION_DEPRECATED_FLOW -> MuunFeatureJson.OS_VERSION_DEPRECATED_FLOW NFC_CARD -> MuunFeatureJson.NFC_CARD NFC_SENSORS -> MuunFeatureJson.NFC_SENSORS + DIAGNOSTIC_MODE -> MuunFeatureJson.DIAGNOSTIC_MODE + + UNSUPPORTED_FEATURE -> MuunFeatureJson.UNSUPPORTED_FEATURE } fun toLibwalletModel(): String = @@ -71,5 +79,8 @@ enum class MuunFeature { OS_VERSION_DEPRECATED_FLOW -> Libwallet.BackendFeatureOsVersionDeprecatedFlow NFC_CARD -> Libwallet.BackendFeatureNfcCard NFC_SENSORS -> Libwallet.BackendFeatureNfcSensors + DIAGNOSTIC_MODE -> Libwallet.BackendFeatureDiagnosticMode + + UNSUPPORTED_FEATURE -> Libwallet.BackendFeatureUnsupported } } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/model/SensorEvent.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/model/SensorEvent.kt new file mode 100644 index 00000000..9842126a --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/model/SensorEvent.kt @@ -0,0 +1,10 @@ +package io.muun.apollo.domain.model + +import org.threeten.bp.ZonedDateTime + +data class SensorEvent( + val eventId: Long, + val eventTimestamp: ZonedDateTime, + val eventType: String, + val eventData: Map, +) \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/model/SensorEventBatch.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/model/SensorEventBatch.kt new file mode 100644 index 00000000..2e70decb --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/model/SensorEventBatch.kt @@ -0,0 +1,5 @@ +package io.muun.apollo.domain.model + +data class SensorEventBatch( + val events: List +) \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/model/SystemUserInfo.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/model/SystemUserInfo.kt deleted file mode 100644 index 6087ee9f..00000000 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/model/SystemUserInfo.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.muun.apollo.domain.model - -data class SystemUserInfo(val creationTimestampInMillis: Long, val isSystemUser: Boolean) \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/model/report/ErrorReport.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/model/report/ErrorReport.kt index bd1e0937..35a15c3d 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/model/report/ErrorReport.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/model/report/ErrorReport.kt @@ -2,6 +2,7 @@ package io.muun.apollo.domain.model.report import android.util.Log import java.io.Serializable +import java.util.UUID import kotlin.math.min /** @@ -10,7 +11,6 @@ import kotlin.math.min * for email reports). */ private const val STACK_TRACE_LIMIT_FOR_EMAIL_REPORTS = 10_000 -private const val STACK_TRACE_LIMIT_FOR_ANALYTICS = 500 data class ErrorReport( val tag: String, @@ -20,18 +20,24 @@ data class ErrorReport( val metadata: MutableMap, ) { - fun print(abridged: Boolean) = - "Tag:$tag\nMessage:$message\nError:${printError(abridged)}\nMetadata:{\n\n${printMetadata()}}" + val uniqueId = UUID.randomUUID().toString() + + fun print(abridged: Boolean): String { + val error = printError(abridged) + val metadata = printMetadata() + return "Id:${uniqueId}\nTag:$tag\nMessage:$message\nError:$error\nMetadata:\n\n$metadata" + } fun printMetadata(): String { - val builder = StringBuilder() + val builder = StringBuilder("{\n") for (key in metadata.keys) { - builder.append("$key=${metadata[key]}\n\n") + builder.append("\t$key=${metadata[key]}\n") } + builder.append("}\n") return builder.toString() } - private fun printError(abridged: Boolean = false): String { + fun printError(abridged: Boolean = false): String { val stackTraceString = Log.getStackTraceString(error) return if (abridged) { val stackTraceCapSizeInChars = @@ -42,13 +48,6 @@ data class ErrorReport( } } - fun printErrorForAnalytics(): String { - val stackTraceString = Log.getStackTraceString(error) - val stackTraceCapSizeInChars = min(STACK_TRACE_LIMIT_FOR_ANALYTICS, stackTraceString.length) - return stackTraceString.substring(0, stackTraceCapSizeInChars) - .replace("\n", " ") - } - fun getTrackingTitle(): String = error.javaClass.simpleName + ":" + error.localizedMessage?.replace("\n", " ") } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/domain/model/report/ErrorReportBuilder.kt b/android/apolloui/src/main/java/io/muun/apollo/domain/model/report/ErrorReportBuilder.kt index 1527e4dd..c5322dd9 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/domain/model/report/ErrorReportBuilder.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/domain/model/report/ErrorReportBuilder.kt @@ -67,7 +67,7 @@ object ErrorReportBuilder { // Prepare the metadata: (needs to happen BEFORE summarization bc we reassign the variable) val metadata = extractMetadata(error) - metadata["recentRequests"] = LoggingRequestTracker.getRecentRequests().toString() + metadata["recentRequests"] = printRecentRequests() error = summarize(error) @@ -75,6 +75,16 @@ object ErrorReportBuilder { return ErrorReport(tag ?: "Apollo", message, error, origError, metadata) } + private fun printRecentRequests(): String { + val builder = StringBuilder("[\n") + for (entry in LoggingRequestTracker.getRecentRequests()) { + builder.append("\t\t$entry\n") + } + builder.append("]") + return builder.toString() + } + + /** Craft a summarized Throwable */ private fun summarize(e: Throwable) = captureStackTrace(e) 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 deleted file mode 100644 index e9140538..00000000 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/app/ApolloApplication.java +++ /dev/null @@ -1,393 +0,0 @@ -package io.muun.apollo.presentation.app; - -import io.muun.apollo.data.async.tasks.MuunWorkerFactory; -import io.muun.apollo.data.debug.HeapDumper; -import io.muun.apollo.data.di.DaggerDataComponent; -import io.muun.apollo.data.di.DataComponent; -import io.muun.apollo.data.di.DataModule; -import io.muun.apollo.data.external.DataComponentProvider; -import io.muun.apollo.data.external.Globals; -import io.muun.apollo.data.external.UserFacingErrorMessages; -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.migration.PreferencesMigrationManager; -import io.muun.apollo.domain.ApplicationLockManager; -import io.muun.apollo.domain.BackgroundTimesProcessor; -import io.muun.apollo.domain.NightModeManager; -import io.muun.apollo.domain.action.UserActions; -import io.muun.apollo.domain.action.realtime.PreloadFeeDataAction; -import io.muun.apollo.domain.action.session.DetectAppUpdateAction; -import io.muun.apollo.domain.analytics.Analytics; -import io.muun.apollo.domain.analytics.AnalyticsEvent; -import io.muun.apollo.domain.libwallet.FeeBumpRefreshPolicy; -import io.muun.apollo.domain.libwallet.LibwalletBridge; -import io.muun.apollo.domain.model.NightMode; -import io.muun.apollo.domain.selector.BitcoinUnitSelector; -import io.muun.apollo.domain.sync.FeeDataSyncer; -import io.muun.apollo.presentation.app.di.ApplicationComponent; -import io.muun.apollo.presentation.app.di.DaggerApplicationComponent; -import io.muun.apollo.presentation.ui.utils.UserFacingErrorMessagesImpl; -import io.muun.common.exception.MissingCaseError; - -import android.app.Application; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.provider.Settings; -import android.util.Log; -import androidx.annotation.CallSuper; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.emoji2.text.EmojiCompat; -import androidx.lifecycle.DefaultLifecycleObserver; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.ProcessLifecycleOwner; -import androidx.multidex.MultiDex; -import androidx.work.Configuration; -import app_provided_data.Config; -import com.google.firebase.analytics.FirebaseAnalytics; -import com.jakewharton.threetenabp.AndroidThreeTen; -import rx.plugins.RxJavaHooks; -import timber.log.Timber; - -import java.util.Iterator; -import java.util.ServiceLoader; -import java.util.concurrent.Executor; -import javax.inject.Inject; -import javax.money.spi.CurrencyProviderSpi; -import javax.validation.constraints.NotNull; - -/** - * Application code shared among all flavors. - */ -public abstract class ApolloApplication extends Application - implements DataComponentProvider, Configuration.Provider, DefaultLifecycleObserver { - - private ApplicationComponent applicationComponent; - - private DataComponent dataComponent; - - @Inject - Executor backgroundExecutor; - - @Inject - PreferencesMigrationManager preferencesMigrationManager; - - @Inject - Analytics analytics; - - @Inject - ApplicationLockManager lockManager; - - @Inject - Config libwalletConfig; - - @Inject - NightModeManager nightModeManager; - - @Inject - DetectAppUpdateAction detectAppUpdate; - - @Inject - BackgroundTimesProcessor backgroundTimesProcessor; - - @Inject - PreloadFeeDataAction preloadFeeData; - - @Inject - FeeDataSyncer feeDataSyncer; - - @Inject - UserActions userActions; - - @Override - protected void attachBaseContext(Context base) { - super.attachBaseContext(base); - MultiDex.install(this); - } - - @Override - public void onCreate() { - // The order of the calls in this method is intentional. - // Please don't rearrange them without understanding the implications. - - super.onCreate(); - - initializeStaticSingletons(); - - HeapDumper.init(this); - Crashlytics.init(this); - StrictMode.INSTANCE.init(); - - ensureCurrencyServicesLoaded(); - - AndroidThreeTen.init(this); - - // Ignore tracking events for Firebase Test Lab devices - if (isFirebaseTestLabDevice()) { - FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false); - } - - initializeDagger(); - - detectAppUpdate.run(); - - setupDebugTools(); - - initializeLibwallet(); - - migrateSharedPreferences(); - - // Must run after the prefs migration. - setNightMode(); - - gcmKeepAlive(); - - loadBigQueryPseudoId(); - - // Register lifecycle observer for app background/foreground/terminate tracking - // Apparently this won't correctly handle/track app crashes or ANRs. - // See: https://stackoverflow.com/a/44461605/901465 (and its comments) - ProcessLifecycleOwner.get().getLifecycle().addObserver(this); - - registerReceiver(lockWhenDisplayIsOff, new IntentFilter(Intent.ACTION_SCREEN_OFF)); - - RxJavaHooks.enableAssemblyTracking(); - - EmojiCompat.init(new BundledEmojiCompatConfig(this)); - - if (lockManager.isLockConfigured()) { - lockManager.setLock(); - } - } - - private void loadBigQueryPseudoId() { - analytics.loadBigQueryPseudoId(); - } - - private void setNightMode() { - final NightMode nightMode = nightModeManager.get(); - - switch (nightMode) { - case DARK: - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); - break; - - case LIGHT: - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); - break; - - case FOLLOW_SYSTEM: - break; // Shouldn't need to do anything, since current theme is provided by system - - default: - throw new MissingCaseError(nightMode, "NightMode preference"); - } - } - - /** - *
-     * See WorkManager docs.
-     * 
- * Shortened URL: ... - */ - @NonNull - @Override - public Configuration getWorkManagerConfiguration() { - Timber.d("[MuunWorkerFactory] Application#getWorkManagerConfiguration()"); - final int loggingLevel = Globals.INSTANCE.isRelease() ? Log.ERROR : Log.VERBOSE; - return new Configuration.Builder() - .setWorkerFactory(new MuunWorkerFactory(this)) - .setMinimumLoggingLevel(loggingLevel) - .build(); - } - - /** - * Detect Google Play Pre-Launch report automatic tests, by identifying Firebase Test Lab - * devices, to ignore tracking events from them. - * See: - * ... - * ... - */ - private boolean isFirebaseTestLabDevice() { - return "true".equals(Settings.System.getString(getContentResolver(), "firebase.test.lab")); - } - - /** - * This configures logging and debugging utilities. It has an override in ApolloDebugApplication - * for additional tooling. - */ - @CallSuper - protected void setupDebugTools() { - Timber.plant(new MuunTree()); - - final boolean isDebug = Globals.INSTANCE.isDebug(); - final boolean isDogfood = Globals.INSTANCE.isDogfood(); - - // Use logcat only for debug builds: - LoggingContext.setSendToLogcat(isDebug || isDogfood); - - // Use Crashlytics always: - LoggingContext.setSendToCrashlytics(true); - } - - @Override - public void onTrimMemory(int level) { - super.onTrimMemory(level); - - // NOTE: this triggers when the user navigates away from the app, but NOT when the phone - // screen is turned off with Apollo in foreground. For that, we use `lockWhenDisplayIsOff`. - if (level == TRIM_MEMORY_UI_HIDDEN) { - lockManager.autoSetLockAfterDelay(); - } - } - - private void initializeStaticSingletons() { - Globals.INSTANCE = new GlobalsImpl(); - UserFacingErrorMessages.INSTANCE = new UserFacingErrorMessagesImpl(this); - } - - /** - * TODO: should be done off the main thread. - */ - private void initializeDagger() { - applicationComponent = DaggerApplicationComponent.builder() - .dataComponent(getDataComponent()) - .build(); - - applicationComponent.inject(this); - } - - private void initializeLibwallet() { - LibwalletBridge.init(libwalletConfig); - LibwalletBridge.startServer(); - } - - /** - * Migrates the shared preferences schema. - */ - private void migrateSharedPreferences() { - preferencesMigrationManager.migrate(); - } - - /** - * TODO: this should probably be called every N minutes. - * - *

... - * - *

... - */ - private void gcmKeepAlive() { - this.sendBroadcast(new Intent("com.google.android.intent.action.GTALK_HEARTBEAT")); - this.sendBroadcast(new Intent("com.google.android.intent.action.MCS_HEARTBEAT")); - } - - /** - * TODO: should be done off the main Thread. - */ - private void ensureCurrencyServicesLoaded() { - // This method eagerly loads the CurrencyProviderSpi instances available in the application - // space, instead of letting the ServiceLoader create them lazily as needed. - - // This fixes an occasional UnknownCurrencyException that would randomly crash the app - // on startup, possibly (never fully understood it) due to a timing issue with the provider. - - final ServiceLoader loader = ServiceLoader - .load(CurrencyProviderSpi.class); - - final Iterator it = loader.iterator(); - - //noinspection WhileLoopReplaceableByForEach - while (it.hasNext()) { - it.next(); // nothing else to do, just populate the ServiceLoader cache. - } - } - - /** - * Returns the Dagger component associated with the Data layer. - */ - @NotNull - public DataComponent getDataComponent() { - - if (dataComponent == null) { - final DataModule dataModule = new DataModule( - this, - (appContext, transformerFactory, userRepository) -> new NotificationServiceImpl( - appContext, - transformerFactory, - new BitcoinUnitSelector(userRepository) - ), - AppStandbyBucketProviderImpl::new, - new HoustonConfigImpl() - ); - - dataComponent = DaggerDataComponent.builder() - .dataModule(dataModule) - .build(); - } - - return dataComponent; - } - - /** - * Returns the Dagger component associated with the Application. - */ - @NotNull - public ApplicationComponent getApplicationComponent() { - return applicationComponent; - } - - @Override - public void onLowMemory() { - Timber.i("Application#onLowMemory"); - super.onLowMemory(); - } - - @Override - public void onTerminate() { - Timber.i("Application#onTerminate"); - super.onTerminate(); - } - - @Override - public void onStart(@NonNull LifecycleOwner owner) { // app moved to foreground - analytics.report(new AnalyticsEvent.E_APP_WILL_ENTER_FOREGROUND()); - backgroundTimesProcessor.enterForeground(); - - if (userActions.isLoggedIn()) { - preloadFeeData.run(FeeBumpRefreshPolicy.FOREGROUND); - feeDataSyncer.enterForeground(); - } - } - - @Override - public void onStop(@NonNull LifecycleOwner owner) { // app moved to background - analytics.report(new AnalyticsEvent.E_APP_WILL_GO_TO_BACKGROUND()); - backgroundTimesProcessor.enterBackground(); - feeDataSyncer.enterBackground(); - } - - @Override - public void onDestroy(@NonNull LifecycleOwner owner) { // app will terminate - getDataComponent().walletClient().shutdown(); - LibwalletBridge.stopServer(); - analytics.report(new AnalyticsEvent.E_APP_WILL_TERMINATE()); - } - - private final BroadcastReceiver lockWhenDisplayIsOff = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - - backgroundExecutor.execute(() -> { - final String action = intent.getAction(); - - if (action.equals(Intent.ACTION_SCREEN_OFF) && lockManager.isLockConfigured()) { - lockManager.setLock(); - } - }); - } - }; -} diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/app/ApolloApplication.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/app/ApolloApplication.kt new file mode 100644 index 00000000..a7804a1f --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/app/ApolloApplication.kt @@ -0,0 +1,348 @@ +package io.muun.apollo.presentation.app + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.provider.Settings +import android.util.Log +import androidx.annotation.CallSuper +import androidx.appcompat.app.AppCompatDelegate +import androidx.emoji2.text.EmojiCompat +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.multidex.MultiDex +import androidx.work.Configuration +import app_provided_data.Config +import com.google.firebase.analytics.FirebaseAnalytics +import com.jakewharton.threetenabp.AndroidThreeTen +import io.muun.apollo.data.async.tasks.MuunWorkerFactory +import io.muun.apollo.data.debug.HeapDumper +import io.muun.apollo.data.di.DaggerDataComponent +import io.muun.apollo.data.di.DataComponent +import io.muun.apollo.data.di.DataModule +import io.muun.apollo.data.external.DataComponentProvider +import io.muun.apollo.data.external.Globals +import io.muun.apollo.data.external.UserFacingErrorMessages +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.migration.PreferencesMigrationManager +import io.muun.apollo.domain.ApplicationLockManager +import io.muun.apollo.domain.BackgroundTimesProcessor +import io.muun.apollo.domain.NightModeManager +import io.muun.apollo.domain.action.UserActions +import io.muun.apollo.domain.action.realtime.PreloadFeeDataAction +import io.muun.apollo.domain.action.session.DetectAppUpdateAction +import io.muun.apollo.domain.analytics.Analytics +import io.muun.apollo.domain.analytics.AnalyticsEvent.E_APP_WILL_ENTER_FOREGROUND +import io.muun.apollo.domain.analytics.AnalyticsEvent.E_APP_WILL_GO_TO_BACKGROUND +import io.muun.apollo.domain.analytics.AnalyticsEvent.E_APP_WILL_TERMINATE +import io.muun.apollo.domain.libwallet.FeeBumpRefreshPolicy +import io.muun.apollo.domain.libwallet.LibwalletBridge +import io.muun.apollo.domain.model.NightMode +import io.muun.apollo.domain.model.NightMode.DARK +import io.muun.apollo.domain.model.NightMode.FOLLOW_SYSTEM +import io.muun.apollo.domain.model.NightMode.LIGHT +import io.muun.apollo.domain.sync.FeeDataSyncer +import io.muun.apollo.presentation.app.di.ApplicationComponent +import io.muun.apollo.presentation.app.di.DaggerApplicationComponent +import io.muun.apollo.presentation.ui.utils.UserFacingErrorMessagesImpl +import rx.plugins.RxJavaHooks +import timber.log.Timber +import java.util.ServiceLoader +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.money.spi.CurrencyProviderSpi + +open class ApolloApplication : Application(), DataComponentProvider, Configuration.Provider, DefaultLifecycleObserver { + + private var applicationComponent: ApplicationComponent? = null + + private var dataComponent: DataComponent? = null + + @Inject + lateinit var backgroundExecutor: Executor + + @Inject + lateinit var preferencesMigrationManager: PreferencesMigrationManager + + @Inject + lateinit var analytics: Analytics + + @Inject + lateinit var lockManager: ApplicationLockManager + + @Inject + lateinit var libwalletConfig: Config + + @Inject + lateinit var nightModeManager: NightModeManager + + @Inject + lateinit var detectAppUpdate: DetectAppUpdateAction + + @Inject + lateinit var backgroundTimesProcessor: BackgroundTimesProcessor + + @Inject + lateinit var preloadFeeData: PreloadFeeDataAction + + @Inject + lateinit var feeDataSyncer: FeeDataSyncer + + @Inject + lateinit var userActions: UserActions + + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(base) + MultiDex.install(this) + } + + override fun onCreate() { + // The order of the calls in this method is intentional. + // Please don't rearrange them without understanding the implications. + + super.onCreate() + + initializeStaticSingletons() + + HeapDumper.init(this) + Crashlytics.init(this) + StrictMode.init() + + ensureCurrencyServicesLoaded() + + AndroidThreeTen.init(this) + + // Ignore tracking events for Firebase Test Lab devices + if (isFirebaseTestLabDevice()) { + FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false) + } + + initializeDagger() + + detectAppUpdate.run() + + setupDebugTools() + + initializeLibwallet() + + migrateSharedPreferences() + + // Must run after the prefs migration. + setNightMode() + + gcmKeepAlive() + + loadBigQueryPseudoId() + + // Register lifecycle observer for app background/foreground/terminate tracking + // Apparently this won't correctly handle/track app crashes or ANRs. + // See: https://stackoverflow.com/a/44461605/901465 (and its comments) + ProcessLifecycleOwner.get().lifecycle.addObserver(this) + + registerReceiver(lockWhenDisplayIsOff, IntentFilter(Intent.ACTION_SCREEN_OFF)) + + RxJavaHooks.enableAssemblyTracking() + + EmojiCompat.init(BundledEmojiCompatConfig(this)) + + if (lockManager.isLockConfigured()) { + lockManager.setLock() + } + } + + private fun loadBigQueryPseudoId() { + analytics.loadBigQueryPseudoId() + } + + private fun setNightMode() { + val nightMode: NightMode = nightModeManager.get() + + when (nightMode) { + DARK -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + LIGHT -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + FOLLOW_SYSTEM -> Unit + } + } + + /** + *

+     * See [WorkManager docs](https://developer.android.com/topic/libraries/architecture/workmanager/
+      advanced/custom-configuration#implement-configuration-provider).
+    
* + * Shortened URL: [...](https://shorturl.at/aflsA) + */ + override val workManagerConfiguration: Configuration by lazy { + Timber.d("[MuunWorkerFactory] Application#getWorkManagerConfiguration()") + val loggingLevel = if (Globals.INSTANCE.isRelease) Log.ERROR else Log.VERBOSE + Configuration.Builder() + .setWorkerFactory(MuunWorkerFactory(this)) + .setMinimumLoggingLevel(loggingLevel) + .build() + } + + /** + * Detect Google Play Pre-Launch report automatic tests, by identifying Firebase Test Lab + * devices, to ignore tracking events from them. + * See: + * [...](https://firebase.google.com/docs/test-lab/android/android-studio) + * [...](https://stackoverflow.com/a/45070039/901465) + */ + private fun isFirebaseTestLabDevice(): Boolean { + return "true" == Settings.System.getString(getContentResolver(), "firebase.test.lab") + } + + /** + * This configures logging and debugging utilities. It has an override in ApolloDebugApplication + * for additional tooling. + */ + @CallSuper + protected open fun setupDebugTools() { + Timber.plant(MuunTree()) + + val isDebug = Globals.INSTANCE.isDebug + val isDogfood = Globals.INSTANCE.isDogfood + + // Use logcat only for debug builds: + LoggingContext.sendToLogcat = isDebug || isDogfood + + // Use Crashlytics always: + LoggingContext.sendToCrashlytics = true + } + + override fun onTrimMemory(level: Int) { + super.onTrimMemory(level) + + // NOTE: this triggers when the user navigates away from the app, but NOT when the phone + // screen is turned off with Apollo in foreground. For that, we use `lockWhenDisplayIsOff`. + if (level == TRIM_MEMORY_UI_HIDDEN) { + lockManager.autoSetLockAfterDelay() + } + } + + private fun initializeStaticSingletons() { + Globals.INSTANCE = GlobalsImpl() + UserFacingErrorMessages.INSTANCE = UserFacingErrorMessagesImpl(this) + } + + /** + * TODO: should be done off the main thread. + */ + private fun initializeDagger() { + applicationComponent = DaggerApplicationComponent.builder() + .dataComponent(getDataComponent()) + .build() + + getApplicationComponent().inject(this) + } + + private fun initializeLibwallet() { + LibwalletBridge.init(libwalletConfig) + LibwalletBridge.startServer() + } + + /** + * Migrates the shared preferences schema. + */ + private fun migrateSharedPreferences() { + preferencesMigrationManager.migrate() + } + + /** + * TODO: this should probably be called every N minutes. + * [...](https://productforums.google.com/forum/#!msg/nexus/fslYqYrULto/lU2D3Qe1mugJ) + * [...](https://github.com/schwabe/ics-openvpn/issues/246) + */ + private fun gcmKeepAlive() { + sendBroadcast(Intent("com.google.android.intent.action.GTALK_HEARTBEAT")) + sendBroadcast(Intent("com.google.android.intent.action.MCS_HEARTBEAT")) + } + + /** + * TODO: should be done off the main Thread. + */ + private fun ensureCurrencyServicesLoaded() { + // This method eagerly loads the CurrencyProviderSpi instances available in the application + // space, instead of letting the ServiceLoader create them lazily as needed. + + // This fixes an occasional UnknownCurrencyException that would randomly crash the app + // on startup, possibly (never fully understood it) due to a timing issue with the provider. + + val loader = ServiceLoader.load(CurrencyProviderSpi::class.java) + + val it = loader.iterator() + + while (it.hasNext()) { + it.next() // nothing else to do, just populate the ServiceLoader cache. + } + } + + /** + * Returns the Dagger component associated with the Data layer. + */ + override fun getDataComponent(): DataComponent { + if (dataComponent == null) { + val dataModule = DataModule(this, HoustonConfigImpl()) + + dataComponent = DaggerDataComponent.builder() + .dataModule(dataModule) + .build() + } + + return dataComponent!! + } + + /** + * Returns the Dagger component associated with the Application. + */ + fun getApplicationComponent(): ApplicationComponent { + return applicationComponent!! + } + + override fun onLowMemory() { + Timber.i("Application#onLowMemory") + super.onLowMemory() + } + + override fun onTerminate() { + Timber.i("Application#onTerminate") + super.onTerminate() + } + + override fun onStart(owner: LifecycleOwner) { // app moved to foreground + analytics.report(E_APP_WILL_ENTER_FOREGROUND()) + backgroundTimesProcessor.enterForeground() + + if (userActions.isLoggedIn()) { + preloadFeeData.run(FeeBumpRefreshPolicy.FOREGROUND) + feeDataSyncer.enterForeground() + } + } + + override fun onStop(owner: LifecycleOwner) { // app moved to background + analytics.report(E_APP_WILL_GO_TO_BACKGROUND()) + backgroundTimesProcessor.enterBackground() + feeDataSyncer.enterBackground() + } + + override fun onDestroy(owner: LifecycleOwner) { // app will terminate + getDataComponent().walletClient().shutdown() + LibwalletBridge.stopServer() + analytics.report(E_APP_WILL_TERMINATE()) + } + + private val lockWhenDisplayIsOff: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent) { + backgroundExecutor.execute { + val action = intent.action + if (action == Intent.ACTION_SCREEN_OFF && lockManager.isLockConfigured) { + lockManager.setLock() + } + } + } + } +} \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/app/di/ApplicationComponent.java b/android/apolloui/src/main/java/io/muun/apollo/presentation/app/di/ApplicationComponent.java deleted file mode 100644 index 36d6ab59..00000000 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/app/di/ApplicationComponent.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.muun.apollo.presentation.app.di; - -import io.muun.apollo.data.di.DataComponent; -import io.muun.apollo.presentation.app.ApolloApplication; -import io.muun.apollo.presentation.ui.base.di.ActivityComponent; -import io.muun.apollo.presentation.ui.base.di.FragmentComponent; -import io.muun.apollo.presentation.ui.base.di.ViewComponent; - -import dagger.Component; - -@PerApplication -@Component(dependencies = DataComponent.class) -public interface ApplicationComponent { - - void inject(ApolloApplication application); - - FragmentComponent fragmentComponent(); - - ActivityComponent activityComponent(); - - ViewComponent viewComponent(); -} diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/app/di/ApplicationComponent.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/app/di/ApplicationComponent.kt new file mode 100644 index 00000000..b0678617 --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/app/di/ApplicationComponent.kt @@ -0,0 +1,21 @@ +package io.muun.apollo.presentation.app.di + +import dagger.Component +import io.muun.apollo.data.di.DataComponent +import io.muun.apollo.presentation.app.ApolloApplication +import io.muun.apollo.presentation.ui.base.di.ActivityComponent +import io.muun.apollo.presentation.ui.base.di.FragmentComponent +import io.muun.apollo.presentation.ui.base.di.ViewComponent + +@PerApplication +@Component(dependencies = [DataComponent::class]) +interface ApplicationComponent { + + fun inject(application: ApolloApplication) + + fun fragmentComponent(): FragmentComponent + + fun activityComponent(): ActivityComponent + + fun viewComponent(): ViewComponent +} diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/BaseActivity.java b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/BaseActivity.java index 3927a673..8b1d4dd9 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/BaseActivity.java +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/BaseActivity.java @@ -41,6 +41,7 @@ import android.view.View; import android.view.WindowManager; import android.widget.Toast; +import androidx.activity.EdgeToEdge; import androidx.annotation.CallSuper; import androidx.annotation.LayoutRes; import androidx.annotation.MenuRes; @@ -210,6 +211,7 @@ protected void onDestroy() { * This ensures proper layout behavior when system UI visibility changes (e.g., keyboard shown). */ protected void setWindowInsets() { + EdgeToEdge.enable(this); WindowCompat.setDecorFitsSystemWindows(this.getWindow(), false); final View rootView = getWindow().getDecorView().getRootView(); 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 8e6419e9..411bc6f2 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 @@ -287,9 +287,8 @@ protected void reportError(Throwable error) { analytics.report(new AnalyticsEvent.E_ERROR( AnalyticsEvent.ERROR_TYPE.GENERIC, - error.getClass().getSimpleName(), - error.getLocalizedMessage(), - errorReport.printErrorForAnalytics() + error, + errorReport )); } @@ -520,11 +519,7 @@ private void showErrorReportDialog(Throwable error, boolean standalone) { final ErrorReport errorReport = ErrorReportBuilder.INSTANCE.build(error); - analytics.report(new AnalyticsEvent.E_ERROR_REPORT_DIALOG( - error.getClass().getSimpleName(), - error.getLocalizedMessage(), - errorReport.printErrorForAnalytics() - )); + analytics.report(new AnalyticsEvent.E_ERROR_REPORT_DIALOG(error, errorReport)); final MuunDialog.Builder builder = new MuunDialog.Builder() .layout(R.layout.dialog_custom_layout) diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/SingleFragmentActivity.java b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/SingleFragmentActivity.java index ef1ba458..aa371bcf 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/SingleFragmentActivity.java +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/SingleFragmentActivity.java @@ -8,6 +8,7 @@ import io.muun.apollo.presentation.ui.view.MuunHeader; import io.muun.common.utils.Preconditions; +import android.annotation.SuppressLint; import android.os.Bundle; import androidx.annotation.IdRes; import androidx.annotation.NonNull; @@ -131,6 +132,7 @@ public void setLoading(boolean loading) { } + @SuppressLint("MissingSuperCall") @Override public void onBackPressed() { if (shouldIgnoreBackAndExit()) { diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/SingleFragmentPresenter.java b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/SingleFragmentPresenter.java index d44da670..16862dab 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/SingleFragmentPresenter.java +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/base/SingleFragmentPresenter.java @@ -40,13 +40,13 @@ public void handleError(Throwable error) { if (parentPresenter != null) { parentPresenter.handleError(error); } else { - Timber.i("ParentPresenter not set in handleError: " + getClass().getSimpleName()); + Timber.i("ParentPresenter not set in handleError: %s", getClass().getSimpleName()); try { // We'll try to handle error "ourselves" but, in case we can't we'll avoid crashing // and log error for further research. super.handleError(error); } catch (Exception e) { - Timber.i("Couldn't handleError 'ourselves': " + getClass().getSimpleName()); + Timber.i("Couldn't handleError 'ourselves': %s", getClass().getSimpleName()); Timber.e(e); } } diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelActivity.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelActivity.kt index 19ed6972..4375ce91 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelActivity.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelActivity.kt @@ -127,6 +127,14 @@ class DebugPanelActivity : BaseActivity(), BaseView { presenter.enterDiagnosticMode() } + debugButtonForceErrorReport.setOnClickListener { + presenter.forceErrorReport() + } + + debugButtonForceErrorReportDialog.setOnClickListener { + presenter.forceErrorReportDialog() + } + debugSwitchAllowMultiSession.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> presenter.toggleMultiSessions() } @@ -227,7 +235,7 @@ class DebugPanelActivity : BaseActivity(), BaseView { // TODO do proper error handling val success = try { - presenter.resetSecuritytCard(nfcSession) + presenter.resetSecurityCard(nfcSession) true } catch (e: Exception) { Timber.e(e) diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelPresenter.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelPresenter.kt index 761ced4b..ca4aa660 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelPresenter.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/debug/DebugPanelPresenter.kt @@ -1,5 +1,6 @@ package io.muun.apollo.presentation.ui.debug +import io.muun.apollo.data.external.Globals import io.muun.apollo.data.nfc.api.NfcSession import io.muun.apollo.domain.action.ContactActions import io.muun.apollo.domain.action.OperationActions @@ -188,7 +189,7 @@ class DebugPanelPresenter @Inject constructor( /** * Reset a security card to enable re-use. Only to be used in local builds. */ - fun resetSecuritytCard(nfcSession: NfcSession) { + fun resetSecurityCard(nfcSession: NfcSession) { debugExecutable.resetSecurityCard(nfcSession) } @@ -216,6 +217,20 @@ class DebugPanelPresenter @Inject constructor( } } + /** + * Trigger a background, non fatal error tracking. Making it reach Crashlytics and Bigquery. + */ + fun forceErrorReport() { + debugExecutable.forceErrorReport() + } + + /** + * Trigger an error report dialog. + */ + fun forceErrorReportDialog() { + handleError(RuntimeException("Forced Error Report Dialog v${Globals.INSTANCE.versionCode}")) + } + override fun handleError(error: Throwable) { if (error is DebugExecutableError) { Timber.e(error) diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/diagnostic/DiagnosticActivity.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/diagnostic/DiagnosticActivity.kt index 292decb2..2ba54624 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/diagnostic/DiagnosticActivity.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/diagnostic/DiagnosticActivity.kt @@ -25,7 +25,7 @@ class DiagnosticActivity : AppCompatActivity() { private lateinit var binding: ActivityDiagnosticBinding private val component: ActivityComponent get() { - return (application as ApolloApplication).applicationComponent.activityComponent() + return (application as ApolloApplication).getApplicationComponent().activityComponent() } @Inject diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/home/HomeFragment.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/home/HomeFragment.kt index 0f74d4ca..4149e251 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/home/HomeFragment.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/home/HomeFragment.kt @@ -2,12 +2,13 @@ package io.muun.apollo.presentation.ui.fragments.home import android.content.Context import android.view.GestureDetector +import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration +import android.view.ViewGroup import android.widget.TextView -import butterknife.BindView -import com.airbnb.lottie.LottieAnimationView +import androidx.viewbinding.ViewBinding import com.skydoves.balloon.ArrowConstraints import com.skydoves.balloon.ArrowOrientation import com.skydoves.balloon.Balloon @@ -15,6 +16,7 @@ import com.skydoves.balloon.BalloonAnimation import com.skydoves.balloon.OnBalloonClickListener import com.skydoves.balloon.createBalloon import io.muun.apollo.R +import io.muun.apollo.databinding.FragmentHomeBinding import io.muun.apollo.domain.model.BitcoinUnit import io.muun.apollo.domain.model.Operation import io.muun.apollo.domain.model.UserActivatedFeatureStatus @@ -22,11 +24,7 @@ import io.muun.apollo.domain.selector.UtxoSetStateSelector import io.muun.apollo.presentation.ui.base.SingleFragment import io.muun.apollo.presentation.ui.utils.StyledStringRes import io.muun.apollo.presentation.ui.utils.getDrawable -import io.muun.apollo.presentation.ui.view.BalanceView -import io.muun.apollo.presentation.ui.view.BlockClock -import io.muun.apollo.presentation.ui.view.MuunButton import io.muun.apollo.presentation.ui.view.MuunHomeCard -import io.muun.apollo.presentation.ui.view.NewOpBadge import io.muun.common.utils.BitcoinUtils import org.threeten.bp.ZonedDateTime import kotlin.math.abs @@ -37,35 +35,8 @@ class HomeFragment : SingleFragment(), HomeFragmentView { private const val NEW_OP_ANIMATION_WINDOW = 15L // In Seconds } - @BindView(R.id.chevron) - lateinit var chevron: LottieAnimationView - - @BindView(R.id.chevron_container) - lateinit var chevronContainer: View - - @BindView(R.id.new_op_badge) - lateinit var newOpBadge: NewOpBadge - - @BindView(R.id.home_balance_view) - lateinit var balanceView: BalanceView - - @BindView(R.id.home_receive_button) - lateinit var receiveButton: MuunButton - - @BindView(R.id.home_send_button) - lateinit var sendButton: MuunButton - - @BindView(R.id.home_taproot_card) - lateinit var taprootCard: MuunHomeCard - - @BindView(R.id.home_security_center_card) - lateinit var securityCenterCard: MuunHomeCard - - @BindView(R.id.home_high_fees_card) - lateinit var highFeesCard: MuunHomeCard - - @BindView(R.id.home_block_clock) - lateinit var blockClock: BlockClock + private val binding: FragmentHomeBinding + get() = getBinding() as FragmentHomeBinding var balloon: Balloon? = null @@ -76,6 +47,10 @@ class HomeFragment : SingleFragment(), HomeFragmentView { override fun getLayoutResource() = R.layout.fragment_home + override fun bindingInflater(): (LayoutInflater, ViewGroup, Boolean) -> ViewBinding { + return FragmentHomeBinding::inflate + } + override fun initializeUi(view: View) { setUpGestureDetectors() initializeCards() @@ -97,22 +72,22 @@ class HomeFragment : SingleFragment(), HomeFragmentView { // Having both allows us to retain the default on click (and thus animation) for the chevron val containerDetector = GestureDetector( - requireContext(), GestureListener(chevron, requireContext(), true) + requireContext(), GestureListener(binding.chevron, requireContext(), true) ) - chevronContainer.setOnTouchListener { _, event -> + binding.chevronContainer.setOnTouchListener { _, event -> if (event.action == MotionEvent.ACTION_UP) { - chevronContainer.performClick() + binding.chevronContainer.performClick() } else { containerDetector.onTouchEvent(event) } } val chevronDetector = GestureDetector( - requireContext(), GestureListener(chevron, requireContext(), false) + requireContext(), GestureListener(binding.chevron, requireContext(), false) ) - chevron.setOnTouchListener { _, event -> + binding.chevron.setOnTouchListener { _, event -> if (event.action == MotionEvent.ACTION_UP) { - chevronContainer.performClick() + binding.chevronContainer.performClick() } else { chevronDetector.onTouchEvent(event) } @@ -120,7 +95,7 @@ class HomeFragment : SingleFragment(), HomeFragmentView { } private fun initializeCards() { - taprootCard.let { + binding.homeTaprootCard.let { it.icon = getDrawable(R.drawable.ic_star) it.setIconTint(R.color.blue) it.body = StyledStringRes(requireContext(), R.string.taproot_card_body) @@ -131,7 +106,7 @@ class HomeFragment : SingleFragment(), HomeFragmentView { } } - securityCenterCard.let { + binding.homeSecurityCenterCard.let { it.icon = getDrawable(R.drawable.ic_lock) it.setIconTint(R.color.blue) it.body = StyledStringRes(requireContext(), R.string.home_security_center_card_body) @@ -142,7 +117,7 @@ class HomeFragment : SingleFragment(), HomeFragmentView { } } - highFeesCard.let { + binding.homeHighFeesCard.let { it.icon = getDrawable(R.drawable.ic_baseline_warning_24px) it.body = StyledStringRes(requireContext(), R.string.home_high_fees_card_body) .toCharSequence() @@ -150,11 +125,11 @@ class HomeFragment : SingleFragment(), HomeFragmentView { } private fun setUpClickListeners() { - balanceView.setOnClickListener { onBalanceClick() } - receiveButton.setOnClickListener { presenter.navigateToReceiveScreen() } - sendButton.setOnClickListener { presenter.navigateToSendScreen() } - chevron.setOnClickListener { presenter.navigateToOperations() } - blockClock.setOnClickListener { + binding.homeBalanceView.setOnClickListener { onBalanceClick() } + binding.homeReceiveButton.setOnClickListener { presenter.navigateToReceiveScreen() } + binding.homeSendButton.setOnClickListener { presenter.navigateToSendScreen() } + binding.chevron.setOnClickListener { presenter.navigateToOperations() } + binding.homeBlockClock.setOnClickListener { presenter.navigateToClockDetail() } } @@ -193,7 +168,7 @@ class HomeFragment : SingleFragment(), HomeFragmentView { override fun onPause() { super.onPause() - chevron.removeAllLottieOnCompositionLoadedListener() + binding.chevron.removeAllLottieOnCompositionLoadedListener() balloon?.dismiss() balloon = null lockManager.setUnlockListener(null) @@ -228,7 +203,7 @@ class HomeFragment : SingleFragment(), HomeFragmentView { setLifecycleOwner(this@HomeFragment) } - chevron.addLottieOnCompositionLoadedListener { + binding.chevron.addLottieOnCompositionLoadedListener { // Turns out the balloon library has a few known limitations regarding multi line text // layout. If we set max width, they seem to solve themselves. But, we can't set a fixed // value in the xml so we do this lovely thing here. @@ -237,45 +212,45 @@ class HomeFragment : SingleFragment(), HomeFragmentView { balloon!!.getContentView().findViewById(R.id.new_home_tooltip_text) textView.maxWidth = requireView().width - 56 * 2 - balloon!!.showAlignTop(chevron) + balloon!!.showAlignTop(binding.chevron) } } override fun setState(homeState: HomeFragmentPresenter.HomeState) { - balanceView.setBalance(homeState) + binding.homeBalanceView.setBalance(homeState) setChevronAnimation(homeState.utxoSetState) // Due to (complex) business logic reasons, only 1 of these cards is currently displayed var displayedMuunHomeCard: MuunHomeCard? = null if (!homeState.user.isRecoverable) { - displayedMuunHomeCard = securityCenterCard + displayedMuunHomeCard = binding.homeSecurityCenterCard } else when (homeState.taprootFeatureStatus) { UserActivatedFeatureStatus.OFF -> {} // Do nothing - UserActivatedFeatureStatus.CAN_PREACTIVATE -> displayedMuunHomeCard = taprootCard - UserActivatedFeatureStatus.CAN_ACTIVATE -> displayedMuunHomeCard = taprootCard - UserActivatedFeatureStatus.PREACTIVATED -> blockClock.visibility = View.VISIBLE + UserActivatedFeatureStatus.CAN_PREACTIVATE -> displayedMuunHomeCard = binding.homeTaprootCard + UserActivatedFeatureStatus.CAN_ACTIVATE -> displayedMuunHomeCard = binding.homeTaprootCard + UserActivatedFeatureStatus.PREACTIVATED -> binding.homeBlockClock.visibility = View.VISIBLE UserActivatedFeatureStatus.SCHEDULED_ACTIVATION -> {} // Do nothing UserActivatedFeatureStatus.ACTIVE -> {} // Do nothing } - blockClock.value = homeState.blocksToTaproot + binding.homeBlockClock.value = homeState.blocksToTaproot if (displayedMuunHomeCard != null) { displayedMuunHomeCard.visibility = View.VISIBLE - if (displayedMuunHomeCard == taprootCard) { - securityCenterCard.visibility = View.GONE - highFeesCard.visibility = View.GONE + if (displayedMuunHomeCard == binding.homeTaprootCard) { + binding.homeSecurityCenterCard.visibility = View.GONE + binding.homeHighFeesCard.visibility = View.GONE } else { - taprootCard.visibility = View.GONE - highFeesCard.visibility = View.GONE + binding.homeTaprootCard.visibility = View.GONE + binding.homeHighFeesCard.visibility = View.GONE } } else { - taprootCard.visibility = View.GONE - securityCenterCard.visibility = View.GONE + binding.homeTaprootCard.visibility = View.GONE + binding.homeSecurityCenterCard.visibility = View.GONE if (homeState.highFees) { - highFeesCard.visibility = View.VISIBLE + binding.homeHighFeesCard.visibility = View.VISIBLE } } } @@ -301,17 +276,17 @@ class HomeFragment : SingleFragment(), HomeFragmentView { } } - newOpBadge.setAmount(amountInBtc, bitcoinUnit) + binding.newOpBadge.setAmount(amountInBtc, bitcoinUnit) // Only show animation for recently received or sent ops if (newOp.creationDate.isAfter(ZonedDateTime.now().minusSeconds(NEW_OP_ANIMATION_WINDOW))) { - newOpBadge.startAnimation(animRes) + binding.newOpBadge.startAnimation(animRes) } } private fun onBalanceClick() { - if (balanceView.isFullyInitialized()) { - val hidden = balanceView.toggleVisibility() + if (binding.homeBalanceView.isFullyInitialized()) { + val hidden = binding.homeBalanceView.toggleVisibility() presenter.setBalanceHidden(hidden) } } @@ -323,7 +298,7 @@ class HomeFragment : SingleFragment(), HomeFragmentView { UtxoSetStateSelector.UtxoSetState.CONFIRMED -> R.raw.chevron_regular } - chevron.setAnimation(animationRes) - chevron.playAnimation() + binding.chevron.setAnimation(animationRes) + binding.chevron.playAnimation() } } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/security_center/SecurityCenterFragment.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/security_center/SecurityCenterFragment.kt index 6e457454..acc4ebb9 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/security_center/SecurityCenterFragment.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/security_center/SecurityCenterFragment.kt @@ -1,13 +1,14 @@ package io.muun.apollo.presentation.ui.fragments.security_center +import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView import androidx.annotation.StringRes -import butterknife.BindView +import androidx.viewbinding.ViewBinding import io.muun.apollo.R +import io.muun.apollo.databinding.FragmentSecurityCenterBinding import io.muun.apollo.domain.model.SecurityCenter import io.muun.apollo.domain.model.SecurityLevel import io.muun.apollo.presentation.ui.base.SingleFragment @@ -16,34 +17,12 @@ import io.muun.apollo.presentation.ui.utils.OS import io.muun.apollo.presentation.ui.utils.StyledStringRes import io.muun.apollo.presentation.ui.utils.getDrawable import io.muun.apollo.presentation.ui.view.MuunHeader.Navigation -import io.muun.apollo.presentation.ui.view.MuunProgressBar import io.muun.apollo.presentation.ui.view.MuunTaskCard class SecurityCenterFragment : SingleFragment(), SecurityCenterView { - @BindView(R.id.tag_email_skipped) - lateinit var emailSkippedTag: View - - @BindView(R.id.task_email) - lateinit var emailTaskCard: MuunTaskCard - - @BindView(R.id.task_recovery_code) - lateinit var recoveryCodeTaskCard: MuunTaskCard - - @BindView(R.id.task_export_keys) - lateinit var exportKeysTaskCard: MuunTaskCard - - @BindView(R.id.progress) - lateinit var progressBar: MuunProgressBar - - @BindView(R.id.progress_bar_subtitle) - lateinit var progressBarSubtitle: TextView - - @BindView(R.id.sc_success_box) - lateinit var successBox: ViewGroup - - @BindView(R.id.button_export_keys_again) - lateinit var exportAgainButton: View + private val binding: FragmentSecurityCenterBinding + get() = getBinding() as FragmentSecurityCenterBinding override fun inject() = component.inject(this) @@ -51,6 +30,9 @@ class SecurityCenterFragment : SingleFragment(), Securi override fun getLayoutResource() = R.layout.fragment_security_center + override fun bindingInflater(): (LayoutInflater, ViewGroup, Boolean) -> ViewBinding { + return FragmentSecurityCenterBinding::inflate + } override fun initializeUi(view: View) { setUpHeader() @@ -70,10 +52,10 @@ class SecurityCenterFragment : SingleFragment(), Securi } private fun setUpCards() { - emailTaskCard.setOnClickListener { presenter.goToEmailSetup() } - recoveryCodeTaskCard.setOnClickListener { presenter.goToRecoveryCodeSetup() } - exportKeysTaskCard.setOnClickListener { presenter.goToExportKeys() } - exportAgainButton.setOnClickListener { presenter.goToExportKeys() } + binding.taskEmail.setOnClickListener { presenter.goToEmailSetup() } + binding.taskRecoveryCode.setOnClickListener { presenter.goToRecoveryCodeSetup() } + binding.taskExportKeys.setOnClickListener { presenter.goToExportKeys() } + binding.buttonExportKeysAgain.setOnClickListener { presenter.goToExportKeys() } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -89,43 +71,43 @@ class SecurityCenterFragment : SingleFragment(), Securi ) { // Reset visibility, will be corrected below: - progressBar.visibility = View.VISIBLE - progressBarSubtitle.visibility = View.VISIBLE - successBox.visibility = View.GONE - exportAgainButton.visibility = View.GONE + binding.progress.visibility = View.VISIBLE + binding.progressBarSubtitle.visibility = View.VISIBLE + binding.scSuccessBox.visibility = View.GONE + binding.buttonExportKeysAgain.visibility = View.GONE // Configure global views: when (securityCenter.getLevel()) { SecurityLevel.ANON, SecurityLevel.SKIPPED_EMAIL_ANON -> { - progressBarSubtitle.setText(R.string.sc_task_header_0) - progressBar.colorRes = R.color.red - progressBar.progress = 0.10 + binding.progressBarSubtitle.setText(R.string.sc_task_header_0) + binding.progress.colorRes = R.color.red + binding.progress.progress = 0.10 } SecurityLevel.EMAIL -> { - progressBarSubtitle.setText(R.string.sc_task_header_1) - progressBar.colorRes = R.color.green - progressBar.progress = 0.50 + binding.progressBarSubtitle.setText(R.string.sc_task_header_1) + binding.progress.colorRes = R.color.green + binding.progress.progress = 0.50 } SecurityLevel.SKIPPED_EMAIL_RC -> { - progressBarSubtitle.setText(R.string.sc_task_header_2) - progressBar.colorRes = R.color.green - progressBar.progress = 0.50 + binding.progressBarSubtitle.setText(R.string.sc_task_header_2) + binding.progress.colorRes = R.color.green + binding.progress.progress = 0.50 } SecurityLevel.EMAIL_AND_RC -> { - progressBarSubtitle.setText(R.string.sc_task_header_2) - progressBar.colorRes = R.color.green - progressBar.progress = 0.80 + binding.progressBarSubtitle.setText(R.string.sc_task_header_2) + binding.progress.colorRes = R.color.green + binding.progress.progress = 0.80 } SecurityLevel.DONE, SecurityLevel.SKIPPED_EMAIL_DONE -> { - progressBar.visibility = View.GONE - progressBarSubtitle.visibility = View.GONE - successBox.visibility = View.VISIBLE - exportAgainButton.visibility = View.VISIBLE + binding.progress.visibility = View.GONE + binding.progressBarSubtitle.visibility = View.GONE + binding.scSuccessBox.visibility = View.VISIBLE + binding.buttonExportKeysAgain.visibility = View.VISIBLE } } @@ -136,34 +118,34 @@ class SecurityCenterFragment : SingleFragment(), Securi setRecoveryCodeSetupStatus(recoveryCodeStatus, securityCenter) setExportKeysStatus(exportKeysStatus, hasOldExportKeysOnly) - if (exportKeysTaskCard.status == MuunTaskCard.Status.DONE && hasOldExportKeysOnly) { - exportKeysTaskCard.setOnClickListener { presenter.goToRecoveryTool() } + if (binding.taskExportKeys.status == MuunTaskCard.Status.DONE && hasOldExportKeysOnly) { + binding.taskExportKeys.setOnClickListener { presenter.goToRecoveryTool() } } } private fun setEmailSetupStatus(status: TaskStatus, sc: SecurityCenter) { if (sc.emailSetupSkipped()) { - emailTaskCard.status = MuunTaskCard.Status.SKIPPED - emailSkippedTag.visibility = View.VISIBLE + binding.taskEmail.status = MuunTaskCard.Status.SKIPPED + binding.tagEmailSkipped.visibility = View.VISIBLE if (!OS.supportsTranslateZ()) { // we can't use translateZ in api levels below 21 :( - emailSkippedTag.bringToFront() - (emailSkippedTag.parent as View).invalidate() + binding.tagEmailSkipped.bringToFront() + (binding.tagEmailSkipped.parent as View).invalidate() } } else { - emailTaskCard.status = toCardStatus(status) - emailSkippedTag.visibility = View.GONE + binding.taskEmail.status = toCardStatus(status) + binding.tagEmailSkipped.visibility = View.GONE } - emailTaskCard.title = when (emailTaskCard.status) { + binding.taskEmail.title = when (binding.taskEmail.status) { MuunTaskCard.Status.DONE -> getString(R.string.task_email_done_title) MuunTaskCard.Status.ACTIVE -> getString(R.string.task_email_pending_title) MuunTaskCard.Status.SKIPPED -> getString(R.string.task_email_pending_title) else -> throw IllegalStateException("This should never happen!") } - emailTaskCard.body = when (emailTaskCard.status) { + binding.taskEmail.body = when (binding.taskEmail.status) { MuunTaskCard.Status.DONE -> stringWithEmail(R.string.task_email_done_body, sc.email()!!) MuunTaskCard.Status.ACTIVE -> getString(R.string.task_email_pending_body) MuunTaskCard.Status.SKIPPED -> getString(R.string.task_email_skipped_body) @@ -171,10 +153,10 @@ class SecurityCenterFragment : SingleFragment(), Securi } if (sc.emailSetupSkipped()) { - emailTaskCard.icon = getDrawable(R.drawable.ic_step_1_skipped) + binding.taskEmail.icon = getDrawable(R.drawable.ic_step_1_skipped) } else { - emailTaskCard.icon = getDrawable(when (status) { + binding.taskEmail.icon = getDrawable(when (status) { TaskStatus.DONE -> R.drawable.ic_check TaskStatus.PENDING -> R.drawable.ic_step_1_blue TaskStatus.BLOCKED -> R.drawable.ic_step_1_gray @@ -183,14 +165,14 @@ class SecurityCenterFragment : SingleFragment(), Securi } private fun setRecoveryCodeSetupStatus(status: TaskStatus, securityCenter: SecurityCenter) { - recoveryCodeTaskCard.status = toCardStatus(status) + binding.taskRecoveryCode.status = toCardStatus(status) - recoveryCodeTaskCard.title = when (status) { + binding.taskRecoveryCode.title = when (status) { TaskStatus.DONE -> getString(R.string.task_rc_title_done) else -> getString(R.string.task_rc_title_pending) } - recoveryCodeTaskCard.body = when (securityCenter.getLevel()) { + binding.taskRecoveryCode.body = when (securityCenter.getLevel()) { SecurityLevel.ANON -> getString(R.string.task_rc_body_inactive) SecurityLevel.SKIPPED_EMAIL_ANON -> getString(R.string.task_rc_body_email_skipped) SecurityLevel.EMAIL -> getString(R.string.task_rc_body_pending) @@ -200,7 +182,7 @@ class SecurityCenterFragment : SingleFragment(), Securi stringWithEmail(R.string.task_rc_body_done, securityCenter.email()!!) } - recoveryCodeTaskCard.icon = getDrawable(when (status) { + binding.taskRecoveryCode.icon = getDrawable(when (status) { TaskStatus.DONE -> R.drawable.ic_check TaskStatus.PENDING -> R.drawable.ic_step_2_blue TaskStatus.BLOCKED -> R.drawable.ic_step_2_gray @@ -208,9 +190,9 @@ class SecurityCenterFragment : SingleFragment(), Securi } private fun setExportKeysStatus(status: TaskStatus, hasOldExportKeysOnly: Boolean) { - exportKeysTaskCard.status = toCardStatus(status) + binding.taskExportKeys.status = toCardStatus(status) - exportKeysTaskCard.title = getString(when (status) { + binding.taskExportKeys.title = getString(when (status) { TaskStatus.DONE -> { if (hasOldExportKeysOnly) { R.string.task_export_keys_done_title_old @@ -221,7 +203,7 @@ class SecurityCenterFragment : SingleFragment(), Securi else -> R.string.task_export_keys_pending_title }) - exportKeysTaskCard.body = StyledStringRes(requireContext(), when (status) { + binding.taskExportKeys.body = StyledStringRes(requireContext(), when (status) { TaskStatus.DONE -> { if (hasOldExportKeysOnly) { R.string.task_export_keys_done_body_old @@ -232,7 +214,7 @@ class SecurityCenterFragment : SingleFragment(), Securi else -> R.string.task_export_keys_pending_body }).toCharSequence() - exportKeysTaskCard.icon = getDrawable(when (status) { + binding.taskExportKeys.icon = getDrawable(when (status) { TaskStatus.DONE -> R.drawable.ic_check TaskStatus.PENDING -> R.drawable.ic_step_3_blue TaskStatus.BLOCKED -> R.drawable.ic_step_3_gray diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/settings/SettingsFragment.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/settings/SettingsFragment.kt index 293b49e7..3ef493df 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/settings/SettingsFragment.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/settings/SettingsFragment.kt @@ -84,6 +84,9 @@ open class SettingsFragment : SingleFragment(), SettingsView @BindView(R.id.settings_lightning) lateinit var lightningSettingsItem: MuunSettingItem + @BindView(R.id.settings_diagnostic) + lateinit var diagnosticSettingsItem: MuunSettingItem + @BindView(R.id.recovery_section) lateinit var recoverySection: View @@ -127,6 +130,7 @@ open class SettingsFragment : SingleFragment(), SettingsView deleteWalletItem.setOnClickListener { deleteWallet() } bitcoinSettingsItem.setOnClickListener { goToBitcoinSettings() } lightningSettingsItem.setOnClickListener { goToLightningSettings() } + diagnosticSettingsItem.setOnClickListener { goToDiagnosticMode() } if (Globals.INSTANCE.isDebug) { // TEMP: code for Taproot QA: @@ -169,6 +173,9 @@ open class SettingsFragment : SingleFragment(), SettingsView bitcoinSettingsItem.visibility = if (showBitcoinSettings(state)) View.VISIBLE else View.GONE + diagnosticSettingsItem.visibility = + if (showDiagnosticSettings(state)) View.VISIBLE else View.GONE + // Helper code for internal builds if (Globals.INSTANCE.isDogfood || Globals.INSTANCE.isDebug) { var textForDisplay = "" @@ -357,6 +364,13 @@ open class SettingsFragment : SingleFragment(), SettingsView presenter.navigateToLightningSettings() } + private fun showDiagnosticSettings(state: SettingsState): Boolean = state.features.contains(MuunFeature.DIAGNOSTIC_MODE) + + + private fun goToDiagnosticMode() { + presenter.navigateToDiagnosticMode() + } + private fun showBitcoinSettings(state: SettingsState): Boolean = when (state.taprootFeatureStatus) { PREACTIVATED, SCHEDULED_ACTIVATION, ACTIVE -> true diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/settings/SettingsPresenter.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/settings/SettingsPresenter.kt index e02e6e91..cc56cd93 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/settings/SettingsPresenter.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/settings/SettingsPresenter.kt @@ -239,6 +239,10 @@ class SettingsPresenter @Inject constructor( return S_SETTINGS() } + fun navigateToDiagnosticMode() { + navigator.navigateToDiagnosticMode(context) + } + fun navigateToLightningSettings() { navigator.navigateToFragment(context, LightningSettingsFragment::class.java) } diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/sync/SyncPresenter.java b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/sync/SyncPresenter.java index cc92c144..bf786256 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/sync/SyncPresenter.java +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/sync/SyncPresenter.java @@ -111,11 +111,13 @@ public void handleError(Throwable error) { if (error instanceof InitialSyncNetworkError || error instanceof InitialSyncError) { super.reportError(error); - view.showErrorDialog( - getContext().getString(R.string.network_error_message), - null, - () -> navigator.navigateToLauncher(view.getViewContext()) - ); + if (getContext() != null) { // if fragment is no longer attached avoid NPEs + view.showErrorDialog( + getContext().getString(R.string.network_error_message), + null, + () -> navigator.navigateToLauncher(getContext()) + ); + } } else { super.handleError(error); } diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/home/HomeActivity.java b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/home/HomeActivity.java index c4ef1e35..b1ad262d 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/home/HomeActivity.java +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/home/HomeActivity.java @@ -12,6 +12,7 @@ import io.muun.apollo.presentation.ui.view.MuunButton; import io.muun.apollo.presentation.ui.view.MuunHeader; +import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -143,6 +144,7 @@ public boolean onCreateOptionsMenu(Menu menu) { return showMenu; } + @SuppressLint("MissingSuperCall") @Override public void onBackPressed() { superOnBackPressed(); diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/lnurl/withdraw/LnUrlWithdrawActivity.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/lnurl/withdraw/LnUrlWithdrawActivity.kt index 0a69da69..2c31ce1b 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/lnurl/withdraw/LnUrlWithdrawActivity.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/lnurl/withdraw/LnUrlWithdrawActivity.kt @@ -1,5 +1,6 @@ package io.muun.apollo.presentation.ui.lnurl.withdraw +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.view.LayoutInflater @@ -97,6 +98,7 @@ class LnUrlWithdrawActivity: SingleFragmentActivity(), L } } + @SuppressLint("MissingSuperCall") override fun onBackPressed() { presenter.handleBack() } diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationActivity.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationActivity.kt index 4d7ef641..dd359b51 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationActivity.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationActivity.kt @@ -1,5 +1,6 @@ package io.muun.apollo.presentation.ui.new_operation +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle @@ -7,6 +8,7 @@ import android.os.Handler import android.text.Html import android.text.TextUtils import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.TextView @@ -89,6 +91,9 @@ class NewOperationActivity : SingleFragmentActivity(), } } + @Inject + lateinit var viewModel: NewOperationViewModel + @Inject lateinit var featureSelector: FeatureSelector @@ -289,6 +294,12 @@ class NewOperationActivity : SingleFragmentActivity(), if (amountInput.visibility == View.VISIBLE) { amountInput.requestFocusInput() } + viewModel.subscribeToAllSensors(this, this) + } + + override fun onPause() { + super.onPause() + viewModel.unsubscribeFromAllSensors() } override fun onStop() { @@ -299,6 +310,11 @@ class NewOperationActivity : SingleFragmentActivity(), } } + override fun onTouchEvent(event: MotionEvent): Boolean { + viewModel.onGestureDetected(event) + return super.onTouchEvent(event) + } + private fun getValidIntentUri(intent: Intent): String { return intent.getStringExtra(OPERATION_URI) // from startActivity ?: intent.dataString // deeplink @@ -557,6 +573,7 @@ class NewOperationActivity : SingleFragmentActivity(), finishActivity() } + @SuppressLint("MissingSuperCall") override fun onBackPressed() { if (shouldIgnoreBackAndExit() || handleBackByOverlayFragment()) { diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationViewModel.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationViewModel.kt new file mode 100644 index 00000000..adc82928 --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationViewModel.kt @@ -0,0 +1,77 @@ +package io.muun.apollo.presentation.ui.new_operation + +import android.content.Context +import android.hardware.SensorManager +import android.view.MotionEvent +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import io.muun.apollo.domain.action.sensor.StoreSensorsDataAction +import io.muun.apollo.domain.model.MuunFeature +import io.muun.apollo.domain.selector.FeatureSelector +import io.muun.apollo.presentation.ui.nfc.SensorUtils +import io.muun.apollo.presentation.ui.nfc.events.GestureEvent +import io.muun.apollo.presentation.ui.nfc.events.ISensorEvent +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch +import javax.inject.Inject + +class NewOperationViewModel @Inject constructor( + private val featureSelector: FeatureSelector, + private val storeSensorsDataAction: StoreSensorsDataAction, +) { + + private var sensorJob: Job? = null + + private val _gestureEvents = MutableSharedFlow(extraBufferCapacity = 64) + private val gestureEvents: Flow = _gestureEvents + + /** + * Subscribes to a merged flow of multiple sensor events using a coroutine launched in the given [lifecycleOwner]'s scope. + * + * Each sensor event is handled and logged, with potential for further processing such as sending data to a server. + * + * TODO: duplicated from [NfcReaderViewModel] unify both implementations. + * + * @param context The context used to obtain the [SensorManager]. + * @param lifecycleOwner The [LifecycleOwner] whose lifecycle is used to scope the coroutine collecting sensor updates. + */ + internal fun subscribeToAllSensors(context: Context, lifecycleOwner: LifecycleOwner) { + + if (!featureSelector.get(MuunFeature.NFC_SENSORS)) { + return + } + + val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + + val mergedFlow = merge( + SensorUtils.mergedSensorFlow(sensorManager), + gestureEvents, + ) + + sensorJob = lifecycleOwner.lifecycleScope.launch { + mergedFlow.collect { event -> + storeSensorsDataAction.run(event) + } + } + } + + /** + * Cancels the active job collecting merged sensor events, effectively unsubscribing from all sensor updates. + */ + internal fun unsubscribeFromAllSensors() { + sensorJob?.cancel() + } + + /** + * Handles a touch gesture event by wrapping the [MotionEvent] data into a [GestureEvent] and emitting it. + * + * @param event The [MotionEvent] containing gesture information such as action type, coordinates, and pointer count. + */ + internal fun onGestureDetected(event: MotionEvent) { + val gesture = SensorUtils.generateGestureEvent(event) + _gestureEvents.tryEmit(gesture) + } +} \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/NfcReaderActivity.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/NfcReaderActivity.kt index 34e4b0af..68a19b5e 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/NfcReaderActivity.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/NfcReaderActivity.kt @@ -11,10 +11,10 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.viewbinding.ViewBinding -import io.grpc.StatusRuntimeException import io.muun.apollo.R import io.muun.apollo.data.nfc.api.NfcSession import io.muun.apollo.databinding.NfcReaderActivityBinding +import io.muun.apollo.domain.libwallet.errors.LibwalletGrpcError import io.muun.apollo.presentation.ui.activity.extension.MuunDialog import io.muun.apollo.presentation.ui.activity.extension.NfcReaderModeExtension import io.muun.apollo.presentation.ui.base.BaseActivity @@ -75,26 +75,7 @@ class NfcReaderActivity : BaseActivity>() { } is NfcReaderViewModel.NfcReadState.Error -> { - - var message = "Error! See Debug logs or dismiss and try again\n\n" + - "${state.cause.message}" - - if (state.cause is StatusRuntimeException - && state.cause.message?.contains("invalid signature:") == true - ) { - val errorMessage = - "Invalid Signature Verification! You're probably tapping with" + - " another (incorrect) security card" - message = errorMessage - } - - Timber.e("NFC Error: ${state.cause.message}") - viewModel.reportNfcError(state) - MuunDialog.Builder() - .title("Security Card Auth Required") - .message(message) - .build() - .let(::showDialog) + handleNfcError(state) } } } @@ -102,6 +83,30 @@ class NfcReaderActivity : BaseActivity>() { } } + private fun handleNfcError(state: NfcReaderViewModel.NfcReadState.Error) { + var message = "Error! See Debug logs or dismiss and try again\n\n${state.cause.message}" + + if (state.cause is LibwalletGrpcError) { + val errorDetail = state.cause.errorDetail + + if (errorDetail?.developerMessage?.contains("invalid signature:") == true) { + message = "Invalid Signature Verification! You're probably tapping with another " + + "(incorrect) security card" + + } else if (errorDetail?.developerMessage?.isNotEmpty() == true) { + message = errorDetail.developerMessage + } + } + + Timber.e("NFC Error: ${state.cause.message}") + viewModel.reportNfcError(state) + MuunDialog.Builder() + .title("Security Card Auth Required") + .message(message) + .build() + .let(::showDialog) + } + override fun onResume() { super.onResume() diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/NfcReaderViewModel.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/NfcReaderViewModel.kt index e76f345a..0722c642 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/NfcReaderViewModel.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/NfcReaderViewModel.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.viewModelScope import io.muun.apollo.data.afs.MetricsProvider import io.muun.apollo.data.nfc.NfcBridgerFactory import io.muun.apollo.data.nfc.api.NfcSession +import io.muun.apollo.domain.action.sensor.StoreSensorsDataAction import io.muun.apollo.domain.analytics.Analytics import io.muun.apollo.domain.analytics.AnalyticsEvent.ERROR_TYPE import io.muun.apollo.domain.analytics.AnalyticsEvent.E_ERROR @@ -40,6 +41,7 @@ class NfcReaderViewModel @Inject constructor( private val nfcBridgerFactory: NfcBridgerFactory, private val analytics: Analytics, private val metricsProvider: MetricsProvider, + private val storeSensorsDataAction: StoreSensorsDataAction, ) : ViewModel() { sealed class NfcReadState { @@ -142,10 +144,7 @@ class NfcReaderViewModel @Inject constructor( sensorJob = lifecycleOwner.lifecycleScope.launch { mergedFlow.collect { event -> - event.handle().forEach { (eventKey, eventValue) -> - Timber.d("Sensor [$eventKey]: $eventValue") - // TODO: send to server for storage - } + storeSensorsDataAction.run(event) } } } @@ -163,12 +162,7 @@ class NfcReaderViewModel @Inject constructor( * @param event The [MotionEvent] containing gesture information such as action type, coordinates, and pointer count. */ internal fun onGestureDetected(event: MotionEvent) { - val gesture = GestureEvent( - action = event.actionMasked, - x = event.x, - y = event.y, - pointerCount = event.pointerCount, - ) + val gesture = SensorUtils.generateGestureEvent(event) _gestureEvents.tryEmit(gesture) } diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/SensorUtils.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/SensorUtils.kt index 406e7505..7caf84eb 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/SensorUtils.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/SensorUtils.kt @@ -6,10 +6,13 @@ import android.hardware.SensorEventListener import android.hardware.SensorManager import android.hardware.TriggerEvent import android.hardware.TriggerEventListener +import android.view.MotionEvent import io.muun.apollo.presentation.ui.nfc.events.Acceleration import io.muun.apollo.presentation.ui.nfc.events.AccelerometerEvent +import io.muun.apollo.presentation.ui.nfc.events.GestureEvent import io.muun.apollo.presentation.ui.nfc.events.ISensorEvent import io.muun.apollo.presentation.ui.nfc.events.MagneticEvent +import io.muun.apollo.presentation.ui.nfc.events.PressureEvent import io.muun.apollo.presentation.ui.nfc.events.Rotation import io.muun.apollo.presentation.ui.nfc.events.RotationEvent import io.muun.apollo.presentation.ui.nfc.events.SignificantMotionEvent @@ -21,8 +24,12 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlin.math.abs +private const val SAMPLING_PERIOD_MS = 10_000 + internal object SensorUtils { + private val eventIdCounter = java.util.concurrent.atomic.AtomicLong(0) + /** * Merges multiple sensor flows into a single [Flow] of [ISensorEvent]s. * @@ -34,12 +41,36 @@ internal object SensorUtils { */ internal fun mergedSensorFlow(sensorManager: SensorManager): Flow { return merge( - subscribeToRotationSensor(sensorManager).map { RotationEvent(it) }, - subscribeToMagneticSensor(sensorManager).map { MagneticEvent(it) }, - subscribeToSignificantMotionSensor(sensorManager).map { SignificantMotionEvent(it) }, + subscribeToRotationSensor(sensorManager).map { + RotationEvent( + id = nextEventId(), + rotation = it, + ) + }, + subscribeToMagneticSensor(sensorManager).map { + MagneticEvent( + id = nextEventId(), + magnetic = it, + ) + }, + subscribeToSignificantMotionSensor(sensorManager).map { + SignificantMotionEvent( + id = nextEventId(), + motion = it, + ) + }, subscribeToAccelerometerSensor(sensorManager).map { - AccelerometerEvent(it) + AccelerometerEvent( + id = nextEventId(), + acceleration = it, + ) }, + subscribeToPressureSensor(sensorManager).map { + PressureEvent( + id = nextEventId(), + pressure = it + ) + } ) } @@ -106,9 +137,9 @@ internal object SensorUtils { } sensorManager.registerListener( - /* listener = */ listener, + /* listener = */ listener, /* sensor = */ sensor, - /* samplingPeriodUs = */ SensorManager.SENSOR_DELAY_NORMAL, + /* samplingPeriodUs = */ SAMPLING_PERIOD_MS, ) awaitClose { @@ -154,9 +185,9 @@ internal object SensorUtils { } sensorManager.registerListener( - listener, - sensor, - SensorManager.SENSOR_DELAY_NORMAL, + /* listener = */ listener, + /* sensor = */ sensor, + /* samplingPeriodUs = */ SAMPLING_PERIOD_MS, ) awaitClose { @@ -250,10 +281,74 @@ internal object SensorUtils { override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} } - sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL) + sensorManager.registerListener( + /* listener = */ listener, + /* sensor = */ sensor, + /* samplingPeriodUs = */ SAMPLING_PERIOD_MS, + ) + + awaitClose { + sensorManager.unregisterListener(listener) + } + } + + /** + * Creates a cold [Flow] that emits pressure measurements from the device's barometric pressure sensor. + * + * Sensor events are sampled with [SensorManager.SENSOR_DELAY_NORMAL], and the listener is automatically + * unregistered when the flow collection is cancelled. + * + * @param sensorManager The [SensorManager] used to access the magnetic field sensor. + * @return A cold [Flow] emitting [Float] values representing barometric pressure measurements. + */ + private fun subscribeToPressureSensor(sensorManager: SensorManager) : Flow = callbackFlow { + val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE) + + if (sensor == null) { + close() + return@callbackFlow + } + + val listener = object : SensorEventListener { + override fun onSensorChanged(event: SensorEvent) { + val pressure = event.values[0] + trySend(pressure) + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} + } + + sensorManager.registerListener( + /* listener = */ listener, + /* sensor = */ sensor, + /* samplingPeriodUs = */ 25_000, // This seems to be the limit of the android API + ) awaitClose { sensorManager.unregisterListener(listener) } } + + /** + * Generates a [GestureEvent] from the provided [MotionEvent], assigning it a unique ID. + * + * @param event The MotionEvent containing gesture details such as action type, + * coordinates, and pointer count. + * @return A new instance of GestureEvent populated with data from the MotionEvent. + */ + internal fun generateGestureEvent(event: MotionEvent) = GestureEvent( + id = nextEventId(), + action = event.actionMasked, + x = event.x, + y = event.y, + pointerCount = event.pointerCount, + ) + + /** + * Generates and returns the next unique event ID by incrementing an internal counter. + * + * @return A unique [Long] representing the next sensor event ID. + */ + private fun nextEventId(): Long = eventIdCounter.incrementAndGet() + } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/AccelerometerEvent.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/AccelerometerEvent.kt index cd6dc1ae..d7f10072 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/AccelerometerEvent.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/AccelerometerEvent.kt @@ -1,11 +1,13 @@ package io.muun.apollo.presentation.ui.nfc.events import android.hardware.SensorManager +import io.muun.apollo.domain.model.SensorEvent +import org.threeten.bp.ZonedDateTime import kotlin.math.abs import kotlin.math.sqrt /** - * Represents the acceleration values along the x, y, and z axes, including the total magnitude vector. + * Represents the acceleration values along the x, y, and z axes. * * @param x Acceleration along the x-axis. * @param y Acceleration along the y-axis. @@ -14,32 +16,49 @@ import kotlin.math.sqrt internal data class Acceleration(val x: Float, val y: Float, val z: Float) /** - * Represents an accelerometer sensor event that includes directional acceleration and its magnitude. + * Represents an accelerometer event, including directional acceleration and computed metrics. * - * Determines if the device is stationary by comparing the acceleration magnitude to [SensorManager.STANDARD_GRAVITY]. + * Implements [ISensorEvent] to provide sensor event metadata and structured data for persistence. * - * @param acceleration The [Acceleration] object containing x, y, z components and the total magnitude. + * @property id The unique identifier of the event. + * @property eventType The type of the sensor event, defaulted to "accelerometer". + * @property acceleration The [Acceleration] data captured at the event. + * @property timestamp The ISO 8601 formatted timestamp when the event was created. */ -internal data class AccelerometerEvent(val acceleration: Acceleration) : ISensorEvent { - override fun handle(): List> { +internal data class AccelerometerEvent( + override val id: Long, + override val eventType: String = "accelerometer", + val acceleration: Acceleration, +) : ISensorEvent { + + override val timestamp: ZonedDateTime = ZonedDateTime.now() + + override fun handle(): SensorEvent { val magnitude = getMagnitude() - return listOf( - "accel_x" to acceleration.x.toString(), - "accel_y" to acceleration.y.toString(), - "accel_z" to acceleration.z.toString(), - "magnitude_vector" to magnitude.toString(), - "stationary" to isStationary(magnitude).toString() + return SensorEvent( + eventId = id, + eventType = eventType, + eventTimestamp = timestamp, + eventData = mapOf( + "accel_x_ms2" to acceleration.x.toString(), + "accel_y_ms2" to acceleration.y.toString(), + "accel_z_ms2" to acceleration.z.toString(), + "magnitude_vector_ms2" to magnitude.toString(), + "stationary" to isStationary(magnitude).toString(), + ) ) } /** - * Checks whether the given acceleration [magnitude] indicates that the device is stationary. + * Determines whether the given acceleration [magnitude] suggests the device is stationary. * - * Compares the magnitude to [SensorManager.STANDARD_GRAVITY] using a small threshold to account for minor variations. + * Compares the magnitude to [SensorManager.STANDARD_GRAVITY] using a small threshold to + * account for minor variations. * * @param magnitude The magnitude of the acceleration vector to evaluate. - * @return `true` if the magnitude is within the threshold of Earth's gravity, indicating the device is stationary. + * @return `true` if the magnitude is within the threshold of Earth's gravity, indicating + * the device is stationary. */ private fun isStationary(magnitude: Float): Boolean { val threshold = 0.1 diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/GestureEvent.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/GestureEvent.kt index 8dea44ee..965c3356 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/GestureEvent.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/GestureEvent.kt @@ -1,14 +1,34 @@ package io.muun.apollo.presentation.ui.nfc.events import android.view.MotionEvent +import io.muun.apollo.domain.model.SensorEvent +import org.threeten.bp.ZonedDateTime +/** + * Represents a gesture event captured from a [MotionEvent], including gesture type and position data. + * + * Implements [ISensorEvent] to provide structured data for gesture interactions. + * + * @property id The unique identifier of the gesture event. + * @property eventType The type of the event, defaulted to "gesture". + * @property action The gesture action type from [MotionEvent]. + * @property x The x-coordinate of the gesture event. + * @property y The y-coordinate of the gesture event. + * @property pointerCount The number of pointers (fingers) involved in the gesture. + * @property timestamp The ISO 8601 formatted timestamp when the event was created. + */ internal data class GestureEvent( + override val id: Long, + override val eventType: String = "gesture", val action: Int, val x: Float, val y: Float, val pointerCount: Int, ) : ISensorEvent { - override fun handle(): List> { + + override val timestamp: ZonedDateTime = ZonedDateTime.now() + + override fun handle(): SensorEvent { val actionName = when (action) { MotionEvent.ACTION_DOWN -> "gesture_down" MotionEvent.ACTION_UP -> "gesture_up" @@ -26,11 +46,16 @@ internal data class GestureEvent( else -> "gesture_unknown_$action" } - return listOf( - "gesture_action" to actionName, - "gesture_x" to x.toString(), - "gesture_y" to y.toString(), - "gesture_pointers" to pointerCount.toString() + return SensorEvent( + eventId = id, + eventType = eventType, + eventTimestamp = timestamp, + eventData = mapOf( + "gesture_action" to actionName, + "gesture_x" to x.toString(), + "gesture_y" to y.toString(), + "gesture_pointers" to pointerCount.toString(), + ) ) } } diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/ISensorEvent.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/ISensorEvent.kt index 9c79ca4e..31f1ab75 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/ISensorEvent.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/ISensorEvent.kt @@ -1,13 +1,35 @@ package io.muun.apollo.presentation.ui.nfc.events +import io.muun.apollo.domain.model.SensorEvent +import org.threeten.bp.ZonedDateTime + /** - * Represents a generic sensor event that can be handled and transformed into key-value pairs. + * Defines the contract for a generic sensor event that provides metadata and a method to convert its data + * into a [SensorEvent] structure for processing or persistence. + * + * Implementations must provide an identifier, event type, and timestamp, and define how the event should be handled. */ interface ISensorEvent { + + /** + * The unique identifier of the sensor event. + */ + val id: Long + + /** + * A string describing the type of the sensor event (e.g., "accelerometer", "gesture"). + */ + val eventType: String + + /** + * The ISO 8601 formatted timestamp when the event occurred. + */ + val timestamp: ZonedDateTime + /** - * Processes the sensor event and returns a list of key-value pairs representing event data. + * Converts the sensor event into a structured SensorEventData object. * - * @return A list of pairs where each key is a descriptive label and each value is the corresponding sensor reading. + * @return A SensorEventData containing the event's ID, type, timestamp, and associated data. */ - fun handle(): List> + fun handle(): SensorEvent } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/MagneticEvent.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/MagneticEvent.kt index 780414be..40467de6 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/MagneticEvent.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/MagneticEvent.kt @@ -1,7 +1,28 @@ package io.muun.apollo.presentation.ui.nfc.events -internal data class MagneticEvent(val magnetic: Float) : ISensorEvent { - override fun handle(): List> { - return listOf("magnetic_field" to magnetic.toString()) +import io.muun.apollo.domain.model.SensorEvent +import org.threeten.bp.ZonedDateTime + +/** + * Represents a magnetic field sensor event, including the magnetic field strength + * in microteslas (µT). + */ +internal data class MagneticEvent( + override val id: Long, + override val eventType: String = "magnetic", + val magnetic: Float, +) : ISensorEvent { + + override val timestamp: ZonedDateTime = ZonedDateTime.now() + + override fun handle(): SensorEvent { + return SensorEvent( + eventId = id, + eventType = eventType, + eventTimestamp = timestamp, + eventData = mapOf( + "magnetic_field_ut" to magnetic.toString(), + ) + ) } } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/PressureEvent.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/PressureEvent.kt new file mode 100644 index 00000000..ebff9efe --- /dev/null +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/PressureEvent.kt @@ -0,0 +1,27 @@ +package io.muun.apollo.presentation.ui.nfc.events + +import io.muun.apollo.domain.model.SensorEvent +import org.threeten.bp.ZonedDateTime + +/** + * Represents a barometric pressure sensor event, including the barometric + * pressure in pascals (pa) + */ +internal data class PressureEvent( + override val id: Long, + override val eventType: String = "pressure", + val pressure: Float, +) : ISensorEvent { + override val timestamp: ZonedDateTime = ZonedDateTime.now() + + override fun handle(): SensorEvent { + return SensorEvent( + eventId = id, + eventType = eventType, + eventTimestamp = timestamp, + eventData = mapOf( + "barometric_pressure_pa" to pressure.toString() + ) + ) + } +} \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/RotationEvent.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/RotationEvent.kt index f12fc64a..06e40cfc 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/RotationEvent.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/RotationEvent.kt @@ -1,5 +1,8 @@ package io.muun.apollo.presentation.ui.nfc.events +import io.muun.apollo.domain.model.SensorEvent +import org.threeten.bp.ZonedDateTime + /** * Represents the device's orientation in degrees along the three axes. * @@ -9,12 +12,24 @@ package io.muun.apollo.presentation.ui.nfc.events */ internal data class Rotation(val xAxis: Float, val yAxis: Float, val zAxis: Float) -internal data class RotationEvent(val rotation: Rotation) : ISensorEvent { - override fun handle(): List> { - return listOf( - "rotation_x" to rotation.xAxis.toString(), - "rotation_y" to rotation.yAxis.toString(), - "rotation_z" to rotation.zAxis.toString() +internal data class RotationEvent( + override val id: Long, + override val eventType: String = "rotation", + val rotation: Rotation, +) : ISensorEvent { + + override val timestamp: ZonedDateTime = ZonedDateTime.now() + + override fun handle(): SensorEvent { + return SensorEvent( + eventId = id, + eventType = eventType, + eventTimestamp = timestamp, + eventData = mapOf( + "rotation_x_deg" to rotation.xAxis.toString(), + "rotation_y_deg" to rotation.yAxis.toString(), + "rotation_z_deg" to rotation.zAxis.toString(), + ) ) } } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/SignificantMotionEvent.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/SignificantMotionEvent.kt index 53cd02f6..8f900bc8 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/SignificantMotionEvent.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/nfc/events/SignificantMotionEvent.kt @@ -1,7 +1,27 @@ package io.muun.apollo.presentation.ui.nfc.events -internal data class SignificantMotionEvent(val motion: Float) : ISensorEvent { - override fun handle(): List> { - return listOf("significant_motion" to motion.toString()) +import io.muun.apollo.domain.model.SensorEvent +import org.threeten.bp.ZonedDateTime + +/** + * Represents a significant motion sensor event, capturing abrupt device movements. + */ +internal data class SignificantMotionEvent( + override val id: Long, + override val eventType: String = "significant_motion", + val motion: Float, +) : ISensorEvent { + + override val timestamp: ZonedDateTime = ZonedDateTime.now() + + override fun handle(): SensorEvent { + return SensorEvent( + eventId = id, + eventType = eventType, + eventTimestamp = timestamp, + eventData = mapOf( + "significant_motion" to motion.toString(), + ) + ) } } \ No newline at end of file diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/show_qr/ShowQrPresenter.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/show_qr/ShowQrPresenter.kt index 814a0c3c..5f6c7a76 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/show_qr/ShowQrPresenter.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/show_qr/ShowQrPresenter.kt @@ -87,21 +87,21 @@ class ShowQrPresenter @Inject constructor( } fun reportNotificationPermissionSkipped() { - analytics.report(AnalyticsEvent.E_PUSH_NOTIFICATIONS_PERMISSION_SKIPPED()) + analytics.report(AnalyticsEvent.E_PUSH_NOTI_PERMISSION_SKIPPED()) setNotificationPermissionSkipped.run() } override fun reportNotificationPermissionAsked() { - analytics.report(AnalyticsEvent.E_PUSH_NOTIFICATIONS_PERMISSION_ASKED()) + analytics.report(AnalyticsEvent.E_PUSH_NOTI_PERMISSION_ASKED()) } private fun reportNotificationPermissionNeverAskAgain() { - analytics.report(AnalyticsEvent.E_PUSH_NOTIFICATIONS_PERMISSION_DECLINED_PERMANENTLY()) + analytics.report(AnalyticsEvent.E_PUSH_NOTI_PERMISSION_PERMA_DECLINED()) setNotificationPermissionNeverAskAgain.run() } fun reportNotificationPermissionDenied(shouldShowRequestPermissionRationale: Boolean) { - analytics.report(AnalyticsEvent.E_PUSH_NOTIFICATIONS_PERMISSION_DECLINED()) + analytics.report(AnalyticsEvent.E_PUSH_NOTI_PERMISSION_DECLINED()) // Here we're abusing the fact that shouldShowRequestPermissionRationale starts in false // and turns to true the first time the user taps on "Don't allow" (e.g it remains false if @@ -122,7 +122,7 @@ class ShowQrPresenter @Inject constructor( } fun reportNotificationPermissionGranted() { - analytics.report(AnalyticsEvent.E_PUSH_NOTIFICATIONS_PERMISSION_GRANTED()) + analytics.report(AnalyticsEvent.E_PUSH_NOTI_PERMISSION_GRANTED()) view.handleNotificationPermissionGranted() } diff --git a/android/apolloui/src/main/res/layout-land/fragment_settings.xml b/android/apolloui/src/main/res/layout-land/fragment_settings.xml index a5d0a3df..500aebb5 100644 --- a/android/apolloui/src/main/res/layout-land/fragment_settings.xml +++ b/android/apolloui/src/main/res/layout-land/fragment_settings.xml @@ -161,6 +161,11 @@ muun:label="@string/settings_lightning" style="@style/settings_item" /> + diff --git a/android/apolloui/src/main/res/layout/debug_activity.xml b/android/apolloui/src/main/res/layout/debug_activity.xml index fb3c7f1f..5da109a1 100644 --- a/android/apolloui/src/main/res/layout/debug_activity.xml +++ b/android/apolloui/src/main/res/layout/debug_activity.xml @@ -166,6 +166,24 @@ android:text="Enter diagnostic mode" tools:ignore="HardcodedText" /> + + + + + + diff --git a/android/apolloui/src/main/res/values-es/strings.xml b/android/apolloui/src/main/res/values-es/strings.xml index a4fdf28c..63d76b0c 100644 --- a/android/apolloui/src/main/res/values-es/strings.xml +++ b/android/apolloui/src/main/res/values-es/strings.xml @@ -1463,6 +1463,7 @@ Configuración avanzada Lightning Network Bitcoin Network + Diagnostic Mode Apariencia Modo oscuro Modo claro diff --git a/android/apolloui/src/main/res/values/strings.xml b/android/apolloui/src/main/res/values/strings.xml index de223faa..26be5c3f 100644 --- a/android/apolloui/src/main/res/values/strings.xml +++ b/android/apolloui/src/main/res/values/strings.xml @@ -1410,6 +1410,7 @@ Advanced settings Lightning Network Bitcoin Network + Diagnostic Mode Appearance Dark mode Light mode diff --git a/common/src/main/java/io/muun/common/api/AndroidBuildInfoJson.java b/common/src/main/java/io/muun/common/api/AndroidBuildInfoJson.java index 115a0ebe..f02150a8 100644 --- a/common/src/main/java/io/muun/common/api/AndroidBuildInfoJson.java +++ b/common/src/main/java/io/muun/common/api/AndroidBuildInfoJson.java @@ -49,6 +49,15 @@ public class AndroidBuildInfoJson { @Nullable public String baseOs; + @Nullable + public String model; + + @Nullable + public String product; + + @Nullable + public String release; + /** * Json constructor. */ @@ -62,30 +71,28 @@ public AndroidBuildInfoJson() { public AndroidBuildInfoJson( @Nullable List abis, @Nullable String fingerprint, - @Nullable String hardware, @Nullable String bootloader, @Nullable String manufacturer, @Nullable String brand, - @Nullable String display, - @Nullable Long time, @Nullable String host, @Nullable String type, @Nullable String radioVersion, @Nullable String securityPatch, - @Nullable String baseOs + @Nullable String model, + @Nullable String product, + @Nullable String release ) { this.abis = abis; this.fingerprint = fingerprint; - this.hardware = hardware; this.bootloader = bootloader; this.manufacturer = manufacturer; this.brand = brand; - this.display = display; - this.time = time; this.host = host; this.type = type; this.radioVersion = radioVersion; this.securityPatch = securityPatch; - this.baseOs = baseOs; + this.model = model; + this.product = product; + this.release = release; } } diff --git a/common/src/main/java/io/muun/common/api/AndroidDeviceFeaturesJson.java b/common/src/main/java/io/muun/common/api/AndroidDeviceFeaturesJson.java index 455c29ee..490c5f8d 100644 --- a/common/src/main/java/io/muun/common/api/AndroidDeviceFeaturesJson.java +++ b/common/src/main/java/io/muun/common/api/AndroidDeviceFeaturesJson.java @@ -79,30 +79,25 @@ private AndroidDeviceFeaturesValue mapDeviceFeaturesValue(Integer value) { * Code constructor. */ public AndroidDeviceFeaturesJson( - @Nullable Integer touch, @Nullable Integer proximity, @Nullable Integer accelerometer, @Nullable Integer gyro, @Nullable Integer compass, @Nullable Integer telephony, - @Nullable Integer cdma, - @Nullable Integer gsm, - @Nullable Integer cameras, @Nullable Integer pc, - @Nullable Integer pip, - @Nullable Integer dactylogram + @Nullable Integer pip ) { - this.touch = mapDeviceFeaturesValue(touch); + this.touch = null; this.proximity = mapDeviceFeaturesValue(proximity); this.accelerometer = mapDeviceFeaturesValue(accelerometer); this.gyro = mapDeviceFeaturesValue(gyro); this.compass = mapDeviceFeaturesValue(compass); this.telephony = mapDeviceFeaturesValue(telephony); - this.cdma = mapDeviceFeaturesValue(cdma); - this.gsm = mapDeviceFeaturesValue(gsm); - this.cameras = mapDeviceFeaturesValue(cameras); + this.cdma = null; + this.gsm = null; + this.cameras = null; this.pc = mapDeviceFeaturesValue(pc); this.pip = mapDeviceFeaturesValue(pip); - this.dactylogram = mapDeviceFeaturesValue(dactylogram); + this.dactylogram = null; } } \ No newline at end of file diff --git a/common/src/main/java/io/muun/common/api/ClientJson.java b/common/src/main/java/io/muun/common/api/ClientJson.java index 33c59bdd..3d2b0a28 100644 --- a/common/src/main/java/io/muun/common/api/ClientJson.java +++ b/common/src/main/java/io/muun/common/api/ClientJson.java @@ -75,9 +75,6 @@ public class ClientJson { @Nullable public Map drmProviderToClientId; - @Nullable - public List androidSystemUsersInfo; - @Nullable public Long androidElapsedRealtimeAtSessionCreationInMillis; @@ -189,6 +186,33 @@ public class ClientJson { @Nullable public Map androidDeviceRegion; + @Nullable + public String androidApplicationId; + + @Nullable + public String iosAppDisplayName; + + @Nullable + public String iosAppId; + + @Nullable + public String iosAppName; + + @Nullable + public String iosAppPrimaryIconHash; + + @Nullable + public Boolean iosIsSoftDevice; + + @Nullable + public String iosSoftDeviceName; + + @Nullable + public Boolean iosHasGyro; + + @Nullable + public Integer iosInstallSource; + /** * Json constructor. */ @@ -211,21 +235,14 @@ public ClientJson( @Nullable final Boolean isRootHint, @Nullable String androidId, final long androidCreationTimestampInMilliseconds, - @Nullable List systemUsersInfo, @SuppressWarnings("NullableProblems") final Map drmProviderClientIds, final long androidElapsedRealtimeAtSessionCreationInMillis, final long androidUptimeAtSessionCreationInMillis, @Nullable final String installSource, @Nullable final String installInitiatingPackageName, - @Nullable final String installInitiatingPackageSigningInfo, - @Nullable final String osBuildFingerprint, - @Nullable final String hardwareName, @Nullable final String systemBootloaderVersion, final int bootCount, @Nullable final String glEsVersion, - @Nullable final Map cpuInfoLegacy, - @Nullable final List> cpuCommonInfo, - @Nullable final List>> cpuPerProcessorInfo, @Nullable final Long googlePlayServicesVersionCode, @Nullable final String googlePlayServicesVersionName, @Nullable final Integer googlePlayServicesClientVersionCode, @@ -240,12 +257,10 @@ public ClientJson( @Nullable final Boolean androidSecurityEnhancedBuild, @Nullable final Boolean androidBridgeRootService, @Nullable final Long androidAppSize, - @Nullable final List androidHardwareAddresses, @Nullable final String androidVbMeta, - @Nullable final String androidEfsCreationTimeInSeconds, @Nullable final Boolean androidIsLowRamDevice, @Nullable final Long androidFirstInstallTimeInMs, - @Nullable final Map deviceRegion + @Nullable final String applicationId ) { this.type = type; this.buildType = buildType; @@ -259,22 +274,15 @@ public ClientJson( this.androidId = androidId; this.androidCreationTimestampInMilliseconds = androidCreationTimestampInMilliseconds; this.drmClientIds = null; - this.androidSystemUsersInfo = systemUsersInfo; this.drmProviderToClientId = drmProviderClientIds; this.androidElapsedRealtimeAtSessionCreationInMillis = androidElapsedRealtimeAtSessionCreationInMillis; this.androidUptimeAtSessionCreationInMillis = androidUptimeAtSessionCreationInMillis; this.installSource = installSource; this.installInitiatingPackageName = installInitiatingPackageName; - this.installInitiatingPackageSigningInfo = installInitiatingPackageSigningInfo; - this.osBuildFingerprint = osBuildFingerprint; - this.hardwareName = hardwareName; this.systemBootloaderVersion = systemBootloaderVersion; this.bootCount = bootCount; this.glEsVersion = glEsVersion; - this.cpuInfoLegacy = cpuInfoLegacy; - this.cpuCommonInfo = cpuCommonInfo; - this.cpuPerProcessorInfo = cpuPerProcessorInfo; this.googlePlayServicesVersionCode = googlePlayServicesVersionCode; this.googlePlayServicesVersionName = googlePlayServicesVersionName; this.googlePlayServicesClientVersionCode = googlePlayServicesClientVersionCode; @@ -289,11 +297,9 @@ public ClientJson( this.androidSecurityEnhancedBuild = androidSecurityEnhancedBuild; this.androidBridgeRootService = androidBridgeRootService; this.androidAppSize = androidAppSize; - this.androidHardwareAddresses = androidHardwareAddresses; this.androidVbMeta = androidVbMeta; - this.androidEfsCreationTimeInSeconds = androidEfsCreationTimeInSeconds; this.androidIsLowRamDevice = androidIsLowRamDevice; this.androidFirstInstallTimeInMs = androidFirstInstallTimeInMs; - this.androidDeviceRegion = deviceRegion; + this.androidApplicationId = applicationId; } } \ No newline at end of file diff --git a/common/src/main/java/io/muun/common/api/SensorEventBatchJson.java b/common/src/main/java/io/muun/common/api/SensorEventBatchJson.java new file mode 100644 index 00000000..c87c3697 --- /dev/null +++ b/common/src/main/java/io/muun/common/api/SensorEventBatchJson.java @@ -0,0 +1,22 @@ +package io.muun.common.api; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class SensorEventBatchJson { + + public List events; + + public SensorEventBatchJson( + List events + ) { + this.events = events; + } + + public SensorEventBatchJson() { + } +} diff --git a/common/src/main/java/io/muun/common/api/SensorEventJson.java b/common/src/main/java/io/muun/common/api/SensorEventJson.java new file mode 100644 index 00000000..02a5e324 --- /dev/null +++ b/common/src/main/java/io/muun/common/api/SensorEventJson.java @@ -0,0 +1,36 @@ +package io.muun.common.api; + +import io.muun.common.dates.MuunZonedDateTime; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class SensorEventJson { + + public Long eventId; + + public MuunZonedDateTime eventTimestamp; + + public String eventType; + + public Map eventData; + + public SensorEventJson( + Long eventId, + MuunZonedDateTime eventTimestamp, + String eventType, + Map eventData + ) { + this.eventId = eventId; + this.eventTimestamp = eventTimestamp; + this.eventType = eventType; + this.eventData = eventData; + } + + public SensorEventJson() { + } +} diff --git a/common/src/main/java/io/muun/common/api/VerifiableMuunKeyJson.java b/common/src/main/java/io/muun/common/api/VerifiableMuunKeyJson.java new file mode 100644 index 00000000..4df3dd04 --- /dev/null +++ b/common/src/main/java/io/muun/common/api/VerifiableMuunKeyJson.java @@ -0,0 +1,38 @@ +package io.muun.common.api; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import javax.annotation.Nonnull; +import javax.validation.constraints.NotNull; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class VerifiableMuunKeyJson { + + @NotNull + // Hpke-encrypted first half of the server cosigning key + // to the user cosigning key + public String firstHalfKeyEncryptedToClient; + + @NotNull + // Hpke-encrypted second half of the server cosigning key + // to the recovery code key + public String secondHalfKeyEncryptedToRecoveryCode; + + public String proof; + + public VerifiableMuunKeyJson() { + + } + + public VerifiableMuunKeyJson( + @Nonnull String firstHalfKeyEncryptedToClient, + @Nonnull String secondHalfKeyEncryptedToRecoveryCode, + String proof + ) { + this.firstHalfKeyEncryptedToClient = firstHalfKeyEncryptedToClient; + this.secondHalfKeyEncryptedToRecoveryCode = secondHalfKeyEncryptedToRecoveryCode; + this.proof = proof; + } +} diff --git a/common/src/main/java/io/muun/common/api/VerifiableServerCosigningKeyJson.java b/common/src/main/java/io/muun/common/api/VerifiableServerCosigningKeyJson.java deleted file mode 100644 index 48269882..00000000 --- a/common/src/main/java/io/muun/common/api/VerifiableServerCosigningKeyJson.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.muun.common.api; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - -import javax.annotation.Nonnull; -import javax.validation.constraints.NotNull; - -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public class VerifiableServerCosigningKeyJson { - - @NotNull - public String ephemeralPublicKey; - - @NotNull - public String paddedServerCosigningKey; - - @NotNull - public String proof; - - public VerifiableServerCosigningKeyJson() { - - } - - public VerifiableServerCosigningKeyJson( - @Nonnull String ephemeralPublicKey, - @Nonnull String paddedServerCosigningKey, - @Nonnull String proof - ) { - this.ephemeralPublicKey = ephemeralPublicKey; - this.paddedServerCosigningKey = paddedServerCosigningKey; - this.proof = proof; - } -} diff --git a/common/src/main/java/io/muun/common/api/houston/HoustonService.java b/common/src/main/java/io/muun/common/api/houston/HoustonService.java index 3970e1e5..18f86756 100644 --- a/common/src/main/java/io/muun/common/api/houston/HoustonService.java +++ b/common/src/main/java/io/muun/common/api/houston/HoustonService.java @@ -42,6 +42,7 @@ import io.muun.common.api.RealTimeData; import io.muun.common.api.RealTimeFeesJson; import io.muun.common.api.RealTimeFeesRequestJson; +import io.muun.common.api.SensorEventBatchJson; import io.muun.common.api.SetupChallengeResponse; import io.muun.common.api.StartEmailSetupJson; import io.muun.common.api.SubmarineSwapJson; @@ -51,7 +52,7 @@ import io.muun.common.api.UserInvoiceJson; import io.muun.common.api.UserJson; import io.muun.common.api.UserProfileJson; -import io.muun.common.api.VerifiableServerCosigningKeyJson; +import io.muun.common.api.VerifiableMuunKeyJson; import io.muun.common.api.beam.notification.NotificationReportJson; import io.muun.common.model.UserPreferences; import io.muun.common.model.VerificationType; @@ -146,8 +147,8 @@ Observable startChallengeSetup( @POST("user/challenge/setup/finish") Completable finishChallengeSetup(@Body ChallengeSetupVerifyJson challengeSetupVerifyJson); - @POST("user/challenge/setup/finish-with-cosigning-key") - Observable finishChallengeSetupWithCosigningKey( + @POST("user/challenge/setup/finish-with-verifiable-muun-key") + Observable finishChallengeSetupWithVerifiableMuunKey( @Body ChallengeSetupVerifyJson challengeSetupVerifyJson ); @@ -222,8 +223,8 @@ Observable finishPasswordChange( @POST("user/profile") Observable createProfile(@Body UserProfileJson userProfileJson); - @GET("user/verifiable-server-cosigning-key") - Observable getVerifiableServerCosigningKey(); + @GET("user/verifiable-muun-key") + Observable getVerifiableServerCosigningKey(); @POST("user/emergency-kit/exported") Observable reportEmergencyKitExported(@Body ExportEmergencyKitJson json); @@ -311,6 +312,9 @@ Completable fulfillIncomingSwap( @POST("integrity/check") Observable checkIntegrity(@Body IntegrityCheck request); + @POST("internal/save-sensor-event-batch") + Observable saveSensorEventBatch(@Body SensorEventBatchJson sensorEventBatchJson); + // --------------------------------------------------------------------------------------------- // Migrations: diff --git a/libwallet/app_provided_data/keys.go b/libwallet/app_provided_data/keys.go index 1874df44..72923178 100644 --- a/libwallet/app_provided_data/keys.go +++ b/libwallet/app_provided_data/keys.go @@ -5,8 +5,11 @@ type KeyData struct { Path string } +// Use keys.KeyProvider instead of KeyProvider to access keys. This is a low-level interface at the +// boundary with native code that should only be used within the keys.KeyProvider wrapper. type KeyProvider interface { FetchUserKey() (*KeyData, error) FetchMuunKey() (*KeyData, error) + FetchEncryptedMuunPrivateKey() (string, error) FetchMaxDerivedIndex() int } diff --git a/libwallet/challenge_keys.go b/libwallet/challenge_keys.go index bea0aebd..e3e57e7d 100644 --- a/libwallet/challenge_keys.go +++ b/libwallet/challenge_keys.go @@ -103,7 +103,7 @@ func (k *ChallengePrivateKey) DecryptKey(decodedInfo *EncryptedPrivateKeyInfo, n rawPrivKey := plaintext[0:32] rawChainCode := plaintext[32:] - privKey, err := NewHDPrivateKeyFromBytes(rawPrivKey, rawChainCode, network) + privKey, err := NewMasterHDPrivateKeyFromBytes(rawPrivKey, rawChainCode, network) if err != nil { return nil, fmt.Errorf("decrypting key: failed to parse key: %w", err) } diff --git a/libwallet/domain/model/bitcoin_hpke/bitcoin_hpke.go b/libwallet/cryptography/bitcoin_hpke/bitcoin_hpke.go similarity index 100% rename from libwallet/domain/model/bitcoin_hpke/bitcoin_hpke.go rename to libwallet/cryptography/bitcoin_hpke/bitcoin_hpke.go diff --git a/libwallet/domain/model/bitcoin_hpke/bitcoin_hpke_test.go b/libwallet/cryptography/bitcoin_hpke/bitcoin_hpke_test.go similarity index 100% rename from libwallet/domain/model/bitcoin_hpke/bitcoin_hpke_test.go rename to libwallet/cryptography/bitcoin_hpke/bitcoin_hpke_test.go diff --git a/libwallet/domain/model/bitcoin_hpke/encrypted_message.go b/libwallet/cryptography/bitcoin_hpke/encrypted_message.go similarity index 88% rename from libwallet/domain/model/bitcoin_hpke/encrypted_message.go rename to libwallet/cryptography/bitcoin_hpke/encrypted_message.go index e53332f3..e2ed8397 100644 --- a/libwallet/domain/model/bitcoin_hpke/encrypted_message.go +++ b/libwallet/cryptography/bitcoin_hpke/encrypted_message.go @@ -3,6 +3,7 @@ package bitcoin_hpke import ( "errors" "github.com/btcsuite/btcd/btcec/v2" + "golang.org/x/crypto/chacha20poly1305" "slices" ) @@ -38,6 +39,10 @@ func (encryptedMessage EncryptedMessage) GetCiphertext() []byte { return encryptedMessage.ciphertext } +func (encryptedMessage EncryptedMessage) PlaintextLengthInBytes() int { + return len(encryptedMessage.ciphertext) - chacha20poly1305.Overhead +} + // This is a companion to ParseEncryptedMessage that allows to know the length in bytes of an encrypted message. func SerializedEncryptedMessageLengthInBytes(plaintextLengthInBytes int) int { return encapsulatedKeyLengthInBytes + plaintextLengthInBytes + authenticationTagLengthInBytes diff --git a/libwallet/domain/model/bitcoin_hpke/i2osp.go b/libwallet/cryptography/bitcoin_hpke/i2osp.go similarity index 100% rename from libwallet/domain/model/bitcoin_hpke/i2osp.go rename to libwallet/cryptography/bitcoin_hpke/i2osp.go diff --git a/libwallet/domain/model/bitcoin_hpke/kem.go b/libwallet/cryptography/bitcoin_hpke/kem.go similarity index 100% rename from libwallet/domain/model/bitcoin_hpke/kem.go rename to libwallet/cryptography/bitcoin_hpke/kem.go diff --git a/libwallet/domain/model/bitcoin_hpke/labeled_hkdf.go b/libwallet/cryptography/bitcoin_hpke/labeled_hkdf.go similarity index 100% rename from libwallet/domain/model/bitcoin_hpke/labeled_hkdf.go rename to libwallet/cryptography/bitcoin_hpke/labeled_hkdf.go diff --git a/libwallet/data/keys/provider.go b/libwallet/data/keys/provider.go new file mode 100644 index 00000000..247d4558 --- /dev/null +++ b/libwallet/data/keys/provider.go @@ -0,0 +1,91 @@ +package keys + +import ( + "fmt" + "github.com/muun/libwallet" + "github.com/muun/libwallet/app_provided_data" +) + +// Provide keys. All keys are already derived at our usual base path "m/schema:1'/recovery:1'" +type KeyProvider interface { + UserPrivateKey() (*libwallet.HDPrivateKey, error) + UserPublicKey() (*libwallet.HDPublicKey, error) + MuunPublicKey() (*libwallet.HDPublicKey, error) + EncryptedMuunPrivateKey() (*libwallet.EncryptedPrivateKeyInfo, error) + MaxDerivedIndex() int +} + +type keyProvider struct { + keyProvider app_provided_data.KeyProvider + network libwallet.Network +} + +func NewKeyProvider(k app_provided_data.KeyProvider, network libwallet.Network) KeyProvider { + return &keyProvider{keyProvider: k, network: network} +} + +func (p *keyProvider) UserPrivateKey() (*libwallet.HDPrivateKey, error) { + userKeyData, err := p.keyProvider.FetchUserKey() + if err != nil { + return nil, err + } + + userPrivKey, err := libwallet.NewHDPrivateKeyFromString(userKeyData.Serialized, userKeyData.Path, &p.network) + if err != nil { + return nil, err + } + + return userPrivKey, nil +} + +func (p *keyProvider) UserPublicKey() (*libwallet.HDPublicKey, error) { + userPrivKey, err := p.UserPrivateKey() + if err != nil { + return nil, err + } + + return userPrivKey.PublicKey(), nil +} + +func (p *keyProvider) MuunPublicKey() (*libwallet.HDPublicKey, error) { + muunKeyData, err := p.keyProvider.FetchMuunKey() + if err != nil { + return nil, err + } + + muunKey, err := libwallet.NewHDPublicKeyFromString(muunKeyData.Serialized, muunKeyData.Path, &p.network) + if err != nil { + return nil, err + } + + return muunKey, nil +} + +func (p *keyProvider) EncryptedMuunPrivateKey() (*libwallet.EncryptedPrivateKeyInfo, error) { + encodedKeyData, err := p.keyProvider.FetchEncryptedMuunPrivateKey() + if err != nil { + return nil, err + } + + return libwallet.DecodeEncryptedPrivateKey(encodedKeyData) +} + +func (p *keyProvider) DecryptMuunPrivateKey(recoveryCode string, encryptedKey *libwallet.EncryptedPrivateKeyInfo, network *libwallet.Network) (*libwallet.DecryptedPrivateKey, error) { + salt := encryptedKey.Salt + decryptionKey, err := libwallet.RecoveryCodeToKey(recoveryCode, salt) + + if err != nil { + return nil, fmt.Errorf("failed to process recovery code: %w", err) + } + + decryptedKey, err := decryptionKey.DecryptKey(encryptedKey, network) + if err != nil { + return nil, fmt.Errorf("failed to decrypt ke: %w", err) + } + + return decryptedKey, nil +} + +func (p *keyProvider) MaxDerivedIndex() int { + return p.keyProvider.FetchMaxDerivedIndex() +} diff --git a/libwallet/domain/action/challenge_keys/finish_challenge_setup_action.go b/libwallet/domain/action/challenge_keys/finish_challenge_setup_action.go new file mode 100644 index 00000000..5a8faba8 --- /dev/null +++ b/libwallet/domain/action/challenge_keys/finish_challenge_setup_action.go @@ -0,0 +1,57 @@ +package challenge_keys + +import ( + "encoding/hex" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/muun/libwallet/domain/action/recovery" + "github.com/muun/libwallet/service" + "github.com/muun/libwallet/service/model" + "github.com/muun/libwallet/storage" + "log/slog" +) + +type FinishChallengeSetupAction struct { + houstonService service.HoustonService + keyValueStorage *storage.KeyValueStorage + computeAndStoreEncryptedMuunKey *recovery.ComputeAndStoreEncryptedMuunKeyAction +} + +func NewFinishChallengeSetupAction( + houstonService service.HoustonService, + keyValueStorage *storage.KeyValueStorage, + computeAndStoreEncryptedMuunKey *recovery.ComputeAndStoreEncryptedMuunKeyAction, +) *FinishChallengeSetupAction { + return &FinishChallengeSetupAction{ + houstonService, + keyValueStorage, + computeAndStoreEncryptedMuunKey, + } +} + +func (action *FinishChallengeSetupAction) Run(recoveryCodePublicKey *btcec.PublicKey) error { + + challengeSetupVerifyJson := model.ChallengeSetupVerifyJson{ + ChallengeType: "RECOVERY_CODE", + PublicKey: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), + } + + verifiableMuunKeyJson, err := action.houstonService.ChallengeSetupFinishWithVerifiableMuunKey( + challengeSetupVerifyJson, + ) + if err != nil { + return err + } + + // If an error occurs during verification we log it, but we do not return it. + err = action.computeAndStoreEncryptedMuunKey.Run( + recoveryCodePublicKey, + &verifiableMuunKeyJson, + ) + if err != nil { + slog.Error( + "An error occurred during encrypted muun key verification", + slog.Any("error", err), + ) + } + return nil +} diff --git a/libwallet/domain/action/challenge_keys/start_challenge_setup_action.go b/libwallet/domain/action/challenge_keys/start_challenge_setup_action.go index c05ee496..bd702c9d 100644 --- a/libwallet/domain/action/challenge_keys/start_challenge_setup_action.go +++ b/libwallet/domain/action/challenge_keys/start_challenge_setup_action.go @@ -6,10 +6,10 @@ import ( ) type StartChallengeSetupAction struct { - HoustonService *service.HoustonService + HoustonService service.HoustonService } -func NewStartChallengeSetupAction(houstonService *service.HoustonService) *StartChallengeSetupAction { +func NewStartChallengeSetupAction(houstonService service.HoustonService) *StartChallengeSetupAction { return &StartChallengeSetupAction{houstonService} } diff --git a/libwallet/domain/action/diagnostic_mode_reports/submit_diagnostic_action.go b/libwallet/domain/action/diagnostic_mode_reports/submit_diagnostic_action.go index 29e2e4bb..042e7983 100644 --- a/libwallet/domain/action/diagnostic_mode_reports/submit_diagnostic_action.go +++ b/libwallet/domain/action/diagnostic_mode_reports/submit_diagnostic_action.go @@ -6,10 +6,10 @@ import ( ) type SubmitDiagnosticAction struct { - houstonService *service.HoustonService + houstonService service.HoustonService } -func NewSubmitDiagnosticAction(service *service.HoustonService) *SubmitDiagnosticAction { +func NewSubmitDiagnosticAction(service service.HoustonService) *SubmitDiagnosticAction { return &SubmitDiagnosticAction{service} } diff --git a/libwallet/domain/action/nfc/pair_security_card_action_v2.go b/libwallet/domain/action/nfc/pair_security_card_action_v2.go new file mode 100644 index 00000000..f6a88273 --- /dev/null +++ b/libwallet/domain/action/nfc/pair_security_card_action_v2.go @@ -0,0 +1,103 @@ +package nfc + +import ( + "crypto/ecdh" + "crypto/rand" + "encoding/binary" + "encoding/hex" + "fmt" + "github.com/muun/libwallet/domain/model/security_card" + "github.com/muun/libwallet/domain/nfc" + "github.com/muun/libwallet/service" + "github.com/muun/libwallet/service/model" + "github.com/muun/libwallet/storage" +) + +type PairSecurityCardActionV2 struct { + keyValueStorage *storage.KeyValueStorage + muunCard *nfc.MuunCardV2 + houstonService service.HoustonService +} + +func NewPairSecurityCardActionV2( + storage *storage.KeyValueStorage, + muunCard *nfc.MuunCardV2, + houstonService service.HoustonService, +) *PairSecurityCardActionV2 { + return &PairSecurityCardActionV2{keyValueStorage: storage, muunCard: muunCard, houstonService: houstonService} +} + +func (ac *PairSecurityCardActionV2) Run() (*security_card.SecurityCardPaired, error) { + challengePair, err := ac.houstonService.ChallengeSecurityCardPair() + if err != nil { + return nil, fmt.Errorf("error requesting challenge to server: %w", err) + } + + serverPublicKey, err := hex.DecodeString(challengePair.ServerPublicKeyInHex) + if err != nil { + return nil, fmt.Errorf("error decoding server key: %w", err) + } + + clientPrivateKey, err := ecdh.P256().GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("error generating client private key: %w", err) + } + + clientPublicKey := clientPrivateKey.PublicKey().Bytes() + + pairingResponse, err := ac.muunCard.Pair(serverPublicKey, clientPublicKey) + if err != nil { + return nil, fmt.Errorf("error during pairing with card: %w", err) + } + + cardPublicKeyInHex := hex.EncodeToString(pairingResponse.CardPublicKey) + clientPublicKeyInHex := hex.EncodeToString(clientPublicKey) + macInHex := hex.EncodeToString(pairingResponse.MAC) + globalSignCardInHex := hex.EncodeToString(pairingResponse.GlobalSignature) + pairingSlot := int(binary.BigEndian.Uint16(pairingResponse.PairingSlot)) + + if pairingResponse.Metadata == nil { + return nil, fmt.Errorf("missing card metadata in pairing response") + } + metadata := mapToSecurityCardMetadataJson(pairingResponse.Metadata) + + registerSecurityCardJson := model.RegisterSecurityCardJson{ + CardPublicKeyInHex: cardPublicKeyInHex, + ClientPublicKeyInHex: clientPublicKeyInHex, + PairingSlot: pairingSlot, + Metadata: metadata, + MacInHex: macInHex, + GlobalSignCardInHex: globalSignCardInHex, + } + + registerSecurityResponse, err := ac.houstonService.RegisterSecurityCard(registerSecurityCardJson) + if err != nil { + return nil, fmt.Errorf("server error registering security card: %w", err) + } + + err = ac.keyValueStorage.Save(storage.KeySecurityCardPaired, true) + if err != nil { + return nil, fmt.Errorf("error storing paired status: %w", err) + } + + return service.MapSecurityCardPaired(registerSecurityResponse), nil +} + +func mapToSecurityCardMetadataJson(metadata *nfc.CardMetadata) model.SecurityCardMetadataJson { + globalPubCardInHex := hex.EncodeToString(metadata.GlobalPubCard[:]) + cardVendorInHex := hex.EncodeToString(metadata.CardVendor[:]) + cardModelInHex := hex.EncodeToString(metadata.CardModel[:]) + firmwareVersion := int(binary.BigEndian.Uint16(metadata.FirmwareVersion[:])) + languageCodeInHex := hex.EncodeToString(metadata.LanguageCode[:]) + + metadataJson := model.SecurityCardMetadataJson{ + GlobalPublicKeyInHex: globalPubCardInHex, + CardVendorInHex: cardVendorInHex, + CardModelInHex: cardModelInHex, + FirmwareVersion: firmwareVersion, + UsageCount: int(metadata.UsageCount), + LanguageCodeInHex: languageCodeInHex, + } + + return metadataJson +} diff --git a/libwallet/domain/action/recovery/broadcast_recovery_tx_action.go b/libwallet/domain/action/recovery/broadcast_recovery_tx_action.go new file mode 100644 index 00000000..ccd84951 --- /dev/null +++ b/libwallet/domain/action/recovery/broadcast_recovery_tx_action.go @@ -0,0 +1,43 @@ +package recovery + +import ( + "bytes" + "encoding/hex" + "fmt" + "github.com/btcsuite/btcd/wire" + "github.com/muun/libwallet/electrum" + "log/slog" +) + +type BroadcastRecoveryTxAction struct { + electrumProvider *electrum.ServerProvider +} + +func NewBroadcastRecoveryTxAction(electrumProvider *electrum.ServerProvider) *BroadcastRecoveryTxAction { + return &BroadcastRecoveryTxAction{ + electrumProvider: electrumProvider, + } +} + +func (s *BroadcastRecoveryTxAction) Run(tx *wire.MsgTx, log *slog.Logger) (string, error) { + client := electrum.NewClient(true, log) + + for !client.IsConnected() { + err := client.Connect(s.electrumProvider.NextServer()) + if err != nil { + return "", err + } + } + + // Encode the transaction for broadcast: + txBytes := new(bytes.Buffer) + + err := tx.BtcEncode(txBytes, wire.ProtocolVersion, wire.WitnessEncoding) + if err != nil { + return "", fmt.Errorf("error while encoding tx: %w", err) + } + + txHex := hex.EncodeToString(txBytes.Bytes()) + + return client.Broadcast(txHex) +} diff --git a/libwallet/domain/action/recovery/build_recovery_tx_action.go b/libwallet/domain/action/recovery/build_recovery_tx_action.go new file mode 100644 index 00000000..6e70ba6d --- /dev/null +++ b/libwallet/domain/action/recovery/build_recovery_tx_action.go @@ -0,0 +1,139 @@ +package recovery + +import ( + "encoding/hex" + "fmt" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/muun/libwallet" + "github.com/muun/libwallet/btcsuitew/txscriptw" + "github.com/muun/libwallet/data/keys" + "github.com/muun/libwallet/scanner" + "math" +) + +const dustThreshold = 546 + +type BuildSweepTxAction struct { + keyProvider keys.KeyProvider + network *libwallet.Network +} + +func NewBuildSweepTxAction(keyProvider keys.KeyProvider, network *libwallet.Network) *BuildSweepTxAction { + return &BuildSweepTxAction{ + keyProvider: keyProvider, + network: network, + } +} + +func (action *BuildSweepTxAction) Run(utxos []*scanner.Utxo, sweepAddress btcutil.Address, feeRateInSatsPerVByte float64) (*wire.MsgTx, error) { + value := int64(0) + + tx := wire.NewMsgTx(2) + for _, utxo := range utxos { + chainHash, err := chainhash.NewHashFromStr(utxo.TxID) + if err != nil { + return nil, err + } + + outpoint := wire.OutPoint{ + Hash: *chainHash, + Index: uint32(utxo.OutputIndex), + } + + tx.AddTxIn(wire.NewTxIn(&outpoint, []byte{}, [][]byte{})) + value += utxo.Amount + } + + script, err := txscriptw.PayToAddrScript(sweepAddress) + if err != nil { + return nil, err + } + + tx.AddTxOut(wire.NewTxOut(value, script)) // Note: No fees yet. + + fullSize := tx.SerializeSize() + witnessSize := fullSize - tx.SerializeSizeStripped() + // virtual size can be non-integer, so store it as float64 + virtualSize := float64(fullSize) + float64(witnessSize)/4 + finalFee := int64(math.Ceil(virtualSize * feeRateInSatsPerVByte)) + + if len(tx.TxOut) != 1 { + return nil, fmt.Errorf("expected 1 output, got %d", len(tx.TxOut)) + } + + if tx.TxOut[0].Value < finalFee { + return nil, fmt.Errorf("fees (%d sats) exceed total funds (%d sats)", finalFee, tx.TxOut[0].Value) + } + + // Reduce value by calculated fee. + tx.TxOut[0].Value -= finalFee + + if tx.TxOut[0].Value < dustThreshold { + return nil, fmt.Errorf("output is sub-dust (%d sats) after deducting fees (%d sats)", tx.TxOut[0].Value, finalFee) + } + + return tx, nil +} + +type input struct { + utxo *scanner.Utxo + muunSignature []byte +} + +func (i *input) OutPoint() libwallet.Outpoint { + return &outpoint{utxo: i.utxo} +} + +func (i *input) Address() libwallet.MuunAddress { + return i.utxo.Address +} + +func (i *input) UserSignature() []byte { + return []byte{} +} + +func (i *input) MuunSignature() []byte { + return i.muunSignature +} + +func (i *input) SubmarineSwapV1() libwallet.InputSubmarineSwapV1 { + return nil +} + +func (i *input) SubmarineSwapV2() libwallet.InputSubmarineSwapV2 { + return nil +} + +func (i *input) IncomingSwap() libwallet.InputIncomingSwap { + return nil +} + +func (i *input) MuunPublicNonce() []byte { + // Will always be nil in this context + // Look at coinV5.signFirstWith for reasons. + return nil +} + +// outpoint is a minimal type that implements libwallet.Outpoint +type outpoint struct { + utxo *scanner.Utxo +} + +func (o *outpoint) TxId() []byte { + raw, err := hex.DecodeString(o.utxo.TxID) + if err != nil { + panic(err) // we wrote this hex value ourselves, no input from anywhere else + } + + return raw +} + +func (o *outpoint) Index() int { + return o.utxo.OutputIndex +} + +func (o *outpoint) Amount() int64 { + return o.utxo.Amount +} diff --git a/libwallet/domain/action/recovery/compute_and_store_encrypted_muun_key_action.go b/libwallet/domain/action/recovery/compute_and_store_encrypted_muun_key_action.go new file mode 100644 index 00000000..577e8850 --- /dev/null +++ b/libwallet/domain/action/recovery/compute_and_store_encrypted_muun_key_action.go @@ -0,0 +1,78 @@ +package recovery + +import ( + "fmt" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/muun/libwallet/data/keys" + "github.com/muun/libwallet/domain/model/verifiable_muun_key" + "github.com/muun/libwallet/service/model" + "github.com/muun/libwallet/storage" + "log/slog" +) + +type ComputeAndStoreEncryptedMuunKeyAction struct { + keyValueStorage *storage.KeyValueStorage + keyProvider keys.KeyProvider +} + +func NewComputeAndStoreEncryptedMuunKeyAction( + keyValueStorage *storage.KeyValueStorage, + keyProvider keys.KeyProvider, +) *ComputeAndStoreEncryptedMuunKeyAction { + return &ComputeAndStoreEncryptedMuunKeyAction{ + keyValueStorage: keyValueStorage, + keyProvider: keyProvider, + } +} + +// Verify and store the resulting encrypted muun key. This action overwrites existing keys. +func (a *ComputeAndStoreEncryptedMuunKeyAction) Run( + recoveryCodePublicKey *btcec.PublicKey, + verifiableMuunKeyJson *model.VerifiableMuunKeyJson, +) error { + slog.Warn("ComputeAndStoreEncryptedMuunKeyAction.Run: start") + + userHDPrivateKey, err := a.keyProvider.UserPrivateKey() + if err != nil { + return fmt.Errorf("error getting user key from KeyProvider: %w", err) + } + + userEcPrivateKey, err := userHDPrivateKey.ECPrivateKey() + if err != nil { + return fmt.Errorf("error obtaining user ec private key: %w", err) + } + + muunHDPublicKey, err := a.keyProvider.MuunPublicKey() + if err != nil { + return fmt.Errorf("error obtaining muun key from KeyProvider: %w", err) + } + + verifiableMuunKey, err := verifiable_muun_key.VerifiableMuunKeyFromJson(verifiableMuunKeyJson) + if err != nil { + return err + } + + encryptedMuunKeyWithVerificationFlag, err := verifiableMuunKey.Verify( + muunHDPublicKey, + userEcPrivateKey, + recoveryCodePublicKey, + ) + if err != nil { + return err + } + + if encryptedMuunKeyWithVerificationFlag.Verified { + slog.Warn("ComputeAndStoreEncryptedMuunKeyAction.Run: store verified key") + + return a.keyValueStorage.Save( + storage.VerifiedEncryptedMuunKey, + encryptedMuunKeyWithVerificationFlag.EncryptedMuunKey) + } + + slog.Warn("ComputeAndStoreEncryptedMuunKeyAction.Run: store unverified key") + + return a.keyValueStorage.Save( + storage.UnverifiedEncryptedMuunKey, + encryptedMuunKeyWithVerificationFlag.EncryptedMuunKey, + ) +} diff --git a/libwallet/domain/action/recovery/get_encrypted_user_key_action.go b/libwallet/domain/action/recovery/get_encrypted_user_key_action.go new file mode 100644 index 00000000..d9d3f95c --- /dev/null +++ b/libwallet/domain/action/recovery/get_encrypted_user_key_action.go @@ -0,0 +1,54 @@ +package recovery + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/muun/libwallet/data/keys" + "github.com/muun/libwallet/domain/model/encrypted_key_v3" + "github.com/muun/libwallet/storage" +) + +type GetEncryptedUserKeyAction struct { + keyValueStorage *storage.KeyValueStorage + keyProvider keys.KeyProvider +} + +func NewGetEncryptedUserKeyAction( + keyValueStorage *storage.KeyValueStorage, + keyProvider keys.KeyProvider, +) *GetEncryptedUserKeyAction { + return &GetEncryptedUserKeyAction{ + keyValueStorage: keyValueStorage, + keyProvider: keyProvider, + } +} + +// Compute the user encrypted key. If we already computed it, it is retrieved from storage so that the contents of the +// emergency kit do not change every time. +func (a *GetEncryptedUserKeyAction) Run(recoveryCodePublicKey *btcec.PublicKey) (string, error) { + + userExtendedPrivateKey, err := a.keyProvider.UserPrivateKey() + if err != nil { + return "", err + } + + rawEncryptedKey, err := a.keyValueStorage.Get(storage.EncryptedUserKey) + if err != nil { + return "", err + } + + if rawEncryptedKey != nil { + return rawEncryptedKey.(string), nil + } + + encryptedKey, err := encrypted_key_v3.EncryptUserKey(userExtendedPrivateKey, recoveryCodePublicKey) + if err != nil { + return "", err + } + + err = a.keyValueStorage.Save(storage.EncryptedUserKey, encryptedKey) + if err != nil { + return "", err + } + + return encryptedKey, nil +} diff --git a/libwallet/domain/action/recovery/may_retrieve_encrypted_muun_key_action.go b/libwallet/domain/action/recovery/may_retrieve_encrypted_muun_key_action.go new file mode 100644 index 00000000..38e797cb --- /dev/null +++ b/libwallet/domain/action/recovery/may_retrieve_encrypted_muun_key_action.go @@ -0,0 +1,51 @@ +package recovery + +import ( + "github.com/muun/libwallet/storage" +) + +type EncryptedMuunKeyStatus int + +const ( + HasVerifiedEncryptedMuunKey EncryptedMuunKeyStatus = iota + OnlyHasUnverifiedEncryptedMuunKey + HasNoEncryptedMuunKey +) + +type MayRetrieveEncryptedMuunKeyAction struct { + keyValueStorage *storage.KeyValueStorage +} + +type EncryptedMuunKeyWithStatus struct { + EncryptedMuunKey *string + Status EncryptedMuunKeyStatus +} + +func NewMayRetrieveEncryptedMuunKeyAction( + keyValueStorage *storage.KeyValueStorage, +) *MayRetrieveEncryptedMuunKeyAction { + return &MayRetrieveEncryptedMuunKeyAction{keyValueStorage: keyValueStorage} +} + +// Try to retrieve the encrypted Muun key from the key value storage, +// without incurring a Houston API call if it is not found in storage. +func (a *MayRetrieveEncryptedMuunKeyAction) Run() (*EncryptedMuunKeyWithStatus, error) { + keys, err := a.keyValueStorage.GetBatch([]string{ + storage.UnverifiedEncryptedMuunKey, + storage.VerifiedEncryptedMuunKey, + }) + + if err != nil { + return nil, err + } + + if key, ok := keys[storage.VerifiedEncryptedMuunKey]; ok && key != nil { + encryptedMuunKey := key.(string) + return &EncryptedMuunKeyWithStatus{EncryptedMuunKey: &encryptedMuunKey, Status: HasVerifiedEncryptedMuunKey}, nil + } else if key, ok := keys[storage.UnverifiedEncryptedMuunKey]; ok && key != nil { + encryptedMuunKey := key.(string) + return &EncryptedMuunKeyWithStatus{EncryptedMuunKey: &encryptedMuunKey, Status: OnlyHasUnverifiedEncryptedMuunKey}, nil + } else { + return &EncryptedMuunKeyWithStatus{EncryptedMuunKey: nil, Status: HasNoEncryptedMuunKey}, nil + } +} diff --git a/libwallet/domain/action/recovery/populate_encrypted_muun_key_action.go b/libwallet/domain/action/recovery/populate_encrypted_muun_key_action.go new file mode 100644 index 00000000..873cccfa --- /dev/null +++ b/libwallet/domain/action/recovery/populate_encrypted_muun_key_action.go @@ -0,0 +1,112 @@ +package recovery + +import ( + "fmt" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/muun/libwallet/data/keys" + "github.com/muun/libwallet/domain/model/verifiable_muun_key" + "github.com/muun/libwallet/service" + "github.com/muun/libwallet/storage" + "log/slog" +) + +type PopulateEncryptedMuunKeyAction struct { + houstonService service.HoustonService + keyValueStorage *storage.KeyValueStorage + keyProvider keys.KeyProvider + mayRetrieveEncryptedMuunKey *MayRetrieveEncryptedMuunKeyAction +} + +func NewPopulateEncryptedMuunKeyAction( + houstonService service.HoustonService, + keyValueStorage *storage.KeyValueStorage, + keyProvider keys.KeyProvider, +) *PopulateEncryptedMuunKeyAction { + return &PopulateEncryptedMuunKeyAction{ + houstonService: houstonService, + keyValueStorage: keyValueStorage, + keyProvider: keyProvider, + mayRetrieveEncryptedMuunKey: NewMayRetrieveEncryptedMuunKeyAction(keyValueStorage), + } +} + +// Populate the encrypted muun key in storage. If we already have an unverified muun key in +// storage, go to houston and try to get a key that can be verified. This action does not overwrite +// existing keys. +func (a *PopulateEncryptedMuunKeyAction) Run(recoveryCodePublicKey *btcec.PublicKey) error { + slog.Warn("PopulateEncryptedMuunKeyAction.Run: start") + + userHDPrivateKey, err := a.keyProvider.UserPrivateKey() + if err != nil { + return fmt.Errorf("error getting user key from KeyProvider: %w", err) + } + + userEcPrivateKey, err := userHDPrivateKey.ECPrivateKey() + if err != nil { + return fmt.Errorf("error obtaining user ec private key: %w", err) + } + + muunHDPublicKey, err := a.keyProvider.MuunPublicKey() + if err != nil { + return fmt.Errorf("error obtaining muun key from KeyProvider: %w", err) + } + + currentStatus, err := a.getCurrentStatus() + if err != nil { + return err + } + + if *currentStatus == HasVerifiedEncryptedMuunKey { + // TODO remove this log once it is not necessary anymore + slog.Warn("PopulateEncryptedMuunKeyAction.Run: verified key is present, return early") + return nil + } + + // we proceed, hoping to obtain a verified key + verifiableMuunKeyJson, err := a.houstonService.VerifiableMuunKey() + if err != nil { + return err + } + + verifiableMuunKey, err := verifiable_muun_key.VerifiableMuunKeyFromJson(&verifiableMuunKeyJson) + if err != nil { + return err + } + + encryptedMuunKeyWithVerificationFlag, err := verifiableMuunKey.Verify( + muunHDPublicKey, + userEcPrivateKey, + recoveryCodePublicKey, + ) + if err != nil { + return err + } + + if encryptedMuunKeyWithVerificationFlag.Verified { + slog.Warn("PopulateEncryptedMuunKeyAction.Run: store verified key") + return a.keyValueStorage.Save( + storage.VerifiedEncryptedMuunKey, + encryptedMuunKeyWithVerificationFlag.EncryptedMuunKey) + } + + if *currentStatus == OnlyHasUnverifiedEncryptedMuunKey { + // Do not overwrite the existing unverified key + slog.Warn("PopulateEncryptedMuunKeyAction.Run: unverified key is present, return") + return nil + } + + slog.Warn("PopulateEncryptedMuunKeyAction.Run: store unverified key") + return a.keyValueStorage.Save( + storage.UnverifiedEncryptedMuunKey, + encryptedMuunKeyWithVerificationFlag.EncryptedMuunKey, + ) +} + +func (a *PopulateEncryptedMuunKeyAction) getCurrentStatus() (*EncryptedMuunKeyStatus, error) { + encryptedMuunKeyWithStatus, err := a.mayRetrieveEncryptedMuunKey.Run() + if err != nil { + return nil, err + } + + return &encryptedMuunKeyWithStatus.Status, nil +} diff --git a/libwallet/domain/action/recovery/scan_for_funds_action.go b/libwallet/domain/action/recovery/scan_for_funds_action.go new file mode 100644 index 00000000..d1174b40 --- /dev/null +++ b/libwallet/domain/action/recovery/scan_for_funds_action.go @@ -0,0 +1,61 @@ +package recovery + +import ( + "fmt" + "github.com/muun/libwallet" + "github.com/muun/libwallet/data/keys" + "github.com/muun/libwallet/electrum" + "github.com/muun/libwallet/scanner" + "log/slog" +) + +type ScanForFundsAction struct { + keyProvider keys.KeyProvider + electrumProvider *electrum.ServerProvider + network *libwallet.Network +} + +func NewScanForFundsAction(keyProvider keys.KeyProvider, electrumProvider *electrum.ServerProvider, network *libwallet.Network) *ScanForFundsAction { + return &ScanForFundsAction{ + keyProvider: keyProvider, + electrumProvider: electrumProvider, + network: network, + } +} + +func (action *ScanForFundsAction) Run(logger *slog.Logger) (<-chan *scanner.Report, error) { + addresses, err := generateAddresses(action.keyProvider) + if err != nil { + return nil, err + } + + const electrumPoolSize = 8 + connectionPool := electrum.NewPool(electrumPoolSize, true, logger) + + utxoScanner := scanner.NewScanner(connectionPool, action.electrumProvider, action.network.ToParams()) + + return utxoScanner.Scan(addresses), nil +} + +// generates a list of addresses to recover +func generateAddresses(keyProvider keys.KeyProvider) (chan libwallet.MuunAddress, error) { + userPubKey, err := keyProvider.UserPublicKey() + if err != nil { + return nil, err + } + + muunKey, err := keyProvider.MuunPublicKey() + if err != nil { + return nil, err + } + + addrGen := scanner.NewAddressGenerator(userPubKey, muunKey, false) + + maxIndex := keyProvider.MaxDerivedIndex() + if maxIndex == 0 { + return nil, fmt.Errorf("cannot generate 0 addresses") + } + addresses := addrGen.Stream(int64(maxIndex)) + + return addresses, nil +} diff --git a/libwallet/domain/action/recovery/sign_recovery_tx_action.go b/libwallet/domain/action/recovery/sign_recovery_tx_action.go new file mode 100644 index 00000000..e993f4cf --- /dev/null +++ b/libwallet/domain/action/recovery/sign_recovery_tx_action.go @@ -0,0 +1,113 @@ +package recovery + +import ( + "bytes" + "fmt" + "github.com/btcsuite/btcd/wire" + "github.com/muun/libwallet" + "github.com/muun/libwallet/data/keys" + "github.com/muun/libwallet/scanner" +) + +type SignSweepTxAction struct { + keyProvider keys.KeyProvider + network *libwallet.Network +} + +func NewSignSweepTxAction(keyProvider keys.KeyProvider, network *libwallet.Network) *SignSweepTxAction { + return &SignSweepTxAction{ + keyProvider: keyProvider, + network: network, + } +} + +func (action *SignSweepTxAction) Run(utxos []*scanner.Utxo, tx *wire.MsgTx, recoveryCode string) (*wire.MsgTx, error) { + userPrivateKey, err := action.keyProvider.UserPrivateKey() + if err != nil { + return nil, err + } + + muunPrivateKey, err := action.fetchMuunPrivateKey(recoveryCode) + if err != nil { + return nil, err + } + + return buildSignedSweepTx(utxos, tx, userPrivateKey, muunPrivateKey) +} + +func (action *SignSweepTxAction) fetchMuunPrivateKey(recoveryCode string) (*libwallet.HDPrivateKey, error) { + encryptedKeyData, err := action.keyProvider.EncryptedMuunPrivateKey() + if err != nil { + return nil, err + } + + muunKeyData, err := decryptKeys(encryptedKeyData, recoveryCode, action.network) + if err != nil { + return nil, err + } + + return muunKeyData.Key, nil +} + +func decryptKeys(encryptedKey *libwallet.EncryptedPrivateKeyInfo, recoveryCode string, network *libwallet.Network) (*libwallet.DecryptedPrivateKey, error) { + decryptionKey, err := libwallet.RecoveryCodeToKey(recoveryCode, encryptedKey.Salt) + + if err != nil { + return nil, fmt.Errorf("failed to process recovery code: %w", err) + } + + decryptedKey, err := decryptionKey.DecryptKey(encryptedKey, network) + if err != nil { + return nil, fmt.Errorf("failed to decrypt key: %w", err) + } + + return decryptedKey, nil +} + +func buildSignedSweepTx(utxos []*scanner.Utxo, unsignedSweepTx *wire.MsgTx, userKey *libwallet.HDPrivateKey, muunKey *libwallet.HDPrivateKey) (*wire.MsgTx, error) { + inputList := &libwallet.InputList{} + userNonces := libwallet.EmptyMusigNonces() + + for _, utxo := range utxos { + inputList.Add(&input{ + utxo, + []byte{}, + }) + + // generate the user nonce for the user signing key + key, err := userKey.DeriveTo(utxo.Address.DerivationPath()) + if err != nil { + return nil, err + } + _, err = userNonces.GenerateNonce(utxo.Address.Version(), key.PublicKey().Raw()) + if err != nil { + return nil, err + } + } + + writer := &bytes.Buffer{} + err := unsignedSweepTx.Serialize(writer) + if err != nil { + return nil, err + } + + sweepTxBytes := writer.Bytes() + + pstx, err := libwallet.NewPartiallySignedTransaction(inputList, sweepTxBytes, userNonces) + if err != nil { + return nil, err + } + + signedTx, err := pstx.FullySign(userKey, muunKey) + if err != nil { + return nil, err + } + + wireTx := wire.NewMsgTx(0) + err = wireTx.BtcDecode(bytes.NewReader(signedTx.Bytes), 0, wire.WitnessEncoding) + if err != nil { + return nil, err + } + + return wireTx, nil +} diff --git a/libwallet/domain/diagnostic_mode/address_scanner.go b/libwallet/domain/diagnostic_mode/address_scanner.go deleted file mode 100644 index 4ddd635d..00000000 --- a/libwallet/domain/diagnostic_mode/address_scanner.go +++ /dev/null @@ -1,83 +0,0 @@ -package diagnostic_mode - -import ( - "bytes" - "fmt" - "github.com/muun/libwallet" - "github.com/muun/libwallet/app_provided_data" - "github.com/muun/libwallet/electrum" - "github.com/muun/libwallet/scanner" - "log/slog" -) - -type DiagnosticSessionData struct { - Id string - DebugLog *bytes.Buffer -} - -var diagnosticData = make(map[string]*DiagnosticSessionData) - -func AddDiagnosticSession(data *DiagnosticSessionData) error { - if _, ok := diagnosticData[data.Id]; ok { - return fmt.Errorf("id %s already exists", data.Id) - } - - diagnosticData[data.Id] = data - return nil -} - -func GetDiagnosticSession(id string) (*DiagnosticSessionData, bool) { - result, ok := diagnosticData[id] - return result, ok -} - -func DeleteDiagnosticSession(id string) { - delete(diagnosticData, id) -} - -// ScanAddresses creates a stream of reports for the generated addresses -func ScanAddresses(keyProvider app_provided_data.KeyProvider, electrumProvider *electrum.ServerProvider, network *libwallet.Network, logger *slog.Logger) (<-chan *scanner.Report, error) { - addresses, err := generateAddresses(keyProvider, network) - if err != nil { - return nil, err - } - - const electrumPoolSize = 8 - connectionPool := electrum.NewPool(electrumPoolSize, true, logger) - - utxoScanner := scanner.NewScanner(connectionPool, electrumProvider, network.ToParams()) - - return utxoScanner.Scan(addresses), nil -} - -// generates a list of addresses to recover -func generateAddresses(keysProvider app_provided_data.KeyProvider, network *libwallet.Network) (chan libwallet.MuunAddress, error) { - userKeyData, err := keysProvider.FetchUserKey() - if err != nil { - return nil, err - } - userPrivKey, err := libwallet.NewHDPrivateKeyFromString(userKeyData.Serialized, userKeyData.Path, network) - if err != nil { - return nil, err - } - userPubKey := userPrivKey.PublicKey() - - muunKeyData, err := keysProvider.FetchMuunKey() - if err != nil { - return nil, err - } - muunKey, err := libwallet.NewHDPublicKeyFromString(muunKeyData.Serialized, muunKeyData.Path, network) - if err != nil { - return nil, err - } - - addrGen := scanner.NewAddressGenerator(userPubKey, muunKey, false) - - maxIndex := keysProvider.FetchMaxDerivedIndex() - if maxIndex == 0 { - return nil, fmt.Errorf("cannot generate 0 addresses") - } - addresses := addrGen.Stream(int64(maxIndex)) - - return addresses, nil -} diff --git a/libwallet/domain/diagnostic_mode/diagnostic_session_data.go b/libwallet/domain/diagnostic_mode/diagnostic_session_data.go new file mode 100644 index 00000000..0e001111 --- /dev/null +++ b/libwallet/domain/diagnostic_mode/diagnostic_session_data.go @@ -0,0 +1,37 @@ +package diagnostic_mode + +import ( + "bytes" + "fmt" + "github.com/btcsuite/btcd/wire" + "github.com/muun/libwallet/scanner" + "log/slog" +) + +type DiagnosticSessionData struct { + Id string + LogBuffer *bytes.Buffer + Logger *slog.Logger + LastScanReport *scanner.Report + SweepTx *wire.MsgTx +} + +var diagnosticData = make(map[string]*DiagnosticSessionData) + +func AddDiagnosticSession(data *DiagnosticSessionData) error { + if _, ok := diagnosticData[data.Id]; ok { + return fmt.Errorf("id %s already exists", data.Id) + } + + diagnosticData[data.Id] = data + return nil +} + +func GetDiagnosticSession(id string) (*DiagnosticSessionData, bool) { + result, ok := diagnosticData[id] + return result, ok +} + +func DeleteDiagnosticSession(id string) { + delete(diagnosticData, id) +} diff --git a/libwallet/domain/model/encrypted_key_v3/domain_separation.go b/libwallet/domain/model/encrypted_key_v3/domain_separation.go new file mode 100644 index 00000000..8cf31464 --- /dev/null +++ b/libwallet/domain/model/encrypted_key_v3/domain_separation.go @@ -0,0 +1,16 @@ +package encrypted_key_v3 + +const ( + userFirstHalfToRecoveryCode = "muun.com/cosigning-key/1/1/recovery-code" + userSecondHalfToRecoveryCode = "muun.com/cosigning-key/1/2/recovery-code" + muunFirstHalfToRecoveryCode = "muun.com/cosigning-key/2/1/recovery-code" + muunSecondHalfToRecoveryCode = "muun.com/cosigning-key/2/2/recovery-code" + MuunFirstHalfToClient = "muun.com/cosigning-key/2/1/client" +) + +type keyBearer uint8 + +const ( + user keyBearer = iota + 1 + muun +) diff --git a/libwallet/domain/model/encrypted_key_v3/encrypted_key.go b/libwallet/domain/model/encrypted_key_v3/encrypted_key.go new file mode 100644 index 00000000..9c3b3419 --- /dev/null +++ b/libwallet/domain/model/encrypted_key_v3/encrypted_key.go @@ -0,0 +1,229 @@ +package encrypted_key_v3 + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/muun/libwallet" + "github.com/muun/libwallet/cryptography/bitcoin_hpke" + "slices" +) + +const ( + version = 3 + privateKeyLenBytes = btcec.PrivKeyBytesLen + chainCodeLenBytes = 32 +) + +func FinishMuunKeyEncryption( + recoveryCodePublicKey *btcec.PublicKey, + firstHalfKey *btcec.PrivateKey, + chainCode []byte, + secondHalfKeyEncryptedToRecoveryCode *bitcoin_hpke.EncryptedMessage, +) (string, error) { + + if len(chainCode) != chainCodeLenBytes { + return "", errors.New("chain code length must be exactly 32 bytes") + } + + firstEncryptedMessage, err := bitcoin_hpke.SingleShotEncrypt( + slices.Concat(firstHalfKey.Serialize(), chainCode[:]), + recoveryCodePublicKey, + []byte(muunFirstHalfToRecoveryCode), + []byte(""), + ) + + if err != nil { + return "", fmt.Errorf("encrypt extended key: failed for first encrypted message: %w", err) + } + + k := encryptedKey{ + version, + muun, + firstEncryptedMessage, + secondHalfKeyEncryptedToRecoveryCode, + } + + return k.serialize(), nil +} + +func EncryptUserKey(userPrivateKey *libwallet.HDPrivateKey, recoveryCodePublicKey *btcec.PublicKey) (string, error) { + + privateKey, err := userPrivateKey.ECPrivateKey() + if err != nil { + return "", fmt.Errorf("encrypt extended key: failed to extract private key %w", err) + } + + a, err := btcec.NewPrivateKey() + if err != nil { + return "", fmt.Errorf("encrypt extended key: failed to create new private key %w", err) + } + b := new(btcec.ModNScalar).Set(&a.Key).Negate().Add(&privateKey.Key).Bytes() + + firstEncryptedMessage, err := bitcoin_hpke.SingleShotEncrypt( + slices.Concat(a.Serialize(), userPrivateKey.ChainCode()), + recoveryCodePublicKey, + []byte(userFirstHalfToRecoveryCode), + []byte(""), + ) + if err != nil { + return "", fmt.Errorf("encrypt extended key: failed for first encrypted message: %w", err) + } + + secondEncryptedMessage, err := bitcoin_hpke.SingleShotEncrypt( + b[:], + recoveryCodePublicKey, + []byte(userSecondHalfToRecoveryCode), + []byte(""), + ) + if err != nil { + return "", fmt.Errorf("encrypt extended key: failed for second encrypted message: %w", err) + } + + ek := encryptedKey{ + version, + user, + firstEncryptedMessage, + secondEncryptedMessage, + } + + return ek.serialize(), nil +} + +func DecryptExtendedKey(recoveryCodePrivateKey *btcec.PrivateKey, encryptedExtendedKey string, network *libwallet.Network) (*libwallet.HDPrivateKey, error) { + + key, err := deserializeEncryptedKeyV3(encryptedExtendedKey) + if err != nil { + return nil, err + } + + var firstHalfKey, secondHalfKey btcec.ModNScalar + + var infoForFirstHalf string + if key.bearer == user { + infoForFirstHalf = userFirstHalfToRecoveryCode + } else { + infoForFirstHalf = muunFirstHalfToRecoveryCode + } + firstMessage, err := key.firstEncryptedMessage.SingleShotDecrypt( + recoveryCodePrivateKey, + []byte(infoForFirstHalf), + []byte(""), + ) + if err != nil { + return nil, err + } + firstHalfKey.SetByteSlice(firstMessage[:privateKeyLenBytes]) + chainCode := firstMessage[privateKeyLenBytes:] + + var infoForSecondHalf string + if key.bearer == user { + infoForSecondHalf = userSecondHalfToRecoveryCode + } else { + infoForSecondHalf = muunSecondHalfToRecoveryCode + } + secondMessage, err := key.secondEncryptedMessage.SingleShotDecrypt( + recoveryCodePrivateKey, + []byte(infoForSecondHalf), + []byte(""), + ) + if err != nil { + return nil, err + } + secondHalfKey.SetByteSlice(secondMessage) + + privateKey := firstHalfKey.Add(&secondHalfKey).Bytes() + + return libwallet.NewBasePathHDPrivateKeyFromBytes(privateKey[:], chainCode, network) +} + +type encryptedKey struct { + version uint8 + bearer keyBearer + firstEncryptedMessage *bitcoin_hpke.EncryptedMessage + secondEncryptedMessage *bitcoin_hpke.EncryptedMessage +} + +func (key *encryptedKey) serialize() string { + keyBytes := slices.Concat( + []byte{key.version}, + []byte{uint8(key.bearer)}, + key.firstEncryptedMessage.Serialize(), + key.secondEncryptedMessage.Serialize(), + ) + + return base64.StdEncoding.EncodeToString(keyBytes) +} + +func deserializeEncryptedKeyV3(serializedKey string) (*encryptedKey, error) { + + serializedKeyBytes, err := base64.StdEncoding.DecodeString(serializedKey) + if err != nil { + return nil, fmt.Errorf("decrypting key: failed to decode from base64 %w", err) + } + reader := bytes.NewReader(serializedKeyBytes) + + versionByte, err := reader.ReadByte() + if err != nil { + return nil, fmt.Errorf("decrypting key: failed to read version %w", err) + } + + if versionByte != 3 { + return nil, fmt.Errorf("decrypting key: expected a v3 key, version byte indicates v%d", versionByte) + } + + bearerByte, err := reader.ReadByte() + if err != nil { + return nil, fmt.Errorf("decrypting key: failed to read bearer %w", err) + } + + bearer, err := validateBearerByte(bearerByte) + if err != nil { + return nil, err + } + + firstEncryptedMessageLenInBytes := bitcoin_hpke.SerializedEncryptedMessageLengthInBytes(privateKeyLenBytes + chainCodeLenBytes) + firstEncryptedMessageBytes := make([]byte, firstEncryptedMessageLenInBytes) + n, err := reader.Read(firstEncryptedMessageBytes[:]) + if err != nil || n != firstEncryptedMessageLenInBytes { + return nil, fmt.Errorf("decrypting key: failed to read firstEncryptedMessage %w", err) + } + firstEncryptedMessage, err := bitcoin_hpke.ParseEncryptedMessage(firstEncryptedMessageBytes[:]) + if err != nil { + return nil, fmt.Errorf("decrypting key: failed to parse firstEncryptedMessage %w", err) + } + + secondEncryptedMessageLenInBytes := bitcoin_hpke.SerializedEncryptedMessageLengthInBytes(privateKeyLenBytes) + secondEncryptedMessageBytes := make([]byte, secondEncryptedMessageLenInBytes) + n, err = reader.Read(secondEncryptedMessageBytes[:]) + if err != nil || n != secondEncryptedMessageLenInBytes { + return nil, fmt.Errorf("decrypting key: failed to read secondEncryptedMessage %w", err) + } + secondEncryptedMessage, err := bitcoin_hpke.ParseEncryptedMessage(secondEncryptedMessageBytes[:]) + if err != nil { + return nil, fmt.Errorf("decrypting key: failed to parse secondEncryptedMessage %w", err) + } + + if reader.Len() > 0 { + return nil, errors.New("decrypting key: key is longer than expected") + } + + return &encryptedKey{ + versionByte, + bearer, + firstEncryptedMessage, + secondEncryptedMessage, + }, nil +} + +func validateBearerByte(bearerByte uint8) (keyBearer, error) { + bearer := keyBearer(bearerByte) + switch bearer { + case user, muun: + return bearer, nil + default: + return bearer, fmt.Errorf("invalid value for key bearer byte: %d", bearerByte) + } +} diff --git a/libwallet/domain/model/encrypted_key_v3/encrypted_key_test.go b/libwallet/domain/model/encrypted_key_v3/encrypted_key_test.go new file mode 100644 index 00000000..70cfdd32 --- /dev/null +++ b/libwallet/domain/model/encrypted_key_v3/encrypted_key_test.go @@ -0,0 +1,122 @@ +package encrypted_key_v3 + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/muun/libwallet" + "github.com/muun/libwallet/cryptography/bitcoin_hpke" + "github.com/muun/libwallet/recoverycode" + "testing" +) + +func TestFinishMuunKeyEncryption(t *testing.T) { + + recoveryCode := recoverycode.Generate() + + recoveryCodePrivateKey, err := recoverycode.ConvertToKey(recoveryCode, "") + if err != nil { + t.Fatal(err) + } + recoveryCodePublicKey := recoveryCodePrivateKey.PubKey() + + muunKey, err := libwallet.NewHDPrivateKey(randomBytes(32), libwallet.Mainnet()) + if err != nil { + t.Fatal(err) + } + + // simulate splitting Muun key and encrypting second half to RC + + firstHalfKey, err := btcec.NewPrivateKey() + if err != nil { + t.Fatal(err) + } + muunECPrivateKey, err := muunKey.ECPrivateKey() + if err != nil { + t.Fatal(err) + } + + secondHalfKeyBytes := new(btcec.ModNScalar).Set(&firstHalfKey.Key).Negate().Add(&muunECPrivateKey.Key).Bytes() + secondHalfKeyEncryptedToRecoveryCode, err := bitcoin_hpke.SingleShotEncrypt( + secondHalfKeyBytes[:], + recoveryCodePublicKey, + []byte(muunSecondHalfToRecoveryCode), + []byte(""), + ) + if err != nil { + t.Fatal(err) + } + + // Now test FinishMuunKeyEncryption + encryptedMuunKey, err := FinishMuunKeyEncryption(recoveryCodePublicKey, firstHalfKey, muunKey.ChainCode(), secondHalfKeyEncryptedToRecoveryCode) + if err != nil { + t.Fatal(err) + } + + decryptedMuunKey, err := DecryptExtendedKey(recoveryCodePrivateKey, encryptedMuunKey, libwallet.Mainnet()) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(decryptedMuunKey.PublicKey().Raw(), muunKey.PublicKey().Raw()) { + t.Fatal("decrypted public key does not match original public key") + } + + if !bytes.Equal(decryptedMuunKey.ChainCode(), muunKey.ChainCode()) { + t.Fatal("decrypted chain code does not match original chain code") + } +} + +func TestEncryptUserKey(t *testing.T) { + + recoveryCode := recoverycode.Generate() + recoveryCodePrivateKey, err := recoverycode.ConvertToKey(recoveryCode, "") + if err != nil { + t.Fatal(err) + } + recoveryCodePublicKey := recoveryCodePrivateKey.PubKey() + + userKey, err := libwallet.NewHDPrivateKey(randomBytes(32), libwallet.Mainnet()) + if err != nil { + t.Fatal(err) + } + + encryptedUserKey, err := EncryptUserKey(userKey, recoveryCodePublicKey) + if err != nil { + t.Fatal(err) + } + + decryptedUserKey, err := DecryptExtendedKey(recoveryCodePrivateKey, encryptedUserKey, libwallet.Mainnet()) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(decryptedUserKey.PublicKey().Raw(), userKey.PublicKey().Raw()) { + t.Fatal("decrypted public key does not match original public key") + } + + if !bytes.Equal(decryptedUserKey.ChainCode(), userKey.ChainCode()) { + t.Fatal("decrypted chain code does not match original chain code") + } +} + +func TestDeserializationFailsDueToWrongVersion(t *testing.T) { + bs := []byte{2, 0, 1} + key := base64.StdEncoding.EncodeToString(bs) + _, err := deserializeEncryptedKeyV3(key) + if err.Error() != "decrypting key: expected a v3 key, version byte indicates v2" { + t.Fatal("Expected to fail due to unexpected muun key version") + } +} + +func randomBytes(count int) []byte { + + buf := make([]byte, count) + _, err := rand.Read(buf) + if err != nil { + panic("couldn't read random bytes") + } + + return buf +} diff --git a/libwallet/domain/model/security_card/security_card_metadata.go b/libwallet/domain/model/security_card/security_card_metadata.go new file mode 100644 index 00000000..eb59afda --- /dev/null +++ b/libwallet/domain/model/security_card/security_card_metadata.go @@ -0,0 +1,10 @@ +package security_card + +type SecurityCardMetadata struct { + GlobalPublicKeyInHex string + CardVendorInHex string + CardModelInHex string + FirmwareVersion int + UsageCount int + LanguageCodeInHex string +} diff --git a/libwallet/domain/model/security_card/security_card_paired.go b/libwallet/domain/model/security_card/security_card_paired.go new file mode 100644 index 00000000..40701121 --- /dev/null +++ b/libwallet/domain/model/security_card/security_card_paired.go @@ -0,0 +1,7 @@ +package security_card + +type SecurityCardPaired struct { + Metadata *SecurityCardMetadata + IsKnownProvider bool + IsCardAlreadyUsed bool +} diff --git a/libwallet/domain/model/verifiable_muun_key/verifiable_muun_key.go b/libwallet/domain/model/verifiable_muun_key/verifiable_muun_key.go new file mode 100644 index 00000000..1c88b7b8 --- /dev/null +++ b/libwallet/domain/model/verifiable_muun_key/verifiable_muun_key.go @@ -0,0 +1,170 @@ +package verifiable_muun_key + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "log/slog" + "math/big" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/muun/libwallet" + "github.com/muun/libwallet/cryptography/bitcoin_hpke" + "github.com/muun/libwallet/domain/model/encrypted_key_v3" + "github.com/muun/libwallet/encryption" + "github.com/muun/libwallet/service/model" +) + +type VerifiableMuunKey struct { + FirstHalfKeyEncryptedToClient *bitcoin_hpke.EncryptedMessage + SecondHalfKeyEncryptedToRecoveryCode *bitcoin_hpke.EncryptedMessage + Proof *string +} + +func VerifiableMuunKeyFromJson(verifiableMuunKeyJson *model.VerifiableMuunKeyJson) (*VerifiableMuunKey, error) { + + firstHalfKeyEncryptedToClientBytes, err := hex.DecodeString(verifiableMuunKeyJson.FirstHalfKeyEncryptedToClient) + if err != nil { + return nil, err + } + + firstHalfKeyEncryptedToClient, err := bitcoin_hpke.ParseEncryptedMessage(firstHalfKeyEncryptedToClientBytes) + if err != nil { + return nil, err + } + + secondHalfKeyEncryptedToRecoveryCodeBytes, err := hex.DecodeString(verifiableMuunKeyJson.SecondHalfKeyEncryptedToRecoveryCode) + if err != nil { + return nil, err + } + + secondHalfKeyEncryptedToRecoveryCode, err := bitcoin_hpke.ParseEncryptedMessage(secondHalfKeyEncryptedToRecoveryCodeBytes) + if err != nil { + return nil, err + } + + return &VerifiableMuunKey{ + FirstHalfKeyEncryptedToClient: firstHalfKeyEncryptedToClient, + SecondHalfKeyEncryptedToRecoveryCode: secondHalfKeyEncryptedToRecoveryCode, + Proof: verifiableMuunKeyJson.Proof, + }, nil + +} + +type EncryptedMuunKeyWithVerificationFlag struct { + // The base64 encoded encrypted muun key that can be decrypted with the recovery code private key. + EncryptedMuunKey string + // A boolean value indicating if the encryption was proven to be correct with a zero-knowledge proof. + Verified bool +} + +// Verify returning an EncryptedMuunKeyWithVerificationFlag. +func (vk *VerifiableMuunKey) Verify( + muunPublicKey *libwallet.HDPublicKey, + userPrivateKey *btcec.PrivateKey, + recoveryCodePublicKey *btcec.PublicKey, +) (*EncryptedMuunKeyWithVerificationFlag, error) { + + muunBtcecPubKey, err := muunPublicKey.ECPubKey() + if err != nil { + return nil, err + } + + firstHalfKeyBytes, err := vk.FirstHalfKeyEncryptedToClient.SingleShotDecrypt( + userPrivateKey, + []byte(encrypted_key_v3.MuunFirstHalfToClient), + []byte(""), + ) + if err != nil { + return nil, err + } + if len(firstHalfKeyBytes) != 32 { + return nil, fmt.Errorf("firstHalfKeyBytes should be 32 bytes") + } + + firstHalfKey, firstHalfPubKey := btcec.PrivKeyFromBytes(firstHalfKeyBytes) + + var secondHalfPubkey = subtractPublicKeys(muunBtcecPubKey, firstHalfPubKey) + + var verified bool + if vk.Proof == nil { + // If no proof is provided we produce an unverified encryptedMuunKey + verified = false + } else { + // TODO For now, a verification error results in an unverified key, without impeding to + // generate it. This will change in the future. + + verified = verifyZeroKnowledgeProof( + secondHalfPubkey, + recoveryCodePublicKey, + vk.SecondHalfKeyEncryptedToRecoveryCode, + *vk.Proof, + ) + } + + encryptedMuunKey, err := encrypted_key_v3.FinishMuunKeyEncryption( + recoveryCodePublicKey, + firstHalfKey, + muunPublicKey.ChainCode(), + vk.SecondHalfKeyEncryptedToRecoveryCode, + ) + if err != nil { + return nil, err + } + + return &EncryptedMuunKeyWithVerificationFlag{EncryptedMuunKey: encryptedMuunKey, Verified: verified}, nil +} + +func verifyZeroKnowledgeProof( + secondHalfPubkey *btcec.PublicKey, + recoveryCodePublicKey *btcec.PublicKey, + secondHalfKeyEncryptedToRecoveryCode *bitcoin_hpke.EncryptedMessage, + proof string, +) bool { + + ciphertext := secondHalfKeyEncryptedToRecoveryCode.GetCiphertext() + + if secondHalfKeyEncryptedToRecoveryCode.PlaintextLengthInBytes() != btcec.PrivKeyBytesLen { + slog.Error( + "error: invalid length for ciphertext.", + "length", + len(ciphertext)) + return false + } + + // For testing we mock proofs and thus we also mock verification + if testing.Testing() && proof == "mock_proof" { + return true + } + + proofBytes, err := base64.StdEncoding.DecodeString(proof) + if err != nil { + slog.Error("error decoding proof bytes", slog.Any("error", err)) + return false + } + + // TODO as a temporary workaround for dl_iterate_phdr (which is currently required by librs) + // not being available in api level 19 we are short-circuiting the verification. This is not + // a problem because proof verification is not currently in use anyway. + _ = proofBytes + result := "ok" + + if result != "ok" { + slog.Error("error calling librs.Plonky2ServerKeyVerify", slog.Any("error", err)) + return false + } + + return true +} + +// Compute the subtraction A - B +func subtractPublicKeys(A, B *btcec.PublicKey) *btcec.PublicKey { + // Recall that -B is given by (B.X, -B.Y). Note also that since B is on the curve, B.Y cannot be zero and therefore + // P-B.Y is already reduced modulo P. Thus there is no need to reduce modulo P in the line below. + rX, rY := btcec.S256().Add(A.X(), A.Y(), B.X(), new(big.Int).Sub(btcec.S256().P, B.Y())) + var X, Y btcec.FieldVal + X.SetByteSlice(encryption.PaddedSerializeBigInt(32, rX)) + Y.SetByteSlice(encryption.PaddedSerializeBigInt(32, rY)) + return btcec.NewPublicKey(&X, &Y) +} diff --git a/libwallet/domain/model/verifiable_muun_key/verifiable_muun_key_test.go b/libwallet/domain/model/verifiable_muun_key/verifiable_muun_key_test.go new file mode 100644 index 00000000..3b3b6cea --- /dev/null +++ b/libwallet/domain/model/verifiable_muun_key/verifiable_muun_key_test.go @@ -0,0 +1,36 @@ +package verifiable_muun_key + +import ( + "encoding/hex" + "github.com/btcsuite/btcd/btcec/v2" + "testing" +) + +func TestSubtractPublicKeys(t *testing.T) { + + aSerialization, err := hex.DecodeString("028e949335c8d8bc841167860949c990ac10c87004f74c4513c39603dddf687dbb") + if err != nil { + t.Fatal(err) + } + + bSerialization, err := hex.DecodeString("0316e7c706d5bfd42194360e7109d0717c18bdba36c24442af99555dc981d1a66b") + if err != nil { + t.Fatal(err) + } + + aMinusBExpected := "03d8047be580c609b8f63ba3c9ffb4b4bf20da0ae655d651fc95afb4f087b9f3d5" + + A, err := btcec.ParsePubKey(aSerialization) + if err != nil { + t.Fatal(err) + } + + B, err := btcec.ParsePubKey(bSerialization) + if err != nil { + t.Fatal(err) + } + + if hex.EncodeToString(subtractPublicKeys(A, B).SerializeCompressed()) != aMinusBExpected { + t.Fatalf("incorrect value for subtractPublicKeys(A,B)") + } +} diff --git a/libwallet/domain/nfc/mock_nfc_bridge.go b/libwallet/domain/nfc/mock_nfc_bridge.go index 46546c28..20286628 100644 --- a/libwallet/domain/nfc/mock_nfc_bridge.go +++ b/libwallet/domain/nfc/mock_nfc_bridge.go @@ -66,6 +66,9 @@ type MockNfcBridge struct { mockCard *MockMuunCard } +// Ensure MockNfcBridge complies with NfcBridge interface API +var _ app_provided_data.NfcBridge = (*MockNfcBridge)(nil) + func NewMockNfcBridge(network *libwallet.Network) *MockNfcBridge { return &MockNfcBridge{ mockCard: &MockMuunCard{ diff --git a/libwallet/domain/nfc/muuncard_v2.go b/libwallet/domain/nfc/muuncard_v2.go new file mode 100644 index 00000000..5cf9c60a --- /dev/null +++ b/libwallet/domain/nfc/muuncard_v2.go @@ -0,0 +1,218 @@ +package nfc + +import ( + "errors" + "fmt" + "github.com/muun/libwallet/app_provided_data" +) + +// Implementation to interact with our reference security card firmware v2. + +const MuuncardV2AppletId = "A00000015100133900" + +// Muuncard V2 specific APDU bytes. +const insMuuncardV2Setup = 0x10 + +// MuuncardV2 specific status words. +const swMuuncardV2WrongLength = 0x6700 +const swMuuncardV2ResponseTooLarge = 0x6B11 +const swMuuncardV2InvalidPubKey = 0x6B12 +const swMuuncardV2CryptoError = 0x6B14 +const swMuuncardV2NoSlotsAvailable = 0x6B16 +const swMuuncardV2InvalidMac = 0x6B17 +const swMuuncardV2InvalidCounter = 0x6B18 +const swMuuncardV2SlotNotPaired = 0x6B19 + +const ( + Secp256R1PointSize = 65 + PairingSlotSize = 2 + MetadataSize = 75 + MacSize = 32 + TotalPairInputSize = Secp256R1PointSize * 2 // C + pub_client = 130 bytes + PairResponseSize = Secp256R1PointSize + PairingSlotSize + MetadataSize + MacSize // 174 bytes +) + +type MuunCardV2 struct { + rawCard *SmartCard +} + +type PairingResponse struct { + CardPublicKey []byte // 65 bytes - Card's ephemeral public key + PairingSlot []byte // 2 bytes - Random pairing identifier + Metadata *CardMetadata // 75 bytes - Metadata including global_pub_card + MAC []byte // 32 bytes - HMAC-SHA256 authentication + GlobalSignature []byte // 70-72 bytes - DER-Encoded ECDSA signature with global private key +} + +type CardMetadata struct { + GlobalPubCard [65]byte // 65 bytes - Card's permanent public key + CardVendor [2]byte // 2 bytes - Vendor name + CardModel [2]byte // 2 bytes - Model name + FirmwareVersion [2]byte // 2 bytes - Firmware version + UsageCount uint16 // 2 bytes - Number of operations performed + LanguageCode [2]byte // 2 bytes - Language preference +} + +func NewCardV2(nfcBridge app_provided_data.NfcBridge) *MuunCardV2 { + return &MuunCardV2{rawCard: newSmartCard(nfcBridge)} +} + +var cardV2StatusToError = map[uint16]*CardError{ + swMuuncardV2WrongLength: {Message: "card rejected input: wrong length", Code: ErrInternal}, + swMuuncardV2InvalidPubKey: {Message: "card rejected public key: invalid format", Code: ErrInternal}, + swMuuncardV2ResponseTooLarge: {Message: "response too large, exceeds APDU limit of 255 bytes", Code: ErrInternal}, + swMuuncardV2CryptoError: {Message: "cryptographic error during pairing", Code: ErrInternal}, + swMuuncardV2NoSlotsAvailable: {Message: "no pairing slots available on card", Code: ErrSlotOccupied}, + swMuuncardV2InvalidMac: {Message: "invalid MAC", Code: ErrInternal}, + swMuuncardV2InvalidCounter: {Message: "invalid counter", Code: ErrInternal}, + swMuuncardV2SlotNotPaired: {Message: "slot not paired", Code: ErrSlotNotInitialized}, +} + +func (c *MuunCardV2) Pair(serverRandomPublicKey, clientPublicKey []byte) (*PairingResponse, error) { + // Validate server random public key format (C) + if len(serverRandomPublicKey) != Secp256R1PointSize { + return nil, fmt.Errorf( + "invalid server random public key length: %d (expected %d)", + len(serverRandomPublicKey), + Secp256R1PointSize, + ) + } + + if serverRandomPublicKey[0] != 0x04 { + return nil, fmt.Errorf( + "invalid server random public key format: expected 0x04 prefix, got 0x%02X", + serverRandomPublicKey[0], + ) + } + + // Validate client public key format (C) + if len(clientPublicKey) != Secp256R1PointSize { + return nil, fmt.Errorf("invalid client public key length: %d (expected %d)", + len(clientPublicKey), + Secp256R1PointSize, + ) + } + + if clientPublicKey[0] != 0x04 { + return nil, fmt.Errorf( + "invalid client public key format: expected 0x04 prefix, got 0x%02X", + clientPublicKey[0], + ) + } + + // Send C || pub_client to card (130 bytes total) + input := make([]byte, 0, TotalPairInputSize) + input = append(input, serverRandomPublicKey...) // C (65 bytes) + input = append(input, clientPublicKey...) // pub_client (65 bytes) + + apdu := newAPDU( + claEdge, + insMuuncardV2Setup, + nullByte, + nullByte, + input, + ) + + response, err := c.transmit(apdu.serialize()) + if err != nil { + return nil, fmt.Errorf("failed to transmit insMuuncardV2Setup: %w", err) + } + + // Parse response: P (65) || index (2) || metadata (75) || mac (32) || signature (variable) + if len(response.Response) < PairResponseSize { + return nil, fmt.Errorf("response too short: %d bytes", len(response.Response)) + } + + offset := 0 + pairingResp := &PairingResponse{ + CardPublicKey: response.Response[offset : offset+Secp256R1PointSize], + } + offset += Secp256R1PointSize + + pairingResp.PairingSlot = response.Response[offset : offset+PairingSlotSize] + offset += PairingSlotSize + + pairingResp.Metadata = parseMetadata(response.Response[offset : offset+MetadataSize]) + offset += MetadataSize + + pairingResp.MAC = response.Response[offset : offset+MacSize] + offset += MacSize + + // Remaining bytes are the signature + pairingResp.GlobalSignature = response.Response[offset:] + + if len(pairingResp.GlobalSignature) < 70 || len(pairingResp.GlobalSignature) > 72 { + return nil, errors.New("invalid global signature length") + } + + if pairingResp.CardPublicKey[0] != 0x04 { + return nil, fmt.Errorf( + "invalid card public key format: expected 0x04 prefix, got 0x%02X", + pairingResp.CardPublicKey[0], + ) + } + + return pairingResp, nil +} + +func parseMetadata(data []byte) *CardMetadata { + if len(data) < MetadataSize { + return nil + } + + offset := 0 + + metadata := &CardMetadata{} + + // Global public key (65 bytes) + copy(metadata.GlobalPubCard[:], data[offset:offset+65]) + offset += 65 + + // Card vendor (2 bytes) + copy(metadata.CardVendor[:], data[offset:offset+2]) + offset += 2 + + // Card model (2 bytes) + copy(metadata.CardModel[:], data[offset:offset+2]) + offset += 2 + + // Firmware version (2 bytes) + copy(metadata.FirmwareVersion[:], data[offset:offset+2]) + offset += 2 + + // Usage count (2 bytes, big-endian) + metadata.UsageCount = uint16(data[offset])<<8 | uint16(data[offset+1]) + offset += 2 + + // Language code (2 bytes) + copy(metadata.LanguageCode[:], data[offset:offset+2]) + offset += 2 + + return metadata +} + +func (c *MuunCardV2) transmit(apdu []byte) (*CardResponse, error) { + + err := c.rawCard.selectApplet(MuuncardV2AppletId) + if err != nil { + return nil, fmt.Errorf("error selecting Muuncard Applet: %w", err) + } + + resp, err := c.rawCard.transmit(apdu) + if err != nil { + return nil, fmt.Errorf("error transmitting APDU: %w", err) + } + + if resp.StatusCode != responseOk { + return nil, mapStatusToCardV2Error(resp.StatusCode) + } + + return resp, nil +} + +func mapStatusToCardV2Error(code uint16) error { + if cardError, ok := cardV2StatusToError[code]; ok { + return cardError + } + return newCardError(ErrInternal, fmt.Sprintf("unknown error code: 0x%x", code)) +} diff --git a/libwallet/domain/recovery/stub.go b/libwallet/domain/recovery/stub.go deleted file mode 100644 index 0b913e8e..00000000 --- a/libwallet/domain/recovery/stub.go +++ /dev/null @@ -1,19 +0,0 @@ -package recovery - -import ( - "encoding/hex" - "github.com/muun/libwallet/service/model" - - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcutil/hdkeychain" - "github.com/muun/libwallet/service" -) - -func FinishRecoveryCodeSetupStub(houstonService *service.HoustonService, extendedServerCosigningPublicKey hdkeychain.ExtendedKey, recoveryCodePublicKey btcec.PublicKey) error { - return houstonService.ChallengeKeySetupFinish(model.ChallengeSetupVerifyJson{ - ChallengeType: "RECOVERY_CODE", - PublicKey: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), - }) - - // TODO: use the new `/finish-with-cosigning-key` endpoint and verify using the serverCosigningKey -} diff --git a/libwallet/encrypted_cosigning_key/encrypted_extended_key.go b/libwallet/encrypted_cosigning_key/encrypted_extended_key.go deleted file mode 100644 index 6b256eab..00000000 --- a/libwallet/encrypted_cosigning_key/encrypted_extended_key.go +++ /dev/null @@ -1,177 +0,0 @@ -package encrypted_cosigning_key - -import ( - "bytes" - "encoding/base64" - "errors" - "fmt" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/muun/libwallet" - "github.com/muun/libwallet/recoverycode" -) - -const ( - version uint8 = 3 - privateKeyLenBytes = btcec.PrivKeyBytesLen - compressedPublicKeyLenBytes = btcec.PubKeyBytesLenCompressed - chainCodeLenBytes = 32 -) - -type encryptedKey struct { - version uint8 - paddedPrivateKey *btcec.PrivateKey - ephemeralPublicKey *btcec.PublicKey - paddedChainCode []byte - ephemeralChainCodePublicKey *btcec.PublicKey -} - -func (key *encryptedKey) serialize() string { - result := make( - []byte, - 0, - 1+privateKeyLenBytes+compressedPublicKeyLenBytes+chainCodeLenBytes+compressedPublicKeyLenBytes, - ) - - buf := bytes.NewBuffer(result) - - buf.WriteByte(version) - buf.Write(key.paddedPrivateKey.Serialize()) - buf.Write(key.ephemeralPublicKey.SerializeCompressed()) - buf.Write(key.paddedChainCode) - buf.Write(key.ephemeralChainCodePublicKey.SerializeCompressed()) - - return base64.StdEncoding.EncodeToString(buf.Bytes()) -} - -func deserializeEncryptedKey(serializedKey string) (*encryptedKey, error) { - - serializedKeyBytes, err := base64.StdEncoding.DecodeString(serializedKey) - if err != nil { - return nil, fmt.Errorf("decrypting key: failed to decode from base64 %w", err) - } - reader := bytes.NewReader(serializedKeyBytes) - - version, err := reader.ReadByte() - - if err != nil { - return nil, fmt.Errorf("decrypting key: failed to read version %w", err) - } - - paddedPrivateKeyBytes := make([]byte, privateKeyLenBytes) - ephemeralPublicKeyBytes := make([]byte, compressedPublicKeyLenBytes) - paddedChainCode := make([]byte, chainCodeLenBytes) - ephemeralChainCodePublicKeyBytes := make([]byte, compressedPublicKeyLenBytes) - - n, err := reader.Read(paddedPrivateKeyBytes) - if err != nil || n != privateKeyLenBytes { - return nil, fmt.Errorf("decrypting key: failed to read paddedPrivateKey %w", err) - } - paddedPrivateKey, _ := btcec.PrivKeyFromBytes(paddedPrivateKeyBytes) - - n, err = reader.Read(ephemeralPublicKeyBytes) - if err != nil || n != compressedPublicKeyLenBytes { - return nil, fmt.Errorf("decrypting key: failed to read ephemeralPublicKey %w", err) - } - ephemeralPublicKey, err := btcec.ParsePubKey(ephemeralPublicKeyBytes) - if err != nil { - return nil, fmt.Errorf("decrypting key: failed to parse ephemeralPublicKey %w", err) - } - - n, err = reader.Read(paddedChainCode) - if err != nil || n != chainCodeLenBytes { - return nil, fmt.Errorf("decrypting key: failed to read chainCode %w", err) - } - - n, err = reader.Read(ephemeralChainCodePublicKeyBytes) - if err != nil || n != compressedPublicKeyLenBytes { - return nil, fmt.Errorf("decrypting key: failed to read ephemeralChainCodePublicKey %w", err) - } - ephemeralChainCodePublicKey, err := btcec.ParsePubKey(ephemeralChainCodePublicKeyBytes) - if err != nil { - return nil, fmt.Errorf("decrypting key: failed to parse ephemeralChainCodePublicKey %w", err) - } - - if reader.Len() > 0 { - return nil, errors.New("decrypting key: key is longer than expected") - } - - return &encryptedKey{ - version, - paddedPrivateKey, - ephemeralPublicKey, - paddedChainCode, - ephemeralChainCodePublicKey, - }, nil -} - -func EncryptExtendedKey(key *libwallet.HDPrivateKey, recoveryCodePublicKey *btcec.PublicKey) (string, error) { - - privateKey, err := key.ECPrivateKey() - if err != nil { - return "", fmt.Errorf("encrypt extended key: failed to extract private key %w", err) - } - - ephemeralPrivateKey, err := btcec.NewPrivateKey() - if err != nil { - return "", fmt.Errorf("encrypt extended key: failed to obtain ephemeral key %w", err) - } - - paddedPrivateKeyBytes, err := paddingEncrypt( - privateKey.Serialize(), - recoveryCodePublicKey, - ephemeralPrivateKey, - ) - if err != nil { - return "", fmt.Errorf("encrypt extended key: failed to encrypt private key %w", err) - } - paddedPrivateKey, _ := btcec.PrivKeyFromBytes(paddedPrivateKeyBytes) - - chaincodeEphemeralPrivateKey, err := btcec.NewPrivateKey() - if err != nil { - return "", fmt.Errorf("encrypt extended key: failed to obtain ephemeral key %w", err) - } - - paddedChainCode, err := paddingEncrypt( - key.ChainCode(), - recoveryCodePublicKey, - chaincodeEphemeralPrivateKey, - ) - if err != nil { - return "", err - } - - encryptedKey := encryptedKey{ - version: version, - paddedPrivateKey: paddedPrivateKey, - ephemeralPublicKey: ephemeralPrivateKey.PubKey(), - paddedChainCode: paddedChainCode, - ephemeralChainCodePublicKey: chaincodeEphemeralPrivateKey.PubKey(), - } - - return encryptedKey.serialize(), nil -} - -func DecryptExtendedKey(recoveryCode string, encryptedExtendedKey string, network *libwallet.Network) (*libwallet.HDPrivateKey, error) { - - key, err := deserializeEncryptedKey(encryptedExtendedKey) - if err != nil { - return nil, err - } - - recoveryCodePrivateKey, err := recoverycode.ConvertToKey(recoveryCode, "") - if err != nil { - return nil, err - } - - privateKey, err := paddingDecrypt(key.paddedPrivateKey.Serialize(), recoveryCodePrivateKey, key.ephemeralPublicKey) - if err != nil { - return nil, err - } - - chainCode, err := paddingDecrypt(key.paddedChainCode, recoveryCodePrivateKey, key.ephemeralChainCodePublicKey) - if err != nil { - return nil, err - } - - return libwallet.NewHDPrivateKeyFromBytes(privateKey, chainCode, network) -} diff --git a/libwallet/encrypted_cosigning_key/encrypted_extended_key_test.go b/libwallet/encrypted_cosigning_key/encrypted_extended_key_test.go deleted file mode 100644 index 9462263e..00000000 --- a/libwallet/encrypted_cosigning_key/encrypted_extended_key_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package encrypted_cosigning_key - -import ( - "bytes" - "crypto/rand" - "github.com/muun/libwallet" - "github.com/muun/libwallet/recoverycode" - "testing" -) - -func TestEncryptExtendedKey(t *testing.T) { - - recoveryCode := recoverycode.Generate() - recoveryCodePrivateKey, err := recoverycode.ConvertToKey(recoveryCode, "") - if err != nil { - t.Fatal(err) - } - recoveryCodePublicKey := recoveryCodePrivateKey.PubKey() - - extendedKey, err := libwallet.NewHDPrivateKey(randomBytes(32), libwallet.Mainnet()) - if err != nil { - t.Fatal(err) - } - - encryptedExtendedKey, err := EncryptExtendedKey(extendedKey, recoveryCodePublicKey) - if err != nil { - t.Fatal(err) - } - - decryptedExtendedKey, err := DecryptExtendedKey(recoveryCode, encryptedExtendedKey, libwallet.Mainnet()) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(decryptedExtendedKey.PublicKey().Raw(), extendedKey.PublicKey().Raw()) { - t.Fatal("decrypted public key does not match original public key") - } - - if !bytes.Equal(decryptedExtendedKey.ChainCode(), extendedKey.ChainCode()) { - t.Fatal("decrypted chain code does not match original chain code") - } -} - -func randomBytes(count int) []byte { - - buf := make([]byte, count) - _, err := rand.Read(buf) - if err != nil { - panic("couldn't read random bytes") - } - - return buf -} diff --git a/libwallet/encrypted_cosigning_key/padding_encryption.go b/libwallet/encrypted_cosigning_key/padding_encryption.go deleted file mode 100644 index 68811409..00000000 --- a/libwallet/encrypted_cosigning_key/padding_encryption.go +++ /dev/null @@ -1,38 +0,0 @@ -package encrypted_cosigning_key - -import ( - "errors" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/muun/libwallet" - "github.com/muun/libwallet/encryption" -) - -func paddingEncrypt(plaintextMessage []byte, receiverKey *btcec.PublicKey, senderKey *btcec.PrivateKey) ([]byte, error) { - - if len(plaintextMessage) != 32 { - return nil, errors.New("plaintext message is not 32 bytes") - } - - var plaintextMessageAsPrivateKey, _ = btcec.PrivKeyFromBytes(plaintextMessage) - padding := computePadding(senderKey, receiverKey) - - return btcec.PrivKeyFromScalar(plaintextMessageAsPrivateKey.Key.Add(&padding.Key)).Serialize(), nil -} - -func paddingDecrypt(encryptedMessage []byte, receiverKey *btcec.PrivateKey, senderKey *btcec.PublicKey) ([]byte, error) { - - if len(encryptedMessage) != 32 { - return nil, errors.New("encrypted message is not 32 bytes") - } - - var encryptedMessageAsPrivateKey, _ = btcec.PrivKeyFromBytes(encryptedMessage) - padding := computePadding(receiverKey, senderKey) - - return btcec.PrivKeyFromScalar(encryptedMessageAsPrivateKey.Key.Add(padding.Key.Negate())).Serialize(), nil -} - -func computePadding(privateKey *btcec.PrivateKey, publicKey *btcec.PublicKey) *btcec.PrivateKey { - X, _ := btcec.S256().ScalarMult(publicKey.X(), publicKey.Y(), privateKey.Serialize()) - padding, _ := btcec.PrivKeyFromBytes(libwallet.SHA256(encryption.PaddedSerializeBigInt(32, X))) - return padding -} diff --git a/libwallet/encrypted_cosigning_key/padding_encryption_test.go b/libwallet/encrypted_cosigning_key/padding_encryption_test.go deleted file mode 100644 index 408821d6..00000000 --- a/libwallet/encrypted_cosigning_key/padding_encryption_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package encrypted_cosigning_key - -import ( - "encoding/hex" - "github.com/btcsuite/btcd/btcec/v2" - "testing" -) - -func TestPaddingEncryptionAndDecryption(t *testing.T) { - - receiverPrivateKeyBytes, err := hex.DecodeString("30bbb2cdefb4bd92d3ba68adc13032d4e9e9be28496e871fe39e0970156c5ea8") - if err != nil { - t.Error(err) - } - receiverPrivateKey, receiverPublicKey := btcec.PrivKeyFromBytes(receiverPrivateKeyBytes) - - secretMessage := "abcdef009999999900000000ffffffffabcdef009999999900000000ffffffff" - secretMessageBytes, err := hex.DecodeString(secretMessage) - if err != nil { - t.Error(err) - } - - ephemeralPrivateKeyBytes, err := hex.DecodeString("8652d2c55b2cf2217a7912fa0d43a1f15c93ecc98495fd44b887cf7232b16acd") - if err != nil { - t.Error(err) - } - ephemeralPrivateKey, ephemeralPublicKey := btcec.PrivKeyFromBytes(ephemeralPrivateKeyBytes) - - encryptedMessage, err := paddingEncrypt(secretMessageBytes, receiverPublicKey, ephemeralPrivateKey) - if err != nil { - t.Error(err) - } - - expectedEncryptedMessage := "81ad50df3d5ca9f7a40d4cf97b22e44b2e27aace183be4e020ccd89ad5552486" - if hex.EncodeToString(encryptedMessage) != expectedEncryptedMessage { - t.Error("encrypted message is not the expected one") - } - - decryptedMessage, err := paddingDecrypt(encryptedMessage, receiverPrivateKey, ephemeralPublicKey) - if err != nil { - t.Error(err) - } - - if hex.EncodeToString(decryptedMessage) != secretMessage { - t.Error("decrypted message is not the original one") - } -} - -func TestPaddingDecrypt(t *testing.T) { - receiverPrivateKeyBytes, err := hex.DecodeString("30bbb2cdefb4bd92d3ba68adc13032d4e9e9be28496e871fe39e0970156c5ea8") - if err != nil { - t.Error(err) - } - receiverPrivateKey, _ := btcec.PrivKeyFromBytes(receiverPrivateKeyBytes) - - ephemeralPublicKeyBytes, err := hex.DecodeString("032afde2011f0a454068d4cbf1c14bd1aec1e25035fb523471efa380f1b1cec450") - if err != nil { - t.Error(err) - } - ephemeralPublicKey, err := btcec.ParsePubKey(ephemeralPublicKeyBytes) - if err != nil { - t.Error(err) - } - - encryptedMessage, err := hex.DecodeString("976b579ddb448dd9964914d4a54d6360cc035c9d43ae2ca03b4a10b1bf69899d") - if err != nil { - t.Error(err) - } - - decryptedMessage, err := paddingDecrypt(encryptedMessage, receiverPrivateKey, ephemeralPublicKey) - if err != nil { - t.Error(err) - } - - expectedSecretMessage := "abcdef009999999900000000ffffffffabcdef009999999900000000ffffffff" - - if hex.EncodeToString(decryptedMessage) != expectedSecretMessage { - t.Error("decrypted message is not the original one") - } -} diff --git a/libwallet/encrypted_cosigning_key/verify.go b/libwallet/encrypted_cosigning_key/verify.go deleted file mode 100644 index 0d43109b..00000000 --- a/libwallet/encrypted_cosigning_key/verify.go +++ /dev/null @@ -1,105 +0,0 @@ -package encrypted_cosigning_key - -import ( - "encoding/base64" - "errors" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcutil/hdkeychain" - "github.com/muun/libwallet/encryption" - "log/slog" - "math/big" - "testing" -) - -type VerifiableCosigningKey struct { - EphemeralPublicKey btcec.PublicKey - PaddedServerCosigningKey btcec.PrivateKey - Proof string -} - -// Return the base64 encoded encrypted server cosigning key that can be decrypted with the recovery code private key. -// The boolean return value is true if the cosigning key was proven to be correct with a zero-knowledge proof. -func ComputeVerifiedEncryptedServerCosigningKey( - serverCosigningPublicKey *hdkeychain.ExtendedKey, - verifiableCosigningKey VerifiableCosigningKey, - recoveryCodePublicKey *btcec.PublicKey, -) (string, bool, error) { - - serverCosigningBtcecPubKey, err := serverCosigningPublicKey.ECPubKey() - if err != nil { - return "", false, err - } - - var sharedSecretPublicKey = subtractPublicKeys(verifiableCosigningKey.PaddedServerCosigningKey.PubKey(), serverCosigningBtcecPubKey) - - var verified bool - if verifiableCosigningKey.Proof == "" { - verified = false - } else { - err = plonky2ServerKeyVerify(verifiableCosigningKey, sharedSecretPublicKey, recoveryCodePublicKey) - // TODO For now, a verification error results in an unverified key, without impeding to generate an encrypted - // key. This will change in the future. - verified = err == nil - slog.Error("error during plonky2 verify", slog.Any("error", err)) - } - - chaincodeEphemeralPrivateKey, err := btcec.NewPrivateKey() - if err != nil { - return "", false, err - } - paddedChainCode, err := paddingEncrypt( - serverCosigningPublicKey.ChainCode(), - recoveryCodePublicKey, - chaincodeEphemeralPrivateKey, - ) - if err != nil { - return "", false, err - } - - key := encryptedKey{ - version, - &verifiableCosigningKey.PaddedServerCosigningKey, - &verifiableCosigningKey.EphemeralPublicKey, - paddedChainCode, - chaincodeEphemeralPrivateKey.PubKey(), - } - - return key.serialize(), verified, nil -} - -func plonky2ServerKeyVerify( - verifiableCosigningKey VerifiableCosigningKey, - plaintextPublicKey *btcec.PublicKey, - recoveryCodePublicKey *btcec.PublicKey, -) error { - - if testing.Testing() && verifiableCosigningKey.Proof == "mock_proof" { - return nil - } - - proofBytes, err := base64.StdEncoding.DecodeString(verifiableCosigningKey.Proof) - if err != nil { - return err - } - - // TODO we should be calling librs here - _ = proofBytes - result := "error: not implemented" - - if result == "ok" { - return nil - } else { - return errors.New(result) - } -} - -// Compute the subtraction A - B -func subtractPublicKeys(A, B *btcec.PublicKey) *btcec.PublicKey { - // Recall that -B is given by (B.X, -B.Y). Note also that since B is on the curve, B.Y cannot be zero and therefore - // P-B.Y is already reduced modulo P. Thus there is no need to reduce modulo P in the line below. - rX, rY := btcec.S256().Add(A.X(), A.Y(), B.X(), new(big.Int).Sub(btcec.S256().P, B.Y())) - var X, Y btcec.FieldVal - X.SetByteSlice(encryption.PaddedSerializeBigInt(32, rX)) - Y.SetByteSlice(encryption.PaddedSerializeBigInt(32, rY)) - return btcec.NewPublicKey(&X, &Y) -} diff --git a/libwallet/encrypted_cosigning_key/verify_test.go b/libwallet/encrypted_cosigning_key/verify_test.go deleted file mode 100644 index 3bcf06bd..00000000 --- a/libwallet/encrypted_cosigning_key/verify_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package encrypted_cosigning_key - -import ( - "bytes" - "encoding/hex" - "fmt" - "io" - "net/http" - "strconv" - "testing" - "time" - - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcutil/hdkeychain" - "github.com/muun/libwallet" - "github.com/muun/libwallet/recoverycode" - "github.com/muun/libwallet/service" - "github.com/muun/libwallet/service/model" -) - -const houstonUrl = "http://localhost:8080" -const proverUrl = "http://localhost:8130" - -func MustDecode(h string, t *testing.T) []byte { - decoded, err := hex.DecodeString(h) - if err != nil { - t.Fatal(err) - } - return decoded -} - -func MustParsePublicKey(key string, t *testing.T) btcec.PublicKey { - pubkey, err := btcec.ParsePubKey(MustDecode(key, t)) - if err != nil { - t.Fatal(err) - } - return *pubkey -} - -func MustParsePrivateKey(key string, t *testing.T) btcec.PrivateKey { - privkey, _ := btcec.PrivKeyFromBytes(MustDecode(key, t)) - return *privkey - -} - -func WaitForCondition(timeout time.Duration, condition func() (bool, error)) error { - end := time.Now().Add(timeout) - - var err error - - for time.Now().Before(end) { - holds, innerErr := condition() - - if holds { - return nil - } - - if innerErr != nil { - err = innerErr - } - - time.Sleep(100 * time.Millisecond) - } - - if err != nil { - return fmt.Errorf("timed out waiting for condition, failed with error %w", err) - } else { - return fmt.Errorf("timed out waiting for condition") - } -} - -func DefaultProvider() *service.TestProvider { - return &service.TestProvider{ - ClientVersion: "1205", - ClientVersionName: "2.9.2", - Language: "en", - ClientType: "FALCON", - BaseURL: houstonUrl, - } -} - -func CreateFirstSession(houstonService service.HoustonService, t *testing.T) model.CreateFirstSessionOkJson { - provider := DefaultProvider() - strClientVersion, err := strconv.Atoi(provider.ClientVersion) - if err != nil { - t.Fatal(err) - } - sessionJson := model.CreateFirstSessionJson{ - Client: model.ClientJson{ - Type: provider.ClientType, - BuildType: "debug", - Version: strClientVersion, - VersionName: provider.ClientVersionName, - Language: provider.Language, - }, - GcmToken: nil, - PrimaryCurrency: "USD", - BasePublicKey: model.PublicKeyJson{ - Key: "tpubDAygaiK3eZ9hpC3aQkxtu5fGSTK4P7QKTwwGExN8hGZytjpEfsrUjtM8ics8Y7YLrvf1GLBZTFjcpmkEP1KKTRyo8D2ku5zz49bRudDrngd", - Path: "m/schema:1'/recovery:1'", - }, - } - sessionOkJson, err := houstonService.CreateFirstSession(sessionJson) - if err != nil { - t.Fatal(err) - } - return sessionOkJson -} - -// This is a valid encrypted private key used for testing -const EncryptedPrivateKey string = "v1:512:1:8:582626b7244e1e72:01b0df9b1ffbdfe8eabae00a2c208317:0a6593c59927797f6a850f1775792378b81078ba29c203b41fac51d6d62069fbb5306e6585c7abab06905d86cc01bc3c0302087edec71c9018191e68e72348e06f2b87c4b5ee0efb55c99fd9f3703ebfc94127bebd41fa72bced4747718a5c43219dd010aa2e9be848e387b3c29f4df3:6d2f736368656d613a31272f7265636f766572793a3127" - -func TestCosigningKeyProofHappyPath_Integration(t *testing.T) { - t.Skip("/verifiable-server-cosigning-key endpoint disabled") - houstonService := service.NewHoustonService(DefaultProvider()) - sessionOkJson := CreateFirstSession(*houstonService, t) - - key, err := hdkeychain.NewKeyFromString(sessionOkJson.CosigningPublicKey.Key) - if err != nil { - t.Fatal(err) - } - - cosigningKey, err := key.ECPubKey() - if err != nil { - t.Fatal(err) - } - - recoveryCode := recoverycode.Generate() - rcPrivKey, err := recoverycode.ConvertToKey(recoveryCode, "") - if err != nil { - t.Fatal(err) - } - rcPubKey := rcPrivKey.PubKey() - - _, err = houstonService.ChallengeKeySetupStart(model.ChallengeSetupJson{ - Type: "RECOVERY_CODE", - PublicKey: hex.EncodeToString(rcPubKey.SerializeCompressed()), - EncryptedPrivateKey: EncryptedPrivateKey, - Version: 2, - Salt: "dfb80ea8c30959e8", - }) - if err != nil { - t.Fatal(err) - } - - err = houstonService.ChallengeKeySetupFinish(model.ChallengeSetupVerifyJson{ - ChallengeType: "RECOVERY_CODE", - PublicKey: hex.EncodeToString(rcPubKey.SerializeCompressed()), - }) - if err != nil { - t.Fatal(err) - } - - // Wait for the proof to finish - // it's a mock proof so actual proving is instant, but it still - // has to propagate through the services - err = WaitForCondition(1*time.Second, func() (bool, error) { - result, err := houstonService.VerifiableServerCosginingKey() - if err != nil { - return false, err - } - return result.Proof != "", nil - }) - if err != nil { - t.Fatalf("Timed out: %s", err) - } - - result, err := houstonService.VerifiableServerCosginingKey() - if err != nil { - t.Fatal(err) - } - - if result.Proof != "mock_proof" { - t.Fatalf("Proof is not `mock_proof`") - } - - verifiableCosigningKey := VerifiableCosigningKey{ - EphemeralPublicKey: MustParsePublicKey(result.EphemeralPublicKey, t), - PaddedServerCosigningKey: MustParsePrivateKey(result.PaddedServerCosigningKey, t), - Proof: result.Proof, - } - - encryptedKey, _, err := ComputeVerifiedEncryptedServerCosigningKey(key, verifiableCosigningKey, rcPubKey) - if err != nil { - t.Fatal(err) - } - - decryptedKey, err := DecryptExtendedKey(recoveryCode, encryptedKey, libwallet.Regtest()) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(decryptedKey.PublicKey().Raw(), cosigningKey.SerializeCompressed()) { - t.Fatalf("Decrypted key does not match original key") - } - - if !bytes.Equal(decryptedKey.ChainCode(), key.ChainCode()) { - t.Fatalf("Decrypted key chain code does not match original key chain code") - } - -} - -func TestCosigningKeyProofUnhappyPath_Integration(t *testing.T) { - t.Skip("/verifiable-server-cosigning-key endpoint disabled") - houstonService := service.NewHoustonService(DefaultProvider()) - sessionOkJson := CreateFirstSession(*houstonService, t) - - key, err := hdkeychain.NewKeyFromString(sessionOkJson.CosigningPublicKey.Key) - if err != nil { - t.Fatal(err) - } - - cosigningKey, err := key.ECPubKey() - if err != nil { - t.Fatal(err) - } - - recoveryCode := recoverycode.Generate() - rcPrivKey, err := recoverycode.ConvertToKey(recoveryCode, "") - rcPubKey := rcPrivKey.PubKey() - - if err != nil { - t.Fatal(err) - } - - // This requests prevents the proof from completing, this exercising the unhappy path - req, err := http.NewRequest("POST", proverUrl+"/testing/delay-job?pattern="+hex.EncodeToString(rcPubKey.SerializeCompressed()), nil) - if err != nil { - t.Fatal(err) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - if resp.StatusCode >= 300 || resp.StatusCode < 200 { - r, _ := io.ReadAll(resp.Body) - t.Fatalf("request failed with status code %d, resp: %s", resp.StatusCode, string(r)) - } - - _, err = houstonService.ChallengeKeySetupStart(model.ChallengeSetupJson{ - Type: "RECOVERY_CODE", - PublicKey: hex.EncodeToString(rcPubKey.SerializeCompressed()), - EncryptedPrivateKey: EncryptedPrivateKey, - Version: 2, - Salt: "dfb80ea8c30959e8", - }) - if err != nil { - t.Fatal(err) - } - - err = houstonService.ChallengeKeySetupFinish(model.ChallengeSetupVerifyJson{ - ChallengeType: "RECOVERY_CODE", - PublicKey: hex.EncodeToString(rcPubKey.SerializeCompressed()), - }) - if err != nil { - t.Fatal(err) - } - - result, err := houstonService.VerifiableServerCosginingKey() - if err != nil { - t.Fatal(err) - } - - if result.Proof != "" { - t.Fatalf("Proof is not empty") - } - - verifiableCosigningKey := VerifiableCosigningKey{ - EphemeralPublicKey: MustParsePublicKey(result.EphemeralPublicKey, t), - PaddedServerCosigningKey: MustParsePrivateKey(result.PaddedServerCosigningKey, t), - Proof: result.Proof, - } - - encryptedKey, _, err := ComputeVerifiedEncryptedServerCosigningKey(key, verifiableCosigningKey, rcPubKey) - if err != nil { - t.Fatal(err) - } - - decryptedKey, err := DecryptExtendedKey(recoveryCode, encryptedKey, libwallet.Regtest()) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(decryptedKey.PublicKey().Raw(), cosigningKey.SerializeCompressed()) { - t.Fatalf("Decrypted key does not match original key") - } - - if !bytes.Equal(decryptedKey.ChainCode(), key.ChainCode()) { - t.Fatalf("Decrypted key chain code does not match original key chain code") - } - -} - -func TestSubtractPublicKeys(t *testing.T) { - - aSerialization, err := hex.DecodeString("028e949335c8d8bc841167860949c990ac10c87004f74c4513c39603dddf687dbb") - if err != nil { - t.Fatal(err) - } - - bSerialization, err := hex.DecodeString("0316e7c706d5bfd42194360e7109d0717c18bdba36c24442af99555dc981d1a66b") - if err != nil { - t.Fatal(err) - } - - aMinusBExpected := "03d8047be580c609b8f63ba3c9ffb4b4bf20da0ae655d651fc95afb4f087b9f3d5" - - A, err := btcec.ParsePubKey(aSerialization) - if err != nil { - t.Fatal(err) - } - - B, err := btcec.ParsePubKey(bSerialization) - if err != nil { - t.Fatal(err) - } - - if hex.EncodeToString(subtractPublicKeys(A, B).SerializeCompressed()) != aMinusBExpected { - t.Fatalf("incorrect value for subtractPublicKeys(A,B)") - } -} diff --git a/libwallet/features.go b/libwallet/features.go index 63f58dc6..09b20160 100644 --- a/libwallet/features.go +++ b/libwallet/features.go @@ -14,6 +14,9 @@ const ( BackendFeatureOsVersionDeprecatedFlow = "OS_VERSION_DEPRECATED_FLOW" BackendFeatureNfcCard = "NFC_CARD" BackendFeatureNfcSensors = "NFC_SENSORS" + BackendFeatureDiagnosticMode = "DIAGNOSTIC_MODE" + + BackendFeatureUnsupported = "UNSUPPORTED_FEATURE" UserActivatedFeatureStatusOff = "off" UserActivatedFeatureStatusCanPreactivate = "can_preactivate" diff --git a/libwallet/hdprivatekey.go b/libwallet/hdprivatekey.go index cd446b85..fdd691ba 100644 --- a/libwallet/hdprivatekey.go +++ b/libwallet/hdprivatekey.go @@ -32,8 +32,9 @@ func NewHDPrivateKey(seed []byte, network *Network) (*HDPrivateKey, error) { return &HDPrivateKey{key: *key, Network: network, Path: "m"}, nil } -// NewHDPrivateKeyFromBytes builds an HD priv key from the compress priv and chain code for a given network -func NewHDPrivateKeyFromBytes(rawKey, chainCode []byte, network *Network) (*HDPrivateKey, error) { +// NewMasterHDPrivateKeyFromBytes builds an HD priv key from the compress priv and chain code for a given network +// The data is assumed to correspond to a master key. +func NewMasterHDPrivateKeyFromBytes(rawKey, chainCode []byte, network *Network) (*HDPrivateKey, error) { parentFP := []byte{0, 0, 0, 0} key := hdkeychain.NewExtendedKey(network.network.HDPrivateKeyID[:], @@ -42,6 +43,21 @@ func NewHDPrivateKeyFromBytes(rawKey, chainCode []byte, network *Network) (*HDPr return &HDPrivateKey{key: *key, Network: network, Path: "m"}, nil } +// NewBasePathHDPrivateKeyFromBytes builds an HD priv key from the compress priv and chain code for a given network. +// The data is assumed to correspond to a key derived at our usual base path "m/schema:1'/recovery:1'". +func NewBasePathHDPrivateKeyFromBytes(rawKey, chainCode []byte, network *Network) (*HDPrivateKey, error) { + + basePath := "m/schema:1'/recovery:1'" + var depth uint8 = 2 + var childNum uint32 = hdkeychain.HardenedKeyStart + 1 + isPrivate := true + parentFP := []byte{0, 0, 0, 0} + key := hdkeychain.NewExtendedKey(network.network.HDPrivateKeyID[:], + rawKey, chainCode, parentFP, depth, childNum, isPrivate) + + return &HDPrivateKey{key: *key, Network: network, Path: basePath}, nil +} + // NewHDPrivateKeyFromString creates an HD priv key from a base58-encoded string // If the parsed key is public, it returns an error func NewHDPrivateKeyFromString(str, path string, network *Network) (*HDPrivateKey, error) { diff --git a/libwallet/hdpublickey.go b/libwallet/hdpublickey.go index d00480b8..54886409 100644 --- a/libwallet/hdpublickey.go +++ b/libwallet/hdpublickey.go @@ -125,3 +125,7 @@ func (p *HDPublicKey) Fingerprint() []byte { func (p *HDPublicKey) ECPubKey() (*btcec.PublicKey, error) { return p.key.ECPubKey() } + +func (p *HDPublicKey) ChainCode() []byte { + return p.key.ChainCode() +} diff --git a/libwallet/librs/generate/src/main.rs b/libwallet/librs/generate/src/main.rs index 9120bc22..12bf2e73 100644 --- a/libwallet/librs/generate/src/main.rs +++ b/libwallet/librs/generate/src/main.rs @@ -8,20 +8,20 @@ fn str_to_arr(s: &str) -> [u8; N] { } const HPKE_EPHEMERAL_PRIVATE_KEY: &str = - "ad9243a485da16dc53d4cdbbf9974f55a6b4d8563eaacffdbd32890e0df3e975"; -const HPKE_EPHEMERAL_PUBLIC_KEY: &str = "04197175cf54953d5637aaa6d9f12424d8fc708e826cf5d3442298f3c6545cfc35ab37898edf5457e4823bf35a8a08a0088236fc9194e048cd1345ca0d4f3afa8f"; + "bb611d7aa2a5688d947085fa5c60e87fb051042854c23fff388945dc7010b0b5"; +const HPKE_EPHEMERAL_PUBLIC_KEY: &str = "0471b55503fb340ec6c202d6cdce7d49c365b78ae2fa3bab06ae87553610006553441e4f7ad3c3c834b0e0538ac241e2adc61c85a10ec7341eb1129edb0caccd0a"; #[allow(dead_code)] const RECOVERY_CODE_PRIVATE_KEY: &str = - "cb027d62281abf5c7c7140477579df6c06e58c04a40c71354018b414af30a2e9"; + "20f5dccb488fe31f95ba0f55ed306df9df2a5a171157838bada35342e71f5d7f"; -const RECOVERY_CODE_PUBLIC_KEY: &str = "04465538823cd6f5438db12bc9b57a35c0f19e5787b517502719da3fe63597566284056b4a89937f529ad7e93a76f3e894d6572db3ef493d29f7d05f2b5ff1939a"; +const RECOVERY_CODE_PUBLIC_KEY: &str = "04dc5489ca59d23d4deebc778850651da1f3da1c505db198df8e5cf9fe322964c7c5ab62cac0b255be7d75606e04bc8015e70c39d6e0d6faaf435eb92c29043ded"; -const PLAINTEXT_SCALAR: &str = "f165aaaa334cd690be86cdfe7a53eca573482c18c6360519e9e3a016e1f6442a"; +const PLAINTEXT_SCALAR: &str = "f9fff35fb0004862359e69bcbb003b0dc8e610e6d82af40a25ed5d75386241df"; -const PLAINTEXT_PUBLIC_KEY: &str = "04a537b6bb21a11d8662317659e2b2a27aa37a4e95b5ececa1b3fdd4a7a772dac8047073a501394fd623a5747b25bbc0a16e934ba865423df840b35a9a5019f511"; +const PLAINTEXT_PUBLIC_KEY: &str = "0468a18701d75331dddbef334c070931cf3561288e78346666fdcc01fb28aac0f17823d00b35cd06eb0508067a345027ab03a716ea825220059a168c6a6d5090db"; -const CIPHERTEXT: &str = "34bfe3253dde55ec3fbe27a9f8cf91293ba40822107f2736eb4fc0228716f320450e78057c76aea4177c6a008ab7b57e"; +const CIPHERTEXT: &str = "23d170accd4b2849fbfa0e8e49f753eefb274c0449ab8ab46e9f35a4e2265f054d7cbab020157c34c5ba61e0e7695608"; fn main() { let (prover_data, verifier_data) = cosigning_key_validation::precompute(); diff --git a/libwallet/librs/librs_test.go b/libwallet/librs/librs_test.go index 45805715..1c00df38 100644 --- a/libwallet/librs/librs_test.go +++ b/libwallet/librs/librs_test.go @@ -18,10 +18,10 @@ func decode(s string) []byte { func TestVerifierOk(t *testing.T) { - HPKE_EPHEMERAL_PUBLIC_KEY := "04197175cf54953d5637aaa6d9f12424d8fc708e826cf5d3442298f3c6545cfc35ab37898edf5457e4823bf35a8a08a0088236fc9194e048cd1345ca0d4f3afa8f" - RECOVERY_CODE_PUBLIC_KEY := "04465538823cd6f5438db12bc9b57a35c0f19e5787b517502719da3fe63597566284056b4a89937f529ad7e93a76f3e894d6572db3ef493d29f7d05f2b5ff1939a" - CIPHERTEXT := "34bfe3253dde55ec3fbe27a9f8cf91293ba40822107f2736eb4fc0228716f320450e78057c76aea4177c6a008ab7b57e" - PLAINTEXT_PUBLIC_KEY := "04a537b6bb21a11d8662317659e2b2a27aa37a4e95b5ececa1b3fdd4a7a772dac8047073a501394fd623a5747b25bbc0a16e934ba865423df840b35a9a5019f511" + HPKE_EPHEMERAL_PUBLIC_KEY := "0471b55503fb340ec6c202d6cdce7d49c365b78ae2fa3bab06ae87553610006553441e4f7ad3c3c834b0e0538ac241e2adc61c85a10ec7341eb1129edb0caccd0a" + RECOVERY_CODE_PUBLIC_KEY := "04dc5489ca59d23d4deebc778850651da1f3da1c505db198df8e5cf9fe322964c7c5ab62cac0b255be7d75606e04bc8015e70c39d6e0d6faaf435eb92c29043ded" + CIPHERTEXT := "23d170accd4b2849fbfa0e8e49f753eefb274c0449ab8ab46e9f35a4e2265f054d7cbab020157c34c5ba61e0e7695608" + PLAINTEXT_PUBLIC_KEY := "0468a18701d75331dddbef334c070931cf3561288e78346666fdcc01fb28aac0f17823d00b35cd06eb0508067a345027ab03a716ea825220059a168c6a6d5090db" res := string(Plonky2ServerKeyVerify( proof, @@ -36,10 +36,10 @@ func TestVerifierOk(t *testing.T) { } func TestVerifierPanic(t *testing.T) { - HPKE_EPHEMERAL_PUBLIC_KEY := "04197175cf54953d5637aaa6d9f12424d8fc708e826cf5d3442298f3c6545cfc35ab37898edf5457e4823bf35a8a08a0088236fc9194e048cd1345ca0d4f3afa8f" - RECOVERY_CODE_PUBLIC_KEY := "04465538823cd6f5438db12bc9b57a35c0f19e5787b517502719da3fe63597566284056b4a89937f529ad7e93a76f3e894d6572db3ef493d29f7d05f2b5ff1939a" - CIPHERTEXT := "34bfe3253dde55ec3fbe27a9f8cf91293ba40822107f2736eb4fc0228716f320450e78057c76aea4177c6a008ab7b57e" - PLAINTEXT_PUBLIC_KEY := "04a537b6bb21a11d8662317659e2b2a27aa37a4e95b5ececa1b3fdd4a7a772dac8047073a501394fd623a5747b25bbc0a16e934ba865423df840b35a9a5019f511" + HPKE_EPHEMERAL_PUBLIC_KEY := "0471b55503fb340ec6c202d6cdce7d49c365b78ae2fa3bab06ae87553610006553441e4f7ad3c3c834b0e0538ac241e2adc61c85a10ec7341eb1129edb0caccd0a" + RECOVERY_CODE_PUBLIC_KEY := "04dc5489ca59d23d4deebc778850651da1f3da1c505db198df8e5cf9fe322964c7c5ab62cac0b255be7d75606e04bc8015e70c39d6e0d6faaf435eb92c29043ded" + CIPHERTEXT := "23d170accd4b2849fbfa0e8e49f753eefb274c0449ab8ab46e9f35a4e2265f054d7cbab020157c34c5ba61e0e7695608" + PLAINTEXT_PUBLIC_KEY := "0468a18701d75331dddbef334c070931cf3561288e78346666fdcc01fb28aac0f17823d00b35cd06eb0508067a345027ab03a716ea825220059a168c6a6d5090db" modifiedProof := append([]byte{}, proof...) modifiedProof[100] = 7 @@ -57,11 +57,11 @@ func TestVerifierPanic(t *testing.T) { } func TestVerifierError(t *testing.T) { - HPKE_EPHEMERAL_PUBLIC_KEY := "04197175cf54953d5637aaa6d9f12424d8fc708e826cf5d3442298f3c6545cfc35ab37898edf5457e4823bf35a8a08a0088236fc9194e048cd1345ca0d4f3afa8f" + HPKE_EPHEMERAL_PUBLIC_KEY := "0471b55503fb340ec6c202d6cdce7d49c365b78ae2fa3bab06ae87553610006553441e4f7ad3c3c834b0e0538ac241e2adc61c85a10ec7341eb1129edb0caccd0a" // changed the first byte in RECOVERY_CODE_PUBLIC_KEY to 0x03 so that the encoding is not valid - RECOVERY_CODE_PUBLIC_KEY := "03465538823cd6f5438db12bc9b57a35c0f19e5787b517502719da3fe63597566284056b4a89937f529ad7e93a76f3e894d6572db3ef493d29f7d05f2b5ff1939a" - CIPHERTEXT := "34bfe3253dde55ec3fbe27a9f8cf91293ba40822107f2736eb4fc0228716f320450e78057c76aea4177c6a008ab7b57e" - PLAINTEXT_PUBLIC_KEY := "04a537b6bb21a11d8662317659e2b2a27aa37a4e95b5ececa1b3fdd4a7a772dac8047073a501394fd623a5747b25bbc0a16e934ba865423df840b35a9a5019f511" + RECOVERY_CODE_PUBLIC_KEY := "03dc5489ca59d23d4deebc778850651da1f3da1c505db198df8e5cf9fe322964c7c5ab62cac0b255be7d75606e04bc8015e70c39d6e0d6faaf435eb92c29043ded" + CIPHERTEXT := "23d170accd4b2849fbfa0e8e49f753eefb274c0449ab8ab46e9f35a4e2265f054d7cbab020157c34c5ba61e0e7695608" + PLAINTEXT_PUBLIC_KEY := "0468a18701d75331dddbef334c070931cf3561288e78346666fdcc01fb28aac0f17823d00b35cd06eb0508067a345027ab03a716ea825220059a168c6a6d5090db" res := string(Plonky2ServerKeyVerify( proof, decode(RECOVERY_CODE_PUBLIC_KEY), diff --git a/libwallet/libwallet_init/init.go b/libwallet/libwallet_init/init.go index 79b51a56..6ecd6c33 100644 --- a/libwallet/libwallet_init/init.go +++ b/libwallet/libwallet_init/init.go @@ -3,10 +3,13 @@ package libwallet_init import ( "errors" "github.com/grpc-ecosystem/go-grpc-middleware" + "github.com/muun/libwallet/data/keys" "github.com/muun/libwallet/domain/action/challenge_keys" "github.com/muun/libwallet/domain/action/diagnostic_mode_reports" nfcActions "github.com/muun/libwallet/domain/action/nfc" + "github.com/muun/libwallet/domain/action/recovery" "github.com/muun/libwallet/domain/nfc" + "github.com/muun/libwallet/electrum" "github.com/muun/libwallet/storage" "log/slog" "net" @@ -26,12 +29,21 @@ var server *grpc.Server var cfg *app_provided_data.Config var keyValueStorage *storage.KeyValueStorage var network *libwallet.Network -var houstonService *service.HoustonService +var houstonService service.HoustonService +var mockHoustonService *service.MockHoustonService +var keyProvider keys.KeyProvider var startChallengeSetupAction *challenge_keys.StartChallengeSetupAction +var finishChallengeSetupAction *challenge_keys.FinishChallengeSetupAction +var computeAndStoreEncryptedMuunKeyAction *recovery.ComputeAndStoreEncryptedMuunKeyAction +var populateEncryptedMuunKeyAction *recovery.PopulateEncryptedMuunKeyAction +var scanForFundsAction *recovery.ScanForFundsAction var submitDiagnosticAction *diagnostic_mode_reports.SubmitDiagnosticAction +var buildSweepTxAction *recovery.BuildSweepTxAction +var signSweepTxAction *recovery.SignSweepTxAction var pairSecurityCardAction *nfcActions.PairSecurityCardAction var resetSecurityCardAction *nfcActions.ResetSecurityCardAction var signMessageSecurityCardAction *nfcActions.SignMessageSecurityCardAction +var pairSecurityCardActionV2 *nfcActions.PairSecurityCardActionV2 // Init configures libwallet func Init(c *app_provided_data.Config) { @@ -47,8 +59,9 @@ func Init(c *app_provided_data.Config) { } if cfg.HttpClientSessionProvider != nil { - houstonService = service.NewHoustonService(c.HttpClientSessionProvider) + houstonService = service.NewHoustonService(cfg.HttpClientSessionProvider) } + mockHoustonService = service.NewMockHoustonService() var storageSchema = storage.BuildStorageSchema() keyValueStorage = storage.NewKeyValueStorage(path.Join(cfg.DataDir, "wallet.db"), storageSchema) @@ -63,17 +76,43 @@ func Init(c *app_provided_data.Config) { default: panic("unknown network: " + c.Network) } + keyProvider = keys.NewKeyProvider(c.KeyProvider, *network) - // TODO do this only for debug builds or something + // TODO do this only for debug builds and while making use of FakeNfcSession or equivalent + // Enables security cards testing in emulators and ui tests. //cfg.NfcBridge = nfc.NewMockNfcBridge(network) muuncard := nfc.NewCard(cfg.NfcBridge) + muuncardV2 := nfc.NewCardV2(cfg.NfcBridge) // Actions + computeAndStoreEncryptedMuunKeyAction = recovery.NewComputeAndStoreEncryptedMuunKeyAction( + keyValueStorage, + keyProvider, + ) + populateEncryptedMuunKeyAction = recovery.NewPopulateEncryptedMuunKeyAction( + houstonService, + keyValueStorage, + keyProvider, + ) startChallengeSetupAction = challenge_keys.NewStartChallengeSetupAction(houstonService) + finishChallengeSetupAction = challenge_keys.NewFinishChallengeSetupAction( + houstonService, + keyValueStorage, + computeAndStoreEncryptedMuunKeyAction, + ) + electrumProvider := electrum.NewServerProvider(electrum.PublicServers) + scanForFundsAction = recovery.NewScanForFundsAction(keyProvider, electrumProvider, network) submitDiagnosticAction = diagnostic_mode_reports.NewSubmitDiagnosticAction(houstonService) + buildSweepTxAction = recovery.NewBuildSweepTxAction(keyProvider, network) + signSweepTxAction = recovery.NewSignSweepTxAction(keyProvider, network) pairSecurityCardAction = nfcActions.NewPairSecurityCardAction(keyValueStorage, muuncard) resetSecurityCardAction = nfcActions.NewResetSecurityCardAction(keyValueStorage, muuncard) - signMessageSecurityCardAction = nfcActions.NewSignMessageSecurityCardAction(keyValueStorage, muuncard, network) + signMessageSecurityCardAction = nfcActions.NewSignMessageSecurityCardAction( + keyValueStorage, + muuncard, + network, + ) + pairSecurityCardActionV2 = nfcActions.NewPairSecurityCardActionV2(keyValueStorage, muuncardV2, mockHoustonService) } func StartServer() error { @@ -98,15 +137,21 @@ func StartServer() error { server = grpc.NewServer(opts...) api.RegisterWalletServiceServer(server, presentation.NewWalletServer( cfg.NfcBridge, - cfg.KeyProvider, + keyProvider, network, houstonService, keyValueStorage, startChallengeSetupAction, + finishChallengeSetupAction, + populateEncryptedMuunKeyAction, + scanForFundsAction, submitDiagnosticAction, + buildSweepTxAction, + signSweepTxAction, pairSecurityCardAction, resetSecurityCardAction, signMessageSecurityCardAction, + pairSecurityCardActionV2, )) listener, err := net.Listen("unix", cfg.SocketPath) diff --git a/libwallet/presentation/api/google/rpc/status.proto b/libwallet/presentation/api/google/rpc/status.proto new file mode 100644 index 00000000..dc14c943 --- /dev/null +++ b/libwallet/presentation/api/google/rpc/status.proto @@ -0,0 +1,49 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.rpc; + +import "google/protobuf/any.proto"; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; +option java_multiple_files = true; +option java_outer_classname = "StatusProto"; +option java_package = "com.google.rpc"; +option objc_class_prefix = "RPC"; + +// The `Status` type defines a logical error model that is suitable for +// different programming environments, including REST APIs and RPC APIs. It is +// used by [gRPC](https://github.com/grpc). Each `Status` message contains +// three pieces of data: error code, error message, and error details. +// +// You can find out more about this error model and how to work with it in the +// [API Design Guide](https://cloud.google.com/apis/design/errors). +message Status { + // The status code, which should be an enum value of + // [google.rpc.Code][google.rpc.Code]. + int32 code = 1; + + // A developer-facing error message, which should be in English. Any + // user-facing error message should be localized and sent in the + // [google.rpc.Status.details][google.rpc.Status.details] field, or localized + // by the client. + string message = 2; + + // A list of messages that carry the error details. There is a common set of + // message types for APIs to use. + repeated google.protobuf.Any details = 3; +} diff --git a/libwallet/presentation/api/wallet_service.pb.go b/libwallet/presentation/api/wallet_service.pb.go index 564c3a78..4362ea74 100644 --- a/libwallet/presentation/api/wallet_service.pb.go +++ b/libwallet/presentation/api/wallet_service.pb.go @@ -202,27 +202,27 @@ func (b0 ErrorDetail_builder) Build() *ErrorDetail { return m0 } -type OperationStatus struct { - state protoimpl.MessageState `protogen:"opaque.v1"` - xxx_hidden_Status string `protobuf:"bytes,1,opt,name=status,proto3"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type XpubResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Base58Xpub string `protobuf:"bytes,1,opt,name=base58_xpub,json=base58Xpub,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *OperationStatus) Reset() { - *x = OperationStatus{} +func (x *XpubResponse) Reset() { + *x = XpubResponse{} mi := &file_wallet_service_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *OperationStatus) String() string { +func (x *XpubResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*OperationStatus) ProtoMessage() {} +func (*XpubResponse) ProtoMessage() {} -func (x *OperationStatus) ProtoReflect() protoreflect.Message { +func (x *XpubResponse) ProtoReflect() protoreflect.Message { mi := &file_wallet_service_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -234,52 +234,53 @@ func (x *OperationStatus) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -func (x *OperationStatus) GetStatus() string { +func (x *XpubResponse) GetBase58Xpub() string { if x != nil { - return x.xxx_hidden_Status + return x.xxx_hidden_Base58Xpub } return "" } -func (x *OperationStatus) SetStatus(v string) { - x.xxx_hidden_Status = v +func (x *XpubResponse) SetBase58Xpub(v string) { + x.xxx_hidden_Base58Xpub = v } -type OperationStatus_builder struct { +type XpubResponse_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. - Status string + Base58Xpub string } -func (b0 OperationStatus_builder) Build() *OperationStatus { - m0 := &OperationStatus{} +func (b0 XpubResponse_builder) Build() *XpubResponse { + m0 := &XpubResponse{} b, x := &b0, m0 _, _ = b, x - x.xxx_hidden_Status = b.Status + x.xxx_hidden_Base58Xpub = b.Base58Xpub return m0 } -type NfcTransmitRequest struct { - state protoimpl.MessageState `protogen:"opaque.v1"` - xxx_hidden_ApduCommand []byte `protobuf:"bytes,1,opt,name=apdu_command,json=apduCommand,proto3"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type SetupSecurityCardResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_IsKnownProvider bool `protobuf:"varint,1,opt,name=is_known_provider,json=isKnownProvider,proto3"` + xxx_hidden_IsCardAlreadyUsed bool `protobuf:"varint,2,opt,name=is_card_already_used,json=isCardAlreadyUsed,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *NfcTransmitRequest) Reset() { - *x = NfcTransmitRequest{} +func (x *SetupSecurityCardResponse) Reset() { + *x = SetupSecurityCardResponse{} mi := &file_wallet_service_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *NfcTransmitRequest) String() string { +func (x *SetupSecurityCardResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*NfcTransmitRequest) ProtoMessage() {} +func (*SetupSecurityCardResponse) ProtoMessage() {} -func (x *NfcTransmitRequest) ProtoReflect() protoreflect.Message { +func (x *SetupSecurityCardResponse) ProtoReflect() protoreflect.Message { mi := &file_wallet_service_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -291,162 +292,41 @@ func (x *NfcTransmitRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -func (x *NfcTransmitRequest) GetApduCommand() []byte { - if x != nil { - return x.xxx_hidden_ApduCommand - } - return nil -} - -func (x *NfcTransmitRequest) SetApduCommand(v []byte) { - if v == nil { - v = []byte{} - } - x.xxx_hidden_ApduCommand = v -} - -type NfcTransmitRequest_builder struct { - _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. - - ApduCommand []byte -} - -func (b0 NfcTransmitRequest_builder) Build() *NfcTransmitRequest { - m0 := &NfcTransmitRequest{} - b, x := &b0, m0 - _, _ = b, x - x.xxx_hidden_ApduCommand = b.ApduCommand - return m0 -} - -type NfcTransmitResponse struct { - state protoimpl.MessageState `protogen:"opaque.v1"` - xxx_hidden_ApduResponse []byte `protobuf:"bytes,1,opt,name=apdu_response,json=apduResponse,proto3"` - xxx_hidden_StatusCode int32 `protobuf:"varint,2,opt,name=status_code,json=statusCode,proto3"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *NfcTransmitResponse) Reset() { - *x = NfcTransmitResponse{} - mi := &file_wallet_service_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *NfcTransmitResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*NfcTransmitResponse) ProtoMessage() {} - -func (x *NfcTransmitResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -func (x *NfcTransmitResponse) GetApduResponse() []byte { +func (x *SetupSecurityCardResponse) GetIsKnownProvider() bool { if x != nil { - return x.xxx_hidden_ApduResponse + return x.xxx_hidden_IsKnownProvider } - return nil + return false } -func (x *NfcTransmitResponse) GetStatusCode() int32 { +func (x *SetupSecurityCardResponse) GetIsCardAlreadyUsed() bool { if x != nil { - return x.xxx_hidden_StatusCode + return x.xxx_hidden_IsCardAlreadyUsed } - return 0 + return false } -func (x *NfcTransmitResponse) SetApduResponse(v []byte) { - if v == nil { - v = []byte{} - } - x.xxx_hidden_ApduResponse = v +func (x *SetupSecurityCardResponse) SetIsKnownProvider(v bool) { + x.xxx_hidden_IsKnownProvider = v } -func (x *NfcTransmitResponse) SetStatusCode(v int32) { - x.xxx_hidden_StatusCode = v +func (x *SetupSecurityCardResponse) SetIsCardAlreadyUsed(v bool) { + x.xxx_hidden_IsCardAlreadyUsed = v } -type NfcTransmitResponse_builder struct { +type SetupSecurityCardResponse_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. - ApduResponse []byte - StatusCode int32 + IsKnownProvider bool + IsCardAlreadyUsed bool } -func (b0 NfcTransmitResponse_builder) Build() *NfcTransmitResponse { - m0 := &NfcTransmitResponse{} +func (b0 SetupSecurityCardResponse_builder) Build() *SetupSecurityCardResponse { + m0 := &SetupSecurityCardResponse{} b, x := &b0, m0 _, _ = b, x - x.xxx_hidden_ApduResponse = b.ApduResponse - x.xxx_hidden_StatusCode = b.StatusCode - return m0 -} - -type XpubResponse struct { - state protoimpl.MessageState `protogen:"opaque.v1"` - xxx_hidden_Base58Xpub string `protobuf:"bytes,1,opt,name=base58_xpub,json=base58Xpub,proto3"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *XpubResponse) Reset() { - *x = XpubResponse{} - mi := &file_wallet_service_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *XpubResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*XpubResponse) ProtoMessage() {} - -func (x *XpubResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -func (x *XpubResponse) GetBase58Xpub() string { - if x != nil { - return x.xxx_hidden_Base58Xpub - } - return "" -} - -func (x *XpubResponse) SetBase58Xpub(v string) { - x.xxx_hidden_Base58Xpub = v -} - -type XpubResponse_builder struct { - _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. - - Base58Xpub string -} - -func (b0 XpubResponse_builder) Build() *XpubResponse { - m0 := &XpubResponse{} - b, x := &b0, m0 - _, _ = b, x - x.xxx_hidden_Base58Xpub = b.Base58Xpub + x.xxx_hidden_IsKnownProvider = b.IsKnownProvider + x.xxx_hidden_IsCardAlreadyUsed = b.IsCardAlreadyUsed return m0 } @@ -459,7 +339,7 @@ type SignMessageSecurityCardRequest struct { func (x *SignMessageSecurityCardRequest) Reset() { *x = SignMessageSecurityCardRequest{} - mi := &file_wallet_service_proto_msgTypes[5] + mi := &file_wallet_service_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -471,7 +351,7 @@ func (x *SignMessageSecurityCardRequest) String() string { func (*SignMessageSecurityCardRequest) ProtoMessage() {} func (x *SignMessageSecurityCardRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[5] + mi := &file_wallet_service_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -517,7 +397,7 @@ type SignMessageSecurityCardResponse struct { func (x *SignMessageSecurityCardResponse) Reset() { *x = SignMessageSecurityCardResponse{} - mi := &file_wallet_service_proto_msgTypes[6] + mi := &file_wallet_service_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -529,7 +409,7 @@ func (x *SignMessageSecurityCardResponse) String() string { func (*SignMessageSecurityCardResponse) ProtoMessage() {} func (x *SignMessageSecurityCardResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[6] + mi := &file_wallet_service_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -587,7 +467,7 @@ type DiagnosticSessionDescriptor struct { func (x *DiagnosticSessionDescriptor) Reset() { *x = DiagnosticSessionDescriptor{} - mi := &file_wallet_service_proto_msgTypes[7] + mi := &file_wallet_service_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -599,7 +479,7 @@ func (x *DiagnosticSessionDescriptor) String() string { func (*DiagnosticSessionDescriptor) ProtoMessage() {} func (x *DiagnosticSessionDescriptor) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[7] + mi := &file_wallet_service_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -644,7 +524,7 @@ type ScanProgressUpdate struct { func (x *ScanProgressUpdate) Reset() { *x = ScanProgressUpdate{} - mi := &file_wallet_service_proto_msgTypes[8] + mi := &file_wallet_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -656,7 +536,7 @@ func (x *ScanProgressUpdate) String() string { func (*ScanProgressUpdate) ProtoMessage() {} func (x *ScanProgressUpdate) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[8] + mi := &file_wallet_service_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -783,7 +663,7 @@ func (b0 ScanProgressUpdate_builder) Build() *ScanProgressUpdate { type case_ScanProgressUpdate_Update protoreflect.FieldNumber func (x case_ScanProgressUpdate_Update) String() string { - md := file_wallet_service_proto_msgTypes[8].Descriptor() + md := file_wallet_service_proto_msgTypes[6].Descriptor() if x == 0 { return "not set" } @@ -816,7 +696,7 @@ type FoundUtxoReport struct { func (x *FoundUtxoReport) Reset() { *x = FoundUtxoReport{} - mi := &file_wallet_service_proto_msgTypes[9] + mi := &file_wallet_service_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -828,7 +708,7 @@ func (x *FoundUtxoReport) String() string { func (*FoundUtxoReport) ProtoMessage() {} func (x *FoundUtxoReport) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[9] + mi := &file_wallet_service_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -886,7 +766,7 @@ type ScanComplete struct { func (x *ScanComplete) Reset() { *x = ScanComplete{} - mi := &file_wallet_service_proto_msgTypes[10] + mi := &file_wallet_service_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -898,7 +778,7 @@ func (x *ScanComplete) String() string { func (*ScanComplete) ProtoMessage() {} func (x *ScanComplete) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[10] + mi := &file_wallet_service_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -944,7 +824,7 @@ type DiagnosticSubmitStatus struct { func (x *DiagnosticSubmitStatus) Reset() { *x = DiagnosticSubmitStatus{} - mi := &file_wallet_service_proto_msgTypes[11] + mi := &file_wallet_service_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -956,7 +836,7 @@ func (x *DiagnosticSubmitStatus) String() string { func (*DiagnosticSubmitStatus) ProtoMessage() {} func (x *DiagnosticSubmitStatus) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[11] + mi := &file_wallet_service_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1005,6 +885,376 @@ func (b0 DiagnosticSubmitStatus_builder) Build() *DiagnosticSubmitStatus { return m0 } +type PrepareSweepTxRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_SessionDescriptor *DiagnosticSessionDescriptor `protobuf:"bytes,1,opt,name=sessionDescriptor,proto3"` + xxx_hidden_DestinationAddress string `protobuf:"bytes,2,opt,name=destinationAddress,proto3"` + xxx_hidden_FeeRateInSatsPerVByte float64 `protobuf:"fixed64,3,opt,name=feeRateInSatsPerVByte,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PrepareSweepTxRequest) Reset() { + *x = PrepareSweepTxRequest{} + mi := &file_wallet_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PrepareSweepTxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrepareSweepTxRequest) ProtoMessage() {} + +func (x *PrepareSweepTxRequest) ProtoReflect() protoreflect.Message { + mi := &file_wallet_service_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *PrepareSweepTxRequest) GetSessionDescriptor() *DiagnosticSessionDescriptor { + if x != nil { + return x.xxx_hidden_SessionDescriptor + } + return nil +} + +func (x *PrepareSweepTxRequest) GetDestinationAddress() string { + if x != nil { + return x.xxx_hidden_DestinationAddress + } + return "" +} + +func (x *PrepareSweepTxRequest) GetFeeRateInSatsPerVByte() float64 { + if x != nil { + return x.xxx_hidden_FeeRateInSatsPerVByte + } + return 0 +} + +func (x *PrepareSweepTxRequest) SetSessionDescriptor(v *DiagnosticSessionDescriptor) { + x.xxx_hidden_SessionDescriptor = v +} + +func (x *PrepareSweepTxRequest) SetDestinationAddress(v string) { + x.xxx_hidden_DestinationAddress = v +} + +func (x *PrepareSweepTxRequest) SetFeeRateInSatsPerVByte(v float64) { + x.xxx_hidden_FeeRateInSatsPerVByte = v +} + +func (x *PrepareSweepTxRequest) HasSessionDescriptor() bool { + if x == nil { + return false + } + return x.xxx_hidden_SessionDescriptor != nil +} + +func (x *PrepareSweepTxRequest) ClearSessionDescriptor() { + x.xxx_hidden_SessionDescriptor = nil +} + +type PrepareSweepTxRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + SessionDescriptor *DiagnosticSessionDescriptor + DestinationAddress string + FeeRateInSatsPerVByte float64 +} + +func (b0 PrepareSweepTxRequest_builder) Build() *PrepareSweepTxRequest { + m0 := &PrepareSweepTxRequest{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_SessionDescriptor = b.SessionDescriptor + x.xxx_hidden_DestinationAddress = b.DestinationAddress + x.xxx_hidden_FeeRateInSatsPerVByte = b.FeeRateInSatsPerVByte + return m0 +} + +type PrepareSweepTxResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_SessionDescriptor *DiagnosticSessionDescriptor `protobuf:"bytes,1,opt,name=sessionDescriptor,proto3"` + xxx_hidden_DestinationAddress string `protobuf:"bytes,2,opt,name=destinationAddress,proto3"` + xxx_hidden_TxSizeInBytes int64 `protobuf:"varint,3,opt,name=txSizeInBytes,proto3"` + xxx_hidden_TxTotalFee int64 `protobuf:"varint,4,opt,name=txTotalFee,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PrepareSweepTxResponse) Reset() { + *x = PrepareSweepTxResponse{} + mi := &file_wallet_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PrepareSweepTxResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrepareSweepTxResponse) ProtoMessage() {} + +func (x *PrepareSweepTxResponse) ProtoReflect() protoreflect.Message { + mi := &file_wallet_service_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *PrepareSweepTxResponse) GetSessionDescriptor() *DiagnosticSessionDescriptor { + if x != nil { + return x.xxx_hidden_SessionDescriptor + } + return nil +} + +func (x *PrepareSweepTxResponse) GetDestinationAddress() string { + if x != nil { + return x.xxx_hidden_DestinationAddress + } + return "" +} + +func (x *PrepareSweepTxResponse) GetTxSizeInBytes() int64 { + if x != nil { + return x.xxx_hidden_TxSizeInBytes + } + return 0 +} + +func (x *PrepareSweepTxResponse) GetTxTotalFee() int64 { + if x != nil { + return x.xxx_hidden_TxTotalFee + } + return 0 +} + +func (x *PrepareSweepTxResponse) SetSessionDescriptor(v *DiagnosticSessionDescriptor) { + x.xxx_hidden_SessionDescriptor = v +} + +func (x *PrepareSweepTxResponse) SetDestinationAddress(v string) { + x.xxx_hidden_DestinationAddress = v +} + +func (x *PrepareSweepTxResponse) SetTxSizeInBytes(v int64) { + x.xxx_hidden_TxSizeInBytes = v +} + +func (x *PrepareSweepTxResponse) SetTxTotalFee(v int64) { + x.xxx_hidden_TxTotalFee = v +} + +func (x *PrepareSweepTxResponse) HasSessionDescriptor() bool { + if x == nil { + return false + } + return x.xxx_hidden_SessionDescriptor != nil +} + +func (x *PrepareSweepTxResponse) ClearSessionDescriptor() { + x.xxx_hidden_SessionDescriptor = nil +} + +type PrepareSweepTxResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + SessionDescriptor *DiagnosticSessionDescriptor + DestinationAddress string + TxSizeInBytes int64 + TxTotalFee int64 +} + +func (b0 PrepareSweepTxResponse_builder) Build() *PrepareSweepTxResponse { + m0 := &PrepareSweepTxResponse{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_SessionDescriptor = b.SessionDescriptor + x.xxx_hidden_DestinationAddress = b.DestinationAddress + x.xxx_hidden_TxSizeInBytes = b.TxSizeInBytes + x.xxx_hidden_TxTotalFee = b.TxTotalFee + return m0 +} + +type SignAndBroadcastSweepTxRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_SessionDescriptor *DiagnosticSessionDescriptor `protobuf:"bytes,1,opt,name=sessionDescriptor,proto3"` + xxx_hidden_RecoveryCode string `protobuf:"bytes,2,opt,name=recoveryCode,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignAndBroadcastSweepTxRequest) Reset() { + *x = SignAndBroadcastSweepTxRequest{} + mi := &file_wallet_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignAndBroadcastSweepTxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignAndBroadcastSweepTxRequest) ProtoMessage() {} + +func (x *SignAndBroadcastSweepTxRequest) ProtoReflect() protoreflect.Message { + mi := &file_wallet_service_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *SignAndBroadcastSweepTxRequest) GetSessionDescriptor() *DiagnosticSessionDescriptor { + if x != nil { + return x.xxx_hidden_SessionDescriptor + } + return nil +} + +func (x *SignAndBroadcastSweepTxRequest) GetRecoveryCode() string { + if x != nil { + return x.xxx_hidden_RecoveryCode + } + return "" +} + +func (x *SignAndBroadcastSweepTxRequest) SetSessionDescriptor(v *DiagnosticSessionDescriptor) { + x.xxx_hidden_SessionDescriptor = v +} + +func (x *SignAndBroadcastSweepTxRequest) SetRecoveryCode(v string) { + x.xxx_hidden_RecoveryCode = v +} + +func (x *SignAndBroadcastSweepTxRequest) HasSessionDescriptor() bool { + if x == nil { + return false + } + return x.xxx_hidden_SessionDescriptor != nil +} + +func (x *SignAndBroadcastSweepTxRequest) ClearSessionDescriptor() { + x.xxx_hidden_SessionDescriptor = nil +} + +type SignAndBroadcastSweepTxRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + SessionDescriptor *DiagnosticSessionDescriptor + RecoveryCode string +} + +func (b0 SignAndBroadcastSweepTxRequest_builder) Build() *SignAndBroadcastSweepTxRequest { + m0 := &SignAndBroadcastSweepTxRequest{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_SessionDescriptor = b.SessionDescriptor + x.xxx_hidden_RecoveryCode = b.RecoveryCode + return m0 +} + +type SignAndBroadcastSweepTxResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_SessionDescriptor *DiagnosticSessionDescriptor `protobuf:"bytes,1,opt,name=sessionDescriptor,proto3"` + xxx_hidden_Txid string `protobuf:"bytes,2,opt,name=txid,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignAndBroadcastSweepTxResponse) Reset() { + *x = SignAndBroadcastSweepTxResponse{} + mi := &file_wallet_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignAndBroadcastSweepTxResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignAndBroadcastSweepTxResponse) ProtoMessage() {} + +func (x *SignAndBroadcastSweepTxResponse) ProtoReflect() protoreflect.Message { + mi := &file_wallet_service_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *SignAndBroadcastSweepTxResponse) GetSessionDescriptor() *DiagnosticSessionDescriptor { + if x != nil { + return x.xxx_hidden_SessionDescriptor + } + return nil +} + +func (x *SignAndBroadcastSweepTxResponse) GetTxid() string { + if x != nil { + return x.xxx_hidden_Txid + } + return "" +} + +func (x *SignAndBroadcastSweepTxResponse) SetSessionDescriptor(v *DiagnosticSessionDescriptor) { + x.xxx_hidden_SessionDescriptor = v +} + +func (x *SignAndBroadcastSweepTxResponse) SetTxid(v string) { + x.xxx_hidden_Txid = v +} + +func (x *SignAndBroadcastSweepTxResponse) HasSessionDescriptor() bool { + if x == nil { + return false + } + return x.xxx_hidden_SessionDescriptor != nil +} + +func (x *SignAndBroadcastSweepTxResponse) ClearSessionDescriptor() { + x.xxx_hidden_SessionDescriptor = nil +} + +type SignAndBroadcastSweepTxResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + SessionDescriptor *DiagnosticSessionDescriptor + Txid string +} + +func (b0 SignAndBroadcastSweepTxResponse_builder) Build() *SignAndBroadcastSweepTxResponse { + m0 := &SignAndBroadcastSweepTxResponse{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_SessionDescriptor = b.SessionDescriptor + x.xxx_hidden_Txid = b.Txid + return m0 +} + type ChallengeSetupRequest struct { state protoimpl.MessageState `protogen:"opaque.v1"` xxx_hidden_Type string `protobuf:"bytes,1,opt,name=type,proto3"` @@ -1018,7 +1268,7 @@ type ChallengeSetupRequest struct { func (x *ChallengeSetupRequest) Reset() { *x = ChallengeSetupRequest{} - mi := &file_wallet_service_proto_msgTypes[12] + mi := &file_wallet_service_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1030,7 +1280,7 @@ func (x *ChallengeSetupRequest) String() string { func (*ChallengeSetupRequest) ProtoMessage() {} func (x *ChallengeSetupRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[12] + mi := &file_wallet_service_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1130,7 +1380,7 @@ type SetupChallengeResponse struct { func (x *SetupChallengeResponse) Reset() { *x = SetupChallengeResponse{} - mi := &file_wallet_service_proto_msgTypes[13] + mi := &file_wallet_service_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1142,7 +1392,7 @@ func (x *SetupChallengeResponse) String() string { func (*SetupChallengeResponse) ProtoMessage() {} func (x *SetupChallengeResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[13] + mi := &file_wallet_service_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1230,16 +1480,15 @@ func (b0 SetupChallengeResponse_builder) Build() *SetupChallengeResponse { } type FinishRecoveryCodeSetupRequest struct { - state protoimpl.MessageState `protogen:"opaque.v1"` - xxx_hidden_ExtendedServerCosigningPublicKeyBase58 string `protobuf:"bytes,1,opt,name=extended_server_cosigning_public_key_base58,json=extendedServerCosigningPublicKeyBase58,proto3"` - xxx_hidden_RecoveryCodePublicKeyHex string `protobuf:"bytes,2,opt,name=recovery_code_public_key_hex,json=recoveryCodePublicKeyHex,proto3"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_RecoveryCodePublicKeyHex string `protobuf:"bytes,1,opt,name=recovery_code_public_key_hex,json=recoveryCodePublicKeyHex,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FinishRecoveryCodeSetupRequest) Reset() { *x = FinishRecoveryCodeSetupRequest{} - mi := &file_wallet_service_proto_msgTypes[14] + mi := &file_wallet_service_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1251,7 +1500,7 @@ func (x *FinishRecoveryCodeSetupRequest) String() string { func (*FinishRecoveryCodeSetupRequest) ProtoMessage() {} func (x *FinishRecoveryCodeSetupRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[14] + mi := &file_wallet_service_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1262,13 +1511,6 @@ func (x *FinishRecoveryCodeSetupRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -func (x *FinishRecoveryCodeSetupRequest) GetExtendedServerCosigningPublicKeyBase58() string { - if x != nil { - return x.xxx_hidden_ExtendedServerCosigningPublicKeyBase58 - } - return "" -} - func (x *FinishRecoveryCodeSetupRequest) GetRecoveryCodePublicKeyHex() string { if x != nil { return x.xxx_hidden_RecoveryCodePublicKeyHex @@ -1276,10 +1518,6 @@ func (x *FinishRecoveryCodeSetupRequest) GetRecoveryCodePublicKeyHex() string { return "" } -func (x *FinishRecoveryCodeSetupRequest) SetExtendedServerCosigningPublicKeyBase58(v string) { - x.xxx_hidden_ExtendedServerCosigningPublicKeyBase58 = v -} - func (x *FinishRecoveryCodeSetupRequest) SetRecoveryCodePublicKeyHex(v string) { x.xxx_hidden_RecoveryCodePublicKeyHex = v } @@ -1287,15 +1525,70 @@ func (x *FinishRecoveryCodeSetupRequest) SetRecoveryCodePublicKeyHex(v string) { type FinishRecoveryCodeSetupRequest_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. - ExtendedServerCosigningPublicKeyBase58 string - RecoveryCodePublicKeyHex string + RecoveryCodePublicKeyHex string } func (b0 FinishRecoveryCodeSetupRequest_builder) Build() *FinishRecoveryCodeSetupRequest { m0 := &FinishRecoveryCodeSetupRequest{} b, x := &b0, m0 _, _ = b, x - x.xxx_hidden_ExtendedServerCosigningPublicKeyBase58 = b.ExtendedServerCosigningPublicKeyBase58 + x.xxx_hidden_RecoveryCodePublicKeyHex = b.RecoveryCodePublicKeyHex + return m0 +} + +type PopulateEncryptedMuunKeyRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_RecoveryCodePublicKeyHex string `protobuf:"bytes,1,opt,name=recovery_code_public_key_hex,json=recoveryCodePublicKeyHex,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PopulateEncryptedMuunKeyRequest) Reset() { + *x = PopulateEncryptedMuunKeyRequest{} + mi := &file_wallet_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PopulateEncryptedMuunKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PopulateEncryptedMuunKeyRequest) ProtoMessage() {} + +func (x *PopulateEncryptedMuunKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_wallet_service_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *PopulateEncryptedMuunKeyRequest) GetRecoveryCodePublicKeyHex() string { + if x != nil { + return x.xxx_hidden_RecoveryCodePublicKeyHex + } + return "" +} + +func (x *PopulateEncryptedMuunKeyRequest) SetRecoveryCodePublicKeyHex(v string) { + x.xxx_hidden_RecoveryCodePublicKeyHex = v +} + +type PopulateEncryptedMuunKeyRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + RecoveryCodePublicKeyHex string +} + +func (b0 PopulateEncryptedMuunKeyRequest_builder) Build() *PopulateEncryptedMuunKeyRequest { + m0 := &PopulateEncryptedMuunKeyRequest{} + b, x := &b0, m0 + _, _ = b, x x.xxx_hidden_RecoveryCodePublicKeyHex = b.RecoveryCodePublicKeyHex return m0 } @@ -1309,7 +1602,7 @@ type Struct struct { func (x *Struct) Reset() { *x = Struct{} - mi := &file_wallet_service_proto_msgTypes[15] + mi := &file_wallet_service_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1321,7 +1614,7 @@ func (x *Struct) String() string { func (*Struct) ProtoMessage() {} func (x *Struct) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[15] + mi := &file_wallet_service_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1367,7 +1660,7 @@ type Value struct { func (x *Value) Reset() { *x = Value{} - mi := &file_wallet_service_proto_msgTypes[16] + mi := &file_wallet_service_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1379,7 +1672,7 @@ func (x *Value) String() string { func (*Value) ProtoMessage() {} func (x *Value) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[16] + mi := &file_wallet_service_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1634,7 +1927,7 @@ func (b0 Value_builder) Build() *Value { type case_Value_Kind protoreflect.FieldNumber func (x case_Value_Kind) String() string { - md := file_wallet_service_proto_msgTypes[16].Descriptor() + md := file_wallet_service_proto_msgTypes[19].Descriptor() if x == 0 { return "not set" } @@ -1691,7 +1984,7 @@ type SaveRequest struct { func (x *SaveRequest) Reset() { *x = SaveRequest{} - mi := &file_wallet_service_proto_msgTypes[17] + mi := &file_wallet_service_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1703,7 +1996,7 @@ func (x *SaveRequest) String() string { func (*SaveRequest) ProtoMessage() {} func (x *SaveRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[17] + mi := &file_wallet_service_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1772,7 +2065,7 @@ type GetRequest struct { func (x *GetRequest) Reset() { *x = GetRequest{} - mi := &file_wallet_service_proto_msgTypes[18] + mi := &file_wallet_service_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1784,7 +2077,7 @@ func (x *GetRequest) String() string { func (*GetRequest) ProtoMessage() {} func (x *GetRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[18] + mi := &file_wallet_service_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1829,7 +2122,7 @@ type GetResponse struct { func (x *GetResponse) Reset() { *x = GetResponse{} - mi := &file_wallet_service_proto_msgTypes[19] + mi := &file_wallet_service_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1841,7 +2134,7 @@ func (x *GetResponse) String() string { func (*GetResponse) ProtoMessage() {} func (x *GetResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[19] + mi := &file_wallet_service_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1897,7 +2190,7 @@ type DeleteRequest struct { func (x *DeleteRequest) Reset() { *x = DeleteRequest{} - mi := &file_wallet_service_proto_msgTypes[20] + mi := &file_wallet_service_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1909,7 +2202,7 @@ func (x *DeleteRequest) String() string { func (*DeleteRequest) ProtoMessage() {} func (x *DeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[20] + mi := &file_wallet_service_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1954,7 +2247,7 @@ type SaveBatchRequest struct { func (x *SaveBatchRequest) Reset() { *x = SaveBatchRequest{} - mi := &file_wallet_service_proto_msgTypes[21] + mi := &file_wallet_service_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1966,7 +2259,7 @@ func (x *SaveBatchRequest) String() string { func (*SaveBatchRequest) ProtoMessage() {} func (x *SaveBatchRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[21] + mi := &file_wallet_service_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2022,7 +2315,7 @@ type GetBatchRequest struct { func (x *GetBatchRequest) Reset() { *x = GetBatchRequest{} - mi := &file_wallet_service_proto_msgTypes[22] + mi := &file_wallet_service_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2034,7 +2327,7 @@ func (x *GetBatchRequest) String() string { func (*GetBatchRequest) ProtoMessage() {} func (x *GetBatchRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[22] + mi := &file_wallet_service_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2079,7 +2372,7 @@ type GetBatchResponse struct { func (x *GetBatchResponse) Reset() { *x = GetBatchResponse{} - mi := &file_wallet_service_proto_msgTypes[23] + mi := &file_wallet_service_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2091,7 +2384,7 @@ func (x *GetBatchResponse) String() string { func (*GetBatchResponse) ProtoMessage() {} func (x *GetBatchResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_service_proto_msgTypes[23] + mi := &file_wallet_service_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2153,291 +2446,357 @@ var file_wallet_service_proto_rawDesc = string([]byte{ 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x29, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x22, 0x37, 0x0a, 0x12, 0x4e, 0x66, 0x63, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x70, 0x64, 0x75, - 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, - 0x61, 0x70, 0x64, 0x75, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x5b, 0x0a, 0x13, 0x4e, - 0x66, 0x63, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x70, 0x64, 0x75, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x61, 0x70, 0x64, 0x75, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x2f, 0x0a, 0x0c, 0x58, 0x70, 0x75, 0x62, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x61, 0x73, 0x65, - 0x35, 0x38, 0x5f, 0x78, 0x70, 0x75, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x62, - 0x61, 0x73, 0x65, 0x35, 0x38, 0x58, 0x70, 0x75, 0x62, 0x22, 0x41, 0x0a, 0x1e, 0x53, 0x69, 0x67, - 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, - 0x43, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x68, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x65, 0x78, 0x22, 0x72, 0x0a, 0x1f, - 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, - 0x69, 0x74, 0x79, 0x43, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x2c, 0x0a, 0x12, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x5f, 0x68, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x73, 0x69, 0x67, - 0x6e, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x65, 0x78, 0x12, 0x21, 0x0a, - 0x0c, 0x69, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x64, - 0x22, 0x3c, 0x0a, 0x1b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x65, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x2f, 0x0a, 0x0c, 0x58, 0x70, 0x75, 0x62, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x61, 0x73, 0x65, 0x35, + 0x38, 0x5f, 0x78, 0x70, 0x75, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x62, 0x61, + 0x73, 0x65, 0x35, 0x38, 0x58, 0x70, 0x75, 0x62, 0x22, 0x78, 0x0a, 0x19, 0x53, 0x65, 0x74, 0x75, + 0x70, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x43, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x69, 0x73, 0x5f, 0x6b, 0x6e, 0x6f, 0x77, + 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0f, 0x69, 0x73, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x12, 0x2f, 0x0a, 0x14, 0x69, 0x73, 0x5f, 0x63, 0x61, 0x72, 0x64, 0x5f, 0x61, 0x6c, 0x72, + 0x65, 0x61, 0x64, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x11, 0x69, 0x73, 0x43, 0x61, 0x72, 0x64, 0x41, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x55, 0x73, + 0x65, 0x64, 0x22, 0x41, 0x0a, 0x1e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x43, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, + 0x68, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x48, 0x65, 0x78, 0x22, 0x72, 0x0a, 0x1f, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x43, 0x61, 0x72, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x73, 0x69, 0x67, 0x6e, + 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x68, 0x65, 0x78, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x48, 0x65, 0x78, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x73, 0x5f, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x73, + 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x64, 0x22, 0x3c, 0x0a, 0x1b, 0x44, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x9c, 0x01, 0x0a, 0x12, 0x53, 0x63, 0x61, 0x6e, + 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x42, + 0x0a, 0x11, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x75, 0x74, 0x78, 0x6f, 0x5f, 0x72, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x72, 0x70, 0x63, 0x2e, + 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x55, 0x74, 0x78, 0x6f, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x48, + 0x00, 0x52, 0x0f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x55, 0x74, 0x78, 0x6f, 0x52, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x12, 0x38, 0x0a, 0x0d, 0x73, 0x63, 0x61, 0x6e, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x72, 0x70, 0x63, 0x2e, + 0x53, 0x63, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x0c, + 0x73, 0x63, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x08, 0x0a, 0x06, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x43, 0x0a, 0x0f, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x55, + 0x74, 0x78, 0x6f, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x26, 0x0a, 0x0c, 0x53, + 0x63, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x22, 0x60, 0x0a, 0x16, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x0a, + 0x0b, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x25, + 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xcd, 0x01, 0x0a, 0x15, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, + 0x65, 0x53, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x4e, 0x0a, 0x11, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x70, 0x63, + 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x11, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, - 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x9c, - 0x01, 0x0a, 0x12, 0x53, 0x63, 0x61, 0x6e, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x42, 0x0a, 0x11, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x75, - 0x74, 0x78, 0x6f, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x14, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x55, 0x74, 0x78, 0x6f, - 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x55, - 0x74, 0x78, 0x6f, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x38, 0x0a, 0x0d, 0x73, 0x63, 0x61, - 0x6e, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x11, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x0c, 0x73, 0x63, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x43, 0x0a, - 0x0f, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x55, 0x74, 0x78, 0x6f, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x22, 0x26, 0x0a, 0x0c, 0x53, 0x63, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x60, 0x0a, 0x16, 0x44, 0x69, - 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x63, - 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xac, 0x01, 0x0a, - 0x15, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x53, 0x65, 0x74, 0x75, 0x70, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, - 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x61, 0x6c, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x12, 0x32, 0x0a, - 0x15, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x65, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, - 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x16, - 0x53, 0x65, 0x74, 0x75, 0x70, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x08, 0x6d, 0x75, 0x75, 0x6e, 0x5f, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, 0x6d, 0x75, 0x75, 0x6e, - 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x14, 0x6d, 0x75, 0x75, 0x6e, 0x5f, 0x6b, - 0x65, 0x79, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x12, 0x6d, 0x75, 0x75, 0x6e, 0x4b, 0x65, 0x79, 0x46, - 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, - 0x09, 0x5f, 0x6d, 0x75, 0x75, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6d, - 0x75, 0x75, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, - 0x69, 0x6e, 0x74, 0x22, 0xbd, 0x01, 0x0a, 0x1e, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x52, 0x65, - 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x75, 0x70, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5b, 0x0a, 0x2b, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, - 0x65, 0x64, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x73, 0x69, 0x67, 0x6e, - 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x62, - 0x61, 0x73, 0x65, 0x35, 0x38, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x26, 0x65, 0x78, 0x74, - 0x65, 0x6e, 0x64, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x73, 0x69, 0x67, - 0x6e, 0x69, 0x6e, 0x67, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x42, 0x61, 0x73, - 0x65, 0x35, 0x38, 0x12, 0x3e, 0x0a, 0x1c, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, - 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, - 0x68, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x72, 0x65, 0x63, 0x6f, 0x76, - 0x65, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, - 0x48, 0x65, 0x78, 0x22, 0x80, 0x01, 0x0a, 0x06, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x12, 0x2f, - 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x46, 0x69, 0x65, 0x6c, - 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, - 0x45, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x20, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x0a, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xeb, 0x01, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x2f, 0x0a, 0x0a, 0x6e, 0x75, 0x6c, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x09, 0x6e, 0x75, 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x6f, 0x75, 0x62, 0x6c, - 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x09, 0x6c, 0x6f, 0x6e, - 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, - 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x62, - 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x48, - 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x06, 0x0a, 0x04, - 0x6b, 0x69, 0x6e, 0x64, 0x22, 0x41, 0x0a, 0x0b, 0x53, 0x61, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x2e, 0x0a, 0x12, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x64, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, + 0x34, 0x0a, 0x15, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x53, 0x61, 0x74, 0x73, + 0x50, 0x65, 0x72, 0x56, 0x42, 0x79, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x15, + 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x53, 0x61, 0x74, 0x73, 0x50, 0x65, 0x72, + 0x56, 0x42, 0x79, 0x74, 0x65, 0x22, 0xde, 0x01, 0x0a, 0x16, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, + 0x65, 0x53, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x4e, 0x0a, 0x11, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x70, + 0x63, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x11, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, + 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x64, 0x65, + 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x24, 0x0a, 0x0d, 0x74, 0x78, 0x53, 0x69, 0x7a, 0x65, 0x49, 0x6e, 0x42, 0x79, 0x74, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x74, 0x78, 0x53, 0x69, 0x7a, 0x65, 0x49, + 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x78, 0x54, 0x6f, 0x74, 0x61, + 0x6c, 0x46, 0x65, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x78, 0x54, 0x6f, + 0x74, 0x61, 0x6c, 0x46, 0x65, 0x65, 0x22, 0x94, 0x01, 0x0a, 0x1e, 0x53, 0x69, 0x67, 0x6e, 0x41, + 0x6e, 0x64, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, + 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4e, 0x0a, 0x11, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x11, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x63, + 0x6f, 0x76, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x85, 0x01, + 0x0a, 0x1f, 0x53, 0x69, 0x67, 0x6e, 0x41, 0x6e, 0x64, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, + 0x73, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x4e, 0x0a, 0x11, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, + 0x70, 0x63, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x11, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, + 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x74, 0x78, 0x69, 0x64, 0x22, 0xac, 0x01, 0x0a, 0x15, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, + 0x6e, 0x67, 0x65, 0x53, 0x65, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x16, 0x53, 0x65, 0x74, 0x75, 0x70, 0x43, 0x68, + 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1e, 0x0a, 0x08, 0x6d, 0x75, 0x75, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x07, 0x6d, 0x75, 0x75, 0x6e, 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, + 0x35, 0x0a, 0x14, 0x6d, 0x75, 0x75, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x66, 0x69, 0x6e, 0x67, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, + 0x12, 0x6d, 0x75, 0x75, 0x6e, 0x4b, 0x65, 0x79, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6d, 0x75, 0x75, 0x6e, 0x5f, + 0x6b, 0x65, 0x79, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6d, 0x75, 0x75, 0x6e, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x22, 0x60, 0x0a, 0x1e, + 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x43, 0x6f, + 0x64, 0x65, 0x53, 0x65, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3e, + 0x0a, 0x1c, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x5f, + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x65, 0x78, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x43, 0x6f, + 0x64, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x48, 0x65, 0x78, 0x22, 0x61, + 0x0a, 0x1f, 0x50, 0x6f, 0x70, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x4d, 0x75, 0x75, 0x6e, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x3e, 0x0a, 0x1c, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, + 0x64, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x65, + 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, + 0x79, 0x43, 0x6f, 0x64, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x48, 0x65, + 0x78, 0x22, 0x80, 0x01, 0x0a, 0x06, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x12, 0x2f, 0x0a, 0x06, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, + 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x45, 0x0a, + 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x20, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, + 0x72, 0x70, 0x63, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0xeb, 0x01, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2f, + 0x0a, 0x0a, 0x6e, 0x75, 0x6c, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x48, 0x00, 0x52, 0x09, 0x6e, 0x75, 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x23, 0x0a, 0x0c, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x09, 0x6c, 0x6f, 0x6e, 0x67, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, + 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, + 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x6b, 0x69, + 0x6e, 0x64, 0x22, 0x41, 0x0a, 0x0b, 0x53, 0x61, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1e, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1e, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x2f, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x21, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x35, 0x0a, 0x10, 0x53, - 0x61, 0x76, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x21, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, - 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, - 0x6d, 0x73, 0x22, 0x25, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x35, 0x0a, 0x10, 0x47, 0x65, 0x74, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x2f, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x21, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x35, 0x0a, 0x10, 0x53, 0x61, 0x76, + 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x2a, 0x33, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0a, 0x0a, - 0x06, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x49, 0x42, - 0x57, 0x41, 0x4c, 0x4c, 0x45, 0x54, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x4f, 0x55, 0x53, - 0x54, 0x4f, 0x4e, 0x10, 0x02, 0x2a, 0x1b, 0x0a, 0x09, 0x4e, 0x75, 0x6c, 0x6c, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x4e, 0x55, 0x4c, 0x4c, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, - 0x10, 0x00, 0x32, 0xb1, 0x08, 0x0a, 0x0d, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x3c, 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x57, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x14, 0x2e, 0x72, - 0x70, 0x63, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x40, 0x0a, 0x0b, 0x4e, 0x66, 0x63, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, - 0x74, 0x12, 0x17, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x66, 0x63, 0x54, 0x72, 0x61, 0x6e, 0x73, - 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x70, 0x63, - 0x2e, 0x4e, 0x66, 0x63, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x75, 0x70, 0x53, 0x65, 0x63, - 0x75, 0x72, 0x69, 0x74, 0x79, 0x43, 0x61, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x1a, 0x11, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x58, 0x70, 0x75, 0x62, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x65, 0x74, 0x53, 0x65, 0x63, - 0x75, 0x72, 0x69, 0x74, 0x79, 0x43, 0x61, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x64, 0x0a, 0x17, 0x53, 0x69, 0x67, - 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, - 0x43, 0x61, 0x72, 0x64, 0x12, 0x23, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, + 0x22, 0x25, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x35, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x2a, 0x33, + 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x43, + 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x49, 0x42, 0x57, 0x41, + 0x4c, 0x4c, 0x45, 0x54, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x4f, 0x55, 0x53, 0x54, 0x4f, + 0x4e, 0x10, 0x02, 0x2a, 0x1b, 0x0a, 0x09, 0x4e, 0x75, 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x0e, 0x0a, 0x0a, 0x4e, 0x55, 0x4c, 0x4c, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x00, + 0x32, 0x8b, 0x0a, 0x0a, 0x0d, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x75, 0x70, 0x53, 0x65, 0x63, 0x75, 0x72, + 0x69, 0x74, 0x79, 0x43, 0x61, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x11, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x58, 0x70, 0x75, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x65, 0x74, 0x53, 0x65, 0x63, 0x75, 0x72, + 0x69, 0x74, 0x79, 0x43, 0x61, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x64, 0x0a, 0x17, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x43, 0x61, - 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x72, 0x70, 0x63, 0x2e, - 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, - 0x69, 0x74, 0x79, 0x43, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x52, 0x0a, 0x16, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, - 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x1a, 0x20, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, + 0x72, 0x64, 0x12, 0x23, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x43, 0x61, 0x72, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, + 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, + 0x79, 0x43, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, + 0x13, 0x53, 0x65, 0x74, 0x75, 0x70, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x43, 0x61, + 0x72, 0x64, 0x56, 0x32, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1e, 0x2e, 0x72, + 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x75, 0x70, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, + 0x43, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x16, + 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x20, + 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, + 0x12, 0x5c, 0x0a, 0x1d, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x44, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x63, 0x61, 0x6e, 0x46, 0x6f, 0x72, 0x55, 0x74, 0x78, 0x6f, + 0x73, 0x12, 0x20, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x1d, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x44, 0x69, - 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x63, 0x61, 0x6e, 0x46, 0x6f, 0x72, 0x55, - 0x74, 0x78, 0x6f, 0x73, 0x12, 0x20, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, - 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x1a, 0x17, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x63, 0x61, - 0x6e, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x30, - 0x01, 0x12, 0x54, 0x0a, 0x13, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x44, 0x69, 0x61, 0x67, 0x6e, - 0x6f, 0x73, 0x74, 0x69, 0x63, 0x4c, 0x6f, 0x67, 0x12, 0x20, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, - 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x1a, 0x1b, 0x2e, 0x72, 0x70, 0x63, - 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x75, 0x62, 0x6d, 0x69, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x4e, 0x0a, 0x13, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, 0x1a, - 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x53, 0x65, - 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x72, 0x70, 0x63, - 0x2e, 0x53, 0x65, 0x74, 0x75, 0x70, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x69, 0x73, - 0x68, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, - 0x75, 0x70, 0x12, 0x23, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x52, + 0x74, 0x6f, 0x72, 0x1a, 0x17, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x50, 0x72, + 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x30, 0x01, 0x12, 0x54, + 0x0a, 0x13, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, + 0x69, 0x63, 0x4c, 0x6f, 0x67, 0x12, 0x20, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x1a, 0x1b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x53, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x49, 0x0a, 0x0e, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x53, + 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x12, 0x1a, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x65, + 0x70, 0x61, 0x72, 0x65, 0x53, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, + 0x53, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x64, 0x0a, 0x17, 0x53, 0x69, 0x67, 0x6e, 0x41, 0x6e, 0x64, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, + 0x61, 0x73, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x12, 0x23, 0x2e, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x41, 0x6e, 0x64, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, + 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x24, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x41, 0x6e, 0x64, 0x42, 0x72, 0x6f, + 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x13, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x68, + 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, 0x1a, 0x2e, 0x72, + 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x53, 0x65, 0x74, 0x75, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, + 0x65, 0x74, 0x75, 0x70, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x75, 0x70, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x30, 0x0a, 0x04, 0x53, 0x61, 0x76, 0x65, 0x12, 0x10, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x61, - 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x12, 0x28, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x0f, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x47, - 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x72, 0x70, 0x63, 0x2e, - 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x06, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x61, 0x76, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x15, - 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, + 0x12, 0x23, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x52, 0x65, 0x63, + 0x6f, 0x76, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a, - 0x08, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x14, 0x2e, 0x72, 0x70, 0x63, 0x2e, - 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x15, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x1f, 0x5a, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x75, 0x75, 0x6e, 0x2f, 0x6c, 0x69, 0x62, 0x77, 0x61, 0x6c, - 0x6c, 0x65, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x58, 0x0a, + 0x18, 0x50, 0x6f, 0x70, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x65, 0x64, 0x4d, 0x75, 0x75, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, 0x72, 0x70, 0x63, 0x2e, + 0x50, 0x6f, 0x70, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x4d, 0x75, 0x75, 0x6e, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x30, 0x0a, 0x04, 0x53, 0x61, 0x76, 0x65, 0x12, + 0x10, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x28, 0x0a, 0x03, 0x47, 0x65, 0x74, + 0x12, 0x0f, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x10, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, + 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x61, 0x76, + 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x15, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x61, 0x76, + 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x12, 0x14, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, + 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x1f, + 0x5a, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x75, 0x75, + 0x6e, 0x2f, 0x6c, 0x69, 0x62, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var file_wallet_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_wallet_service_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_wallet_service_proto_msgTypes = make([]protoimpl.MessageInfo, 28) var file_wallet_service_proto_goTypes = []any{ (ErrorType)(0), // 0: rpc.ErrorType (NullValue)(0), // 1: rpc.NullValue (*ErrorDetail)(nil), // 2: rpc.ErrorDetail - (*OperationStatus)(nil), // 3: rpc.OperationStatus - (*NfcTransmitRequest)(nil), // 4: rpc.NfcTransmitRequest - (*NfcTransmitResponse)(nil), // 5: rpc.NfcTransmitResponse - (*XpubResponse)(nil), // 6: rpc.XpubResponse - (*SignMessageSecurityCardRequest)(nil), // 7: rpc.SignMessageSecurityCardRequest - (*SignMessageSecurityCardResponse)(nil), // 8: rpc.SignMessageSecurityCardResponse - (*DiagnosticSessionDescriptor)(nil), // 9: rpc.DiagnosticSessionDescriptor - (*ScanProgressUpdate)(nil), // 10: rpc.ScanProgressUpdate - (*FoundUtxoReport)(nil), // 11: rpc.FoundUtxoReport - (*ScanComplete)(nil), // 12: rpc.ScanComplete - (*DiagnosticSubmitStatus)(nil), // 13: rpc.DiagnosticSubmitStatus - (*ChallengeSetupRequest)(nil), // 14: rpc.ChallengeSetupRequest - (*SetupChallengeResponse)(nil), // 15: rpc.SetupChallengeResponse - (*FinishRecoveryCodeSetupRequest)(nil), // 16: rpc.FinishRecoveryCodeSetupRequest - (*Struct)(nil), // 17: rpc.Struct - (*Value)(nil), // 18: rpc.Value - (*SaveRequest)(nil), // 19: rpc.SaveRequest - (*GetRequest)(nil), // 20: rpc.GetRequest - (*GetResponse)(nil), // 21: rpc.GetResponse - (*DeleteRequest)(nil), // 22: rpc.DeleteRequest - (*SaveBatchRequest)(nil), // 23: rpc.SaveBatchRequest - (*GetBatchRequest)(nil), // 24: rpc.GetBatchRequest - (*GetBatchResponse)(nil), // 25: rpc.GetBatchResponse - nil, // 26: rpc.Struct.FieldsEntry - (*emptypb.Empty)(nil), // 27: google.protobuf.Empty + (*XpubResponse)(nil), // 3: rpc.XpubResponse + (*SetupSecurityCardResponse)(nil), // 4: rpc.SetupSecurityCardResponse + (*SignMessageSecurityCardRequest)(nil), // 5: rpc.SignMessageSecurityCardRequest + (*SignMessageSecurityCardResponse)(nil), // 6: rpc.SignMessageSecurityCardResponse + (*DiagnosticSessionDescriptor)(nil), // 7: rpc.DiagnosticSessionDescriptor + (*ScanProgressUpdate)(nil), // 8: rpc.ScanProgressUpdate + (*FoundUtxoReport)(nil), // 9: rpc.FoundUtxoReport + (*ScanComplete)(nil), // 10: rpc.ScanComplete + (*DiagnosticSubmitStatus)(nil), // 11: rpc.DiagnosticSubmitStatus + (*PrepareSweepTxRequest)(nil), // 12: rpc.PrepareSweepTxRequest + (*PrepareSweepTxResponse)(nil), // 13: rpc.PrepareSweepTxResponse + (*SignAndBroadcastSweepTxRequest)(nil), // 14: rpc.SignAndBroadcastSweepTxRequest + (*SignAndBroadcastSweepTxResponse)(nil), // 15: rpc.SignAndBroadcastSweepTxResponse + (*ChallengeSetupRequest)(nil), // 16: rpc.ChallengeSetupRequest + (*SetupChallengeResponse)(nil), // 17: rpc.SetupChallengeResponse + (*FinishRecoveryCodeSetupRequest)(nil), // 18: rpc.FinishRecoveryCodeSetupRequest + (*PopulateEncryptedMuunKeyRequest)(nil), // 19: rpc.PopulateEncryptedMuunKeyRequest + (*Struct)(nil), // 20: rpc.Struct + (*Value)(nil), // 21: rpc.Value + (*SaveRequest)(nil), // 22: rpc.SaveRequest + (*GetRequest)(nil), // 23: rpc.GetRequest + (*GetResponse)(nil), // 24: rpc.GetResponse + (*DeleteRequest)(nil), // 25: rpc.DeleteRequest + (*SaveBatchRequest)(nil), // 26: rpc.SaveBatchRequest + (*GetBatchRequest)(nil), // 27: rpc.GetBatchRequest + (*GetBatchResponse)(nil), // 28: rpc.GetBatchResponse + nil, // 29: rpc.Struct.FieldsEntry + (*emptypb.Empty)(nil), // 30: google.protobuf.Empty } var file_wallet_service_proto_depIdxs = []int32{ 0, // 0: rpc.ErrorDetail.type:type_name -> rpc.ErrorType - 11, // 1: rpc.ScanProgressUpdate.found_utxo_report:type_name -> rpc.FoundUtxoReport - 12, // 2: rpc.ScanProgressUpdate.scan_complete:type_name -> rpc.ScanComplete - 26, // 3: rpc.Struct.fields:type_name -> rpc.Struct.FieldsEntry - 1, // 4: rpc.Value.null_value:type_name -> rpc.NullValue - 18, // 5: rpc.SaveRequest.value:type_name -> rpc.Value - 18, // 6: rpc.GetResponse.value:type_name -> rpc.Value - 17, // 7: rpc.SaveBatchRequest.items:type_name -> rpc.Struct - 17, // 8: rpc.GetBatchResponse.items:type_name -> rpc.Struct - 18, // 9: rpc.Struct.FieldsEntry.value:type_name -> rpc.Value - 27, // 10: rpc.WalletService.DeleteWallet:input_type -> google.protobuf.Empty - 4, // 11: rpc.WalletService.NfcTransmit:input_type -> rpc.NfcTransmitRequest - 27, // 12: rpc.WalletService.SetupSecurityCard:input_type -> google.protobuf.Empty - 27, // 13: rpc.WalletService.ResetSecurityCard:input_type -> google.protobuf.Empty - 7, // 14: rpc.WalletService.SignMessageSecurityCard:input_type -> rpc.SignMessageSecurityCardRequest - 27, // 15: rpc.WalletService.StartDiagnosticSession:input_type -> google.protobuf.Empty - 9, // 16: rpc.WalletService.PerformDiagnosticScanForUtxos:input_type -> rpc.DiagnosticSessionDescriptor - 9, // 17: rpc.WalletService.SubmitDiagnosticLog:input_type -> rpc.DiagnosticSessionDescriptor - 14, // 18: rpc.WalletService.StartChallengeSetup:input_type -> rpc.ChallengeSetupRequest - 16, // 19: rpc.WalletService.FinishRecoveryCodeSetup:input_type -> rpc.FinishRecoveryCodeSetupRequest - 19, // 20: rpc.WalletService.Save:input_type -> rpc.SaveRequest - 20, // 21: rpc.WalletService.Get:input_type -> rpc.GetRequest - 22, // 22: rpc.WalletService.Delete:input_type -> rpc.DeleteRequest - 23, // 23: rpc.WalletService.SaveBatch:input_type -> rpc.SaveBatchRequest - 24, // 24: rpc.WalletService.GetBatch:input_type -> rpc.GetBatchRequest - 3, // 25: rpc.WalletService.DeleteWallet:output_type -> rpc.OperationStatus - 5, // 26: rpc.WalletService.NfcTransmit:output_type -> rpc.NfcTransmitResponse - 6, // 27: rpc.WalletService.SetupSecurityCard:output_type -> rpc.XpubResponse - 27, // 28: rpc.WalletService.ResetSecurityCard:output_type -> google.protobuf.Empty - 8, // 29: rpc.WalletService.SignMessageSecurityCard:output_type -> rpc.SignMessageSecurityCardResponse - 9, // 30: rpc.WalletService.StartDiagnosticSession:output_type -> rpc.DiagnosticSessionDescriptor - 10, // 31: rpc.WalletService.PerformDiagnosticScanForUtxos:output_type -> rpc.ScanProgressUpdate - 13, // 32: rpc.WalletService.SubmitDiagnosticLog:output_type -> rpc.DiagnosticSubmitStatus - 15, // 33: rpc.WalletService.StartChallengeSetup:output_type -> rpc.SetupChallengeResponse - 27, // 34: rpc.WalletService.FinishRecoveryCodeSetup:output_type -> google.protobuf.Empty - 27, // 35: rpc.WalletService.Save:output_type -> google.protobuf.Empty - 21, // 36: rpc.WalletService.Get:output_type -> rpc.GetResponse - 27, // 37: rpc.WalletService.Delete:output_type -> google.protobuf.Empty - 27, // 38: rpc.WalletService.SaveBatch:output_type -> google.protobuf.Empty - 25, // 39: rpc.WalletService.GetBatch:output_type -> rpc.GetBatchResponse - 25, // [25:40] is the sub-list for method output_type - 10, // [10:25] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 9, // 1: rpc.ScanProgressUpdate.found_utxo_report:type_name -> rpc.FoundUtxoReport + 10, // 2: rpc.ScanProgressUpdate.scan_complete:type_name -> rpc.ScanComplete + 7, // 3: rpc.PrepareSweepTxRequest.sessionDescriptor:type_name -> rpc.DiagnosticSessionDescriptor + 7, // 4: rpc.PrepareSweepTxResponse.sessionDescriptor:type_name -> rpc.DiagnosticSessionDescriptor + 7, // 5: rpc.SignAndBroadcastSweepTxRequest.sessionDescriptor:type_name -> rpc.DiagnosticSessionDescriptor + 7, // 6: rpc.SignAndBroadcastSweepTxResponse.sessionDescriptor:type_name -> rpc.DiagnosticSessionDescriptor + 29, // 7: rpc.Struct.fields:type_name -> rpc.Struct.FieldsEntry + 1, // 8: rpc.Value.null_value:type_name -> rpc.NullValue + 21, // 9: rpc.SaveRequest.value:type_name -> rpc.Value + 21, // 10: rpc.GetResponse.value:type_name -> rpc.Value + 20, // 11: rpc.SaveBatchRequest.items:type_name -> rpc.Struct + 20, // 12: rpc.GetBatchResponse.items:type_name -> rpc.Struct + 21, // 13: rpc.Struct.FieldsEntry.value:type_name -> rpc.Value + 30, // 14: rpc.WalletService.SetupSecurityCard:input_type -> google.protobuf.Empty + 30, // 15: rpc.WalletService.ResetSecurityCard:input_type -> google.protobuf.Empty + 5, // 16: rpc.WalletService.SignMessageSecurityCard:input_type -> rpc.SignMessageSecurityCardRequest + 30, // 17: rpc.WalletService.SetupSecurityCardV2:input_type -> google.protobuf.Empty + 30, // 18: rpc.WalletService.StartDiagnosticSession:input_type -> google.protobuf.Empty + 7, // 19: rpc.WalletService.PerformDiagnosticScanForUtxos:input_type -> rpc.DiagnosticSessionDescriptor + 7, // 20: rpc.WalletService.SubmitDiagnosticLog:input_type -> rpc.DiagnosticSessionDescriptor + 12, // 21: rpc.WalletService.PrepareSweepTx:input_type -> rpc.PrepareSweepTxRequest + 14, // 22: rpc.WalletService.SignAndBroadcastSweepTx:input_type -> rpc.SignAndBroadcastSweepTxRequest + 16, // 23: rpc.WalletService.StartChallengeSetup:input_type -> rpc.ChallengeSetupRequest + 18, // 24: rpc.WalletService.FinishRecoveryCodeSetup:input_type -> rpc.FinishRecoveryCodeSetupRequest + 19, // 25: rpc.WalletService.PopulateEncryptedMuunKey:input_type -> rpc.PopulateEncryptedMuunKeyRequest + 22, // 26: rpc.WalletService.Save:input_type -> rpc.SaveRequest + 23, // 27: rpc.WalletService.Get:input_type -> rpc.GetRequest + 25, // 28: rpc.WalletService.Delete:input_type -> rpc.DeleteRequest + 26, // 29: rpc.WalletService.SaveBatch:input_type -> rpc.SaveBatchRequest + 27, // 30: rpc.WalletService.GetBatch:input_type -> rpc.GetBatchRequest + 3, // 31: rpc.WalletService.SetupSecurityCard:output_type -> rpc.XpubResponse + 30, // 32: rpc.WalletService.ResetSecurityCard:output_type -> google.protobuf.Empty + 6, // 33: rpc.WalletService.SignMessageSecurityCard:output_type -> rpc.SignMessageSecurityCardResponse + 4, // 34: rpc.WalletService.SetupSecurityCardV2:output_type -> rpc.SetupSecurityCardResponse + 7, // 35: rpc.WalletService.StartDiagnosticSession:output_type -> rpc.DiagnosticSessionDescriptor + 8, // 36: rpc.WalletService.PerformDiagnosticScanForUtxos:output_type -> rpc.ScanProgressUpdate + 11, // 37: rpc.WalletService.SubmitDiagnosticLog:output_type -> rpc.DiagnosticSubmitStatus + 13, // 38: rpc.WalletService.PrepareSweepTx:output_type -> rpc.PrepareSweepTxResponse + 15, // 39: rpc.WalletService.SignAndBroadcastSweepTx:output_type -> rpc.SignAndBroadcastSweepTxResponse + 17, // 40: rpc.WalletService.StartChallengeSetup:output_type -> rpc.SetupChallengeResponse + 30, // 41: rpc.WalletService.FinishRecoveryCodeSetup:output_type -> google.protobuf.Empty + 30, // 42: rpc.WalletService.PopulateEncryptedMuunKey:output_type -> google.protobuf.Empty + 30, // 43: rpc.WalletService.Save:output_type -> google.protobuf.Empty + 24, // 44: rpc.WalletService.Get:output_type -> rpc.GetResponse + 30, // 45: rpc.WalletService.Delete:output_type -> google.protobuf.Empty + 30, // 46: rpc.WalletService.SaveBatch:output_type -> google.protobuf.Empty + 28, // 47: rpc.WalletService.GetBatch:output_type -> rpc.GetBatchResponse + 31, // [31:48] is the sub-list for method output_type + 14, // [14:31] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_wallet_service_proto_init() } @@ -2445,12 +2804,12 @@ func file_wallet_service_proto_init() { if File_wallet_service_proto != nil { return } - file_wallet_service_proto_msgTypes[8].OneofWrappers = []any{ + file_wallet_service_proto_msgTypes[6].OneofWrappers = []any{ (*scanProgressUpdate_FoundUtxoReport)(nil), (*scanProgressUpdate_ScanComplete)(nil), } - file_wallet_service_proto_msgTypes[13].OneofWrappers = []any{} - file_wallet_service_proto_msgTypes[16].OneofWrappers = []any{ + file_wallet_service_proto_msgTypes[15].OneofWrappers = []any{} + file_wallet_service_proto_msgTypes[19].OneofWrappers = []any{ (*value_NullValue)(nil), (*value_DoubleValue)(nil), (*value_IntValue)(nil), @@ -2464,7 +2823,7 @@ func file_wallet_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_wallet_service_proto_rawDesc), len(file_wallet_service_proto_rawDesc)), NumEnums: 2, - NumMessages: 25, + NumMessages: 28, NumExtensions: 0, NumServices: 1, }, diff --git a/libwallet/presentation/api/wallet_service.proto b/libwallet/presentation/api/wallet_service.proto index 787b874d..d5f18581 100644 --- a/libwallet/presentation/api/wallet_service.proto +++ b/libwallet/presentation/api/wallet_service.proto @@ -7,22 +7,22 @@ import "google/protobuf/empty.proto"; option go_package = "github.com/muun/libwallet/api"; service WalletService { - rpc DeleteWallet(google.protobuf.Empty) returns (OperationStatus); - - // V2 - use then discard API - rpc NfcTransmit(NfcTransmitRequest) returns (NfcTransmitResponse); - // V3 - NFC security cards Native->Libwallet API rpc SetupSecurityCard(google.protobuf.Empty) returns (XpubResponse); rpc ResetSecurityCard(google.protobuf.Empty) returns (google.protobuf.Empty); rpc SignMessageSecurityCard(SignMessageSecurityCardRequest) returns (SignMessageSecurityCardResponse); + rpc SetupSecurityCardV2(google.protobuf.Empty) returns (SetupSecurityCardResponse); // Diagnostic Mode API rpc StartDiagnosticSession(google.protobuf.Empty) returns (DiagnosticSessionDescriptor); rpc PerformDiagnosticScanForUtxos(DiagnosticSessionDescriptor) returns (stream ScanProgressUpdate); rpc SubmitDiagnosticLog(DiagnosticSessionDescriptor) returns (DiagnosticSubmitStatus); + rpc PrepareSweepTx(PrepareSweepTxRequest) returns (PrepareSweepTxResponse); + rpc SignAndBroadcastSweepTx(SignAndBroadcastSweepTxRequest) returns (SignAndBroadcastSweepTxResponse); + rpc StartChallengeSetup(ChallengeSetupRequest) returns (SetupChallengeResponse); rpc FinishRecoveryCodeSetup(FinishRecoveryCodeSetupRequest) returns (google.protobuf.Empty); + rpc PopulateEncryptedMuunKey(PopulateEncryptedMuunKeyRequest) returns (google.protobuf.Empty); // Key-Value Storage rpc Save(SaveRequest) returns (google.protobuf.Empty); @@ -45,23 +45,15 @@ message ErrorDetail { string developer_message = 4; } -message OperationStatus { - string status = 1; -} - -message NfcTransmitRequest { - bytes apdu_command = 1; -} - -message NfcTransmitResponse { - bytes apdu_response = 1; - int32 status_code = 2; -} - message XpubResponse { string base58_xpub = 1; } +message SetupSecurityCardResponse { + bool is_known_provider = 1; + bool is_card_already_used = 2; +} + message SignMessageSecurityCardRequest { string message_hex = 1; } @@ -96,6 +88,29 @@ message DiagnosticSubmitStatus { string status_message = 2; } +message PrepareSweepTxRequest { + DiagnosticSessionDescriptor sessionDescriptor = 1; + string destinationAddress = 2; + double feeRateInSatsPerVByte = 3; +} + +message PrepareSweepTxResponse { + DiagnosticSessionDescriptor sessionDescriptor = 1; + string destinationAddress = 2; + int64 txSizeInBytes = 3; + int64 txTotalFee = 4; +} + +message SignAndBroadcastSweepTxRequest { + DiagnosticSessionDescriptor sessionDescriptor = 1; + string recoveryCode = 2; +} + +message SignAndBroadcastSweepTxResponse { + DiagnosticSessionDescriptor sessionDescriptor = 1; + string txid = 2; +} + message ChallengeSetupRequest { string type = 1; string public_key = 2; @@ -110,8 +125,11 @@ message SetupChallengeResponse { } message FinishRecoveryCodeSetupRequest { - string extended_server_cosigning_public_key_base58 = 1; - string recovery_code_public_key_hex = 2; + string recovery_code_public_key_hex = 1; +} + +message PopulateEncryptedMuunKeyRequest { + string recovery_code_public_key_hex = 1; } enum NullValue { diff --git a/libwallet/presentation/api/wallet_service_grpc.pb.go b/libwallet/presentation/api/wallet_service_grpc.pb.go index b7849f6b..d1c39700 100644 --- a/libwallet/presentation/api/wallet_service_grpc.pb.go +++ b/libwallet/presentation/api/wallet_service_grpc.pb.go @@ -20,16 +20,18 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - WalletService_DeleteWallet_FullMethodName = "/rpc.WalletService/DeleteWallet" - WalletService_NfcTransmit_FullMethodName = "/rpc.WalletService/NfcTransmit" WalletService_SetupSecurityCard_FullMethodName = "/rpc.WalletService/SetupSecurityCard" WalletService_ResetSecurityCard_FullMethodName = "/rpc.WalletService/ResetSecurityCard" WalletService_SignMessageSecurityCard_FullMethodName = "/rpc.WalletService/SignMessageSecurityCard" + WalletService_SetupSecurityCardV2_FullMethodName = "/rpc.WalletService/SetupSecurityCardV2" WalletService_StartDiagnosticSession_FullMethodName = "/rpc.WalletService/StartDiagnosticSession" WalletService_PerformDiagnosticScanForUtxos_FullMethodName = "/rpc.WalletService/PerformDiagnosticScanForUtxos" WalletService_SubmitDiagnosticLog_FullMethodName = "/rpc.WalletService/SubmitDiagnosticLog" + WalletService_PrepareSweepTx_FullMethodName = "/rpc.WalletService/PrepareSweepTx" + WalletService_SignAndBroadcastSweepTx_FullMethodName = "/rpc.WalletService/SignAndBroadcastSweepTx" WalletService_StartChallengeSetup_FullMethodName = "/rpc.WalletService/StartChallengeSetup" WalletService_FinishRecoveryCodeSetup_FullMethodName = "/rpc.WalletService/FinishRecoveryCodeSetup" + WalletService_PopulateEncryptedMuunKey_FullMethodName = "/rpc.WalletService/PopulateEncryptedMuunKey" WalletService_Save_FullMethodName = "/rpc.WalletService/Save" WalletService_Get_FullMethodName = "/rpc.WalletService/Get" WalletService_Delete_FullMethodName = "/rpc.WalletService/Delete" @@ -41,19 +43,20 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type WalletServiceClient interface { - DeleteWallet(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OperationStatus, error) - // V2 - use then discard API - NfcTransmit(ctx context.Context, in *NfcTransmitRequest, opts ...grpc.CallOption) (*NfcTransmitResponse, error) // V3 - NFC security cards Native->Libwallet API SetupSecurityCard(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*XpubResponse, error) ResetSecurityCard(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) SignMessageSecurityCard(ctx context.Context, in *SignMessageSecurityCardRequest, opts ...grpc.CallOption) (*SignMessageSecurityCardResponse, error) + SetupSecurityCardV2(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SetupSecurityCardResponse, error) // Diagnostic Mode API StartDiagnosticSession(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DiagnosticSessionDescriptor, error) PerformDiagnosticScanForUtxos(ctx context.Context, in *DiagnosticSessionDescriptor, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ScanProgressUpdate], error) SubmitDiagnosticLog(ctx context.Context, in *DiagnosticSessionDescriptor, opts ...grpc.CallOption) (*DiagnosticSubmitStatus, error) + PrepareSweepTx(ctx context.Context, in *PrepareSweepTxRequest, opts ...grpc.CallOption) (*PrepareSweepTxResponse, error) + SignAndBroadcastSweepTx(ctx context.Context, in *SignAndBroadcastSweepTxRequest, opts ...grpc.CallOption) (*SignAndBroadcastSweepTxResponse, error) StartChallengeSetup(ctx context.Context, in *ChallengeSetupRequest, opts ...grpc.CallOption) (*SetupChallengeResponse, error) FinishRecoveryCodeSetup(ctx context.Context, in *FinishRecoveryCodeSetupRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + PopulateEncryptedMuunKey(ctx context.Context, in *PopulateEncryptedMuunKeyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // Key-Value Storage Save(ctx context.Context, in *SaveRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) @@ -70,26 +73,6 @@ func NewWalletServiceClient(cc grpc.ClientConnInterface) WalletServiceClient { return &walletServiceClient{cc} } -func (c *walletServiceClient) DeleteWallet(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OperationStatus, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(OperationStatus) - err := c.cc.Invoke(ctx, WalletService_DeleteWallet_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *walletServiceClient) NfcTransmit(ctx context.Context, in *NfcTransmitRequest, opts ...grpc.CallOption) (*NfcTransmitResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(NfcTransmitResponse) - err := c.cc.Invoke(ctx, WalletService_NfcTransmit_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *walletServiceClient) SetupSecurityCard(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*XpubResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(XpubResponse) @@ -120,6 +103,16 @@ func (c *walletServiceClient) SignMessageSecurityCard(ctx context.Context, in *S return out, nil } +func (c *walletServiceClient) SetupSecurityCardV2(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SetupSecurityCardResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SetupSecurityCardResponse) + err := c.cc.Invoke(ctx, WalletService_SetupSecurityCardV2_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *walletServiceClient) StartDiagnosticSession(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DiagnosticSessionDescriptor, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DiagnosticSessionDescriptor) @@ -159,6 +152,26 @@ func (c *walletServiceClient) SubmitDiagnosticLog(ctx context.Context, in *Diagn return out, nil } +func (c *walletServiceClient) PrepareSweepTx(ctx context.Context, in *PrepareSweepTxRequest, opts ...grpc.CallOption) (*PrepareSweepTxResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PrepareSweepTxResponse) + err := c.cc.Invoke(ctx, WalletService_PrepareSweepTx_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) SignAndBroadcastSweepTx(ctx context.Context, in *SignAndBroadcastSweepTxRequest, opts ...grpc.CallOption) (*SignAndBroadcastSweepTxResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SignAndBroadcastSweepTxResponse) + err := c.cc.Invoke(ctx, WalletService_SignAndBroadcastSweepTx_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *walletServiceClient) StartChallengeSetup(ctx context.Context, in *ChallengeSetupRequest, opts ...grpc.CallOption) (*SetupChallengeResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SetupChallengeResponse) @@ -179,6 +192,16 @@ func (c *walletServiceClient) FinishRecoveryCodeSetup(ctx context.Context, in *F return out, nil } +func (c *walletServiceClient) PopulateEncryptedMuunKey(ctx context.Context, in *PopulateEncryptedMuunKeyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, WalletService_PopulateEncryptedMuunKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *walletServiceClient) Save(ctx context.Context, in *SaveRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) @@ -233,19 +256,20 @@ func (c *walletServiceClient) GetBatch(ctx context.Context, in *GetBatchRequest, // All implementations must embed UnimplementedWalletServiceServer // for forward compatibility. type WalletServiceServer interface { - DeleteWallet(context.Context, *emptypb.Empty) (*OperationStatus, error) - // V2 - use then discard API - NfcTransmit(context.Context, *NfcTransmitRequest) (*NfcTransmitResponse, error) // V3 - NFC security cards Native->Libwallet API SetupSecurityCard(context.Context, *emptypb.Empty) (*XpubResponse, error) ResetSecurityCard(context.Context, *emptypb.Empty) (*emptypb.Empty, error) SignMessageSecurityCard(context.Context, *SignMessageSecurityCardRequest) (*SignMessageSecurityCardResponse, error) + SetupSecurityCardV2(context.Context, *emptypb.Empty) (*SetupSecurityCardResponse, error) // Diagnostic Mode API StartDiagnosticSession(context.Context, *emptypb.Empty) (*DiagnosticSessionDescriptor, error) PerformDiagnosticScanForUtxos(*DiagnosticSessionDescriptor, grpc.ServerStreamingServer[ScanProgressUpdate]) error SubmitDiagnosticLog(context.Context, *DiagnosticSessionDescriptor) (*DiagnosticSubmitStatus, error) + PrepareSweepTx(context.Context, *PrepareSweepTxRequest) (*PrepareSweepTxResponse, error) + SignAndBroadcastSweepTx(context.Context, *SignAndBroadcastSweepTxRequest) (*SignAndBroadcastSweepTxResponse, error) StartChallengeSetup(context.Context, *ChallengeSetupRequest) (*SetupChallengeResponse, error) FinishRecoveryCodeSetup(context.Context, *FinishRecoveryCodeSetupRequest) (*emptypb.Empty, error) + PopulateEncryptedMuunKey(context.Context, *PopulateEncryptedMuunKeyRequest) (*emptypb.Empty, error) // Key-Value Storage Save(context.Context, *SaveRequest) (*emptypb.Empty, error) Get(context.Context, *GetRequest) (*GetResponse, error) @@ -262,12 +286,6 @@ type WalletServiceServer interface { // pointer dereference when methods are called. type UnimplementedWalletServiceServer struct{} -func (UnimplementedWalletServiceServer) DeleteWallet(context.Context, *emptypb.Empty) (*OperationStatus, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteWallet not implemented") -} -func (UnimplementedWalletServiceServer) NfcTransmit(context.Context, *NfcTransmitRequest) (*NfcTransmitResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method NfcTransmit not implemented") -} func (UnimplementedWalletServiceServer) SetupSecurityCard(context.Context, *emptypb.Empty) (*XpubResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SetupSecurityCard not implemented") } @@ -277,6 +295,9 @@ func (UnimplementedWalletServiceServer) ResetSecurityCard(context.Context, *empt func (UnimplementedWalletServiceServer) SignMessageSecurityCard(context.Context, *SignMessageSecurityCardRequest) (*SignMessageSecurityCardResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SignMessageSecurityCard not implemented") } +func (UnimplementedWalletServiceServer) SetupSecurityCardV2(context.Context, *emptypb.Empty) (*SetupSecurityCardResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetupSecurityCardV2 not implemented") +} func (UnimplementedWalletServiceServer) StartDiagnosticSession(context.Context, *emptypb.Empty) (*DiagnosticSessionDescriptor, error) { return nil, status.Errorf(codes.Unimplemented, "method StartDiagnosticSession not implemented") } @@ -286,12 +307,21 @@ func (UnimplementedWalletServiceServer) PerformDiagnosticScanForUtxos(*Diagnosti func (UnimplementedWalletServiceServer) SubmitDiagnosticLog(context.Context, *DiagnosticSessionDescriptor) (*DiagnosticSubmitStatus, error) { return nil, status.Errorf(codes.Unimplemented, "method SubmitDiagnosticLog not implemented") } +func (UnimplementedWalletServiceServer) PrepareSweepTx(context.Context, *PrepareSweepTxRequest) (*PrepareSweepTxResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PrepareSweepTx not implemented") +} +func (UnimplementedWalletServiceServer) SignAndBroadcastSweepTx(context.Context, *SignAndBroadcastSweepTxRequest) (*SignAndBroadcastSweepTxResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignAndBroadcastSweepTx not implemented") +} func (UnimplementedWalletServiceServer) StartChallengeSetup(context.Context, *ChallengeSetupRequest) (*SetupChallengeResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method StartChallengeSetup not implemented") } func (UnimplementedWalletServiceServer) FinishRecoveryCodeSetup(context.Context, *FinishRecoveryCodeSetupRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method FinishRecoveryCodeSetup not implemented") } +func (UnimplementedWalletServiceServer) PopulateEncryptedMuunKey(context.Context, *PopulateEncryptedMuunKeyRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method PopulateEncryptedMuunKey not implemented") +} func (UnimplementedWalletServiceServer) Save(context.Context, *SaveRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Save not implemented") } @@ -328,42 +358,6 @@ func RegisterWalletServiceServer(s grpc.ServiceRegistrar, srv WalletServiceServe s.RegisterService(&WalletService_ServiceDesc, srv) } -func _WalletService_DeleteWallet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(emptypb.Empty) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(WalletServiceServer).DeleteWallet(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: WalletService_DeleteWallet_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(WalletServiceServer).DeleteWallet(ctx, req.(*emptypb.Empty)) - } - return interceptor(ctx, in, info, handler) -} - -func _WalletService_NfcTransmit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(NfcTransmitRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(WalletServiceServer).NfcTransmit(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: WalletService_NfcTransmit_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(WalletServiceServer).NfcTransmit(ctx, req.(*NfcTransmitRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _WalletService_SetupSecurityCard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { @@ -418,6 +412,24 @@ func _WalletService_SignMessageSecurityCard_Handler(srv interface{}, ctx context return interceptor(ctx, in, info, handler) } +func _WalletService_SetupSecurityCardV2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletServiceServer).SetupSecurityCardV2(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WalletService_SetupSecurityCardV2_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletServiceServer).SetupSecurityCardV2(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _WalletService_StartDiagnosticSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { @@ -465,6 +477,42 @@ func _WalletService_SubmitDiagnosticLog_Handler(srv interface{}, ctx context.Con return interceptor(ctx, in, info, handler) } +func _WalletService_PrepareSweepTx_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PrepareSweepTxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletServiceServer).PrepareSweepTx(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WalletService_PrepareSweepTx_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletServiceServer).PrepareSweepTx(ctx, req.(*PrepareSweepTxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WalletService_SignAndBroadcastSweepTx_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignAndBroadcastSweepTxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletServiceServer).SignAndBroadcastSweepTx(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WalletService_SignAndBroadcastSweepTx_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletServiceServer).SignAndBroadcastSweepTx(ctx, req.(*SignAndBroadcastSweepTxRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _WalletService_StartChallengeSetup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ChallengeSetupRequest) if err := dec(in); err != nil { @@ -501,6 +549,24 @@ func _WalletService_FinishRecoveryCodeSetup_Handler(srv interface{}, ctx context return interceptor(ctx, in, info, handler) } +func _WalletService_PopulateEncryptedMuunKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PopulateEncryptedMuunKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletServiceServer).PopulateEncryptedMuunKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WalletService_PopulateEncryptedMuunKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletServiceServer).PopulateEncryptedMuunKey(ctx, req.(*PopulateEncryptedMuunKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _WalletService_Save_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SaveRequest) if err := dec(in); err != nil { @@ -598,14 +664,6 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "rpc.WalletService", HandlerType: (*WalletServiceServer)(nil), Methods: []grpc.MethodDesc{ - { - MethodName: "DeleteWallet", - Handler: _WalletService_DeleteWallet_Handler, - }, - { - MethodName: "NfcTransmit", - Handler: _WalletService_NfcTransmit_Handler, - }, { MethodName: "SetupSecurityCard", Handler: _WalletService_SetupSecurityCard_Handler, @@ -618,6 +676,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SignMessageSecurityCard", Handler: _WalletService_SignMessageSecurityCard_Handler, }, + { + MethodName: "SetupSecurityCardV2", + Handler: _WalletService_SetupSecurityCardV2_Handler, + }, { MethodName: "StartDiagnosticSession", Handler: _WalletService_StartDiagnosticSession_Handler, @@ -626,6 +688,14 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SubmitDiagnosticLog", Handler: _WalletService_SubmitDiagnosticLog_Handler, }, + { + MethodName: "PrepareSweepTx", + Handler: _WalletService_PrepareSweepTx_Handler, + }, + { + MethodName: "SignAndBroadcastSweepTx", + Handler: _WalletService_SignAndBroadcastSweepTx_Handler, + }, { MethodName: "StartChallengeSetup", Handler: _WalletService_StartChallengeSetup_Handler, @@ -634,6 +704,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{ MethodName: "FinishRecoveryCodeSetup", Handler: _WalletService_FinishRecoveryCodeSetup_Handler, }, + { + MethodName: "PopulateEncryptedMuunKey", + Handler: _WalletService_PopulateEncryptedMuunKey_Handler, + }, { MethodName: "Save", Handler: _WalletService_Save_Handler, diff --git a/libwallet/presentation/muun_key_verification_test.go b/libwallet/presentation/muun_key_verification_test.go new file mode 100644 index 00000000..88f81e3d --- /dev/null +++ b/libwallet/presentation/muun_key_verification_test.go @@ -0,0 +1,701 @@ +package presentation + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/muun/libwallet" + "github.com/muun/libwallet/domain/action/challenge_keys" + "github.com/muun/libwallet/domain/action/recovery" + "github.com/muun/libwallet/domain/model/encrypted_key_v3" + "github.com/muun/libwallet/presentation/api" + "github.com/muun/libwallet/recoverycode" + "github.com/muun/libwallet/service/model" + "github.com/muun/libwallet/storage" + "github.com/test-go/testify/assert" + "io" + "net/http" + "testing" + "time" +) + +func TestEncryptedMuunKeyAfterFinishSetupRecoveryCode_Integration(t *testing.T) { + + setupKeyValueStorage(t, storage.BuildStorageSchema()) + + recoveryCode := recoverycode.Generate() + recoveryCodePrivateKey, err := recoverycode.ConvertToKey(recoveryCode, "") + if err != nil { + t.Fatal(err) + } + + userPrivateKey, err := libwallet.NewHDPrivateKey(randomBytes(32), libwallet.Regtest()) + if err != nil { + t.Fatal(err) + } + + encryptedPrivateKey, err := libwallet.KeyEncrypt(userPrivateKey, recoveryCode) + if err != nil { + t.Fatal(err) + } + + recoveryCodePublicKey := recoveryCodePrivateKey.PubKey() + + createFirstSessionOkJson := createFirstSession(t, userPrivateKey.PublicKey()) + muunPublicKey, err := libwallet.NewHDPublicKeyFromString( + createFirstSessionOkJson.CosigningPublicKey.Key, + createFirstSessionOkJson.CosigningPublicKey.Path, + libwallet.Regtest()) + if err != nil { + t.Fatal(err) + } + + walletServer.keyProvider = NewMockKeyProvider(userPrivateKey, muunPublicKey, 0) + computeAndStoreEncryptedMuunKeyAction := recovery.NewComputeAndStoreEncryptedMuunKeyAction( + walletServer.keyValueStorage, + walletServer.keyProvider, + ) + walletServer.finishChallengeSetup = challenge_keys.NewFinishChallengeSetupAction( + walletServer.houstonService, + walletServer.keyValueStorage, + computeAndStoreEncryptedMuunKeyAction, + ) + + _, err = walletServer.StartChallengeSetup( + context.Background(), + api.ChallengeSetupRequest_builder{ + Type: "RECOVERY_CODE", + PublicKey: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), + Salt: "dfb80ea8c30959e8", + EncryptedPrivateKey: encryptedPrivateKey, + Version: 2, + }.Build(), + ) + if err != nil { + t.Fatal(err) + } + + _, err = walletServer.FinishRecoveryCodeSetup( + context.Background(), + api.FinishRecoveryCodeSetupRequest_builder{ + RecoveryCodePublicKeyHex: hex.EncodeToString( + recoveryCodePublicKey.SerializeCompressed(), + ), + }.Build(), + ) + if err != nil { + t.Fatal(err) + } + + mayRetrieveEncryptedMuunKey := recovery.NewMayRetrieveEncryptedMuunKeyAction( + walletServer.keyValueStorage, + ) + encryptedMuunKeyWithStatus, err := mayRetrieveEncryptedMuunKey.Run() + if err != nil { + t.Fatal(err) + } + if encryptedMuunKeyWithStatus.Status == recovery.HasNoEncryptedMuunKey { + t.Fatal("Encrypted muun key should be available") + } + + decryptAndAssertDecryptedMuunKeyIsCorrect( + t, + recoveryCodePrivateKey, + *encryptedMuunKeyWithStatus.EncryptedMuunKey, + muunPublicKey, + ) +} + +func TestPollForVerifiedEncryptedMuunKey_Integration(t *testing.T) { + + setupKeyValueStorage(t, storage.BuildStorageSchema()) + + recoveryCode := recoverycode.Generate() + recoveryCodePrivateKey, err := recoverycode.ConvertToKey(recoveryCode, "") + if err != nil { + t.Fatal(err) + } + + userPrivateKey, err := libwallet.NewHDPrivateKey(randomBytes(32), libwallet.Regtest()) + if err != nil { + t.Fatal(err) + } + + encryptedPrivateKey, err := libwallet.KeyEncrypt(userPrivateKey, recoveryCode) + if err != nil { + t.Fatal(err) + } + + recoveryCodePublicKey := recoveryCodePrivateKey.PubKey() + + createFirstSessionOkJson := createFirstSession(t, userPrivateKey.PublicKey()) + muunPublicKey, err := libwallet.NewHDPublicKeyFromString( + createFirstSessionOkJson.CosigningPublicKey.Key, + createFirstSessionOkJson.CosigningPublicKey.Path, + libwallet.Regtest()) + if err != nil { + t.Fatal(err) + } + + walletServer.keyProvider = NewMockKeyProvider(userPrivateKey, muunPublicKey, 0) + computeAndStoreEncryptedMuunKeyAction := recovery.NewComputeAndStoreEncryptedMuunKeyAction( + walletServer.keyValueStorage, + walletServer.keyProvider, + ) + walletServer.finishChallengeSetup = challenge_keys.NewFinishChallengeSetupAction( + walletServer.houstonService, + walletServer.keyValueStorage, + computeAndStoreEncryptedMuunKeyAction, + ) + walletServer.populateEncryptedMuunKey = recovery.NewPopulateEncryptedMuunKeyAction( + walletServer.houstonService, + walletServer.keyValueStorage, + walletServer.keyProvider, + ) + + _, err = walletServer.StartChallengeSetup( + context.Background(), + api.ChallengeSetupRequest_builder{ + Type: "RECOVERY_CODE", + PublicKey: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), + Salt: "dfb80ea8c30959e8", + EncryptedPrivateKey: encryptedPrivateKey, + Version: 2, + }.Build(), + ) + if err != nil { + t.Fatal(err) + } + + _, err = walletServer.FinishRecoveryCodeSetup( + context.Background(), + api.FinishRecoveryCodeSetupRequest_builder{ + RecoveryCodePublicKeyHex: hex.EncodeToString( + recoveryCodePublicKey.SerializeCompressed(), + ), + }.Build(), + ) + if err != nil { + t.Fatal(err) + } + + err = WaitForCondition(1*time.Second, func() (bool, error) { + result, err := walletServer.houstonService.VerifiableMuunKey() + if err != nil { + return false, err + } + return result.Proof != nil, nil + }) + if err != nil { + t.Fatalf("Timed out: %s", err) + } + + // We now poll with the PopulateEncryptedMuunKey endpoint + + populateRequest := api.PopulateEncryptedMuunKeyRequest_builder{ + RecoveryCodePublicKeyHex: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), + }.Build() + + _, err = walletServer.PopulateEncryptedMuunKey(context.Background(), populateRequest) + if err != nil { + t.Fatal(err) + } + + // Now we should have a verified encrypted muun key + mayRetrieveEncryptedMuunKey := recovery.NewMayRetrieveEncryptedMuunKeyAction( + walletServer.keyValueStorage, + ) + encryptedMuunKeyWithStatus, err := mayRetrieveEncryptedMuunKey.Run() + if err != nil { + t.Fatal(err) + } + if encryptedMuunKeyWithStatus.Status != recovery.HasVerifiedEncryptedMuunKey { + t.Fatal("The user should have a verified muun key at this point.") + } + + // We poll again + _, err = walletServer.PopulateEncryptedMuunKey(context.Background(), populateRequest) + if err != nil { + t.Fatal(err) + } + + encryptedMuunKeyWithStatusAgain, err := mayRetrieveEncryptedMuunKey.Run() + if err != nil { + t.Fatal(err) + } + + if encryptedMuunKeyWithStatus.Status != recovery.HasVerifiedEncryptedMuunKey { + t.Fatal("The user should still have a verified muun key at this point.") + } + + // Polling should not modify the encrypted muun key + if *encryptedMuunKeyWithStatus.EncryptedMuunKey != *encryptedMuunKeyWithStatusAgain.EncryptedMuunKey { + t.Fatal("The verified muun key should not change when polling") + } + + decryptAndAssertDecryptedMuunKeyIsCorrect( + t, + recoveryCodePrivateKey, + *encryptedMuunKeyWithStatus.EncryptedMuunKey, + muunPublicKey, + ) +} + +func TestPollForVerifiedEncryptedMuunKeyWithDelay_Integration(t *testing.T) { + + setupKeyValueStorage(t, storage.BuildStorageSchema()) + + recoveryCode := recoverycode.Generate() + recoveryCodePrivateKey, err := recoverycode.ConvertToKey(recoveryCode, "") + if err != nil { + t.Fatal(err) + } + + userPrivateKey, err := libwallet.NewHDPrivateKey(randomBytes(32), libwallet.Regtest()) + if err != nil { + t.Fatal(err) + } + + encryptedPrivateKey, err := libwallet.KeyEncrypt(userPrivateKey, recoveryCode) + if err != nil { + t.Fatal(err) + } + + recoveryCodePublicKey := recoveryCodePrivateKey.PubKey() + + createFirstSessionOkJson := createFirstSession(t, userPrivateKey.PublicKey()) + muunPublicKey, err := libwallet.NewHDPublicKeyFromString( + createFirstSessionOkJson.CosigningPublicKey.Key, + createFirstSessionOkJson.CosigningPublicKey.Path, + libwallet.Regtest()) + if err != nil { + t.Fatal(err) + } + walletServer.keyProvider = NewMockKeyProvider(userPrivateKey, muunPublicKey, 0) + computeAndStoreEncryptedMuunKeyAction := recovery.NewComputeAndStoreEncryptedMuunKeyAction( + walletServer.keyValueStorage, + walletServer.keyProvider, + ) + walletServer.finishChallengeSetup = challenge_keys.NewFinishChallengeSetupAction( + walletServer.houstonService, + walletServer.keyValueStorage, + computeAndStoreEncryptedMuunKeyAction, + ) + walletServer.populateEncryptedMuunKey = recovery.NewPopulateEncryptedMuunKeyAction( + walletServer.houstonService, + walletServer.keyValueStorage, + walletServer.keyProvider, + ) + + delayProverJob(t, recoveryCodePublicKey) + + _, err = walletServer.StartChallengeSetup( + context.Background(), + api.ChallengeSetupRequest_builder{ + Type: "RECOVERY_CODE", + PublicKey: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), + Salt: "dfb80ea8c30959e8", + EncryptedPrivateKey: encryptedPrivateKey, + Version: 2, + }.Build(), + ) + if err != nil { + t.Fatal(err) + } + + _, err = walletServer.FinishRecoveryCodeSetup( + context.Background(), + api.FinishRecoveryCodeSetupRequest_builder{ + RecoveryCodePublicKeyHex: hex.EncodeToString( + recoveryCodePublicKey.SerializeCompressed(), + ), + }.Build(), + ) + if err != nil { + t.Fatal(err) + } + + mayRetrieveEncryptedMuunKey := recovery.NewMayRetrieveEncryptedMuunKeyAction(walletServer.keyValueStorage) + encryptedMuunKeyWithStatus, err := mayRetrieveEncryptedMuunKey.Run() + if err != nil { + t.Fatal(err) + } + if encryptedMuunKeyWithStatus.Status != recovery.OnlyHasUnverifiedEncryptedMuunKey { + t.Fatal("The user should only have an unverified key at this point.") + } + + // We now poll with the PopulateEncryptedMuunKey endpoint + _, err = walletServer.PopulateEncryptedMuunKey( + context.Background(), + api.PopulateEncryptedMuunKeyRequest_builder{ + RecoveryCodePublicKeyHex: hex.EncodeToString( + recoveryCodePublicKey.SerializeCompressed(), + ), + }.Build(), + ) + if err != nil { + t.Fatal(err) + } + + encryptedMuunKeyWithStatusAgain, err := mayRetrieveEncryptedMuunKey.Run() + if err != nil { + t.Fatal(err) + } + + if encryptedMuunKeyWithStatusAgain.Status != recovery.OnlyHasUnverifiedEncryptedMuunKey { + t.Fatal("The user should only have an unverified key at this point.") + } + + // Polling should not modify the encrypted muun key + if *encryptedMuunKeyWithStatus.EncryptedMuunKey != *encryptedMuunKeyWithStatusAgain.EncryptedMuunKey { + t.Fatal("The unverified muun key should not change when polling") + } + + decryptAndAssertDecryptedMuunKeyIsCorrect( + t, + recoveryCodePrivateKey, + *encryptedMuunKeyWithStatus.EncryptedMuunKey, + muunPublicKey, + ) +} + +func TestVerifiedMuunKeyForExistingUsers_Integration(t *testing.T) { + + setupKeyValueStorage(t, storage.BuildStorageSchema()) + + recoveryCode := recoverycode.Generate() + recoveryCodePrivateKey, err := recoverycode.ConvertToKey(recoveryCode, "") + if err != nil { + t.Fatal(err) + } + + userPrivateKey, err := libwallet.NewHDPrivateKey(randomBytes(32), libwallet.Regtest()) + if err != nil { + t.Fatal(err) + } + + encryptedPrivateKey, err := libwallet.KeyEncrypt(userPrivateKey, recoveryCode) + if err != nil { + t.Fatal(err) + } + + recoveryCodePublicKey := recoveryCodePrivateKey.PubKey() + + createFirstSessionOkJson := createFirstSession(t, userPrivateKey.PublicKey()) + muunPublicKey, err := libwallet.NewHDPublicKeyFromString( + createFirstSessionOkJson.CosigningPublicKey.Key, + createFirstSessionOkJson.CosigningPublicKey.Path, + libwallet.Regtest()) + if err != nil { + t.Fatal(err) + } + + walletServer.keyProvider = NewMockKeyProvider(userPrivateKey, muunPublicKey, 0) + computeAndStoreEncryptedMuunKeyAction := recovery.NewComputeAndStoreEncryptedMuunKeyAction( + walletServer.keyValueStorage, + walletServer.keyProvider, + ) + walletServer.finishChallengeSetup = challenge_keys.NewFinishChallengeSetupAction( + walletServer.houstonService, + walletServer.keyValueStorage, + computeAndStoreEncryptedMuunKeyAction, + ) + walletServer.populateEncryptedMuunKey = recovery.NewPopulateEncryptedMuunKeyAction( + walletServer.houstonService, + walletServer.keyValueStorage, + walletServer.keyProvider, + ) + + _, err = walletServer.StartChallengeSetup( + context.Background(), + api.ChallengeSetupRequest_builder{ + Type: "RECOVERY_CODE", + PublicKey: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), + Salt: "dfb80ea8c30959e8", + EncryptedPrivateKey: encryptedPrivateKey, + Version: 2, + }.Build(), + ) + if err != nil { + t.Fatal(err) + } + + // Use the legacy finish endpoint + err = walletServer.houstonService.ChallengeKeySetupFinish(model.ChallengeSetupVerifyJson{ + ChallengeType: "RECOVERY_CODE", + PublicKey: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), + }) + if err != nil { + t.Fatal(err) + } + + // The user should not have an encrypted muun key at this point + mayRetrieveEncryptedMuunKey := recovery.NewMayRetrieveEncryptedMuunKeyAction( + walletServer.keyValueStorage, + ) + encryptedMuunKeyWithStatus, err := mayRetrieveEncryptedMuunKey.Run() + if err != nil { + t.Fatal(err) + } + if encryptedMuunKeyWithStatus.Status != recovery.HasNoEncryptedMuunKey { + t.Fatal("The user should not have an encrypted muun key at this point.") + } + + err = WaitForCondition(1*time.Second, func() (bool, error) { + result, err := walletServer.houstonService.VerifiableMuunKey() + if err != nil { + return false, err + } + return result.Proof != nil, nil + }) + if err != nil { + t.Fatalf("Timed out: %s", err) + } + + // We poll with the PopulateEncryptedMuunKey endpoint simulating a migration + _, err = walletServer.PopulateEncryptedMuunKey( + context.Background(), + api.PopulateEncryptedMuunKeyRequest_builder{ + RecoveryCodePublicKeyHex: hex.EncodeToString( + recoveryCodePublicKey.SerializeCompressed(), + ), + }.Build(), + ) + if err != nil { + t.Fatal(err) + } + + encryptedMuunKeyWithStatus, err = mayRetrieveEncryptedMuunKey.Run() + if err != nil { + t.Fatal(err) + } + if encryptedMuunKeyWithStatus.Status != recovery.HasVerifiedEncryptedMuunKey { + t.Fatal("The user should have a verified muun key at this point.") + } + + decryptAndAssertDecryptedMuunKeyIsCorrect( + t, + recoveryCodePrivateKey, + *encryptedMuunKeyWithStatus.EncryptedMuunKey, + muunPublicKey, + ) +} + +func TestUnverifiedEncryptedMuunKeyForExistingUsers_Integration(t *testing.T) { + + setupKeyValueStorage(t, storage.BuildStorageSchema()) + + recoveryCode := recoverycode.Generate() + recoveryCodePrivateKey, err := recoverycode.ConvertToKey(recoveryCode, "") + if err != nil { + t.Fatal(err) + } + + userPrivateKey, err := libwallet.NewHDPrivateKey(randomBytes(32), libwallet.Regtest()) + if err != nil { + t.Fatal(err) + } + + encryptedPrivateKey, err := libwallet.KeyEncrypt(userPrivateKey, recoveryCode) + if err != nil { + t.Fatal(err) + } + + recoveryCodePublicKey := recoveryCodePrivateKey.PubKey() + + createFirstSessionOkJson := createFirstSession(t, userPrivateKey.PublicKey()) + muunPublicKey, err := libwallet.NewHDPublicKeyFromString( + createFirstSessionOkJson.CosigningPublicKey.Key, + createFirstSessionOkJson.CosigningPublicKey.Path, + libwallet.Regtest(), + ) + if err != nil { + t.Fatal(err) + } + + walletServer.keyProvider = NewMockKeyProvider(userPrivateKey, muunPublicKey, 0) + computeAndStoreEncryptedMuunKeyAction := recovery.NewComputeAndStoreEncryptedMuunKeyAction( + walletServer.keyValueStorage, + walletServer.keyProvider, + ) + walletServer.finishChallengeSetup = challenge_keys.NewFinishChallengeSetupAction( + walletServer.houstonService, + walletServer.keyValueStorage, + computeAndStoreEncryptedMuunKeyAction, + ) + walletServer.populateEncryptedMuunKey = recovery.NewPopulateEncryptedMuunKeyAction( + walletServer.houstonService, + walletServer.keyValueStorage, + walletServer.keyProvider, + ) + + delayProverJob(t, recoveryCodePublicKey) + + _, err = walletServer.StartChallengeSetup( + context.Background(), + api.ChallengeSetupRequest_builder{ + Type: "RECOVERY_CODE", + PublicKey: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), + Salt: "dfb80ea8c30959e8", + EncryptedPrivateKey: encryptedPrivateKey, + Version: 2, + }.Build(), + ) + if err != nil { + t.Fatal(err) + } + + // Use the legacy finish endpoint + err = walletServer.houstonService.ChallengeKeySetupFinish(model.ChallengeSetupVerifyJson{ + ChallengeType: "RECOVERY_CODE", + PublicKey: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), + }) + if err != nil { + t.Fatal(err) + } + + // The user should not have an encrypted muun key at this point + mayRetrieveEncryptedMuunKey := recovery.NewMayRetrieveEncryptedMuunKeyAction( + walletServer.keyValueStorage, + ) + encryptedMuunKeyWithStatus, err := mayRetrieveEncryptedMuunKey.Run() + if err != nil { + t.Fatal(err) + } + if encryptedMuunKeyWithStatus.Status != recovery.HasNoEncryptedMuunKey { + t.Fatal("The user should not have an encrypted muun key at this point.") + } + + // We poll with the PopulateEncryptedMuunKey endpoint simulating a migration + _, err = walletServer.PopulateEncryptedMuunKey( + context.Background(), + api.PopulateEncryptedMuunKeyRequest_builder{ + RecoveryCodePublicKeyHex: hex.EncodeToString( + recoveryCodePublicKey.SerializeCompressed(), + ), + }.Build(), + ) + if err != nil { + t.Fatal(err) + } + + // Now the user should have an unverified encrypted muun key + encryptedMuunKeyWithStatus, err = mayRetrieveEncryptedMuunKey.Run() + if err != nil { + t.Fatal(err) + } + if encryptedMuunKeyWithStatus.Status != recovery.OnlyHasUnverifiedEncryptedMuunKey { + t.Fatal("The user should only have an unverified key at this point.") + } + + decryptAndAssertDecryptedMuunKeyIsCorrect( + t, + recoveryCodePrivateKey, + *encryptedMuunKeyWithStatus.EncryptedMuunKey, + muunPublicKey, + ) +} + +func decryptAndAssertDecryptedMuunKeyIsCorrect( + t *testing.T, + recoveryCodePrivateKey *btcec.PrivateKey, + encryptedMuunKey string, + expectedMuunPublicKey *libwallet.HDPublicKey, +) { + + decryptedMuunPrivateKey, err := encrypted_key_v3.DecryptExtendedKey( + recoveryCodePrivateKey, + encryptedMuunKey, + libwallet.Regtest()) + if err != nil { + t.Fatal(err) + } + + // Comparing muunPublicKey.String() to decryptedMuunKey.PublicKey().String() won't work because + // we set the parentFingerprint to zero when reconstructing the key. + // + // We can instead compare the public keys and the chaincodes and also compare the keys after + // deriving at some path. + + if !bytes.Equal(expectedMuunPublicKey.Raw(), decryptedMuunPrivateKey.PublicKey().Raw()) { + t.Fatal("decrypted public key does not match original public key") + } + + if !bytes.Equal(expectedMuunPublicKey.ChainCode(), decryptedMuunPrivateKey.ChainCode()) { + t.Fatal("decrypted chain code does not match original chain code") + } + + somePath := "m/schema:1'/recovery:1'/a:2/b:3/c:5" + derivedPrivateKey, err := decryptedMuunPrivateKey.DeriveTo(somePath) + if err != nil { + t.Fatal(err) + } + derivedPublicKey, err := expectedMuunPublicKey.DeriveTo(somePath) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, derivedPublicKey.String(), derivedPrivateKey.PublicKey().String()) +} + +func WaitForCondition(timeout time.Duration, condition func() (bool, error)) error { + end := time.Now().Add(timeout) + + var err error + + for time.Now().Before(end) { + holds, innerErr := condition() + + if holds { + return nil + } + + if innerErr != nil { + err = innerErr + } + + time.Sleep(100 * time.Millisecond) + } + + if err != nil { + return fmt.Errorf("timed out waiting for condition, failed with error %w", err) + } else { + return fmt.Errorf("timed out waiting for condition") + } +} + +// 127.0.0.1 instead of localhost to avoid problems with network interfaces in local env +const proverUrl = "http://127.0.0.1:8130" + +func delayProverJob(t *testing.T, recoveryCodePublicKey *btcec.PublicKey) { + // This requests prevents the proof from completing, this exercising the unhappy path + requestUrl := proverUrl + "/testing/delay-job?pattern=" + hex.EncodeToString( + recoveryCodePublicKey.SerializeCompressed(), + ) + req, err := http.NewRequest("POST", requestUrl, nil) + if err != nil { + t.Fatal(err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode >= 300 || resp.StatusCode < 200 { + r, _ := io.ReadAll(resp.Body) + t.Fatalf("request failed with status code %d, resp: %s", resp.StatusCode, string(r)) + } + +} + +func randomBytes(count int) []byte { + buf := make([]byte, count) + _, err := rand.Read(buf) + if err != nil { + panic("couldn't read random bytes") + } + + return buf +} diff --git a/libwallet/presentation/wallet_server.go b/libwallet/presentation/wallet_server.go index 9f480597..6551975f 100644 --- a/libwallet/presentation/wallet_server.go +++ b/libwallet/presentation/wallet_server.go @@ -5,20 +5,20 @@ import ( "context" "encoding/hex" "fmt" + "github.com/btcsuite/btcd/btcutil" "log/slog" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil/base58" - "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/google/uuid" "github.com/muun/libwallet" "github.com/muun/libwallet/app_provided_data" + "github.com/muun/libwallet/data/keys" "github.com/muun/libwallet/domain/action/challenge_keys" "github.com/muun/libwallet/domain/action/diagnostic_mode_reports" "github.com/muun/libwallet/domain/action/nfc" + "github.com/muun/libwallet/domain/action/recovery" "github.com/muun/libwallet/domain/diagnostic_mode" - "github.com/muun/libwallet/domain/recovery" - "github.com/muun/libwallet/electrum" apierrors "github.com/muun/libwallet/errors" "github.com/muun/libwallet/presentation/api" "github.com/muun/libwallet/service" @@ -30,86 +30,104 @@ import ( type WalletServer struct { api.UnsafeWalletServiceServer - nfcBridge app_provided_data.NfcBridge - keysProvider app_provided_data.KeyProvider - network *libwallet.Network - houstonService *service.HoustonService - keyValueStorage *storage.KeyValueStorage - startChallengeSetup *challenge_keys.StartChallengeSetupAction - submitDiagnostic *diagnostic_mode_reports.SubmitDiagnosticAction - pairSecurityCard *nfc.PairSecurityCardAction - resetSecurityCard *nfc.ResetSecurityCardAction - signMessageSecurityCard *nfc.SignMessageSecurityCardAction + nfcBridge app_provided_data.NfcBridge + keyProvider keys.KeyProvider + network *libwallet.Network + houstonService service.HoustonService + keyValueStorage *storage.KeyValueStorage + startChallengeSetup *challenge_keys.StartChallengeSetupAction + finishChallengeSetup *challenge_keys.FinishChallengeSetupAction + populateEncryptedMuunKey *recovery.PopulateEncryptedMuunKeyAction + scanForFunds *recovery.ScanForFundsAction + submitDiagnostic *diagnostic_mode_reports.SubmitDiagnosticAction + buildSweepTx *recovery.BuildSweepTxAction + signSweepTx *recovery.SignSweepTxAction + pairSecurityCard *nfc.PairSecurityCardAction + resetSecurityCard *nfc.ResetSecurityCardAction + signMessageSecurityCard *nfc.SignMessageSecurityCardAction + pairSecurityCardV2 *nfc.PairSecurityCardActionV2 } func NewWalletServer( nfcBridge app_provided_data.NfcBridge, - keysProvider app_provided_data.KeyProvider, + keyProvider keys.KeyProvider, network *libwallet.Network, - houstonService *service.HoustonService, + houstonService service.HoustonService, keyValueStorage *storage.KeyValueStorage, startChallengeSetup *challenge_keys.StartChallengeSetupAction, + finishChallengeSetup *challenge_keys.FinishChallengeSetupAction, + obtainVerifiedEncryptedMuunKeyIfAbsent *recovery.PopulateEncryptedMuunKeyAction, + scanForFunds *recovery.ScanForFundsAction, submitDiagnostic *diagnostic_mode_reports.SubmitDiagnosticAction, + buildSweepTx *recovery.BuildSweepTxAction, + signSweepTxAction *recovery.SignSweepTxAction, pairSecurityCard *nfc.PairSecurityCardAction, resetSecurityCard *nfc.ResetSecurityCardAction, signMessageSecurityCard *nfc.SignMessageSecurityCardAction, + pairSecurityCardV2 *nfc.PairSecurityCardActionV2, ) *WalletServer { return &WalletServer{ - nfcBridge: nfcBridge, - keysProvider: keysProvider, - network: network, - houstonService: houstonService, - keyValueStorage: keyValueStorage, - startChallengeSetup: startChallengeSetup, - submitDiagnostic: submitDiagnostic, - pairSecurityCard: pairSecurityCard, - resetSecurityCard: resetSecurityCard, - signMessageSecurityCard: signMessageSecurityCard, + nfcBridge: nfcBridge, + keyProvider: keyProvider, + network: network, + houstonService: houstonService, + keyValueStorage: keyValueStorage, + startChallengeSetup: startChallengeSetup, + finishChallengeSetup: finishChallengeSetup, + populateEncryptedMuunKey: obtainVerifiedEncryptedMuunKeyIfAbsent, + scanForFunds: scanForFunds, + submitDiagnostic: submitDiagnostic, + buildSweepTx: buildSweepTx, + signSweepTx: signSweepTxAction, + pairSecurityCard: pairSecurityCard, + resetSecurityCard: resetSecurityCard, + signMessageSecurityCard: signMessageSecurityCard, + pairSecurityCardV2: pairSecurityCardV2, } } // Check we actually implement the interface var _ api.WalletServiceServer = (*WalletServer)(nil) -func (WalletServer) DeleteWallet(context.Context, *emptypb.Empty) (*api.OperationStatus, error) { - // For now, do nothing. This will probably change in the future. - return api.OperationStatus_builder{ - Status: "ok", - }.Build(), nil -} - -func (ws WalletServer) NfcTransmit(ctx context.Context, req *api.NfcTransmitRequest) (*api.NfcTransmitResponse, error) { - - fmt.Printf("WalletServer: nfcTransmit") - slog.Debug("WalletServer: nfcTransmit") - - nfcBridgeResponse, err := ws.nfcBridge.Transmit(req.GetApduCommand()) +func (ws WalletServer) SetupSecurityCard( + ctx context.Context, + message *emptypb.Empty, +) (*api.XpubResponse, error) { + extendedPublicKey, err := ws.pairSecurityCard.Run() if err != nil { - // TODO error logging - return nil, err + return nil, fmt.Errorf("error pairing security card: %w", err) } - return api.NfcTransmitResponse_builder{ - ApduResponse: nfcBridgeResponse.Response, - StatusCode: nfcBridgeResponse.StatusCode, + base58Xpub := base58.Encode(extendedPublicKey.RawBytes) + + return api.XpubResponse_builder{ + Base58Xpub: base58Xpub, }.Build(), nil } -func (ws WalletServer) SetupSecurityCard(ctx context.Context, message *emptypb.Empty) (*api.XpubResponse, error) { - extendedPublicKey, err := ws.pairSecurityCard.Run() +func (ws WalletServer) SetupSecurityCardV2( + ctx context.Context, + message *emptypb.Empty, +) (*api.SetupSecurityCardResponse, error) { + response, err := ws.pairSecurityCardV2.Run() if err != nil { return nil, fmt.Errorf("error pairing security card: %w", err) } - base58Xpub := base58.Encode(extendedPublicKey.RawBytes) + slog.Debug("card paired succesfully") + slog.Debug("MetadaCard", "metadata", response.Metadata) - return api.XpubResponse_builder{ - Base58Xpub: base58Xpub, + return api.SetupSecurityCardResponse_builder{ + IsKnownProvider: response.IsKnownProvider, + IsCardAlreadyUsed: response.IsCardAlreadyUsed, }.Build(), nil } -func (ws WalletServer) ResetSecurityCard(ctx context.Context, message *emptypb.Empty) (*emptypb.Empty, error) { +func (ws WalletServer) ResetSecurityCard( + ctx context.Context, + message *emptypb.Empty, +) (*emptypb.Empty, error) { err := ws.resetSecurityCard.Run() if err != nil { return nil, err @@ -136,10 +154,22 @@ func (ws WalletServer) SignMessageSecurityCard( }.Build(), nil } -func (ws WalletServer) StartDiagnosticSession(ctx context.Context, empty *emptypb.Empty) (*api.DiagnosticSessionDescriptor, error) { +func (ws WalletServer) StartDiagnosticSession( + ctx context.Context, + empty *emptypb.Empty, +) (*api.DiagnosticSessionDescriptor, error) { sessionId := uuid.NewString() + + logBuffer := bytes.NewBuffer(nil) + textHandler := slog.NewTextHandler(logBuffer, &slog.HandlerOptions{ + Level: slog.LevelInfo, + }) + debugLog := slog.New(textHandler) + err := diagnostic_mode.AddDiagnosticSession(&diagnostic_mode.DiagnosticSessionData{ - Id: sessionId, + Id: sessionId, + LogBuffer: logBuffer, + Logger: debugLog, }) if err != nil { return nil, err @@ -149,22 +179,20 @@ func (ws WalletServer) StartDiagnosticSession(ctx context.Context, empty *emptyp }.Build(), nil } -func (ws WalletServer) PerformDiagnosticScanForUtxos(descriptor *api.DiagnosticSessionDescriptor, g grpc.ServerStreamingServer[api.ScanProgressUpdate]) error { +func (ws WalletServer) PerformDiagnosticScanForUtxos( + descriptor *api.DiagnosticSessionDescriptor, + g grpc.ServerStreamingServer[api.ScanProgressUpdate], +) error { sessionId := descriptor.GetSessionId() if sessionData, ok := diagnostic_mode.GetDiagnosticSession(sessionId); ok { - sessionData.DebugLog = bytes.NewBuffer(nil) - textHandler := slog.NewTextHandler(sessionData.DebugLog, &slog.HandlerOptions{ - Level: slog.LevelInfo, - }) - - var servers []string = electrum.PublicServers - reports, err := diagnostic_mode.ScanAddresses(ws.keysProvider, electrum.NewServerProvider(servers), ws.network, slog.New(textHandler)) + reports, err := ws.scanForFunds.Run(sessionData.Logger) if err != nil { return err } for report := range reports { + sessionData.LastScanReport = report for _, utxo := range report.UtxosFound { _ = g.Send(api.ScanProgressUpdate_builder{ FoundUtxoReport: api.FoundUtxoReport_builder{ @@ -185,10 +213,13 @@ func (ws WalletServer) PerformDiagnosticScanForUtxos(descriptor *api.DiagnosticS } } -func (ws WalletServer) SubmitDiagnosticLog(ctx context.Context, descriptor *api.DiagnosticSessionDescriptor) (*api.DiagnosticSubmitStatus, error) { +func (ws WalletServer) SubmitDiagnosticLog( + ctx context.Context, + descriptor *api.DiagnosticSessionDescriptor, +) (*api.DiagnosticSubmitStatus, error) { sessionId := descriptor.GetSessionId() if session, ok := diagnostic_mode.GetDiagnosticSession(sessionId); ok { - err := ws.submitDiagnostic.Run(sessionId, session.DebugLog.String()) + err := ws.submitDiagnostic.Run(sessionId, session.LogBuffer.String()) if err != nil { return nil, err } @@ -203,11 +234,68 @@ func (ws WalletServer) SubmitDiagnosticLog(ctx context.Context, descriptor *api. } } +func (ws WalletServer) PrepareSweepTx(ctx context.Context, parameters *api.PrepareSweepTxRequest) (*api.PrepareSweepTxResponse, error) { + destinationAddressString := parameters.GetDestinationAddress() + address, err := btcutil.DecodeAddress(destinationAddressString, ws.network.ToParams()) + if err != nil { + return nil, err + } + + descriptor := parameters.GetSessionDescriptor() + + sessionId := descriptor.GetSessionId() + if session, ok := diagnostic_mode.GetDiagnosticSession(sessionId); ok { + session.SweepTx, err = ws.buildSweepTx.Run( + session.LastScanReport.UtxosFound, + address, + parameters.GetFeeRateInSatsPerVByte(), + ) + if err != nil { + return nil, err + } + + return api.PrepareSweepTxResponse_builder{ + SessionDescriptor: descriptor, + DestinationAddress: destinationAddressString, + TxSizeInBytes: int64(session.SweepTx.SerializeSize()), + }.Build(), nil + } else { + return nil, fmt.Errorf("invalid sessionId %s", sessionId) + } +} + +func (ws WalletServer) SignAndBroadcastSweepTx(ctx context.Context, confirmation *api.SignAndBroadcastSweepTxRequest) (*api.SignAndBroadcastSweepTxResponse, error) { + sessionId := confirmation.GetSessionDescriptor().GetSessionId() + if session, ok := diagnostic_mode.GetDiagnosticSession(sessionId); ok { + signedTx, err := ws.signSweepTx.Run( + session.LastScanReport.UtxosFound, + session.SweepTx, + confirmation.GetRecoveryCode(), + ) + if err != nil { + return nil, err + } + var buf bytes.Buffer + err = signedTx.Serialize(&buf) + if err != nil { + return nil, err + } + txString := hex.EncodeToString(buf.Bytes()) + + return nil, fmt.Errorf("signed tx %v but did not broadcast", txString) + } else { + return nil, fmt.Errorf("invalid sessionId %s", sessionId) + } +} + // StartChallengeSetup is part of a migration test. // This is a minimal switch to call houston from libwallet instead of from native code. // Do NOT treat this as a reference. -// Future implementations should move native logic as much as possible to libwallet instead of duplicating this pattern. -func (ws WalletServer) StartChallengeSetup(ctx context.Context, req *api.ChallengeSetupRequest) (*api.SetupChallengeResponse, error) { +// Future implementations should move native logic as much as possible to libwallet instead of +// duplicating this pattern. +func (ws WalletServer) StartChallengeSetup( + ctx context.Context, req *api.ChallengeSetupRequest, +) (*api.SetupChallengeResponse, error) { challengeSetupJson := model.ChallengeSetupJson{ Type: req.GetType(), @@ -228,25 +316,38 @@ func (ws WalletServer) StartChallengeSetup(ctx context.Context, req *api.Challen }.Build(), nil } -func (ws WalletServer) FinishRecoveryCodeSetup(ctx context.Context, keys *api.FinishRecoveryCodeSetupRequest) (*emptypb.Empty, error) { - extendedServerCosigningPublicKey, err := hdkeychain.NewKeyFromString(keys.GetExtendedServerCosigningPublicKeyBase58()) +func (ws WalletServer) FinishRecoveryCodeSetup( + ctx context.Context, + req *api.FinishRecoveryCodeSetupRequest, +) (*emptypb.Empty, error) { + + recoveryCodePublicKey, err := hexToPublicKey(req.GetRecoveryCodePublicKeyHex()) if err != nil { - return nil, fmt.Errorf("error parsing extended server cosigning key: %w", err) + return nil, fmt.Errorf("error parsing recovery code public key: %w", err) } - if extendedServerCosigningPublicKey.IsPrivate() { - return nil, fmt.Errorf("extended server cosigning key must be public") + err = ws.finishChallengeSetup.Run(recoveryCodePublicKey) + if err != nil { + return nil, err } - recoveryCodePublicKey, err := hexToPublicKey(keys.GetRecoveryCodePublicKeyHex()) + return &emptypb.Empty{}, nil +} + +func (ws WalletServer) PopulateEncryptedMuunKey( + ctx context.Context, + req *api.PopulateEncryptedMuunKeyRequest, +) (*emptypb.Empty, error) { + recoveryCodePublicKey, err := hexToPublicKey(req.GetRecoveryCodePublicKeyHex()) if err != nil { return nil, fmt.Errorf("error parsing recovery code public key: %w", err) } - err = recovery.FinishRecoveryCodeSetupStub(ws.houstonService, *extendedServerCosigningPublicKey, *recoveryCodePublicKey) + err = ws.populateEncryptedMuunKey.Run(recoveryCodePublicKey) if err != nil { return nil, err } + return &emptypb.Empty{}, nil } diff --git a/libwallet/presentation/wallet_server_test.go b/libwallet/presentation/wallet_server_test.go index 602966a9..06f4248b 100644 --- a/libwallet/presentation/wallet_server_test.go +++ b/libwallet/presentation/wallet_server_test.go @@ -4,17 +4,24 @@ import ( "context" "encoding/hex" "errors" + "fmt" "github.com/grpc-ecosystem/go-grpc-middleware" + "github.com/muun/libwallet/data/keys" + "github.com/muun/libwallet/domain/action/challenge_keys" + "github.com/muun/libwallet/domain/action/recovery" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/resolver" "google.golang.org/grpc/status" + "log" "net" + "os" "path" "strconv" + "strings" "testing" + "time" - "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/muun/libwallet" apierrors "github.com/muun/libwallet/errors" "github.com/muun/libwallet/presentation/api" @@ -29,7 +36,8 @@ import ( var bufconnListener *bufconn.Listener var walletServer = &WalletServer{} -const houstonUrl string = "http://localhost:8080" +// 127.0.0.1 instead of localhost to avoid problems with network interfaces in local env +const houstonUrl string = "http://127.0.0.1:8080" func defaultProvider() *service.TestProvider { return &service.TestProvider{ @@ -44,6 +52,7 @@ func defaultProvider() *service.TestProvider { func init() { walletServer.network = libwallet.Regtest() walletServer.houstonService = service.NewHoustonService(defaultProvider()) + walletServer.startChallengeSetup = challenge_keys.NewStartChallengeSetupAction(walletServer.houstonService) // Initialize grpc server of WalletService with bufconn bufconnListener = bufconn.Listen(1024 * 1024) @@ -69,10 +78,57 @@ func init() { }() } +// TestMain is run before all tests defined in this package +func TestMain(m *testing.M) { + shouldRunHealthcheck := false + + for _, arg := range os.Args { + if strings.Contains(arg, "-test.run=_Integration") { + shouldRunHealthcheck = true + break + } + } + + if shouldRunHealthcheck { + log.Println("Running healthcheck setup for integration tests...") + + if err := waitForHealthcheck(); err != nil { + log.Fatalf("Healthcheck failed: %v", err) + } + } else { + log.Println("Skipping healthcheck setup (not running integration tests).") + } + + code := m.Run() + os.Exit(code) +} + +// waitForHealthcheck pings the healthcheck endpoint until it gets a 200 OK +func waitForHealthcheck() error { + timeout := 30 * time.Second + interval := 500 * time.Millisecond + deadline := time.Now().Add(timeout) + for { + if time.Now().After(deadline) { + return fmt.Errorf("healthcheck failed after %s", timeout) + } + + err := walletServer.houstonService.HealthCheck() + + if err == nil { + log.Println("Healthcheck successful.") + return nil + } + + log.Println("Healthcheck failed:", err) + time.Sleep(interval) + } +} + func TestSaveAndGetAndDelete(t *testing.T) { t.Run("success when saving, reading and deleting a key-value pair", func(t *testing.T) { - setupKeyValueStorage(t) + setupKeyValueStorage(t, buildStorageSchemaForTests()) // Initialize grpc client of WalletService with bufconn conn, ctx := newGrpcClient(t) @@ -135,7 +191,7 @@ func TestSaveAndGetAndDelete(t *testing.T) { t.Run("return error when SaveRequest does not have a key defined", func(t *testing.T) { - setupKeyValueStorage(t) + setupKeyValueStorage(t, buildStorageSchemaForTests()) // Initialize grpc client of WalletService with bufconn conn, ctx := newGrpcClient(t) @@ -184,7 +240,7 @@ func TestSaveAndGetAndDelete(t *testing.T) { t.Run("return error when SaveRequest has an invalid key", func(t *testing.T) { - setupKeyValueStorage(t) + setupKeyValueStorage(t, buildStorageSchemaForTests()) // Initialize grpc client of WalletService with bufconn conn, ctx := newGrpcClient(t) @@ -237,7 +293,7 @@ func TestSaveAndGetAndDelete(t *testing.T) { t.Run("success when saving a key with NullValue", func(t *testing.T) { - setupKeyValueStorage(t) + setupKeyValueStorage(t, buildStorageSchemaForTests()) // Initialize grpc client of WalletService with bufconn conn, ctx := newGrpcClient(t) @@ -280,7 +336,7 @@ func TestSaveAndGetAndDelete(t *testing.T) { func TestSaveBatchAndGetBatch(t *testing.T) { t.Run("success when saving and reading key-value pairs in batches", func(t *testing.T) { - setupKeyValueStorage(t) + setupKeyValueStorage(t, buildStorageSchemaForTests()) // Initialize grpc client of WalletService with bufconn conn, ctx := newGrpcClient(t) @@ -360,7 +416,7 @@ func TestSaveBatchAndGetBatch(t *testing.T) { func TestErrorInterceptors(t *testing.T) { t.Run("return internal error when rpc execution raises a panic", func(t *testing.T) { - setupKeyValueStorage(t) + setupKeyValueStorage(t, buildStorageSchemaForTests()) // Initialize grpc client of WalletService with bufconn conn, ctx := newGrpcClient(t) @@ -486,52 +542,77 @@ func TestErrorInterceptors(t *testing.T) { } func TestFinishRecoveryCodeSetupEndpoint_Integration(t *testing.T) { - encryptedPrivateKey := "v1:512:1:8:582626b7244e1e72:01b0df9b1ffbdfe8eabae00a2c208317:0a6593c59927797f6a850f1775792378b81078ba29c203b41fac51d6d62069fbb5306e6585c7abab06905d86cc01bc3c0302087edec71c9018191e68e72348e06f2b87c4b5ee0efb55c99fd9f3703ebfc94127bebd41fa72bced4747718a5c43219dd010aa2e9be848e387b3c29f4df3:6d2f736368656d613a31272f7265636f766572793a3127" + setupKeyValueStorage(t, storage.BuildStorageSchema()) - seed, err := hdkeychain.GenerateSeed(64 /*bytes*/) - if err != nil { - t.Fatalf("seed generation failed") - } - master, err := hdkeychain.NewMaster(seed, walletServer.network.ToParams()) + recoveryCode := recoverycode.Generate() + recoveryCodePrivateKey, err := recoverycode.ConvertToKey(recoveryCode, "") if err != nil { - t.Fatalf("master seed generation failed") + t.Fatal(err) } - masterPublic, err := master.Neuter() + + userPrivateKey, err := libwallet.NewHDPrivateKey( + []byte("1234567891011121314"), + libwallet.Regtest(), + ) if err != nil { - t.Fatalf("derivation failed") + t.Fatal(err) } - recoveryCode := recoverycode.Generate() - recoveryCodePrivateKey, err := recoverycode.ConvertToKey(recoveryCode, "") + encryptedPrivateKey, err := libwallet.KeyEncrypt(userPrivateKey, recoveryCode) if err != nil { t.Fatal(err) } recoveryCodePublicKey := recoveryCodePrivateKey.PubKey() - createFirstSession(t) + createFirstSessionOkJson := createFirstSession(t, userPrivateKey.PublicKey()) + muunPublicKey, err := libwallet.NewHDPublicKeyFromString( + createFirstSessionOkJson.CosigningPublicKey.Key, + createFirstSessionOkJson.CosigningPublicKey.Path, + libwallet.Regtest()) + if err != nil { + t.Fatal(err) + } - _, err = walletServer.houstonService.ChallengeKeySetupStart(model.ChallengeSetupJson{ - Type: "RECOVERY_CODE", - PublicKey: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), - EncryptedPrivateKey: encryptedPrivateKey, - Version: 2, - Salt: "dfb80ea8c30959e8", - }) + walletServer.keyProvider = NewMockKeyProvider(userPrivateKey, muunPublicKey, 0) + computeAndStoreEncryptedMuunKeyAction := recovery.NewComputeAndStoreEncryptedMuunKeyAction( + walletServer.keyValueStorage, + walletServer.keyProvider, + ) + walletServer.finishChallengeSetup = challenge_keys.NewFinishChallengeSetupAction( + walletServer.houstonService, + walletServer.keyValueStorage, + computeAndStoreEncryptedMuunKeyAction, + ) + + _, err = walletServer.StartChallengeSetup( + context.Background(), + api.ChallengeSetupRequest_builder{ + Type: "RECOVERY_CODE", + PublicKey: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), + Salt: "dfb80ea8c30959e8", + EncryptedPrivateKey: encryptedPrivateKey, + Version: 2, + }.Build(), + ) if err != nil { t.Fatal(err) } - _, err = walletServer.FinishRecoveryCodeSetup(context.Background(), api.FinishRecoveryCodeSetupRequest_builder{ - ExtendedServerCosigningPublicKeyBase58: masterPublic.String(), - RecoveryCodePublicKeyHex: hex.EncodeToString(recoveryCodePublicKey.SerializeCompressed()), - }.Build()) + _, err = walletServer.FinishRecoveryCodeSetup( + context.Background(), + api.FinishRecoveryCodeSetupRequest_builder{ + RecoveryCodePublicKeyHex: hex.EncodeToString( + recoveryCodePublicKey.SerializeCompressed(), + ), + }.Build(), + ) if err != nil { t.Fatal(err) } } -func createFirstSession(t *testing.T) model.CreateFirstSessionOkJson { +func createFirstSession(t *testing.T, key *libwallet.HDPublicKey) model.CreateFirstSessionOkJson { provider := defaultProvider() strClientVersion, err := strconv.Atoi(provider.ClientVersion) if err != nil { @@ -548,7 +629,7 @@ func createFirstSession(t *testing.T) model.CreateFirstSessionOkJson { GcmToken: nil, PrimaryCurrency: "USD", BasePublicKey: model.PublicKeyJson{ - Key: "tpubDAygaiK3eZ9hpC3aQkxtu5fGSTK4P7QKTwwGExN8hGZytjpEfsrUjtM8ics8Y7YLrvf1GLBZTFjcpmkEP1KKTRyo8D2ku5zz49bRudDrngd", + Key: key.String(), Path: "m/schema:1'/recovery:1'", }, } @@ -559,10 +640,10 @@ func createFirstSession(t *testing.T) model.CreateFirstSessionOkJson { return sessionOkJson } -func setupKeyValueStorage(t *testing.T) { +func setupKeyValueStorage(t *testing.T, schema map[string]storage.Classification) { // Create a new empty DB providing a new dataFilePath dataFilePath := path.Join(t.TempDir(), "test.db") - keyValueStorage := storage.NewKeyValueStorage(dataFilePath, buildStorageSchemaForTests()) + keyValueStorage := storage.NewKeyValueStorage(dataFilePath, schema) // For testing purpose, change reference to this new keyValueStorage in order to have a new empty DB walletServer.keyValueStorage = keyValueStorage @@ -651,3 +732,45 @@ func buildStorageSchemaForTests() map[string]storage.Classification { }, } } + +type mockKeyProvider struct { + userPrivateKey *libwallet.HDPrivateKey + muunPublicKey *libwallet.HDPublicKey + maxDerivedIndex int +} + +func NewMockKeyProvider( + userPrivateKey *libwallet.HDPrivateKey, + muunPublicKey *libwallet.HDPublicKey, + maxDerivedIndex int, +) keys.KeyProvider { + return &mockKeyProvider{ + userPrivateKey: userPrivateKey, + muunPublicKey: muunPublicKey, + maxDerivedIndex: maxDerivedIndex, + } +} + +func (m *mockKeyProvider) UserPrivateKey() (*libwallet.HDPrivateKey, error) { + return m.userPrivateKey, nil +} + +func (m *mockKeyProvider) UserPublicKey() (*libwallet.HDPublicKey, error) { + return m.userPrivateKey.PublicKey(), nil +} + +func (m *mockKeyProvider) MuunPublicKey() (*libwallet.HDPublicKey, error) { + return m.muunPublicKey, nil +} + +func (m *mockKeyProvider) MaxDerivedIndex() int { + return m.maxDerivedIndex +} + +func (m *mockKeyProvider) EncryptedMuunPrivateKey() (*libwallet.EncryptedPrivateKeyInfo, error) { + return nil, errors.New("not implemented") +} + +func (m *mockKeyProvider) SetMaxDerivedIndex(maxDerivedIndex int) { + m.maxDerivedIndex = maxDerivedIndex +} diff --git a/libwallet/service/houston.go b/libwallet/service/houston.go index 77d6e1b0..6e28c479 100644 --- a/libwallet/service/houston.go +++ b/libwallet/service/houston.go @@ -5,15 +5,42 @@ import ( "github.com/muun/libwallet/service/model" ) -type HoustonService struct { +type HoustonService interface { + HealthCheck() error + ChallengeKeySetupStart(req model.ChallengeSetupJson) (model.SetupChallengeResponseJson, error) + ChallengeKeySetupFinish(req model.ChallengeSetupVerifyJson) error + ChallengeSetupFinishWithVerifiableMuunKey(req model.ChallengeSetupVerifyJson) (model.VerifiableMuunKeyJson, error) + VerifiableMuunKey() (model.VerifiableMuunKeyJson, error) + CreateFirstSession(createSessionJson model.CreateFirstSessionJson) (model.CreateFirstSessionOkJson, error) + FetchFeeWindow() (model.FeeWindowJson, error) + SubmitDiagnosticsScanData(req model.DiagnosticScanDataJson) error + ChallengeSecurityCardPair() (model.ChallengeSecurityCardPairJson, error) + RegisterSecurityCard(req model.RegisterSecurityCardJson) (model.RegisterSecurityCardOkJson, error) + ChallengeSecurityCardSign() (model.ChallengeSecurityCardSignJson, error) + SolveSecurityCardChallenge(req model.SolveSecurityCardChallengeJson) error +} + +type HoustonClient struct { client client } -func NewHoustonService(configurator app_provided_data.HttpClientSessionProvider) *HoustonService { - return &HoustonService{client: client{configurator: configurator}} +var _ HoustonService = (*HoustonClient)(nil) + +func NewHoustonService(configurator app_provided_data.HttpClientSessionProvider) HoustonService { + return &HoustonClient{client: client{configurator: configurator}} } -func (h *HoustonService) ChallengeKeySetupStart(req model.ChallengeSetupJson) (model.SetupChallengeResponseJson, error) { +func (h *HoustonClient) HealthCheck() error { + r := request[any]{ + Method: MethodGet, + Path: "/admin/healthcheck", + Body: nil, + } + _, err := r.do(&h.client) + return err +} + +func (h *HoustonClient) ChallengeKeySetupStart(req model.ChallengeSetupJson) (model.SetupChallengeResponseJson, error) { r := request[model.SetupChallengeResponseJson]{ Method: MethodPost, Path: "/user/challenge/setup/start", @@ -22,7 +49,7 @@ func (h *HoustonService) ChallengeKeySetupStart(req model.ChallengeSetupJson) (m return r.do(&h.client) } -func (h *HoustonService) ChallengeKeySetupFinish(req model.ChallengeSetupVerifyJson) error { +func (h *HoustonClient) ChallengeKeySetupFinish(req model.ChallengeSetupVerifyJson) error { r := request[any]{ Method: MethodPost, Path: "/user/challenge/setup/finish", @@ -33,16 +60,27 @@ func (h *HoustonService) ChallengeKeySetupFinish(req model.ChallengeSetupVerifyJ return err } -func (h *HoustonService) VerifiableServerCosginingKey() (model.VerifiableServerCosigningKeyJson, error) { - r := request[model.VerifiableServerCosigningKeyJson]{ +func (h *HoustonClient) ChallengeSetupFinishWithVerifiableMuunKey(req model.ChallengeSetupVerifyJson) (model.VerifiableMuunKeyJson, error) { + + r := request[model.VerifiableMuunKeyJson]{ + Method: MethodPost, + Path: "/user/challenge/setup/finish-with-verifiable-muun-key", + Body: req, + } + + return r.do(&h.client) +} + +func (h *HoustonClient) VerifiableMuunKey() (model.VerifiableMuunKeyJson, error) { + r := request[model.VerifiableMuunKeyJson]{ Method: MethodGet, - Path: "/user/verifiable-server-cosigning-key", + Path: "/user/verifiable-muun-key", } return r.do(&h.client) } -func (h *HoustonService) CreateFirstSession( +func (h *HoustonClient) CreateFirstSession( createSessionJson model.CreateFirstSessionJson, ) (model.CreateFirstSessionOkJson, error) { @@ -54,7 +92,7 @@ func (h *HoustonService) CreateFirstSession( return r.do(&h.client) } -func (h *HoustonService) FetchFeeWindow() (model.FeeWindowJson, error) { +func (h *HoustonClient) FetchFeeWindow() (model.FeeWindowJson, error) { r := request[model.FeeWindowJson]{ Method: MethodGet, Path: "fees/latest", @@ -62,7 +100,7 @@ func (h *HoustonService) FetchFeeWindow() (model.FeeWindowJson, error) { return r.do(&h.client) } -func (h *HoustonService) SubmitDiagnosticsScanData(req model.DiagnosticScanDataJson) error { +func (h *HoustonClient) SubmitDiagnosticsScanData(req model.DiagnosticScanDataJson) error { r := request[any]{ Method: MethodPost, Path: "diagnostics/submit_scan_data", @@ -71,3 +109,25 @@ func (h *HoustonService) SubmitDiagnosticsScanData(req model.DiagnosticScanDataJ _, err := r.do(&h.client) return err } + +func (h *HoustonClient) ChallengeSecurityCardPair() (model.ChallengeSecurityCardPairJson, error) { + //TODO implement me + panic("implement me") +} + +func (h *HoustonClient) RegisterSecurityCard( + req model.RegisterSecurityCardJson, +) (model.RegisterSecurityCardOkJson, error) { + //TODO implement me + panic("implement me") +} + +func (h *HoustonClient) ChallengeSecurityCardSign() (model.ChallengeSecurityCardSignJson, error) { + //TODO implement me + panic("implement me") +} + +func (h *HoustonClient) SolveSecurityCardChallenge(req model.SolveSecurityCardChallengeJson) error { + //TODO implement me + panic("implement me") +} diff --git a/libwallet/service/houston_test.go b/libwallet/service/houston_test.go index 8d4b64f3..db95ccb4 100644 --- a/libwallet/service/houston_test.go +++ b/libwallet/service/houston_test.go @@ -2,12 +2,81 @@ package service import ( "encoding/json" + "fmt" "github.com/muun/libwallet/service/model" + "log" + "os" "strconv" + "strings" "testing" + "time" ) -const houstonUrl = "http://localhost:8080" +// 127.0.0.1 instead of localhost to avoid problems with network interfaces in local env +const houstonUrl string = "http://127.0.0.1:8080" + +func defaultProvider() *TestProvider { + return &TestProvider{ + ClientVersion: "1205", + ClientVersionName: "2.9.2", + Language: "en", + ClientType: "FALCON", + BaseURL: houstonUrl, + } +} + +// TestMain is run before all tests defined in this package +func TestMain(m *testing.M) { + shouldRunHealthcheck := false + + for _, arg := range os.Args { + if strings.Contains(arg, "-test.run=_Integration") { + shouldRunHealthcheck = true + break + } + } + + if shouldRunHealthcheck { + log.Println("Running healthcheck setup for integration tests...") + + if err := waitForHealthcheck(); err != nil { + log.Fatalf("Healthcheck failed: %v", err) + } + } else { + log.Println("Skipping healthcheck setup (not running integration tests).") + } + + code := m.Run() + os.Exit(code) +} + +// waitForHealthcheck pings the healthcheck endpoint until it gets a 200 OK +func waitForHealthcheck() error { + timeout := 30 * time.Second + interval := 500 * time.Millisecond + deadline := time.Now().Add(timeout) + for { + if time.Now().After(deadline) { + return fmt.Errorf("healthcheck failed after %s", timeout) + } + + houstonService := NewHoustonService(defaultProvider()).(*HoustonClient) + + r := request[any]{ + Method: MethodGet, + Path: "/admin/healthcheck", + } + _, err := r.do(&houstonService.client) + + if err == nil { + log.Println("Healthcheck successful.") + return nil + } + + log.Println("Healthcheck failed:", err) + time.Sleep(interval) + } +} func TestInvalidEndpoint_Integration(t *testing.T) { provider := TestProvider{ @@ -17,7 +86,7 @@ func TestInvalidEndpoint_Integration(t *testing.T) { ClientType: "APOLLO", BaseURL: houstonUrl, } - houstonService := NewHoustonService(&provider) + houstonService := NewHoustonService(&provider).(*HoustonClient) r := request[any]{ Method: MethodGet, Path: "/invalid-endpoint", @@ -28,9 +97,11 @@ func TestInvalidEndpoint_Integration(t *testing.T) { } devError := HoustonResponseError{} - err = json.Unmarshal([]byte(err.Error()), &devError) - if err != nil { - t.Fatal(err) + + unmarshallingErr := json.Unmarshal([]byte(err.Error()), &devError) + if unmarshallingErr != nil { + log.Println(err.Error()[:]) + t.Fatal(unmarshallingErr) } if devError.RequestId == 0 { t.Fatal("RequestId should not be zero") @@ -59,7 +130,7 @@ func TestValidEndpointButNoAuthToken_Integration(t *testing.T) { ClientType: "APOLLO", BaseURL: houstonUrl, } - houstonService := NewHoustonService(&provider) + houstonService := NewHoustonService(&provider).(*HoustonClient) r := request[any]{ Method: MethodGet, Path: "/user", @@ -70,9 +141,11 @@ func TestValidEndpointButNoAuthToken_Integration(t *testing.T) { } devError := HoustonResponseError{} - err = json.Unmarshal([]byte(err.Error()), &devError) - if err != nil { - t.Fatal(err) + + unmarshallingErr := json.Unmarshal([]byte(err.Error()), &devError) + if unmarshallingErr != nil { + log.Println(err.Error()[:]) + t.Fatal(unmarshallingErr) } if devError.RequestId == 0 { t.Fatal("RequestId should not be zero") @@ -111,10 +184,13 @@ func TestValidEndpointButInvalidAuthToken_Integration(t *testing.T) { } houstonError := HoustonResponseError{} - err = json.Unmarshal([]byte(err.Error()), &houstonError) - if err != nil { - t.Fatal(err) + + unmarshallingErr := json.Unmarshal([]byte(err.Error()), &houstonError) + if unmarshallingErr != nil { + log.Println(err.Error()[:]) + t.Fatal(unmarshallingErr) } + if houstonError.RequestId == 0 { t.Fatal("RequestId should not be zero") } diff --git a/libwallet/service/mock_houston.go b/libwallet/service/mock_houston.go new file mode 100644 index 00000000..21bae150 --- /dev/null +++ b/libwallet/service/mock_houston.go @@ -0,0 +1,474 @@ +package service + +import ( + "crypto/ecdh" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "github.com/muun/libwallet/service/model" + "log/slog" + "math/big" + "reflect" + "time" +) + +type RandomPrivateKeyMetadata struct { + privateKey *ecdh.PrivateKey + timeStamp time.Time +} + +type MockHoustonService struct { + lastRandomPrivateKeyMetadata *RandomPrivateKeyMetadata + securityCardPaired *model.SecurityCardMetadataJson + secretCardBytes [32]byte + pairingSlot int +} + +var _ HoustonService = (*MockHoustonService)(nil) + +const challengeTimeoutInSeconds = 90 + +func NewMockHoustonService() *MockHoustonService { + return &MockHoustonService{} +} + +func (m *MockHoustonService) HealthCheck() error { + //TODO implement me + panic("implement me") +} + +func (m *MockHoustonService) ChallengeKeySetupStart(req model.ChallengeSetupJson) (model.SetupChallengeResponseJson, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockHoustonService) ChallengeKeySetupFinish(req model.ChallengeSetupVerifyJson) error { + //TODO implement me + panic("implement me") +} + +func (m *MockHoustonService) ChallengeSetupFinishWithVerifiableMuunKey(req model.ChallengeSetupVerifyJson) (model.VerifiableMuunKeyJson, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockHoustonService) VerifiableMuunKey() (model.VerifiableMuunKeyJson, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockHoustonService) CreateFirstSession(createSessionJson model.CreateFirstSessionJson) (model.CreateFirstSessionOkJson, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockHoustonService) FetchFeeWindow() (model.FeeWindowJson, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockHoustonService) SubmitDiagnosticsScanData(req model.DiagnosticScanDataJson) error { + //TODO implement me + panic("implement me") +} + +func (m *MockHoustonService) ChallengeSecurityCardPair() (model.ChallengeSecurityCardPairJson, error) { + + randomPrivateKey, err := ecdh.P256().GenerateKey(rand.Reader) + if err != nil { + return model.ChallengeSecurityCardPairJson{}, fmt.Errorf("error generating private key: %w", err) + } + + m.lastRandomPrivateKeyMetadata = &RandomPrivateKeyMetadata{ + privateKey: randomPrivateKey, + timeStamp: time.Now(), + } + + randomPublicKey := randomPrivateKey.PublicKey().Bytes() + + return model.ChallengeSecurityCardPairJson{ + ServerPublicKeyInHex: hex.EncodeToString(randomPublicKey), + }, nil +} + +func (m *MockHoustonService) RegisterSecurityCard( + req model.RegisterSecurityCardJson, +) (model.RegisterSecurityCardOkJson, error) { + timeSinceLastChallenge := time.Since(m.lastRandomPrivateKeyMetadata.timeStamp).Seconds() + if timeSinceLastChallenge > challengeTimeoutInSeconds { + return model.RegisterSecurityCardOkJson{}, errors.New("challenge was already invalidated") + } + + cardPublicKeyBytes, err := hex.DecodeString(req.CardPublicKeyInHex) + if err != nil { + return model.RegisterSecurityCardOkJson{}, fmt.Errorf("error decoding card pub key: %w", err) + } + + clientPublicKeyBytes, err := hex.DecodeString(req.ClientPublicKeyInHex) + if err != nil { + return model.RegisterSecurityCardOkJson{}, fmt.Errorf("error decoding client pub key: %w", err) + } + + sharedPoint, err := performECDH(m.lastRandomPrivateKeyMetadata.privateKey.Bytes(), cardPublicKeyBytes) + if err != nil { + return model.RegisterSecurityCardOkJson{}, fmt.Errorf("ECDH error: %w", err) + } + + // Compute secret_card = sha256(shared_point) + secretCard := sha256.Sum256(sharedPoint) + macSecretCard := secretCard[:16] + encSecretCard := secretCard[16:] + + slog.Debug("macSecretCard", slog.String("secret", hex.EncodeToString(macSecretCard))) + slog.Debug("encSecretCard", slog.String("secret", hex.EncodeToString(encSecretCard))) + + metadataBytes, err := SecurityCardMetadataToBytes(req.Metadata) + if err != nil { + return model.RegisterSecurityCardOkJson{}, fmt.Errorf("error decoding card metadata: %w", err) + } + + receivedMacBytes, err := hex.DecodeString(req.MacInHex) + if err != nil { + return model.RegisterSecurityCardOkJson{}, fmt.Errorf("error decoding mac: %w", err) + } + + // Verify MAC: mac = hmac(mac_secret_card, C || P || index || metadata || pub_client) + err = verifyPairingMAC( + cardPublicKeyBytes, + IntTo2Bytes(req.PairingSlot), + metadataBytes, + m.lastRandomPrivateKeyMetadata.privateKey.PublicKey().Bytes(), + clientPublicKeyBytes, + macSecretCard, + receivedMacBytes, + ) + if err != nil { + return model.RegisterSecurityCardOkJson{}, fmt.Errorf("mac verify error: %w", err) + } + + globalSignCardBytes, err := hex.DecodeString(req.GlobalSignCardInHex) + if err != nil { + return model.RegisterSecurityCardOkJson{}, fmt.Errorf("error decoding global sign card: %w", err) + } + + globalPublicKeyBytes, err := hex.DecodeString(req.Metadata.GlobalPublicKeyInHex) + if err != nil { + return model.RegisterSecurityCardOkJson{}, fmt.Errorf("error decoding global public card: %w", err) + } + + // Verify signed MAC with global public key + isValidated, err := m.VerifySignature(globalPublicKeyBytes, receivedMacBytes, globalSignCardBytes) + if err != nil { + return model.RegisterSecurityCardOkJson{}, fmt.Errorf("error with mac sig verification: %w", err) + } + + if !isValidated { + return model.RegisterSecurityCardOkJson{}, errors.New("mac signature is wrong") + } + + // Store card and secret data + m.securityCardPaired = &req.Metadata + m.secretCardBytes = secretCard + m.pairingSlot = req.PairingSlot + + // TODO: Check if something should change on metadata returned + enrichedMetadata := req.Metadata + + return model.RegisterSecurityCardOkJson{ + Metadata: enrichedMetadata, + IsKnownProvider: true, + IsCardAlreadyUsed: false, + }, nil +} + +func (m *MockHoustonService) ChallengeSecurityCardSign() (model.ChallengeSecurityCardSignJson, error) { + randomPrivateKey, err := ecdh.P256().GenerateKey(rand.Reader) + if err != nil { + return model.ChallengeSecurityCardSignJson{}, fmt.Errorf("error generating private key: %w", err) + } + + m.lastRandomPrivateKeyMetadata = &RandomPrivateKeyMetadata{ + privateKey: randomPrivateKey, + timeStamp: time.Now(), + } + + m.securityCardPaired.UsageCount += 1 + + randomPublicKey := randomPrivateKey.PublicKey().Bytes() + + challengeMac := m.makeChallengeSignMac(randomPublicKey) + + return model.ChallengeSecurityCardSignJson{ + ServerPublicKeyInHex: hex.EncodeToString(randomPublicKey), + CardUsageCount: m.securityCardPaired.UsageCount, + MacInHex: hex.EncodeToString(challengeMac), + }, nil +} + +func (m *MockHoustonService) SolveSecurityCardChallenge(req model.SolveSecurityCardChallengeJson) error { + timeSinceLastChallenge := time.Since(m.lastRandomPrivateKeyMetadata.timeStamp).Seconds() + if timeSinceLastChallenge > challengeTimeoutInSeconds { + return errors.New("challenge was already invalidated") + } + + serverPublicKeyBytes := m.lastRandomPrivateKeyMetadata.privateKey.PublicKey().Bytes() + cardPublicKeyBytes, err := hex.DecodeString(req.PublicKeyInHex) + if err != nil { + return errors.New("error decoding card pub key") + } + + receivedMac, err := hex.DecodeString(req.MacInHex) + if err != nil { + return errors.New("error decoding received mac") + } + + err = m.verifySolveChallengeMac(receivedMac, serverPublicKeyBytes, cardPublicKeyBytes) + if err != nil { + return fmt.Errorf("mac verification error:%w", err) + } + + // Calculate and store new secret card + sharedPoint, err := performECDH(m.lastRandomPrivateKeyMetadata.privateKey.Bytes(), cardPublicKeyBytes) + if err != nil { + return fmt.Errorf("ECDH error: %w", err) + } + + // Compute new_secret_card = sha256(shared_point) + m.secretCardBytes = sha256.Sum256(sharedPoint) + + return nil +} + +func (m *MockHoustonService) verifySolveChallengeMac( + receivedMac, + serverPublicKeyBytes, + cardPublicKeyBytes []byte, +) error { + // Construct MAC input with: secret_card || C || P + macInput := make([]byte, 0, 162) + macInput = append(macInput, m.secretCardBytes[:]...) // secret_card (32 bytes) + macInput = append(macInput, serverPublicKeyBytes...) // C (server ephemeral pub key 65 bytes) + macInput = append(macInput, cardPublicKeyBytes...) // P (card ephemeral pub key 65 bytes) + + // Compute expected MAC using mac_secret_card (secret_card[16:]) + expectedMAC := computeHMACSHA256(m.secretCardBytes[16:], macInput) + + fmt.Printf("Expected MAC: %x", expectedMAC) + fmt.Printf("Received MAC: %x", receivedMac) + + // Compare MACs + if !reflect.DeepEqual(receivedMac, expectedMAC) { + return fmt.Errorf("MAC mismatch - expected: %x, got: %x", expectedMAC, receivedMac) + } + return nil +} + +func (m *MockHoustonService) makeChallengeSignMac(randomPublicKey []byte) []byte { + // Construct MAC input with: secretCard || C || UsageCount || PairingSlot + macInput := make([]byte, 0, 101) + macInput = append(macInput, m.secretCardBytes[:]...) // last secret card, 32 bytes + macInput = append(macInput, randomPublicKey...) // P (card public key, 65 bytes) + macInput = append(macInput, IntTo2Bytes(m.securityCardPaired.UsageCount)...) // usage count (2 bytes) + macInput = append(macInput, IntTo2Bytes(m.pairingSlot)...) // pairing slot (2 bytes) + + // Compute expected MAC using mac_secret_card (secret_card[16:]) + challengeMac := computeHMACSHA256(m.secretCardBytes[16:], macInput) + return challengeMac +} + +// performECDH performs ECDH key agreement +func performECDH(privateKeyBytes, publicKeyBytes []byte) ([]byte, error) { + curve := elliptic.P256() + + // Parse public key + if len(publicKeyBytes) != 65 || publicKeyBytes[0] != 0x04 { + return nil, errors.New("invalid public key format") + } + + x := new(big.Int).SetBytes(publicKeyBytes[1:33]) + y := new(big.Int).SetBytes(publicKeyBytes[33:65]) + + // Verify the public key is on the curve + if !curve.IsOnCurve(x, y) { + return nil, errors.New("public key not on curve") + } + + // Perform scalar multiplication + sharedX, sharedY := curve.ScalarMult(x, y, privateKeyBytes) + + // Convert shared point to uncompressed format + sharedSecret := make([]byte, 65) + sharedSecret[0] = 0x04 + + xBytes := sharedX.Bytes() + yBytes := sharedY.Bytes() + + copy(sharedSecret[1+32-len(xBytes):33], xBytes) + copy(sharedSecret[33+32-len(yBytes):65], yBytes) + + return sharedSecret, nil +} + +// computeHMACSHA256 computes HMAC-SHA256 +func computeHMACSHA256(key, data []byte) []byte { + const blockSize = 64 // SHA-256 block size + + // Key preprocessing + if len(key) > blockSize { + hash := sha256.Sum256(key) + key = hash[:] + } + + // Pad key to block size + paddedKey := make([]byte, blockSize) + copy(paddedKey, key) + + // Create inner and outer padding + innerPad := make([]byte, blockSize) + outerPad := make([]byte, blockSize) + + for i := 0; i < blockSize; i++ { + innerPad[i] = paddedKey[i] ^ 0x36 + outerPad[i] = paddedKey[i] ^ 0x5c + } + + // Inner hash + innerHash := sha256.New() + innerHash.Write(innerPad) + innerHash.Write(data) + innerResult := innerHash.Sum(nil) + + // Outer hash + outerHash := sha256.New() + outerHash.Write(outerPad) + outerHash.Write(innerResult) + + return outerHash.Sum(nil) +} + +func verifyPairingMAC(cardPublicKey, + pairingSlot, + metadata, + serverRandomPubKey, + clientPubKey, + macSecretCard, + receivedMac []byte, +) error { + // Construct MAC input with: C || P || index || metadata || pub_client + macInput := make([]byte, 0, 272) + macInput = append(macInput, serverRandomPubKey...) // C (server random key, 65 bytes) + macInput = append(macInput, cardPublicKey...) // P (card public key, 65 bytes) + macInput = append(macInput, pairingSlot...) // index (2 bytes) + macInput = append(macInput, metadata...) // metadata (75 bytes) + macInput = append(macInput, clientPubKey...) // pub_client (65 bytes) + + // Compute expected MAC using mac_secret_card (secret_card[16:]) + expectedMAC := computeHMACSHA256(macSecretCard, macInput) + + fmt.Printf("Expected MAC: %x", expectedMAC) + fmt.Printf("Received MAC: %x", receivedMac) + + // Compare MACs + if !reflect.DeepEqual(receivedMac, expectedMAC) { + return fmt.Errorf("MAC mismatch - expected: %x, got: %x", expectedMAC, receivedMac) + } + + return nil +} + +func SecurityCardMetadataToBytes(m model.SecurityCardMetadataJson) ([]byte, error) { + handleError := func(err error) error { + return fmt.Errorf("error decoding metadata: %w", err) + } + const MetadataSize = 75 + buf := make([]byte, 0, MetadataSize) // 75 bytes + + // Global public key (65 bytes) + globalPublicKeyBytes, err := hex.DecodeString(m.GlobalPublicKeyInHex) + if err != nil { + return nil, handleError(err) + } + buf = append(buf, globalPublicKeyBytes...) + + // Card vendor (2 bytes) + cardVendorBytes, err := hex.DecodeString(m.CardVendorInHex) + if err != nil { + return nil, handleError(err) + } + buf = append(buf, cardVendorBytes...) + + // Card model (2 bytes) + cardModelBytes, err := hex.DecodeString(m.CardModelInHex) + if err != nil { + return nil, handleError(err) + } + buf = append(buf, cardModelBytes...) + + // Firmware version (2 bytes) + buf = append(buf, IntTo2Bytes(m.FirmwareVersion)...) + + // Usage count (2 bytes, big-endian) + buf = append(buf, IntTo2Bytes(m.UsageCount)...) + + // Language code (2 bytes) + languageCodeBytes, err := hex.DecodeString(m.LanguageCodeInHex) + if err != nil { + return nil, handleError(err) + } + buf = append(buf, languageCodeBytes...) + + return buf, nil +} + +func IntTo2Bytes(n int) []byte { + hi := byte(n >> 8) + lo := byte(n & 0xFF) + return []byte{hi, lo} +} + +// VerifySignature verifies a signature from a muuncard. +func (m *MockHoustonService) VerifySignature(publicKeyBytes, messageBytes, signedMessageBytes []byte) (bool, error) { + + // verify expected public key + if len(publicKeyBytes) != 65 || publicKeyBytes[0] != 0x04 { + return false, nil + } + + ecdhPub, err := ecdh.P256().NewPublicKey(publicKeyBytes) + if err != nil { + return false, nil + } + + pub, err := ecdhToECDSAPublicKey(ecdhPub) + if err != nil { + return false, nil + } + + h := sha256.Sum256(messageBytes) + + // Verify the signature + return ecdsa.VerifyASN1(pub, h[:], signedMessageBytes), nil +} + +// ecdhToECDSAPublicKey converts an *ecdh.PublicKey into an *ecdsa.PublicKey. +// Hacky workaround to avoid deprecated elliptic.Unmarshal() and ignoring lint check. +// Only works for P-256 NIST curve, which is what we are using. +// Once we upgrade to Go 1.25 we could use proper support to transform ecdh.PublicKey to +// ecdsa.PublicKey. for now this is what we got. +func ecdhToECDSAPublicKey(key *ecdh.PublicKey) (*ecdsa.PublicKey, error) { + if key.Curve() != ecdh.P256() { + return nil, errors.New("public key curve not supported. We work with P256") + } + + rawKey := key.Bytes() + return &ecdsa.PublicKey{ + Curve: elliptic.P256(), + // For + X: big.NewInt(0).SetBytes(rawKey[1:33]), + Y: big.NewInt(0).SetBytes(rawKey[33:]), + }, nil +} diff --git a/libwallet/service/model/challenge_security_card_pair_json.go b/libwallet/service/model/challenge_security_card_pair_json.go new file mode 100644 index 00000000..aef18c5a --- /dev/null +++ b/libwallet/service/model/challenge_security_card_pair_json.go @@ -0,0 +1,5 @@ +package model + +type ChallengeSecurityCardPairJson struct { + ServerPublicKeyInHex string `json:"serverPublicKeyInHex"` +} diff --git a/libwallet/service/model/challenge_security_card_sign_json.go b/libwallet/service/model/challenge_security_card_sign_json.go new file mode 100644 index 00000000..56253372 --- /dev/null +++ b/libwallet/service/model/challenge_security_card_sign_json.go @@ -0,0 +1,7 @@ +package model + +type ChallengeSecurityCardSignJson struct { + ServerPublicKeyInHex string `json:"serverPublicKeyInHex"` + CardUsageCount int `json:"cardUsageCount"` + MacInHex string `json:"macInHex"` +} diff --git a/libwallet/service/model/register_security_card_json.go b/libwallet/service/model/register_security_card_json.go new file mode 100644 index 00000000..c8d2ba9b --- /dev/null +++ b/libwallet/service/model/register_security_card_json.go @@ -0,0 +1,10 @@ +package model + +type RegisterSecurityCardJson struct { + CardPublicKeyInHex string `json:"cardPublicKeyInHex"` + ClientPublicKeyInHex string `json:"clientPublicKeyInHex"` + PairingSlot int `json:"pairingSlot"` + Metadata SecurityCardMetadataJson `json:"metadata"` + MacInHex string `json:"macInHex"` + GlobalSignCardInHex string `json:"globalSignCardInHex"` +} diff --git a/libwallet/service/model/register_security_card_ok_json.go b/libwallet/service/model/register_security_card_ok_json.go new file mode 100644 index 00000000..e39b425c --- /dev/null +++ b/libwallet/service/model/register_security_card_ok_json.go @@ -0,0 +1,7 @@ +package model + +type RegisterSecurityCardOkJson struct { + Metadata SecurityCardMetadataJson `json:"metadata"` + IsKnownProvider bool `json:"isKnownProvider"` + IsCardAlreadyUsed bool `json:"isCardAlreadyUsed"` +} diff --git a/libwallet/service/model/security_card_metadata.go b/libwallet/service/model/security_card_metadata.go new file mode 100644 index 00000000..157f41f3 --- /dev/null +++ b/libwallet/service/model/security_card_metadata.go @@ -0,0 +1,10 @@ +package model + +type SecurityCardMetadataJson struct { + GlobalPublicKeyInHex string `json:"globalPublicKeyInHex"` + CardVendorInHex string `json:"cardVendorInHex"` + CardModelInHex string `json:"cardModelInHex"` + FirmwareVersion int `json:"firmwareVersion"` + UsageCount int `json:"usageCount"` + LanguageCodeInHex string `json:"languageCodeInHex"` +} diff --git a/libwallet/service/model/solve_security_card_challenge_json.go b/libwallet/service/model/solve_security_card_challenge_json.go new file mode 100644 index 00000000..64e94f10 --- /dev/null +++ b/libwallet/service/model/solve_security_card_challenge_json.go @@ -0,0 +1,6 @@ +package model + +type SolveSecurityCardChallengeJson struct { + PublicKeyInHex string `json:"publicKeyInHex"` + MacInHex string `json:"macInHex"` +} diff --git a/libwallet/service/model/verifiable_muun_key_json.go b/libwallet/service/model/verifiable_muun_key_json.go new file mode 100644 index 00000000..002d2139 --- /dev/null +++ b/libwallet/service/model/verifiable_muun_key_json.go @@ -0,0 +1,7 @@ +package model + +type VerifiableMuunKeyJson struct { + FirstHalfKeyEncryptedToClient string `json:"firstHalfKeyEncryptedToClient"` + SecondHalfKeyEncryptedToRecoveryCode string `json:"secondHalfKeyEncryptedToRecoveryCode"` + Proof *string `json:"proof"` +} diff --git a/libwallet/service/model/verifiable_server_cosigning_key_json.go b/libwallet/service/model/verifiable_server_cosigning_key_json.go deleted file mode 100644 index e662f24e..00000000 --- a/libwallet/service/model/verifiable_server_cosigning_key_json.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -type VerifiableServerCosigningKeyJson struct { - EphemeralPublicKey string `json:"ephemeralPublicKey"` - PaddedServerCosigningKey string `json:"paddedServerCosigningKey"` - SharedSecretPublicKey string `json:"sharedSecretPublicKey"` - Proof string `json:"proof"` -} diff --git a/libwallet/service/model_objects_mapper.go b/libwallet/service/model_objects_mapper.go new file mode 100644 index 00000000..974f996b --- /dev/null +++ b/libwallet/service/model_objects_mapper.go @@ -0,0 +1,25 @@ +package service + +import ( + "github.com/muun/libwallet/domain/model/security_card" + "github.com/muun/libwallet/service/model" +) + +func MapSecurityCardPaired(in model.RegisterSecurityCardOkJson) *security_card.SecurityCardPaired { + return &security_card.SecurityCardPaired{ + Metadata: MapSecurityCardMetadata(in.Metadata), + IsKnownProvider: in.IsKnownProvider, + IsCardAlreadyUsed: in.IsCardAlreadyUsed, + } +} + +func MapSecurityCardMetadata(in model.SecurityCardMetadataJson) *security_card.SecurityCardMetadata { + return &security_card.SecurityCardMetadata{ + GlobalPublicKeyInHex: in.GlobalPublicKeyInHex, + CardVendorInHex: in.CardVendorInHex, + CardModelInHex: in.CardModelInHex, + FirmwareVersion: in.FirmwareVersion, + UsageCount: in.UsageCount, + LanguageCodeInHex: in.LanguageCodeInHex, + } +} diff --git a/libwallet/storage/schema.go b/libwallet/storage/schema.go index 1752a0e5..0de3c1ce 100644 --- a/libwallet/storage/schema.go +++ b/libwallet/storage/schema.go @@ -26,6 +26,12 @@ const ( KeyIsBalanceHidden string = "isBalanceHidden" KeyNightMode string = "nightMode" KeySecurityCardXpubSerialized string = "securityCardXpubSerialized" + KeySecurityCardPaired string = "securityCardClientPaired" + // TODO: These three are marked as prototypes to avoid accidentally setting the non-prototype fields + // in a consumer device before finalizing the design. Before production, the "Prototype" suffix must be removed + UnverifiedEncryptedMuunKey string = "unverifiedEncryptedMuungKeyPrototype" + VerifiedEncryptedMuunKey string = "verifiedEncryptedMuunKeyPrototype" + EncryptedUserKey string = "encryptedUserKeyPrototype" ) type ValueType interface { @@ -137,5 +143,29 @@ func BuildStorageSchema() map[string]Classification { SecurityCritical: false, ValueType: &StringType{}, }, + UnverifiedEncryptedMuunKey: { + BackupType: AsyncAutoBackup, + BackupSecurity: Plain, + SecurityCritical: false, + ValueType: &StringType{}, + }, + VerifiedEncryptedMuunKey: { + BackupType: AsyncAutoBackup, + BackupSecurity: Authenticated, + SecurityCritical: true, + ValueType: &StringType{}, + }, + EncryptedUserKey: { + BackupType: AsyncAutoBackup, + BackupSecurity: Authenticated, + SecurityCritical: true, + ValueType: &StringType{}, + }, + KeySecurityCardPaired: { + BackupType: AsyncAutoBackup, + BackupSecurity: NotApplicable, + SecurityCritical: false, + ValueType: &BoolType{}, + }, } } diff --git a/prover/libs/plonky2-cosigning-key-validation/src/circuit.rs b/prover/libs/plonky2-cosigning-key-validation/src/circuit.rs index f8efb1b1..99dd8cd1 100644 --- a/prover/libs/plonky2-cosigning-key-validation/src/circuit.rs +++ b/prover/libs/plonky2-cosigning-key-validation/src/circuit.rs @@ -21,8 +21,7 @@ pub const D: usize = 2; pub type C = KeccakGoldilocksConfig; pub type F = >::F; -pub const INFO: &str = "muun.com/cosigning-key/2/2"; -pub const AAD: &str = ""; +pub const INFO: &[u8] = b"muun.com/cosigning-key/2/2/recovery-code"; pub struct Circuit { pub circuit: CircuitData, @@ -34,6 +33,7 @@ impl Circuit { let config = CircuitConfig { zero_knowledge: true, num_wires: 190, + num_routed_wires: 85, ..CircuitConfig::standard_recursion_config() }; @@ -48,8 +48,9 @@ impl Circuit { let recovery_code_precomputed_windowed_mul = builder.add_virtual_precomputed_windowed_mul_target(); - let info = builder.constant_bytes(INFO.as_bytes()); - let aad = builder.constant_bytes(AAD.as_bytes()); + let info = builder.constant_bytes(INFO); + + let aad = builder.constant_bytes(&[]); let plaintext_scalar_bytes = serialize_private_key(&mut builder, plaintext_scalar.clone()); diff --git a/prover/libs/plonky2-cosigning-key-validation/src/lib.rs b/prover/libs/plonky2-cosigning-key-validation/src/lib.rs index 1905b502..438943df 100644 --- a/prover/libs/plonky2-cosigning-key-validation/src/lib.rs +++ b/prover/libs/plonky2-cosigning-key-validation/src/lib.rs @@ -3,8 +3,6 @@ mod inputs; mod interface; mod testing; -pub use circuit::AAD; -pub use circuit::INFO; pub use interface::Proof; pub use interface::ProverData; pub use interface::ProverInputs; diff --git a/prover/libs/plonky2-cosigning-key-validation/src/testing.rs b/prover/libs/plonky2-cosigning-key-validation/src/testing.rs index f63efdf7..7c76b77d 100644 --- a/prover/libs/plonky2-cosigning-key-validation/src/testing.rs +++ b/prover/libs/plonky2-cosigning-key-validation/src/testing.rs @@ -3,21 +3,21 @@ mod tests { use crate::*; const HPKE_EPHEMERAL_PRIVATE_KEY: &str = - "ad9243a485da16dc53d4cdbbf9974f55a6b4d8563eaacffdbd32890e0df3e975"; - const HPKE_EPHEMERAL_PUBLIC_KEY: &str = "04197175cf54953d5637aaa6d9f12424d8fc708e826cf5d3442298f3c6545cfc35ab37898edf5457e4823bf35a8a08a0088236fc9194e048cd1345ca0d4f3afa8f"; + "bb611d7aa2a5688d947085fa5c60e87fb051042854c23fff388945dc7010b0b5"; + const HPKE_EPHEMERAL_PUBLIC_KEY: &str = "0471b55503fb340ec6c202d6cdce7d49c365b78ae2fa3bab06ae87553610006553441e4f7ad3c3c834b0e0538ac241e2adc61c85a10ec7341eb1129edb0caccd0a"; #[allow(dead_code)] const RECOVERY_CODE_PRIVATE_KEY: &str = - "cb027d62281abf5c7c7140477579df6c06e58c04a40c71354018b414af30a2e9"; + "20f5dccb488fe31f95ba0f55ed306df9df2a5a171157838bada35342e71f5d7f"; - const RECOVERY_CODE_PUBLIC_KEY: &str = "04465538823cd6f5438db12bc9b57a35c0f19e5787b517502719da3fe63597566284056b4a89937f529ad7e93a76f3e894d6572db3ef493d29f7d05f2b5ff1939a"; + const RECOVERY_CODE_PUBLIC_KEY: &str = "04dc5489ca59d23d4deebc778850651da1f3da1c505db198df8e5cf9fe322964c7c5ab62cac0b255be7d75606e04bc8015e70c39d6e0d6faaf435eb92c29043ded"; const PLAINTEXT_SCALAR: &str = - "f165aaaa334cd690be86cdfe7a53eca573482c18c6360519e9e3a016e1f6442a"; + "f9fff35fb0004862359e69bcbb003b0dc8e610e6d82af40a25ed5d75386241df"; - const PLAINTEXT_PUBLIC_KEY: &str = "04a537b6bb21a11d8662317659e2b2a27aa37a4e95b5ececa1b3fdd4a7a772dac8047073a501394fd623a5747b25bbc0a16e934ba865423df840b35a9a5019f511"; + const PLAINTEXT_PUBLIC_KEY: &str = "0468a18701d75331dddbef334c070931cf3561288e78346666fdcc01fb28aac0f17823d00b35cd06eb0508067a345027ab03a716ea825220059a168c6a6d5090db"; - const CIPHERTEXT: &str = "34bfe3253dde55ec3fbe27a9f8cf91293ba40822107f2736eb4fc0228716f320450e78057c76aea4177c6a008ab7b57e"; + const CIPHERTEXT: &str = "23d170accd4b2849fbfa0e8e49f753eefb274c0449ab8ab46e9f35a4e2265f054d7cbab020157c34c5ba61e0e7695608"; fn str_to_arr(s: &str) -> [u8; N] { hex::decode(s).unwrap().try_into().unwrap() diff --git a/tools/libwallet-android.sh b/tools/libwallet-android.sh index f06f874a..f8262223 100755 --- a/tools/libwallet-android.sh +++ b/tools/libwallet-android.sh @@ -31,6 +31,24 @@ GOCACHE="$build_dir/android" rm -rf "$GOCACHE"/src-android-* 2>/dev/null \ || echo "No src-android-* directories found in GOCACHE." +# Set linker flags for 16KB page alignment required by Android targetSdk 35+ +# CGO_LDFLAGS: Passes flags to the C linker when building Go code with CGO +# LDFLAGS: General linker flags that may be used by the build system +# Both are set to ensure compatibility across different build scenarios +# +# -Wl,-z,max-page-size=16384: Sets maximum page size to 16KB (16384 bytes) +# This defines the largest page size the linker can use for memory alignment +# Required for compatibility with Android's new 16KB page size support +# +# -Wl,-z,common-page-size=16384: Sets common page size to 16KB +# This aligns data sections to 16KB boundaries for optimal memory management +# Ensures proper alignment of shared library segments in memory +# +# Android 16KB page size support: https://developer.android.com/guide/practices/page-sizes +# GNU LD linker options: https://sourceware.org/binutils/docs/ld/Options.html +# CGO_LDFLAGS documentation: https://pkg.go.dev/cmd/cgo +export CGO_LDFLAGS="-Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384" + # Finally run gomobile bind using the version pinned by the go.mod file. # We need -androidapi 19 to set the min api targeted by the NDK. # The -trimpath and -ldflags are passed on to go build and are part of keeping the build reproducible. @@ -42,5 +60,7 @@ go run golang.org/x/mobile/cmd/gomobile bind \ . ./newop ./app_provided_data ./libwallet_init st=$? + +echo "" echo "rebuilt gomobile with status $? to $libwallet" exit $st