From 7c55095e00869437211db5a171efc3a3cf1d4f8f Mon Sep 17 00:00:00 2001 From: "franco.mucci" Date: Wed, 25 Mar 2026 19:08:02 -0300 Subject: [PATCH] Release source code for 55.7 (1507) --- android/CHANGELOG.md | 10 +++ android/Dockerfile | 2 +- android/apolloui/build.gradle | 4 +- .../data/preferences/MinFeeRateRepository.kt | 3 +- .../apollo/domain/analytics/AnalyticsEvent.kt | 13 +++- .../fragments/manual_fee/ManualFeeFragment.kt | 7 +- .../recommended_fee/RecommendedFeeFragment.kt | 2 +- .../RecommendedFeePresenter.kt | 16 +++-- .../ui/new_operation/ConfirmStateViewModel.kt | 7 -- .../ui/new_operation/NewOperationActivity.kt | 3 +- .../ui/new_operation/NewOperationPresenter.kt | 18 ++--- .../src/main/java/io/muun/common/Rules.java | 9 ++- libwallet/librs/libs.Dockerfile | 2 +- tools/strip-elf-debug.py | 66 +++++++++++++++++++ tools/verify-apollo.sh | 5 ++ 15 files changed, 135 insertions(+), 32 deletions(-) create mode 100644 tools/strip-elf-debug.py diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 188ee360..af4256d8 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -6,6 +6,16 @@ follow [https://changelog.md/](https://changelog.md/) guidelines. ## [Unreleased] +## [55.7] - 2026-03-25 + +### ADDED + +- Subsat support + +### FIXED + +- Repro builds + ## [55.6] - 2026-02-18 ### FIXED diff --git a/android/Dockerfile b/android/Dockerfile index bf5cbd12..740b7632 100644 --- a/android/Dockerfile +++ b/android/Dockerfile @@ -61,7 +61,7 @@ RUN sdkmanager --install "ndk;${NDK_VERSION}" ENV ANDROID_NDK_HOME ${ANDROID_HOME}/ndk/${NDK_VERSION} -FROM rust:1.84 AS rust_builder +FROM rust:1.84@sha256:738ae99a3d75623f41e6882566b4ef37e38a9840244a47efd4a0ca22e9628b88 AS rust_builder ARG RUSTUP_TOOLCHAIN=nightly-2024-12-16 ARG TARGETS="aarch64-unknown-linux-musl x86_64-unknown-linux-musl aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android" diff --git a/android/apolloui/build.gradle b/android/apolloui/build.gradle index 0821bc67..2c8d5a0f 100644 --- a/android/apolloui/build.gradle +++ b/android/apolloui/build.gradle @@ -101,8 +101,8 @@ android { applicationId "io.muun.apollo" minSdk 21 targetSdk 35 - versionCode 1506 - versionName "55.6" + versionCode 1507 + versionName "55.7" // Use default Proguard file, bundled with Android Gradle Plugin // See: https://issuetracker.google.com/issues/126772206 diff --git a/android/apolloui/src/main/java/io/muun/apollo/data/preferences/MinFeeRateRepository.kt b/android/apolloui/src/main/java/io/muun/apollo/data/preferences/MinFeeRateRepository.kt index 2172bd20..54c6ae90 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/data/preferences/MinFeeRateRepository.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/data/preferences/MinFeeRateRepository.kt @@ -3,6 +3,7 @@ package io.muun.apollo.data.preferences import android.content.Context import io.muun.apollo.data.preferences.adapter.DoublePreferenceAdapter import io.muun.apollo.data.preferences.rx.Preference +import io.muun.common.Rules.OP_MINIMUM_FEE_VALUE import javax.inject.Inject // Open for mockito to mock/spy @@ -18,7 +19,7 @@ open class MinFeeRateRepository @Inject constructor( private val preference: Preference get() = rxSharedPreferences.getObject( KEY, - 1.0, // Default (lowest limit) just to avoid NPE problems before RTD is pulled + OP_MINIMUM_FEE_VALUE, // Default (lowest limit) just to avoid NPE problems before RTD is pulled DoublePreferenceAdapter.INSTANCE ) 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 4b501c98..c1cc4000 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 @@ -157,7 +157,18 @@ sealed class AnalyticsEvent(metadataKeyValues: List> = listOf( class S_PASSWORD_CHANGE_START : AnalyticsEvent() class S_PASSWORD_CHANGE_END : AnalyticsEvent() class S_SEND : AnalyticsEvent() - class S_SELECT_FEE : AnalyticsEvent() + class S_SELECT_FEE( + feeRateFastInVBytes: Double, + feeRateMidInVBytes: Double, + feeRateSlowInVBytes: Double, + ) : AnalyticsEvent( + listOf( + "fast" to feeRateFastInVBytes, + "mid" to feeRateMidInVBytes, + "slow" to feeRateSlowInVBytes + ) + ) + class S_MANUALLY_ENTER_FEE : AnalyticsEvent() class S_EMERGENCY_KIT_SLIDES(step: Int) : AnalyticsEvent(listOf("step" to step)) class S_EMERGENCY_KIT_SAVE : AnalyticsEvent() diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/manual_fee/ManualFeeFragment.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/manual_fee/ManualFeeFragment.kt index 5a4ab54c..fda4366d 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/manual_fee/ManualFeeFragment.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/manual_fee/ManualFeeFragment.kt @@ -98,7 +98,7 @@ class ManualFeeFragment : SingleFragment(), ManualFeeView { handleFeeRateTooHigh() feeRateInSatsPerVbyte < state.minFeeRateForTarget(feeWindow.slowConfTarget) -> - handleWarningFeeRateLow() + handleWarningFeeRateLow(feeRateInSatsPerVbyte) else -> { // All good! @@ -146,12 +146,15 @@ class ManualFeeFragment : SingleFragment(), ManualFeeView { ) } - private fun handleWarningFeeRateLow() { + private fun handleWarningFeeRateLow(feeRateInSatsPerVbyte: Double) { binding.statusMessage.setWarning( R.string.manual_fee_low_warning_message, R.string.manual_fee_low_warning_desc ) binding.confirmFee.isEnabled = true // just a warning + binding.confirmFee.setOnClickListener { + presenter.confirmFee(feeRateInSatsPerVbyte) + } } private fun onHowThisWorksClick() { diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/recommended_fee/RecommendedFeeFragment.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/recommended_fee/RecommendedFeeFragment.kt index 2c153c8f..5333f474 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/recommended_fee/RecommendedFeeFragment.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/recommended_fee/RecommendedFeeFragment.kt @@ -51,7 +51,7 @@ class RecommendedFeeFragment : SingleFragment(), Recomm @BindString(R.string.fee_options_whats_this) lateinit var whatsThisText: String - private var selectedFeeRateInVBytes: Double? = null // Starts with null if no rate zpre-selected + private var selectedFeeRateInVBytes: Double? = null // Starts with null if no rate pre-selected @State lateinit var mBitcoinUnit: BitcoinUnit diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/recommended_fee/RecommendedFeePresenter.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/recommended_fee/RecommendedFeePresenter.kt index 3827a07c..7571705c 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/recommended_fee/RecommendedFeePresenter.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/fragments/recommended_fee/RecommendedFeePresenter.kt @@ -1,18 +1,18 @@ package io.muun.apollo.presentation.ui.fragments.recommended_fee import android.os.Bundle -import io.muun.apollo.domain.analytics.AnalyticsEvent import io.muun.apollo.domain.analytics.AnalyticsEvent.S_MORE_INFO import io.muun.apollo.domain.analytics.AnalyticsEvent.S_MORE_INFO_TYPE import io.muun.apollo.domain.analytics.AnalyticsEvent.S_SELECT_FEE import io.muun.apollo.domain.selector.BitcoinUnitSelector import io.muun.apollo.presentation.ui.base.SingleFragmentPresenter import io.muun.apollo.presentation.ui.base.di.PerFragment +import newop.EditFeeState import javax.inject.Inject @PerFragment class RecommendedFeePresenter @Inject constructor( - private val bitcoinUnitSel: BitcoinUnitSelector + private val bitcoinUnitSel: BitcoinUnitSelector, ) : SingleFragmentPresenter() { override fun setUp(arguments: Bundle) { @@ -22,6 +22,7 @@ class RecommendedFeePresenter @Inject constructor( parentPresenter .watchEditFeeState() .doOnNext(view::setState) + .doOnNext(this::reportScreen) .let(this::subscribeTo) } @@ -33,8 +34,15 @@ class RecommendedFeePresenter @Inject constructor( parentPresenter.editFeeManually() } - override fun getEntryEvent(): AnalyticsEvent = - S_SELECT_FEE() + fun reportScreen(state: EditFeeState) { + // TODO this is a bit duplicated in view::setState, we could refactor a bit better + val feeWindow = state.resolved.paymentContext.feeWindow + val feeRateFastInVBytes = state.minFeeRateForTarget(feeWindow.fastConfTarget) + val feeRateMidInVBytes = state.minFeeRateForTarget(feeWindow.mediumConfTarget) + val feeRateSlowInVBytes = state.minFeeRateForTarget(feeWindow.slowConfTarget) + + analytics.report(S_SELECT_FEE(feeRateFastInVBytes, feeRateMidInVBytes, feeRateSlowInVBytes)) + } fun reportShowSelectFeeInfo() { analytics.report(S_MORE_INFO(S_MORE_INFO_TYPE.SELECT_FEE)) diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/ConfirmStateViewModel.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/ConfirmStateViewModel.kt index e4ab225f..73977319 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/ConfirmStateViewModel.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/ConfirmStateViewModel.kt @@ -47,13 +47,6 @@ class ConfirmStateViewModel private constructor( get() = resolved.paymentContext - // Yes, the impl calls a property that states fee rate is in SatsPerVByte but that is a very - // long legacy naming error, that property's unit are satoshis per weight unit. Trust me, I'm - // an engineer. - val feeRateInSatsPerWeight: Double - get() = - amountInfo.feeRateInSatsPerVByte - val onchainFee: BitcoinAmount get() = if (validated.swapInfo == null) { 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 abef558a..7ca601f8 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 @@ -25,7 +25,6 @@ import io.muun.apollo.domain.analytics.AnalyticsEvent.S_MANUALLY_ENTER_FEE import io.muun.apollo.domain.analytics.AnalyticsEvent.S_NEW_OP_AMOUNT import io.muun.apollo.domain.analytics.AnalyticsEvent.S_NEW_OP_CONFIRMATION import io.muun.apollo.domain.analytics.AnalyticsEvent.S_NEW_OP_DESCRIPTION -import io.muun.apollo.domain.analytics.AnalyticsEvent.S_SELECT_FEE import io.muun.apollo.domain.analytics.NewOperationOrigin import io.muun.apollo.domain.libwallet.adapt import io.muun.apollo.domain.libwallet.destinationPubKey @@ -576,7 +575,7 @@ class NewOperationActivity : SingleFragmentActivity(), } override fun goToEditFeeManually() { - generateAppEvent(S_SELECT_FEE().eventId) + generateAppEvent("s_select_fee") replaceFragment(ManualFeeFragment(), true) } diff --git a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationPresenter.kt b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationPresenter.kt index cbadc3ca..b5eae6f5 100644 --- a/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationPresenter.kt +++ b/android/apolloui/src/main/java/io/muun/apollo/presentation/ui/new_operation/NewOperationPresenter.kt @@ -799,11 +799,9 @@ class NewOperationPresenter @Inject constructor( private fun opSubmittedMetadata(stateVm: ConfirmStateViewModel): ArrayList> { val paymentType = stateVm.paymentIntent.getPaymentType() val payCtx = stateVm.paymentContext - val selectedFeeRate = Preconditions.checkNotNull(stateVm.feeRateInSatsPerWeight) - - val objects = ArrayList>() + val selectedFeeRate = Preconditions.checkNotNull(stateVm.amountInfo.feeRateInSatsPerVByte) val type: AnalyticsEvent.E_FEE_OPTION_TYPE = getFeeOptionTypeParam(selectedFeeRate, payCtx) - val feeRateInSatsPerVbyte = Rules.toSatsPerVbyte(selectedFeeRate) + val amount = BitcoinAmount.fromLibwallet(stateVm.amountInfo.amount) val fee = BitcoinAmount.fromLibwallet(stateVm.validated.fee) val total = BitcoinAmount.fromLibwallet(stateVm.validated.total) @@ -821,8 +819,9 @@ class NewOperationPresenter @Inject constructor( val feeBumpPolicy = stateVm.validated.feeBumpInfo?.refreshPolicy val feeBumpSecondsSinceLastUpdate = stateVm.validated.feeBumpInfo?.secondsSinceLastUpdate + val objects = ArrayList>() objects.add(("fee_type" to type.name.lowercase(Locale.getDefault()))) - objects.add(("sats_per_virtual_byte" to feeRateInSatsPerVbyte)) + objects.add(("sats_per_virtual_byte" to selectedFeeRate)) objects.add(("amount" to SerializationUtils.serializeBitcoinAmount(amount))) objects.add(("fee" to SerializationUtils.serializeBitcoinAmount(fee))) objects.add(("total" to SerializationUtils.serializeBitcoinAmount(total))) @@ -848,7 +847,7 @@ class NewOperationPresenter @Inject constructor( } private fun getFeeOptionTypeParam( - selectedFeeRate: Double, + selectedFeeRateInSatsPerVByte: Double, payCtx: newop.PaymentContext, ): AnalyticsEvent.E_FEE_OPTION_TYPE { @@ -856,19 +855,20 @@ class NewOperationPresenter @Inject constructor( val editFeeState = EditFeeState() editFeeState.resolved = Resolved().apply { this.paymentContext = payCtx } + // These are in satsPerVByte, that's the unit of feeWindow, targettedFees. Avoid long names. val fastFeeRate = editFeeState.minFeeRateForTarget(payCtx.feeWindow.fastConfTarget) val mediumFeeRate = editFeeState.minFeeRateForTarget(payCtx.feeWindow.mediumConfTarget) val slowFeeRate = editFeeState.minFeeRateForTarget(payCtx.feeWindow.slowConfTarget) val type: AnalyticsEvent.E_FEE_OPTION_TYPE = when { - Rules.feeRateEquals(selectedFeeRate, fastFeeRate) -> + Rules.feeRateEquals(selectedFeeRateInSatsPerVByte, fastFeeRate) -> AnalyticsEvent.E_FEE_OPTION_TYPE.FAST - Rules.feeRateEquals(selectedFeeRate, mediumFeeRate) -> + Rules.feeRateEquals(selectedFeeRateInSatsPerVByte, mediumFeeRate) -> AnalyticsEvent.E_FEE_OPTION_TYPE.MEDIUM - Rules.feeRateEquals(selectedFeeRate, slowFeeRate) -> + Rules.feeRateEquals(selectedFeeRateInSatsPerVByte, slowFeeRate) -> AnalyticsEvent.E_FEE_OPTION_TYPE.SLOW else -> diff --git a/common/src/main/java/io/muun/common/Rules.java b/common/src/main/java/io/muun/common/Rules.java index 821432bf..1ce34da1 100644 --- a/common/src/main/java/io/muun/common/Rules.java +++ b/common/src/main/java/io/muun/common/Rules.java @@ -38,8 +38,15 @@ public class Rules { /** Useful to keep track of conversions. **/ public static final int VBYTE_TO_WEIGHT_UNIT_RATIO = 4; // 1 vbyte == 4 WU + /** The minimum fee value for an operation. **/ + public static final double OP_MINIMUM_FEE_VALUE = 0.1d; + + /** The minimum fee value for an operation for legacy clients. **/ + public static final double OP_MINIMUM_FEE_VALUE_LEGACY = 1d; + /** The minimum fee rate for an operation, in satoshis per weight unit. */ - public static final double OP_MINIMUM_FEE_RATE = 1d / VBYTE_TO_WEIGHT_UNIT_RATIO; + public static final double OP_MINIMUM_FEE_RATE = + OP_MINIMUM_FEE_VALUE / VBYTE_TO_WEIGHT_UNIT_RATIO; /** The maximum fee rate for an operation, in satoshis per weight unit. */ public static final double OP_MAXIMUM_FEE_RATE = 999d / VBYTE_TO_WEIGHT_UNIT_RATIO; diff --git a/libwallet/librs/libs.Dockerfile b/libwallet/librs/libs.Dockerfile index b306c32c..87e5e7c4 100644 --- a/libwallet/librs/libs.Dockerfile +++ b/libwallet/librs/libs.Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.84 +FROM rust:1.84@sha256:738ae99a3d75623f41e6882566b4ef37e38a9840244a47efd4a0ca22e9628b88 ARG RUSTUP_TOOLCHAIN=nightly-2024-12-16 diff --git a/tools/strip-elf-debug.py b/tools/strip-elf-debug.py new file mode 100644 index 00000000..c9dccdaf --- /dev/null +++ b/tools/strip-elf-debug.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +""" +Zeroes out .debug_* ELF sections in all .so files found under the given directories. + +Go's compiler generates non-deterministic DWARF string tables that don't affect +compiled code but cause binary diffs between otherwise identical builds. +Zeroing these sections before comparing allows a clean diff. + +Usage: strip-elf-debug.py [dir2 ...] +""" + +import sys +import os +import struct + + +def strip_elf_debug(filename): + with open(filename, 'r+b') as f: + data = bytearray(f.read()) + + if data[:4] != b'\x7fELF': + return + + ei_class = data[4] # 1=32-bit, 2=64-bit + endian = '<' if data[5] == 1 else '>' + + if ei_class == 2: + e_shoff = struct.unpack_from(endian + 'Q', data, 40)[0] + e_shentsize, e_shnum, e_shstrndx = struct.unpack_from(endian + 'HHH', data, 58) + + def get_sh(i): + b = e_shoff + i * e_shentsize + return (struct.unpack_from(endian + 'I', data, b)[0], + struct.unpack_from(endian + 'Q', data, b + 24)[0], + struct.unpack_from(endian + 'Q', data, b + 32)[0]) + else: + e_shoff = struct.unpack_from(endian + 'I', data, 32)[0] + e_shentsize, e_shnum, e_shstrndx = struct.unpack_from(endian + 'HHH', data, 46) + + def get_sh(i): + b = e_shoff + i * e_shentsize + return (struct.unpack_from(endian + 'I', data, b)[0], + struct.unpack_from(endian + 'I', data, b + 16)[0], + struct.unpack_from(endian + 'I', data, b + 20)[0]) + + _, strtab_off, strtab_size = get_sh(e_shstrndx) + strtab = data[strtab_off:strtab_off + strtab_size] + + for i in range(e_shnum): + sh_name, sh_off, sh_size = get_sh(i) + name = strtab[sh_name:strtab.find(b'\x00', sh_name)].decode('utf-8', errors='replace') + if name.startswith('.debug') and sh_off > 0 and sh_size > 0: + data[sh_off:sh_off + sh_size] = b'\x00' * sh_size + + with open(filename, 'wb') as f: + f.write(data) + + +for root_dir in sys.argv[1:]: + for dirpath, _, files in os.walk(root_dir): + for fname in files: + if fname.endswith('.so'): + try: + strip_elf_debug(os.path.join(dirpath, fname)) + except Exception: + pass diff --git a/tools/verify-apollo.sh b/tools/verify-apollo.sh index 6d5d8a1f..8424a42c 100755 --- a/tools/verify-apollo.sh +++ b/tools/verify-apollo.sh @@ -82,6 +82,11 @@ rm -rf "$tmp/baseline"/* unzip -q -o "$baseline_apk_dir" -d "$tmp/baseline" rm -r "$tmp"/baseline/{META-INF,resources.arsc} +echo "Stripping DWARF debug sections from .so files..." +# Go's compiler generates non-deterministic DWARF string tables that don't affect +# compiled code but cause binary diffs. Zero out .debug_* ELF sections before comparing. +python3 "$(dirname "$0")/strip-elf-debug.py" "$tmp/to_verify" "$tmp/baseline" + echo "Comparing files..." diff_non_lib=$(diff -r "$tmp/to_verify" "$tmp/baseline" || true)