Skip to content

Commit

Permalink
Merge pull request #140 from muun/51.6-release-branch
Browse files Browse the repository at this point in the history
Apollo: Release source code for 51.6
  • Loading branch information
acrespo authored Jan 22, 2024
2 parents 5850b84 + 7d0d67f commit 2edc05e
Show file tree
Hide file tree
Showing 20 changed files with 312 additions and 73 deletions.
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]

## [51.6] - 2024-01-17

### FIXED

- A problem regarding background/foreground event tracking

### CHANGED

- Enhanced reliability of certain specific swaps' execution

## [51.5] - 2023-12-22

### FIXED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.muun.apollo.data.os.GooglePlayServicesHelper;
import io.muun.apollo.data.serialization.dates.ApolloZonedDateTime;
import io.muun.apollo.domain.libwallet.Invoice;
import io.muun.apollo.domain.model.BackgroundEvent;
import io.muun.apollo.domain.model.BitcoinAmount;
import io.muun.apollo.domain.model.EmergencyKitExport;
import io.muun.apollo.domain.model.IncomingSwapFulfillmentData;
Expand All @@ -17,6 +18,7 @@
import io.muun.apollo.domain.model.SystemUserInfo;
import io.muun.apollo.domain.model.user.UserProfile;
import io.muun.common.api.AndroidSystemUserInfoJson;
import io.muun.common.api.BackgroundEventJson;
import io.muun.common.api.BitcoinAmountJson;
import io.muun.common.api.ChallengeKeyJson;
import io.muun.common.api.ChallengeSetupJson;
Expand Down Expand Up @@ -508,7 +510,26 @@ public FeedbackJson mapFeedback(String content) {
* Create a Submarine Swap Request.
*/
public SubmarineSwapRequestJson mapSubmarineSwapRequest(SubmarineSwapRequest request) {
return new SubmarineSwapRequestJson(request.invoice, request.swapExpirationInBlocks);
return new SubmarineSwapRequestJson(
request.invoice,
request.swapExpirationInBlocks,
request.origin.name().toLowerCase(Locale.getDefault()), // match analytics event
mapBackgroundTimes(request.bkgTimes)
);
}

private List<BackgroundEventJson> mapBackgroundTimes(List<BackgroundEvent> bkgTimes) {

final List<BackgroundEventJson> result = new ArrayList<>();

for (BackgroundEvent bkgTime : bkgTimes) {
result.add(new BackgroundEventJson(
bkgTime.getBeginTimeInMillis(),
bkgTime.getDurationInMillis()
));
}

return result;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,38 @@ class AuthHeaderInterceptor @Inject constructor(
private val authRepository: AuthRepository,
) : BaseInterceptor() {

override fun processRequest(request: Request): Request {
override fun processRequest(originalRequest: Request): Request {
// Attach the request header if a token is available:
val serverJwt = authRepository.serverJwt.orElse(null)
return if (serverJwt != null) {
request.newBuilder()
originalRequest.newBuilder()
.addHeader(HeaderUtils.AUTHORIZATION, "Bearer $serverJwt")
.build()

} else {
request
originalRequest
}
}

override fun processResponse(response: Response): Response {
override fun processResponse(originalResponse: Response): Response {
// Save the token in the response header if one is found
val authHeaderValue = response.header(HeaderUtils.AUTHORIZATION)
val authHeaderValue = originalResponse.header(HeaderUtils.AUTHORIZATION)
HeaderUtils.getBearerTokenFromHeader(authHeaderValue)
.ifPresent { serverJwt: String -> authRepository.storeServerJwt(serverJwt) }

// We need a reliable way (across all envs: local, CI, prd, etc...) to identify the logout
// requests. We could inject HoustonConfig and build the entire URL (minus port)
// or... we can do this :)
val url = response.request().url().url().toString()
val url = originalResponse.request().url().url().toString()
val isLogout = url.endsWith("sessions/logout")

if (!isLogout) {
val sessionStatusHeaderValue = response.header(HeaderUtils.SESSION_STATUS)
val sessionStatusHeaderValue = originalResponse.header(HeaderUtils.SESSION_STATUS)
HeaderUtils.getSessionStatusFromHeader(sessionStatusHeaderValue)
.ifPresent { sessionStatus: SessionStatus ->
authRepository.storeSessionStatus(sessionStatus)
}
}
return response
return originalResponse
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ class LanguageHeaderInterceptor @Inject constructor(
private val applicationContext: Context,
) : BaseInterceptor() {

override fun processRequest(request: Request): Request {
override fun processRequest(originalRequest: Request): Request {
val language = applicationContext.locale().language
return request.newBuilder()
return originalRequest.newBuilder()
.addHeader(
HeaderUtils.CLIENT_LANGUAGE,
language.ifEmpty { HeaderUtils.DEFAULT_LANGUAGE_VALUE }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.muun.apollo.data.preferences

import android.content.Context
import io.muun.apollo.data.preferences.adapter.JsonListPreferenceAdapter
import io.muun.apollo.data.preferences.rx.Preference
import io.muun.apollo.domain.model.BackgroundEvent
import javax.inject.Inject

class BackgroundTimesRepository @Inject constructor(
context: Context,
repositoryRegistry: RepositoryRegistry,
) : BaseRepository(context, repositoryRegistry) {

companion object {
private const val BACKGROUND_TIMES_KEY = "background_times_key"
private const val LAST_BACKGROUND_BEGIN_TIME_KEY = "last_background_begin_time_key"
}

private class StoredBackgroundEvent {
var beginTimeInMillis: Long = 0
var durationInMillis: Long = 0

/**
* Constructor from the model.
*/
constructor(bkgEvent: BackgroundEvent) {
beginTimeInMillis = bkgEvent.beginTimeInMillis
durationInMillis = bkgEvent.durationInMillis

}

/**
* JSON constructor.
*/
@Suppress("unused")
constructor()

fun toModel(): BackgroundEvent {
return BackgroundEvent(
beginTimeInMillis,
durationInMillis
)
}
}

override fun getFileName(): String =
"background_times"

private val lastBackgroundBeginTimePreference: Preference<Long?> =
rxSharedPreferences.getLong(LAST_BACKGROUND_BEGIN_TIME_KEY, null)

private val backgroundTimesPreferences: Preference<List<StoredBackgroundEvent>> =
rxSharedPreferences.getObject(
BACKGROUND_TIMES_KEY,
emptyList(),
JsonListPreferenceAdapter(StoredBackgroundEvent::class.java)
)

fun recordEnterBackground() {
lastBackgroundBeginTimePreference.set(System.currentTimeMillis())
}

fun getLastBackgroundBeginTime(): Long? {
return lastBackgroundBeginTimePreference.get()
}

fun recordBackgroundEvent(bkgBeginTime: Long, duration: Long) {
val storedBkgTimes = getBackgroundTimes()
val bkgTimes = storedBkgTimes.toMutableList()

bkgTimes.add(BackgroundEvent(bkgBeginTime, duration))

storeBkgTimes(bkgTimes)
lastBackgroundBeginTimePreference.set(null)
}

fun getBackgroundTimes(): List<BackgroundEvent> {
return backgroundTimesPreferences.get()!!.map { it.toModel() }
}

fun pruneIfGreaterThan(maxBkgTimesArraySize: Int) {
val storedBkgTimes = getBackgroundTimes()
val bkgTimes = storedBkgTimes.takeLast(maxBkgTimesArraySize)

storeBkgTimes(bkgTimes)
}

private fun storeBkgTimes(bkgTimes: List<BackgroundEvent>) {
val storedBkgTimes = bkgTimes.map { it.toJson() }
backgroundTimesPreferences.set(storedBkgTimes)
}

private fun BackgroundEvent.toJson(): StoredBackgroundEvent =
StoredBackgroundEvent(this)
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ class RepositoryRegistry {
PlayIntegrityNonceRepository::class.java,
NotificationPermissionStateRepository::class.java,
NotificationPermissionDeniedRepository::class.java,
NotificationPermissionSkippedRepository::class.java
NotificationPermissionSkippedRepository::class.java,
BackgroundTimesRepository::class.java
)

// Notable exceptions:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.muun.apollo.domain

import io.muun.apollo.data.preferences.BackgroundTimesRepository
import javax.inject.Inject

class BackgroundTimesService @Inject constructor(
private val backgroundTimesRepository: BackgroundTimesRepository,
) {

private val MAX_BKG_TIMES_ARRAY_SIZE: Int = 100

fun enterBackground() {
backgroundTimesRepository.recordEnterBackground()
}

fun enterForeground() {
backgroundTimesRepository.pruneIfGreaterThan(MAX_BKG_TIMES_ARRAY_SIZE)

val backgroundBeginTime = backgroundTimesRepository.getLastBackgroundBeginTime()
@Suppress("FoldInitializerAndIfToElvis")
if (backgroundBeginTime == null) {
return
}

val duration = System.currentTimeMillis() - backgroundBeginTime
backgroundTimesRepository.recordBackgroundEvent(backgroundBeginTime, duration)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package io.muun.apollo.domain.action.operation

import androidx.annotation.VisibleForTesting
import io.muun.apollo.data.net.HoustonClient
import io.muun.apollo.data.preferences.BackgroundTimesRepository
import io.muun.apollo.data.preferences.FeeWindowRepository
import io.muun.apollo.data.preferences.KeysRepository
import io.muun.apollo.domain.action.base.BaseAsyncAction1
import io.muun.apollo.domain.action.base.BaseAsyncAction2
import io.muun.apollo.domain.analytics.NewOperationOrigin
import io.muun.apollo.domain.errors.newop.InvalidSwapException
import io.muun.apollo.domain.errors.newop.InvoiceExpiredException
import io.muun.apollo.domain.libwallet.DecodedInvoice
Expand Down Expand Up @@ -45,41 +47,55 @@ import javax.inject.Inject
import javax.inject.Singleton
import javax.money.MonetaryAmount


@Singleton
class ResolveLnInvoiceAction @Inject internal constructor(
private val network: NetworkParameters,
private val houstonClient: HoustonClient,
private val keysRepository: KeysRepository,
private val feeWindowRepository: FeeWindowRepository,
) : BaseAsyncAction1<String, PaymentRequest>() {
private val backgroundTimesRepository: BackgroundTimesRepository
) : BaseAsyncAction2<String, NewOperationOrigin, PaymentRequest>() {

companion object {
private const val BLOCKS_IN_A_DAY = 24 * 6 // this is 144
private const val DAYS_IN_A_WEEK = 7
}

override fun action(rawInvoice: String): Observable<PaymentRequest> =
override fun action(
rawInvoice: String,
origin: NewOperationOrigin,
): Observable<PaymentRequest> =
Observable.defer {
resolveLnUri(rawInvoice)
resolveLnUri(rawInvoice, origin)
}

private fun resolveLnUri(rawInvoice: String): Observable<PaymentRequest> {
private fun resolveLnUri(
rawInvoice: String,
origin: NewOperationOrigin,
): Observable<PaymentRequest> {
val invoice = decodeInvoice(network, rawInvoice)

if (invoice.expirationTime.isBefore(DateUtils.now())) {
throw InvoiceExpiredException(invoice.original)
}

return prepareSwap(buildSubmarineSwapRequest(invoice))
return prepareSwap(buildSubmarineSwapRequest(invoice, origin))
.map { swap: SubmarineSwap -> buildPaymentRequest(invoice, swap) }
}

private fun buildSubmarineSwapRequest(invoice: DecodedInvoice): SubmarineSwapRequest {
private fun buildSubmarineSwapRequest(
invoice: DecodedInvoice,
origin: NewOperationOrigin,
): SubmarineSwapRequest {
// We used to care a lot about this number for v1 swaps since it was the refund time
// With swaps v2 we have collaborative refunds so we don't quite care and go for the max
val swapExpirationInBlocks = BLOCKS_IN_A_DAY * DAYS_IN_A_WEEK
return SubmarineSwapRequest(invoice.original, swapExpirationInBlocks)
return SubmarineSwapRequest(
invoice.original,
swapExpirationInBlocks,
origin,
backgroundTimesRepository.getBackgroundTimes()
)
}

private fun buildPaymentRequest(invoice: DecodedInvoice, swap: SubmarineSwap): PaymentRequest {
Expand Down Expand Up @@ -161,7 +177,7 @@ class ResolveLnInvoiceAction @Inject internal constructor(
* Validate Submarine Swap Server response. The end goal is to verify that the redeem script
* returned by the server is the script that is actually encoded in the reported swap address.
*/
fun validateSwap(
private fun validateSwap(
originalInvoice: String,
originalExpirationInBlocks: Int,
userPublicKeyPair: PublicKeyPair,
Expand Down Expand Up @@ -216,15 +232,15 @@ class ResolveLnInvoiceAction @Inject internal constructor(
// Check that the witness script was computed according to the given parameters
val witnessScript = createWitnessScript(
Encodings.hexToBytes(paymentHashInHex),
userPublicKey.getPublicKeyBytes(),
muunPublicKey.getPublicKeyBytes(),
userPublicKey.publicKeyBytes,
muunPublicKey.publicKeyBytes,
Encodings.hexToBytes(fundingOutput.serverPublicKeyInHex),
fundingOutput.expirationInBlocks!!.toLong()
)

// Check that the script hashes to the output address we'll be using
val outputAddress: Address = createAddress(network, witnessScript)
if (!outputAddress.toString().equals(fundingOutput.outputAddress)) {
if (outputAddress.toString() != fundingOutput.outputAddress) {
return false
}

Expand All @@ -243,7 +259,7 @@ class ResolveLnInvoiceAction @Inject internal constructor(
/**
* Create the witness script for spending the submarine swap output.
*/
fun createWitnessScript(
private fun createWitnessScript(
swapPaymentHash256: ByteArray?,
userPublicKey: ByteArray?,
muunPublicKey: ByteArray?,
Expand Down Expand Up @@ -312,13 +328,13 @@ class ResolveLnInvoiceAction @Inject internal constructor(
.op(OP_CHECKSIG)
.op(OP_ENDIF)
.build()
.getProgram()
.program
}

/**
* Create an address.
*/
fun createAddress(network: NetworkParameters?, witnessScript: ByteArray?): Address {
private fun createAddress(network: NetworkParameters?, witnessScript: ByteArray?): Address {
val witnessScriptHash: ByteArray = Sha256Hash.hash(witnessScript)
return SegwitAddress.fromHash(network, witnessScriptHash)
}
Expand Down
Loading

0 comments on commit 2edc05e

Please sign in to comment.