diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt index abe23304a..98cf5dfc7 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt @@ -395,7 +395,10 @@ class RustBackend private constructor( ) } - override suspend fun setTransactionStatus(txId: ByteArray, status: Long) = withContext(SdkDispatchers.DATABASE_IO) { + override suspend fun setTransactionStatus( + txId: ByteArray, + status: Long + ) = withContext(SdkDispatchers.DATABASE_IO) { Companion.setTransactionStatus( dataDbFile.absolutePath, txId, diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniTransactionDataRequest.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniTransactionDataRequest.kt index 157fc223b..a5f9eb702 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniTransactionDataRequest.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniTransactionDataRequest.kt @@ -32,4 +32,4 @@ interface JniTransactionDataRequest { } } } -} \ No newline at end of file +} diff --git a/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/RawTransactionUnsafe.kt b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/RawTransactionUnsafe.kt index 0eb279921..0c1ac4dfb 100644 --- a/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/RawTransactionUnsafe.kt +++ b/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/model/RawTransactionUnsafe.kt @@ -31,7 +31,7 @@ sealed class RawTransactionUnsafe(open val data: ByteArray) { companion object { fun new(rawTransaction: RawTransaction): RawTransactionUnsafe { - val data = rawTransaction.data.toByteArray(); + val data = rawTransaction.data.toByteArray() return when (rawTransaction.height) { -1L -> OrphanedBlock(data) 0L -> Mempool(data) @@ -39,4 +39,9 @@ sealed class RawTransactionUnsafe(open val data: ByteArray) { } } } + + /** + * This is a safe [toString] function that prints only non-sensitive parts + */ + override fun toString() = "RawTransactionUnsafe: type: ${this::class.simpleName}" } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/FakeRustBackend.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/FakeRustBackend.kt index 77c549d56..987146345 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/FakeRustBackend.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/FakeRustBackend.kt @@ -67,7 +67,10 @@ internal class FakeRustBackend( TODO("Not yet implemented") } - override suspend fun setTransactionStatus(txId: ByteArray, status: Long) { + override suspend fun setTransactionStatus( + txId: ByteArray, + status: Long + ) { TODO("Not yet implemented") } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt index ee2d5e43a..9c7a22b9e 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt @@ -17,6 +17,7 @@ import cash.z.ecc.android.sdk.block.processor.model.VerifySuggestedScanRange import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDecryptError import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDownloadError +import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxSetStatusError import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.MismatchedConsensusBranch import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.MismatchedNetwork import cash.z.ecc.android.sdk.exception.InitializeException @@ -43,14 +44,17 @@ import cash.z.ecc.android.sdk.internal.model.JniBlockMeta import cash.z.ecc.android.sdk.internal.model.ScanRange import cash.z.ecc.android.sdk.internal.model.SubtreeRoot import cash.z.ecc.android.sdk.internal.model.SuggestScanRangePriority +import cash.z.ecc.android.sdk.internal.model.TransactionStatus import cash.z.ecc.android.sdk.internal.model.TreeState import cash.z.ecc.android.sdk.internal.model.WalletSummary import cash.z.ecc.android.sdk.internal.model.ext.from import cash.z.ecc.android.sdk.internal.model.ext.toBlockHeight +import cash.z.ecc.android.sdk.internal.model.ext.toTransactionStatus import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.PercentDecimal +import cash.z.ecc.android.sdk.model.RawTransaction import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -1872,6 +1876,7 @@ class CompactBlockProcessor internal constructor( emit(SyncingResult.EnhanceSuccess) } + @Suppress("LongMethod") private suspend fun enhanceTransaction( transaction: DbTransactionOverview, backend: TypesafeBackend, @@ -1894,30 +1899,51 @@ class CompactBlockProcessor internal constructor( "Fetching transaction (txid:${transaction.txIdString()} block:${transaction .minedHeight})" } - val transactionData = + val rawTransactionUnsafe = fetchTransaction( - transactionId = transaction.txIdString(), - rawTransactionId = transaction.rawId.byteArray, - minedHeight = transaction.minedHeight, + transactionOverview = transaction, downloader = downloader, - network = network ) - // Decrypting and storing transaction is run just once, since we consider it more stable - Twig.verbose { - "Decrypting and storing transaction " + - "(txid:${transaction.txIdString()} block:${transaction.minedHeight})" + Twig.debug { "Transaction fetched: $rawTransactionUnsafe" } + + // We need to distinct between three possible state of the fetched transaction + when (rawTransactionUnsafe) { + is RawTransactionUnsafe.MainChain -> { + // Decrypting and storing transaction is run just once, since we consider it more stable + Twig.verbose { + "Decrypting and storing transaction (txid:${transaction.txIdString()}, " + + "block:${transaction.minedHeight})" + } + decryptTransaction( + rawTransaction = + RawTransaction.new( + rawTransactionUnsafe = rawTransactionUnsafe, + network = network + ), + backend = backend + ) + } + is RawTransactionUnsafe.Mempool, + is RawTransactionUnsafe.OrphanedBlock -> { + Twig.verbose { + "Setting status of transaction (txid:${transaction.txIdString()}, " + + "block:${transaction.minedHeight})" + } + setTransactionStatus( + transactionRawId = transaction.rawId.byteArray, + status = rawTransactionUnsafe.toTransactionStatus(network), + height = transaction.minedHeight, + backend = backend + ) + } } - decryptTransaction( - transactionData = transactionData.first, - minedHeight = transactionData.second, - backend = backend - ) Twig.debug { "Done enhancing transaction (txid:${transaction.txIdString()} block:${transaction .minedHeight})" } + SyncingResult.EnhanceSuccess } catch (exception: CompactBlockProcessorException.EnhanceTransactionError) { SyncingResult.EnhanceFailed( @@ -1929,61 +1955,74 @@ class CompactBlockProcessor internal constructor( return result } - // TODO [#1254]: CompactblockProcessor.fetchTransaction pass txId twice - // TODO [#1254]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1254 @Throws(EnhanceTxDownloadError::class) private suspend fun fetchTransaction( - transactionId: String, - rawTransactionId: ByteArray, - minedHeight: BlockHeight, + transactionOverview: DbTransactionOverview, downloader: CompactBlockDownloader, - network: ZcashNetwork - ): Pair { + ): RawTransactionUnsafe { + var transactionResult: RawTransactionUnsafe? = null val traceScope = TraceScope("CompactBlockProcessor.fetchTransaction") - var transactionDataResult: Pair? = null + retryUpToAndThrow(TRANSACTION_FETCH_RETRIES) { failedAttempts -> if (failedAttempts == 0) { - Twig.debug { "Starting to fetch transaction (txid:$transactionId, block:$minedHeight)" } + Twig.debug { + "Starting to fetch transaction (txid:${transactionOverview.txIdString()}, " + + "block:${transactionOverview.minedHeight})" + } } else { Twig.warn { - "Retrying to fetch transaction (txid:$transactionId, block:$minedHeight) after" + - " $failedAttempts failure(s)..." + "Retrying to fetch transaction (txid:${transactionOverview.txIdString()}, " + + "block:${transactionOverview.minedHeight} after $failedAttempts failure(s)..." } } - when (val response = downloader.fetchTransaction(rawTransactionId)) { - is Response.Success -> { - val currentMinedHeight = when (response.result) { - is RawTransactionUnsafe.MainChain -> runCatching { - (response.result as RawTransactionUnsafe.MainChain).height.toBlockHeight( - network - ) - }.getOrNull() - else -> null + + transactionResult = + when ( + val response = + downloader.fetchTransaction( + transactionOverview.rawId + .byteArray + ) + ) { + is Response.Success -> response.result + is Response.Failure -> { + throw EnhanceTxDownloadError(transactionOverview.minedHeight, response.toThrowable()) } - transactionDataResult = Pair(response.result.data, currentMinedHeight) } - is Response.Failure -> { - throw EnhanceTxDownloadError(minedHeight, response.toThrowable()) - } - } } traceScope.end() // Result is fetched or EnhanceTxDownloadError is thrown after all attempts failed at this point - return transactionDataResult!! + return transactionResult!! } @Throws(EnhanceTxDecryptError::class) private suspend fun decryptTransaction( - transactionData: ByteArray, - minedHeight: BlockHeight?, + rawTransaction: RawTransaction, backend: TypesafeBackend, ) { val traceScope = TraceScope("CompactBlockProcessor.decryptTransaction") runCatching { - backend.decryptAndStoreTransaction(transactionData, minedHeight) + backend.decryptAndStoreTransaction(rawTransaction.data, rawTransaction.height) + }.onFailure { + traceScope.end() + throw EnhanceTxDecryptError(rawTransaction.height, it) + } + traceScope.end() + } + + @Throws(EnhanceTxSetStatusError::class) + private suspend fun setTransactionStatus( + transactionRawId: ByteArray, + status: TransactionStatus, + height: BlockHeight, + backend: TypesafeBackend, + ) { + val traceScope = TraceScope("CompactBlockProcessor.setTransactionStatus") + runCatching { + backend.setTransactionStatus(transactionRawId, status) }.onFailure { traceScope.end() - throw EnhanceTxDecryptError(minedHeight, it) + throw EnhanceTxSetStatusError(height, it) } traceScope.end() } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt index 3a8e31046..01896f946 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt @@ -102,7 +102,7 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? = cause: Throwable ) : CompactBlockProcessorException(message, cause) { class EnhanceTxDownloadError( - height: BlockHeight, + height: BlockHeight?, cause: Throwable ) : EnhanceTransactionError( "Error while attempting to download a transaction to enhance", @@ -118,6 +118,15 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? = height, cause ) + + class EnhanceTxSetStatusError( + height: BlockHeight?, + cause: Throwable + ) : EnhanceTransactionError( + "Error while attempting to set status of a transaction to the Rust backend", + height, + cause + ) } class MismatchedNetwork(clientNetwork: String?, serverNetwork: String?) : CompactBlockProcessorException( diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt index bf032678a..34c732dba 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt @@ -260,15 +260,13 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke ) = backend .decryptAndStoreTransaction(tx, minedHeight?.value) - override suspend fun setTransactionStatus(txId: ByteArray, status: TransactionStatus) = backend - .setTransactionStatus( - txId, when (status) { - is TransactionStatus.Mined -> status.height.value - is TransactionStatus.NotInMainChain -> -1L - // TxidNotRecognized - else -> -2L - } - ) + override suspend fun setTransactionStatus( + txId: ByteArray, + status: TransactionStatus + ) = backend.setTransactionStatus( + txId = txId, + status = status.toPrimitiveValue() + ) override fun getSaplingReceiver(ua: String): String? = backend.getSaplingReceiver(ua) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/TransactionDataRequest.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/TransactionDataRequest.kt index 0fd22a95b..6928f7d50 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/TransactionDataRequest.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/TransactionDataRequest.kt @@ -1,7 +1,6 @@ package cash.z.ecc.android.sdk.internal.model import cash.z.ecc.android.sdk.exception.SdkException -import cash.z.ecc.android.sdk.internal.ext.isInUIntRange import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -35,18 +34,19 @@ interface TransactionDataRequest { return when (jni) { is JniTransactionDataRequest.GetStatus -> GetStatus(jni.txid) is JniTransactionDataRequest.Enhancement -> Enhancement(jni.txid) - is JniTransactionDataRequest.SpendsFromAddress -> SpendsFromAddress( - jni.address, - BlockHeight.new(zcashNetwork, jni.startHeight), - if (jni.endHeight == -1L) { - null - } else { - BlockHeight.new(zcashNetwork, jni.endHeight) - } - ) + is JniTransactionDataRequest.SpendsFromAddress -> + SpendsFromAddress( + jni.address, + BlockHeight.new(zcashNetwork, jni.startHeight), + if (jni.endHeight == -1L) { + null + } else { + BlockHeight.new(zcashNetwork, jni.endHeight) + } + ) else -> throw SdkException("Unknown JniTransactionDataRequest variant", cause = null) } } } -} \ No newline at end of file +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/TransactionStatus.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/TransactionStatus.kt index 1aa420ed7..f9c7c9e0e 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/TransactionStatus.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/TransactionStatus.kt @@ -2,8 +2,22 @@ package cash.z.ecc.android.sdk.internal.model import cash.z.ecc.android.sdk.model.BlockHeight -interface TransactionStatus { - class TxidNotRecognized : TransactionStatus - class NotInMainChain : TransactionStatus - data class Mined(val height: BlockHeight) : TransactionStatus -} \ No newline at end of file +sealed class TransactionStatus { + abstract fun toPrimitiveValue(): Long + + data class Mined(val height: BlockHeight) : TransactionStatus() { + override fun toPrimitiveValue() = height.value + } + + data object NotInMainChain : TransactionStatus() { + private const val NOT_IN_MAIN_CHAIN = -1L + + override fun toPrimitiveValue() = NOT_IN_MAIN_CHAIN + } + + data object TxidNotRecognized : TransactionStatus() { + private const val TXID_NOT_RECOGNIZED = -2L + + override fun toPrimitiveValue() = TXID_NOT_RECOGNIZED + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ext/RawTransactionUnsafeExt.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ext/RawTransactionUnsafeExt.kt new file mode 100644 index 000000000..becb19e1d --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ext/RawTransactionUnsafeExt.kt @@ -0,0 +1,16 @@ +package cash.z.ecc.android.sdk.internal.model.ext + +import cash.z.ecc.android.sdk.internal.model.TransactionStatus +import cash.z.ecc.android.sdk.model.ZcashNetwork +import co.electriccoin.lightwallet.client.model.RawTransactionUnsafe +import co.electriccoin.lightwallet.client.model.RawTransactionUnsafe.MainChain +import co.electriccoin.lightwallet.client.model.RawTransactionUnsafe.Mempool +import co.electriccoin.lightwallet.client.model.RawTransactionUnsafe.OrphanedBlock + +internal fun RawTransactionUnsafe.toTransactionStatus(network: ZcashNetwork): TransactionStatus { + return when (this) { + is MainChain -> TransactionStatus.Mined(height.toBlockHeight(network)) + is Mempool -> TransactionStatus.NotInMainChain + is OrphanedBlock -> TransactionStatus.TxidNotRecognized + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/RawTransaction.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/RawTransaction.kt new file mode 100644 index 000000000..a9dcf9237 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/RawTransaction.kt @@ -0,0 +1,52 @@ +package cash.z.ecc.android.sdk.model + +import cash.z.ecc.android.sdk.internal.model.ext.toBlockHeight +import cash.z.ecc.android.sdk.model.RawTransaction.Companion.new +import co.electriccoin.lightwallet.client.model.RawTransactionUnsafe + +/** + * Represents a safe type transaction object obtained from Light wallet server. It contains complete transaction data. + * + * New instances are constructed using the [new] factory method. + * + * @param data The complete data of the transaction. + * @param height The transaction mined height. + */ +data class RawTransaction internal constructor( + val data: ByteArray, + val height: BlockHeight? +) { + init { + require(data.isNotEmpty()) { "Empty RawTransaction data" } + } + + companion object { + fun new( + rawTransactionUnsafe: RawTransactionUnsafe.MainChain, + network: ZcashNetwork + ): RawTransaction { + return RawTransaction( + data = rawTransactionUnsafe.data, + height = rawTransactionUnsafe.height.toBlockHeight(network) + ) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RawTransaction + + if (!data.contentEquals(other.data)) return false + if (height != other.height) return false + + return true + } + + override fun hashCode(): Int { + var result = data.contentHashCode() + result = 31 * result + height.hashCode() + return result + } +}