[#743] Add API for reading memos

This commit is contained in:
Carter Jernigan 2022-10-24 09:09:29 -04:00 committed by Carter Jernigan
parent 4c2807aefd
commit f169b112ef
12 changed files with 187 additions and 12 deletions

View File

@ -9,6 +9,7 @@ Change Log
- `Synchronizer.getLegacySaplingAddress` - `Synchronizer.getLegacySaplingAddress`
- `Synchronizer.getLegacyTransparentAddress` - `Synchronizer.getLegacyTransparentAddress`
- `Synchronizer.isValidUnifiedAddr` - `Synchronizer.isValidUnifiedAddr`
- `Synchronizer.getMemos(TransactionOverview)`
- `cash.z.ecc.android.sdk.model`: - `cash.z.ecc.android.sdk.model`:
- `Account` - `Account`
- `FirstClassByteArray` - `FirstClassByteArray`

View File

@ -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.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.PendingTransaction 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.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -99,7 +102,8 @@ import kotlin.coroutines.EmptyCoroutineContext
class SdkSynchronizer internal constructor( class SdkSynchronizer internal constructor(
private val storage: DerivedDataRepository, private val storage: DerivedDataRepository,
private val txManager: OutboundTransactionManager, private val txManager: OutboundTransactionManager,
val processor: CompactBlockProcessor val processor: CompactBlockProcessor,
private val rustBackend: RustBackend
) : Synchronizer { ) : Synchronizer {
// pools // pools
@ -318,6 +322,21 @@ class SdkSynchronizer internal constructor(
processor.quickRewind() processor.quickRewind()
} }
override fun getMemos(transactionOverview: TransactionOverview): Flow<String> {
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 // Storage APIs
// //
@ -708,12 +727,14 @@ internal object DefaultSynchronizerFactory {
fun new( fun new(
repository: DerivedDataRepository, repository: DerivedDataRepository,
txManager: OutboundTransactionManager, txManager: OutboundTransactionManager,
processor: CompactBlockProcessor processor: CompactBlockProcessor,
rustBackend: RustBackend
): Synchronizer { ): Synchronizer {
return SdkSynchronizer( return SdkSynchronizer(
repository, repository,
txManager, txManager,
processor processor,
rustBackend
) )
} }

View File

@ -359,6 +359,11 @@ interface Synchronizer {
suspend fun quickRewind() suspend fun quickRewind()
/**
* Returns a list of memos for a transaction.
*/
fun getMemos(transactionOverview: TransactionOverview): Flow<String>
// //
// Error Handling // Error Handling
// //
@ -547,7 +552,8 @@ interface Synchronizer {
return SdkSynchronizer( return SdkSynchronizer(
repository, repository,
txManager, txManager,
processor processor,
rustBackend
) )
} }

View File

@ -63,6 +63,11 @@ internal class DbDerivedDataRepository(
override val allTransactions: Flow<List<TransactionOverview>> override val allTransactions: Flow<List<TransactionOverview>>
get() = invalidatingFlow.map { derivedDataDb.allTransactionView.getAllTransactions().toList() } 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() { override suspend fun close() {
derivedDataDb.close() derivedDataDb.close()
} }

View File

@ -13,7 +13,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
internal class DerivedDataDb private constructor( internal class DerivedDataDb private constructor(
private val zcashNetwork: ZcashNetwork, zcashNetwork: ZcashNetwork,
private val sqliteDatabase: SupportSQLiteDatabase private val sqliteDatabase: SupportSQLiteDatabase
) { ) {
val accountTable = AccountTable(sqliteDatabase) val accountTable = AccountTable(sqliteDatabase)
@ -28,6 +28,10 @@ internal class DerivedDataDb private constructor(
val receivedTransactionView = ReceivedTransactionView(zcashNetwork, sqliteDatabase) val receivedTransactionView = ReceivedTransactionView(zcashNetwork, sqliteDatabase)
val sentNotesTable = SentNoteTable(zcashNetwork, sqliteDatabase)
val receivedNotesTable = ReceivedNoteTable(zcashNetwork, sqliteDatabase)
suspend fun close() { suspend fun close() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
sqliteDatabase.close() sqliteDatabase.close()

View File

@ -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
}

View File

@ -28,7 +28,7 @@ internal class ReceivedTransactionView(
} }
suspend fun count() = sqliteDatabase.queryAndMap( suspend fun count() = sqliteDatabase.queryAndMap(
AccountTableDefinition.TABLE_NAME, ReceivedTransactionViewDefinition.VIEW_NAME,
columns = PROJECTION_COUNT, columns = PROJECTION_COUNT,
cursorParser = { it.getLong(0) } cursorParser = { it.getLong(0) }
).first() ).first()

View File

@ -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
}

View File

@ -104,5 +104,9 @@ internal interface DerivedDataRepository {
/** A flow of all the inbound and outbound confirmed transactions */ /** A flow of all the inbound and outbound confirmed transactions */
val allTransactions: Flow<List<TransactionOverview>> val allTransactions: Flow<List<TransactionOverview>>
fun getSentNoteIds(transactionId: Long): Flow<Long>
fun getReceivedNoteIds(transactionId: Long): Flow<Long>
suspend fun close() suspend fun close()
} }

View File

@ -441,14 +441,14 @@ internal class RustBackend private constructor(
dbDataPath: String, dbDataPath: String,
idNote: Long, idNote: Long,
networkId: Int networkId: Int
): String ): String?
@JvmStatic @JvmStatic
private external fun getSentMemoAsUtf8( private external fun getSentMemoAsUtf8(
dbDataPath: String, dbDataPath: String,
dNote: Long, dNote: Long,
networkId: Int networkId: Int
): String ): String?
@JvmStatic @JvmStatic
private external fun validateCombinedChain( private external fun validateCombinedChain(

View File

@ -64,9 +64,9 @@ internal interface RustBackendWelding {
fun getBranchIdForHeight(height: BlockHeight): Long 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 suspend fun getVerifiedBalance(account: Int = 0): Zatoshi

View File

@ -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 * Note that both sent and received transactions will have a positive net value. Consumers of this class must
*/ */
data class TransactionOverview( data class TransactionOverview internal constructor(
val id: Long, internal val id: Long,
val rawId: FirstClassByteArray, val rawId: FirstClassByteArray,
val minedHeight: BlockHeight, val minedHeight: BlockHeight,
val expiryHeight: BlockHeight, val expiryHeight: BlockHeight,