diff --git a/CHANGELOG.md b/CHANGELOG.md index a6117a4e..9d84dde1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Change Log - `Synchronizer.getLegacySaplingAddress` - `Synchronizer.getLegacyTransparentAddress` - `Synchronizer.isValidUnifiedAddr` + - `Synchronizer.getMemos(TransactionOverview)` - `cash.z.ecc.android.sdk.model`: - `Account` - `FirstClassByteArray` 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 7d5d33ab..d87cf90e 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 @@ -45,6 +45,7 @@ import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.PendingTransaction +import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi @@ -74,8 +75,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext @@ -99,7 +102,8 @@ import kotlin.coroutines.EmptyCoroutineContext class SdkSynchronizer internal constructor( private val storage: DerivedDataRepository, private val txManager: OutboundTransactionManager, - val processor: CompactBlockProcessor + val processor: CompactBlockProcessor, + private val rustBackend: RustBackend ) : Synchronizer { // pools @@ -318,6 +322,21 @@ class SdkSynchronizer internal constructor( processor.quickRewind() } + override fun getMemos(transactionOverview: TransactionOverview): Flow { + return when (transactionOverview.isSentTransaction) { + true -> { + val sentNoteIds = storage.getSentNoteIds(transactionOverview.id) + + sentNoteIds.map { rustBackend.getSentMemoAsUtf8(it) }.filterNotNull() + } + false -> { + val receivedNoteIds = storage.getReceivedNoteIds(transactionOverview.id) + + receivedNoteIds.map { rustBackend.getReceivedMemoAsUtf8(it) }.filterNotNull() + } + } + } + // // Storage APIs // @@ -708,12 +727,14 @@ internal object DefaultSynchronizerFactory { fun new( repository: DerivedDataRepository, txManager: OutboundTransactionManager, - processor: CompactBlockProcessor + processor: CompactBlockProcessor, + rustBackend: RustBackend ): Synchronizer { return SdkSynchronizer( repository, txManager, - processor + processor, + rustBackend ) } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index e53b9885..acda3526 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt @@ -359,6 +359,11 @@ interface Synchronizer { suspend fun quickRewind() + /** + * Returns a list of memos for a transaction. + */ + fun getMemos(transactionOverview: TransactionOverview): Flow + // // Error Handling // @@ -547,7 +552,8 @@ interface Synchronizer { return SdkSynchronizer( repository, txManager, - processor + processor, + rustBackend ) } 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 8f26577c..cccfe1dc 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 @@ -63,6 +63,11 @@ internal class DbDerivedDataRepository( override val allTransactions: Flow> get() = invalidatingFlow.map { derivedDataDb.allTransactionView.getAllTransactions().toList() } + override fun getSentNoteIds(transactionId: Long) = derivedDataDb.sentNotesTable.getSentNoteIds(transactionId) + + override fun getReceivedNoteIds(transactionId: Long) = + derivedDataDb.receivedNotesTable.getReceivedNoteIds(transactionId) + override suspend fun close() { derivedDataDb.close() } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt index ec5d063b..bf02fe00 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext internal class DerivedDataDb private constructor( - private val zcashNetwork: ZcashNetwork, + zcashNetwork: ZcashNetwork, private val sqliteDatabase: SupportSQLiteDatabase ) { val accountTable = AccountTable(sqliteDatabase) @@ -28,6 +28,10 @@ internal class DerivedDataDb private constructor( val receivedTransactionView = ReceivedTransactionView(zcashNetwork, sqliteDatabase) + val sentNotesTable = SentNoteTable(zcashNetwork, sqliteDatabase) + + val receivedNotesTable = ReceivedNoteTable(zcashNetwork, sqliteDatabase) + suspend fun close() { withContext(Dispatchers.IO) { sqliteDatabase.close() diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/ReceivedNoteTable.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/ReceivedNoteTable.kt new file mode 100644 index 00000000..03fa4541 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/ReceivedNoteTable.kt @@ -0,0 +1,68 @@ +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.model.ZcashNetwork +import java.util.Locale + +internal class ReceivedNoteTable( + @Suppress("UnusedPrivateMember") + private val zcashNetwork: ZcashNetwork, + private val sqliteDatabase: SupportSQLiteDatabase +) { + companion object { + + private val ORDER_BY = String.format( + Locale.ROOT, + "%s ASC", // $NON-NLS + ReceivedNoteTableDefinition.COLUMN_INTEGER_ID + ) + + private val PROJECTION_ID = arrayOf(ReceivedNoteTableDefinition.COLUMN_INTEGER_ID) + + private val SELECT_BY_TRANSACTION_ID = String.format( + Locale.ROOT, + "%s = ?", // $NON-NLS + ReceivedNoteTableDefinition.COLUMN_INTEGER_TRANSACTION_ID + ) + } + + fun getReceivedNoteIds(transactionId: Long) = + sqliteDatabase.queryAndMap( + table = ReceivedNoteTableDefinition.TABLE_NAME, + columns = PROJECTION_ID, + selection = SELECT_BY_TRANSACTION_ID, + selectionArgs = arrayOf(transactionId), + orderBy = ORDER_BY, + cursorParser = { + val idColumnIndex = it.getColumnIndex(ReceivedNoteTableDefinition.COLUMN_INTEGER_ID) + + it.getLong(idColumnIndex) + } + ) +} + +// https://github.com/zcash/librustzcash/blob/277d07c79c7a08907b05a6b29730b74cdb238b97/zcash_client_sqlite/src/wallet/init.rs#L364 +internal object ReceivedNoteTableDefinition { + const val TABLE_NAME = "received_notes" // $NON-NLS + + const val COLUMN_INTEGER_ID = "id_note" // $NON-NLS + + const val COLUMN_INTEGER_TRANSACTION_ID = "tx" // $NON-NLS + + const val COLUMN_INTEGER_OUTPUT_INDEX = "output_index" // $NON-NLS + + const val COLUMN_INTEGER_ACCOUNT = "account" // $NON-NLS + + const val COLUMN_BLOB_DIVERSIFIER = "diversifier" // $NON-NLS + + const val COLUMN_INTEGER_VALUE = "value" // $NON-NLS + + const val COLUMN_BLOB_RCM = "rcm" // $NON-NLS + + const val COLUMN_BLOB_NF = "nf" // $NON-NLS + + const val COLUMN_BLOB_MEMO = "memo" // $NON-NLS + + const val COLUMN_INTEGER_SPENT = "spent" // $NON-NLS +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/ReceivedTransactionView.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/ReceivedTransactionView.kt index 107f1c12..7b973448 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/ReceivedTransactionView.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/ReceivedTransactionView.kt @@ -28,7 +28,7 @@ internal class ReceivedTransactionView( } suspend fun count() = sqliteDatabase.queryAndMap( - AccountTableDefinition.TABLE_NAME, + ReceivedTransactionViewDefinition.VIEW_NAME, columns = PROJECTION_COUNT, cursorParser = { it.getLong(0) } ).first() diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/SentNoteTable.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/SentNoteTable.kt new file mode 100644 index 00000000..b8e1d082 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/SentNoteTable.kt @@ -0,0 +1,66 @@ +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.model.ZcashNetwork +import java.util.Locale + +internal class SentNoteTable( + @Suppress("UnusedPrivateMember") + private val zcashNetwork: ZcashNetwork, + private val sqliteDatabase: SupportSQLiteDatabase +) { + companion object { + + private val ORDER_BY = String.format( + Locale.ROOT, + "%s ASC", // $NON-NLS + SentNoteTableDefinition.COLUMN_INTEGER_ID + ) + + private val PROJECTION_ID = arrayOf(SentNoteTableDefinition.COLUMN_INTEGER_ID) + + private val SELECT_BY_TRANSACTION_ID = String.format( + Locale.ROOT, + "%s = ?", // $NON-NLS + SentNoteTableDefinition.COLUMN_INTEGER_TRANSACTION_ID + ) + } + + fun getSentNoteIds(transactionId: Long) = + sqliteDatabase.queryAndMap( + table = SentNoteTableDefinition.TABLE_NAME, + columns = PROJECTION_ID, + selection = SELECT_BY_TRANSACTION_ID, + selectionArgs = arrayOf(transactionId), + orderBy = ORDER_BY, + cursorParser = { + val idColumnIndex = it.getColumnIndex(SentNoteTableDefinition.COLUMN_INTEGER_ID) + + it.getLong(idColumnIndex) + } + ) +} + +// https://github.com/zcash/librustzcash/blob/277d07c79c7a08907b05a6b29730b74cdb238b97/zcash_client_sqlite/src/wallet/init.rs#L393 +internal object SentNoteTableDefinition { + const val TABLE_NAME = "sent_notes" // $NON-NLS + + const val COLUMN_INTEGER_ID = "id_note" // $NON-NLS + + const val COLUMN_INTEGER_TRANSACTION_ID = "tx" // $NON-NLS + + const val COLUMN_INTEGER_OUTPUT_POOL = "output_pool" // $NON-NLS + + const val COLUMN_INTEGER_OUTPUT_INDEX = "output_index" // $NON-NLS + + const val COLUMN_INTEGER_FROM_ACCOUNT = "from_account" // $NON-NLS + + const val COLUMN_STRING_TO_ADDRESS = "to_address" // $NON-NLS + + const val COLUMN_INTEGER_TO_ACCOUNT = "to_account" // $NON-NLS + + const val COLUMN_INTEGER_VALUE = "value" // $NON-NLS + + const val COLUMN_BLOB_MEMO = "memo" // $NON-NLS +} 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 25d6966e..39a90981 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 @@ -104,5 +104,9 @@ internal interface DerivedDataRepository { /** A flow of all the inbound and outbound confirmed transactions */ val allTransactions: Flow> + fun getSentNoteIds(transactionId: Long): Flow + + fun getReceivedNoteIds(transactionId: Long): Flow + suspend fun close() } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt index eb4b82c5..c896c36d 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt @@ -441,14 +441,14 @@ internal class RustBackend private constructor( dbDataPath: String, idNote: Long, networkId: Int - ): String + ): String? @JvmStatic private external fun getSentMemoAsUtf8( dbDataPath: String, dNote: Long, networkId: Int - ): String + ): String? @JvmStatic private external fun validateCombinedChain( diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt index 68855692..45c9afe3 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt @@ -64,9 +64,9 @@ internal interface RustBackendWelding { fun getBranchIdForHeight(height: BlockHeight): Long - suspend fun getReceivedMemoAsUtf8(idNote: Long): String + suspend fun getReceivedMemoAsUtf8(idNote: Long): String? - suspend fun getSentMemoAsUtf8(idNote: Long): String + suspend fun getSentMemoAsUtf8(idNote: Long): String? suspend fun getVerifiedBalance(account: Int = 0): Zatoshi diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/TransactionOverview.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/TransactionOverview.kt index a824a776..e5bb1456 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/TransactionOverview.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/TransactionOverview.kt @@ -5,8 +5,8 @@ package cash.z.ecc.android.sdk.model * * Note that both sent and received transactions will have a positive net value. Consumers of this class must */ -data class TransactionOverview( - val id: Long, +data class TransactionOverview internal constructor( + internal val id: Long, val rawId: FirstClassByteArray, val minedHeight: BlockHeight, val expiryHeight: BlockHeight,