diff --git a/.gitignore b/.gitignore index fc0bfdd3..739054b7 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ captures/ .idea/protoeditor.xml .idea/appInsightsSettings.xml .idea/migrations.xml +.idea/deploymentTargetSelector.xml *.iml # Keystore files diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt index 639f5212..5fbb24d5 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt @@ -94,6 +94,7 @@ interface Backend { @Throws(RuntimeException::class) suspend fun getMemoAsUtf8( txId: ByteArray, + protocol: Int, outputIndex: Int ): String? 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 12ff998b..d57006cb 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 @@ -125,11 +125,13 @@ class RustBackend private constructor( override suspend fun getMemoAsUtf8( txId: ByteArray, + protocol: Int, outputIndex: Int ) = withContext(SdkDispatchers.DATABASE_IO) { getMemoAsUtf8( dataDbFile.absolutePath, txId, + protocol, outputIndex, networkId = networkId ) @@ -501,6 +503,7 @@ class RustBackend private constructor( private external fun getMemoAsUtf8( dbDataPath: String, txId: ByteArray, + poolType: Int, outputIndex: Int, networkId: Int ): String? diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ZcashProtocol.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ZcashProtocol.kt new file mode 100644 index 00000000..f8a91050 --- /dev/null +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ZcashProtocol.kt @@ -0,0 +1,41 @@ +package cash.z.ecc.android.sdk.internal.model + +/** + * An enumeration of supported Zcash protocols. + */ +@Suppress("MagicNumber") +enum class ZcashProtocol { + TRANSPARENT { + override val poolCode = 0 + }, + SAPLING { + override val poolCode = 2 + }, + ORCHARD { + override val poolCode = 3 + }; + + abstract val poolCode: Int + + fun isShielded() = this == SAPLING || this == ORCHARD + + companion object { + fun validate(poolTypeCode: Int): Boolean { + return when (poolTypeCode) { + TRANSPARENT.poolCode, + SAPLING.poolCode, + ORCHARD.poolCode -> true + else -> false + } + } + + fun fromPoolType(poolCode: Int): ZcashProtocol { + return when (poolCode) { + TRANSPARENT.poolCode -> TRANSPARENT + SAPLING.poolCode -> SAPLING + ORCHARD.poolCode -> ORCHARD + else -> error("Unsupported pool type: $poolCode") + } + } + } +} diff --git a/backend-lib/src/main/rust/lib.rs b/backend-lib/src/main/rust/lib.rs index 0f6e972a..cb81ec8b 100644 --- a/backend-lib/src/main/rust/lib.rs +++ b/backend-lib/src/main/rust/lib.rs @@ -799,15 +799,25 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getTotalT unwrap_exc_or(&mut env, res, -1) } +fn parse_protocol(code: i32) -> Option { + match code { + 2 => Some(ShieldedProtocol::Sapling), + 3 => Some(ShieldedProtocol::Orchard), + _ => None + } +} + #[no_mangle] pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getMemoAsUtf8<'local>( mut env: JNIEnv<'local>, _: JClass<'local>, db_data: JString<'local>, txid_bytes: JByteArray<'local>, + pool_type: jint, output_index: jint, network_id: jint, ) -> jstring { + let res = catch_unwind(&mut env, |env| { let _span = tracing::info_span!("RustBackend.getMemoAsUtf8").entered(); let network = parse_network(network_id as u32)?; @@ -815,10 +825,11 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getMemoAs let txid_bytes = env.convert_byte_array(txid_bytes)?; let txid = TxId::read(&txid_bytes[..])?; + let protocol = parse_protocol(pool_type).ok_or(format_err!("Shielded protocol not recognized: {}", pool_type))?; let output_index = u16::try_from(output_index)?; let memo = (&db_data) - .get_memo(NoteId::new(txid, ShieldedProtocol::Sapling, output_index)) + .get_memo(NoteId::new(txid, protocol, output_index)) .map_err(|e| format_err!("An error occurred retrieving the memo, {}", e)) .and_then(|memo| match memo { Some(Memo::Empty) => Ok("".to_string()), 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 4922dcae..64d8891d 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 @@ -150,6 +150,7 @@ internal class FakeRustBackend( override suspend fun getMemoAsUtf8( txId: ByteArray, + protocol: Int, outputIndex: Int ): String? = error("Intentionally not implemented in mocked FakeRustBackend implementation.") diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index d24f78f7..e6aa5ada 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -327,17 +327,25 @@ class SdkSynchronizer private constructor( } override fun getMemos(transactionOverview: TransactionOverview): Flow { - return storage.getSaplingOutputIndices(transactionOverview.rawId).map { - runCatching { - backend.getMemoAsUtf8(transactionOverview.rawId.byteArray, it) - }.onFailure { - Twig.error { "Failed to get memo with: $it" } - }.onSuccess { - Twig.debug { "Transaction memo queried: $it" } - }.fold( - onSuccess = { it ?: "" }, - onFailure = { "" } - ) + return storage.getOutputProperties(transactionOverview.rawId).map { properties -> + if (!properties.protocol.isShielded()) { + "" + } else { + runCatching { + backend.getMemoAsUtf8( + txId = transactionOverview.rawId.byteArray, + protocol = properties.protocol, + outputIndex = properties.index + ) + }.onFailure { + Twig.error { "Failed to get memo with: $it" } + }.onSuccess { + Twig.debug { "Transaction memo queried: $it" } + }.fold( + onSuccess = { it ?: "" }, + onFailure = { "" } + ) + } } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt index 2030caf0..51960dd2 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt @@ -7,6 +7,7 @@ import cash.z.ecc.android.sdk.internal.model.ScanSummary import cash.z.ecc.android.sdk.internal.model.SubtreeRoot 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.ZcashProtocol import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.FirstClassByteArray @@ -75,6 +76,7 @@ internal interface TypesafeBackend { suspend fun getMemoAsUtf8( txId: ByteArray, + protocol: ZcashProtocol, outputIndex: Int ): String? 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 24818865..e072dda1 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 @@ -8,6 +8,7 @@ import cash.z.ecc.android.sdk.internal.model.ScanSummary import cash.z.ecc.android.sdk.internal.model.SubtreeRoot 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.ZcashProtocol import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.FirstClassByteArray @@ -147,8 +148,14 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke override suspend fun getMemoAsUtf8( txId: ByteArray, + protocol: ZcashProtocol, outputIndex: Int - ): String? = backend.getMemoAsUtf8(txId, outputIndex) + ): String? = + backend.getMemoAsUtf8( + txId = txId, + protocol = protocol.poolCode, + outputIndex = outputIndex + ) override suspend fun initDataDb(seed: ByteArray?) { val ret = backend.initDataDb(seed) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt index 799d638c..330f6d29 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt @@ -53,8 +53,9 @@ internal class DbDerivedDataRepository( override val allTransactions: Flow> get() = invalidatingFlow.map { derivedDataDb.allTransactionView.getAllTransactions().toList() } - override fun getSaplingOutputIndices(transactionId: FirstClassByteArray) = - derivedDataDb.txOutputsView.getSaplingOutputIndices(transactionId) + override fun getOutputProperties(transactionId: FirstClassByteArray) = + derivedDataDb.txOutputsView + .getOutputProperties(transactionId) override fun getRecipients(transactionId: FirstClassByteArray): Flow { return derivedDataDb.txOutputsView.getRecipients(transactionId) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TxOutputsView.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TxOutputsView.kt index f567fd9a..80321852 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TxOutputsView.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TxOutputsView.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal.db.derived import androidx.sqlite.db.SupportSQLiteDatabase import cash.z.ecc.android.sdk.internal.db.queryAndMap +import cash.z.ecc.android.sdk.internal.model.OutputProperties import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.TransactionRecipient @@ -22,7 +23,11 @@ internal class TxOutputsView( TxOutputsViewDefinition.COLUMN_BLOB_TRANSACTION_ID ) - private val PROJECTION_OUTPUT_INDEX = arrayOf(TxOutputsViewDefinition.COLUMN_INTEGER_OUTPUT_INDEX) + private val PROJECTION_OUTPUT_PROPERTIES = + arrayOf( + TxOutputsViewDefinition.COLUMN_INTEGER_OUTPUT_INDEX, + TxOutputsViewDefinition.COLUMN_INTEGER_OUTPUT_POOL, + ) private val PROJECTION_RECIPIENT = arrayOf( @@ -40,17 +45,22 @@ internal class TxOutputsView( ) } - fun getSaplingOutputIndices(transactionId: FirstClassByteArray) = + fun getOutputProperties(transactionId: FirstClassByteArray) = sqliteDatabase.queryAndMap( table = TxOutputsViewDefinition.VIEW_NAME, - columns = PROJECTION_OUTPUT_INDEX, + columns = PROJECTION_OUTPUT_PROPERTIES, selection = SELECT_BY_TRANSACTION_ID_AND_NOT_CHANGE, selectionArgs = arrayOf(transactionId.byteArray), orderBy = ORDER_BY, cursorParser = { val idColumnOutputIndex = it.getColumnIndex(TxOutputsViewDefinition.COLUMN_INTEGER_OUTPUT_INDEX) + val idColumnOutputPoolIndex = it.getColumnIndex(TxOutputsViewDefinition.COLUMN_INTEGER_OUTPUT_POOL) - it.getInt(idColumnOutputIndex) + OutputProperties.new( + index = it.getInt(idColumnOutputIndex), + // Converting blob to Int + poolType = it.getInt(idColumnOutputPoolIndex) + ) } ) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/OutputProperties.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/OutputProperties.kt new file mode 100644 index 00000000..c99c08d6 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/OutputProperties.kt @@ -0,0 +1,25 @@ +package cash.z.ecc.android.sdk.internal.model + +/** + * Typesafe model class representing different properties from [TxOutputsView] + */ +internal class OutputProperties( + val index: Int, + val protocol: ZcashProtocol +) { + companion object { + fun new( + index: Int, + poolType: Int + ): OutputProperties { + require(index >= 0) { "Output index: $index must be greater or equal to zero" } + + require(ZcashProtocol.validate(poolType)) { "Output poolType: $poolType unknown" } + + return OutputProperties( + index = index, + protocol = ZcashProtocol.fromPoolType(poolType) + ) + } + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/DerivedDataRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/DerivedDataRepository.kt index f1f0dede..b8156a7f 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/DerivedDataRepository.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/DerivedDataRepository.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal.repository import cash.z.ecc.android.sdk.internal.model.DbTransactionOverview import cash.z.ecc.android.sdk.internal.model.EncodedTransaction +import cash.z.ecc.android.sdk.internal.model.OutputProperties import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.TransactionRecipient @@ -81,7 +82,7 @@ internal interface DerivedDataRepository { val allTransactions: Flow> - fun getSaplingOutputIndices(transactionId: FirstClassByteArray): Flow + fun getOutputProperties(transactionId: FirstClassByteArray): Flow fun getRecipients(transactionId: FirstClassByteArray): Flow