diff --git a/api/build.gradle.kts b/api/build.gradle.kts index dac4b0039..8d33b5c93 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { implementation(Libs.rxjava) implementation(Libs.kotlinx_coroutines_core) implementation(Libs.kotlinx_serialization_json) + implementation(Libs.kotlinx_datetime) implementation(Libs.inject) implementation(Libs.grpc_okhttp) diff --git a/api/src/main/java/com/getcode/model/Kin.kt b/api/src/main/java/com/getcode/model/Kin.kt index c5f3e3d6f..cfd3f0a3f 100644 --- a/api/src/main/java/com/getcode/model/Kin.kt +++ b/api/src/main/java/com/getcode/model/Kin.kt @@ -63,10 +63,13 @@ data class Kin(val quarks: Long): Value { } } -fun min(a: Kin, b: Kin): Kin { +private fun min(a: Kin, b: Kin): Kin { if (a.quarks > b.quarks) { return b } return a -} \ No newline at end of file +} + +val Kin.description: String + get() = "K ${toKinTruncating().quarks} ${fractionalQuarks()}" \ No newline at end of file diff --git a/api/src/main/java/com/getcode/network/client/Client_Transaction.kt b/api/src/main/java/com/getcode/network/client/Client_Transaction.kt index 6c1f4a565..637c0af1c 100644 --- a/api/src/main/java/com/getcode/network/client/Client_Transaction.kt +++ b/api/src/main/java/com/getcode/network/client/Client_Transaction.kt @@ -31,7 +31,9 @@ import com.getcode.solana.keys.base58 import com.getcode.solana.organizer.GiftCardAccount import com.getcode.solana.organizer.Organizer import com.getcode.solana.organizer.Relationship +import com.getcode.utils.TraceType import com.getcode.utils.flowInterval +import com.getcode.utils.trace import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers @@ -337,15 +339,20 @@ fun Client.withdrawExternally( Completable.complete() }.doOnComplete { Timber.d(steps.joinToString("\n")) - } + }.concatWith( // 6. Execute withdrawal - .concatWith( - withdraw( - amount = amount, - organizer = organizer, - destination = destination - ) + withdraw( + amount = amount, + organizer = organizer, + destination = destination ) + ).doOnComplete { + trace( + tag = "Trx", + message = "Withdraw completed", + type = TraceType.Process + ) + } } private fun Client.withdraw( @@ -517,6 +524,13 @@ fun Client.receiveFromPrimaryIfWithinLimits(organizer: Organizer): Completable { } } .ignoreElement() + .andThen { + trace( + tag = "Trx", + message = "Received from primary", + type = TraceType.Process + ) + } .andThen { fetchLimits(true) } } diff --git a/api/src/main/java/com/getcode/network/client/TransactionReceiver.kt b/api/src/main/java/com/getcode/network/client/TransactionReceiver.kt index 5050fcd9c..48764a281 100644 --- a/api/src/main/java/com/getcode/network/client/TransactionReceiver.kt +++ b/api/src/main/java/com/getcode/network/client/TransactionReceiver.kt @@ -76,8 +76,24 @@ class TransactionReceiver @Inject constructor( organizer = organizer ).blockingGet() + trace( + tag = "Trx", + message = "Received from relationship", + type = TraceType.Process, + metadata = { + "domain" to relationship.domain.relationshipHost + "kin" to relationship.partialBalance + } + ) + receivedTotal += relationship.partialBalance + trace( + tag = "Trx", + message = "Received from incoming", + type = TraceType.Process + ) + if (intent is IntentPublicTransfer) { setTray(organizer, intent.resultTray) } diff --git a/api/src/main/java/com/getcode/network/exchange/Exchange.kt b/api/src/main/java/com/getcode/network/exchange/Exchange.kt index f9283db33..604c813ee 100644 --- a/api/src/main/java/com/getcode/network/exchange/Exchange.kt +++ b/api/src/main/java/com/getcode/network/exchange/Exchange.kt @@ -11,6 +11,7 @@ import com.getcode.network.core.NetworkOracle import com.getcode.network.repository.PrefRepository import com.getcode.utils.ErrorUtils import com.getcode.utils.TraceType +import com.getcode.utils.format import com.getcode.utils.trace import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -23,6 +24,7 @@ import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.datetime.Instant import timber.log.Timber import java.util.Date import javax.inject.Inject @@ -249,6 +251,14 @@ class CodeExchange @Inject constructor( rates.rateForUsd()!! } + trace(tag = "Background", + message = "Updated rates", + type = TraceType.Process, + metadata = { + "date" to Instant.fromEpochMilliseconds(rates.dateMillis).format("yyyy-MM-dd HH:mm:ss") + } + ) + } @OptIn(ExperimentalTime::class) diff --git a/api/src/main/java/com/getcode/network/repository/TransactionRepository.kt b/api/src/main/java/com/getcode/network/repository/TransactionRepository.kt index 00561753c..9a65b20d7 100644 --- a/api/src/main/java/com/getcode/network/repository/TransactionRepository.kt +++ b/api/src/main/java/com/getcode/network/repository/TransactionRepository.kt @@ -584,6 +584,17 @@ class TransactionRepository @Inject constructor( .doOnSuccess { setLimits(it) setMaximumDeposit(it.maxDeposit) + trace( + tag = "Trx", + message = "Fetched limits", + type = TraceType.Process, + metadata = { + val sendLimit = it.sendLimitFor(CurrencyCode.USD) + if (sendLimit != null) { + "limitNextTx" to sendLimit + } + } + ) } .doOnError(ErrorUtils::handleError) .toFlowable() diff --git a/api/src/main/java/com/getcode/solana/organizer/Organizer.kt b/api/src/main/java/com/getcode/solana/organizer/Organizer.kt index a17f0c733..ecc5fe89b 100644 --- a/api/src/main/java/com/getcode/solana/organizer/Organizer.kt +++ b/api/src/main/java/com/getcode/solana/organizer/Organizer.kt @@ -8,7 +8,9 @@ import com.getcode.model.Kin import com.getcode.model.unusable import com.getcode.network.repository.getPublicKeyBase58 import com.getcode.solana.keys.* +import com.getcode.utils.TraceType import com.getcode.utils.timedTrace +import com.getcode.utils.trace import timber.log.Timber class Organizer( @@ -59,6 +61,15 @@ class Organizer( this.accountInfos = infos tray.createRelationships(infos) propagateBalances() + + trace( + tag = "Organizer", + message = "Fetched account infos", + type = TraceType.Process, + metadata = { + "tray" to tray.reportableRepresentation() + } + ) } fun getAccountInfo() = accountInfos diff --git a/api/src/main/java/com/getcode/solana/organizer/Tray.kt b/api/src/main/java/com/getcode/solana/organizer/Tray.kt index bbda8f243..2a2888509 100644 --- a/api/src/main/java/com/getcode/solana/organizer/Tray.kt +++ b/api/src/main/java/com/getcode/solana/organizer/Tray.kt @@ -1,6 +1,5 @@ package com.getcode.solana.organizer -import android.content.Context import com.getcode.crypt.DerivePath import com.getcode.crypt.DerivedKey import com.getcode.crypt.MnemonicPhrase @@ -8,9 +7,11 @@ import com.getcode.model.AccountInfo import com.getcode.model.Domain import com.getcode.model.Kin import com.getcode.model.RelationshipBox +import com.getcode.model.description import com.getcode.solana.keys.PublicKey +import com.getcode.solana.keys.base58 import com.getcode.utils.TraceType -import com.getcode.utils.timedTrace +import com.getcode.utils.padded import com.getcode.utils.trace import kotlin.math.min @@ -866,6 +867,29 @@ class Tray( return container } + fun reportableRepresentation(): List { + return listOf( + string(named = "Primary ", partialAccount = owner), + string(named = "Incoming ", partialAccount = incoming), + string(named = "Outgoing ", partialAccount = outgoing), + string("1 ", slot = slot(SlotType.Bucket1)), + string("10 ", slot = slot(SlotType.Bucket10)), + string("100 ", slot = slot(SlotType.Bucket100)), + string("1k ", slot = slot(SlotType.Bucket1k)), + string("10k ", slot = slot(SlotType.Bucket10k)), + string("100k ", slot = slot(SlotType.Bucket100k)), + string("1m ", slot = slot(SlotType.Bucket1m)), + ) + } + + private fun string(named: String, partialAccount: PartialAccount): String { + return "$named ${partialAccount.getCluster().vaultPublicKey.base58().padded(44)}) ${partialAccount.partialBalance.description}" + } + + private fun string(named: String, slot: Slot): String { + return "$named ${slot.getCluster().vaultPublicKey.base58().padded(44)}) ${slot.partialBalance.description}" + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/app/src/main/java/com/getcode/util/Instant.kt b/api/src/main/java/com/getcode/utils/Instant.kt similarity index 69% rename from app/src/main/java/com/getcode/util/Instant.kt rename to api/src/main/java/com/getcode/utils/Instant.kt index d31c36213..bb4a80e9a 100644 --- a/app/src/main/java/com/getcode/util/Instant.kt +++ b/api/src/main/java/com/getcode/utils/Instant.kt @@ -1,4 +1,4 @@ -package com.getcode.util +package com.getcode.utils import kotlinx.datetime.DatePeriod import kotlinx.datetime.DateTimeUnit @@ -9,6 +9,9 @@ import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.minus import kotlinx.datetime.plus import kotlinx.datetime.toLocalDateTime +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale fun Instant.toLocalDate(timeZone: TimeZone = TimeZone.currentSystemDefault()) = toLocalDateTime(timeZone).date @@ -18,4 +21,13 @@ fun LocalDate.atStartOfDay(tz: TimeZone = TimeZone.currentSystemDefault()) = atS fun LocalDate.atEndOfDay(tz: TimeZone = TimeZone.currentSystemDefault()): Instant { val tomorrowAtMidnight = ((this + DatePeriod(days = 1)).atStartOfDayIn(tz)) return tomorrowAtMidnight.minus(value = 1, unit = DateTimeUnit.NANOSECOND) +} + +fun Instant.format(format: String = "yyyy-MM-dd"): String { + val epoch = this.toEpochMilliseconds() + + val formatter = SimpleDateFormat(format, Locale.getDefault()) + val date = Date(epoch) + + return formatter.format(date) } \ No newline at end of file diff --git a/api/src/main/java/com/getcode/utils/Logging.kt b/api/src/main/java/com/getcode/utils/Logging.kt index be475b31d..f73a90a45 100644 --- a/api/src/main/java/com/getcode/utils/Logging.kt +++ b/api/src/main/java/com/getcode/utils/Logging.kt @@ -67,25 +67,28 @@ fun trace( message: String, tag: String? = null, type: TraceType = TraceType.Log, + metadata: MetadataBuilder.() -> Unit = {}, error: Throwable? = null ) { - val tree = if (tag == null) Timber else Timber.tag(tag) - val traceMessage = if (tag == null) message else "trace : $message" + val tagBlock = tag?.let { "[$it] " } + val tree = if (tagBlock == null) Timber else Timber.tag(tagBlock) - tree.d(traceMessage) + tree.d(message) + + val metadataMap = metadata { metadata() } if (Bugsnag.isStarted()) { val breadcrumb = if (tag != null) { - "$tag | $traceMessage" + "$tagBlock $message" } else { - traceMessage + message } val breadcrumbType = type.toBugsnagBreadcrumbType() if (breadcrumbType != null) { Bugsnag.leaveBreadcrumb( breadcrumb, - emptyMap(), + metadataMap, breadcrumbType ) } @@ -98,6 +101,7 @@ fun timedTrace( message: String, tag: String? = null, type: TraceType = TraceType.Log, + metadata: MetadataBuilder.() -> Unit = {}, error: Throwable? = null, block: () -> T ): T { @@ -107,7 +111,23 @@ fun timedTrace( } val newMessage = "$message took ${time.inWholeMilliseconds}ms" - trace(newMessage, tag, type, error) + trace(newMessage, tag, type, metadata, error) return result +} + +class MetadataBuilder { + private val map = mutableMapOf() + + infix fun String.to(value: Any) { + map[this] = value + } + + fun build(): Map = map +} + +fun metadata(block: MetadataBuilder.() -> Unit): Map { + val builder = MetadataBuilder() + builder.block() + return builder.build() } \ No newline at end of file diff --git a/api/src/main/java/com/getcode/utils/String.kt b/api/src/main/java/com/getcode/utils/String.kt index eb9404360..1180ef6f4 100644 --- a/api/src/main/java/com/getcode/utils/String.kt +++ b/api/src/main/java/com/getcode/utils/String.kt @@ -30,5 +30,15 @@ fun String.base64EncodedData(): ByteArray { return data } +fun String.padded(minCount: Int): String { + return if (this.length < minCount) { + val toInsert = minCount - this.length + val padding = " ".repeat(toInsert) + this + padding + } else { + this + } +} + typealias Base64String = String typealias Base58String = String \ No newline at end of file diff --git a/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt b/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt index 24220f7af..2c8973560 100644 --- a/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt +++ b/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt @@ -27,7 +27,9 @@ import com.getcode.util.CurrencyUtils import com.getcode.util.resources.ResourceHelper import com.getcode.util.resources.ResourceType import com.getcode.utils.ErrorUtils +import com.getcode.utils.TraceType import com.getcode.utils.installationId +import com.getcode.utils.trace import com.getcode.view.MainActivity import com.google.firebase.Firebase import com.google.firebase.installations.installations @@ -189,6 +191,15 @@ class CodePushMessagingService : FirebaseMessagingService(), .setContentIntent(buildContentIntent(type)) notificationManager.notify(title.hashCode(), notificationBuilder.build()) + + trace( + tag = "Push", + message = "Push notification shown", + metadata = { + "category" to type.name + }, + type = TraceType.Process + ) } private fun updateOrganizerAndSwap() = launch { diff --git a/app/src/main/java/com/getcode/util/AccountUtils.kt b/app/src/main/java/com/getcode/util/AccountUtils.kt index 92feaa779..7c4c3a079 100644 --- a/app/src/main/java/com/getcode/util/AccountUtils.kt +++ b/app/src/main/java/com/getcode/util/AccountUtils.kt @@ -91,6 +91,14 @@ object AccountUtils { } else { currentAttempt++ if (currentAttempt < maxRetries) { + trace( + tag = "Account", + message = "Retrying login", + metadata = { + "count" to currentAttempt + }, + type = TraceType.Process, + ) trace("Retrying after ${delayDuration.inWholeMilliseconds} ms...", type = TraceType.Log) delay(delayDuration.inWholeMilliseconds) } diff --git a/app/src/main/java/com/getcode/util/DateUtils.kt b/app/src/main/java/com/getcode/util/DateUtils.kt index 91fa913d4..bcce855dd 100644 --- a/app/src/main/java/com/getcode/util/DateUtils.kt +++ b/app/src/main/java/com/getcode/util/DateUtils.kt @@ -5,6 +5,8 @@ import android.text.format.DateFormat import android.text.format.DateUtils import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext +import com.getcode.utils.atStartOfDay +import com.getcode.utils.toLocalDate import kotlinx.datetime.Clock import kotlinx.datetime.Instant import java.util.Calendar diff --git a/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt b/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt index c7471481e..fbadad2aa 100644 --- a/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt @@ -81,10 +81,12 @@ import com.getcode.util.resources.ResourceHelper import com.getcode.util.showNetworkError import com.getcode.util.vibration.Vibrator import com.getcode.utils.ErrorUtils +import com.getcode.utils.TraceType import com.getcode.utils.base64EncodedData import com.getcode.utils.catchSafely import com.getcode.utils.network.NetworkConnectivityListener import com.getcode.utils.nonce +import com.getcode.utils.trace import com.getcode.vendor.Base58 import com.getcode.view.BaseViewModel import com.kik.kikx.models.ScannableKikCode @@ -456,7 +458,19 @@ class HomeViewModel @Inject constructor( ) }, onError = { cancelSend(style = PresentationStyle.Slide) }, - present = { data -> presentSend(data, bill, vibrate) } + present = { data -> + if (!bill.didReceive) { + trace( + tag = "Bill", + message = "Pull out cash", + metadata = { + "amount" to bill.amount + }, + type = TraceType.User, + ) + } + presentSend(data, bill, vibrate) + } ) } @@ -653,13 +667,41 @@ class HomeViewModel @Inject constructor( when (codePayload.kind) { Kind.Cash, - Kind.GiftCard -> attemptReceive(organizer, codePayload) + Kind.GiftCard -> { + trace( + tag = "Bill", + message = "Scanned cash", + type = TraceType.User, + ) + attemptReceive(organizer, codePayload) + } Kind.RequestPayment, - Kind.RequestPaymentV2 -> attemptPayment(codePayload) + Kind.RequestPaymentV2 -> { + trace( + tag = "Bill", + message = "Scanned request card", + type = TraceType.User, + ) + attemptPayment(codePayload) + } - Kind.Login -> attemptLogin(codePayload) - Kind.Tip -> attemptTip(codePayload) + Kind.Login -> { + trace( + tag = "Bill", + message = "Scanned login card", + type = TraceType.User, + ) + attemptLogin(codePayload) + } + Kind.Tip -> { + trace( + tag = "Bill", + message = "Scanned tip card", + type = TraceType.User, + ) + attemptTip(codePayload) + } } } @@ -695,6 +737,12 @@ class HomeViewModel @Inject constructor( tipController.clearTwitterSplat() + trace( + tag = "Bill", + message = "Show my tip card", + type = TraceType.User, + ) + withContext(Dispatchers.Main) { uiFlow.update { val billState = it.billState.copy(