Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apollo: Release source code for 51.6 #140

Merged
merged 1 commit into from
Jan 22, 2024
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]

## [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