Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions android/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion android/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions android/apolloui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,7 +19,7 @@ open class MinFeeRateRepository @Inject constructor(
private val preference: Preference<Double>
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
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,18 @@ sealed class AnalyticsEvent(metadataKeyValues: List<Pair<String, Any>> = 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class ManualFeeFragment : SingleFragment<ManualFeePresenter>(), ManualFeeView {
handleFeeRateTooHigh()

feeRateInSatsPerVbyte < state.minFeeRateForTarget(feeWindow.slowConfTarget) ->
handleWarningFeeRateLow()
handleWarningFeeRateLow(feeRateInSatsPerVbyte)

else -> {
// All good!
Expand Down Expand Up @@ -146,12 +146,15 @@ class ManualFeeFragment : SingleFragment<ManualFeePresenter>(), 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class RecommendedFeeFragment : SingleFragment<RecommendedFeePresenter>(), 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RecommendedFeeView, RecommendedFeeParentPresenter>() {

override fun setUp(arguments: Bundle) {
Expand All @@ -22,6 +22,7 @@ class RecommendedFeePresenter @Inject constructor(
parentPresenter
.watchEditFeeState()
.doOnNext(view::setState)
.doOnNext(this::reportScreen)
.let(this::subscribeTo)
}

Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -576,7 +575,7 @@ class NewOperationActivity : SingleFragmentActivity<NewOperationPresenter>(),
}

override fun goToEditFeeManually() {
generateAppEvent(S_SELECT_FEE().eventId)
generateAppEvent("s_select_fee")
replaceFragment(ManualFeeFragment(), true)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -799,11 +799,9 @@ class NewOperationPresenter @Inject constructor(
private fun opSubmittedMetadata(stateVm: ConfirmStateViewModel): ArrayList<Pair<String, Any>> {
val paymentType = stateVm.paymentIntent.getPaymentType()
val payCtx = stateVm.paymentContext
val selectedFeeRate = Preconditions.checkNotNull(stateVm.feeRateInSatsPerWeight)

val objects = ArrayList<Pair<String, Any>>()
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)
Expand All @@ -821,8 +819,9 @@ class NewOperationPresenter @Inject constructor(
val feeBumpPolicy = stateVm.validated.feeBumpInfo?.refreshPolicy
val feeBumpSecondsSinceLastUpdate = stateVm.validated.feeBumpInfo?.secondsSinceLastUpdate

val objects = ArrayList<Pair<String, Any>>()
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)))
Expand All @@ -848,27 +847,28 @@ class NewOperationPresenter @Inject constructor(
}

private fun getFeeOptionTypeParam(
selectedFeeRate: Double,
selectedFeeRateInSatsPerVByte: Double,
payCtx: newop.PaymentContext,
): AnalyticsEvent.E_FEE_OPTION_TYPE {

// TODO: expose minFeeRateForTarget for newop.PaymentContext (or live with this monstrosity)
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 ->
Expand Down
9 changes: 8 additions & 1 deletion common/src/main/java/io/muun/common/Rules.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion libwallet/librs/libs.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM rust:1.84
FROM rust:1.84@sha256:738ae99a3d75623f41e6882566b4ef37e38a9840244a47efd4a0ca22e9628b88

ARG RUSTUP_TOOLCHAIN=nightly-2024-12-16

Expand Down
66 changes: 66 additions & 0 deletions tools/strip-elf-debug.py
Original file line number Diff line number Diff line change
@@ -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 <dir1> [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
5 changes: 5 additions & 0 deletions tools/verify-apollo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading