forked from hush/hush-android-wallet-sdk
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
9.3 KiB
225 lines
9.3 KiB
package cash.z.ecc.android.sdk.internal.transaction
|
|
|
|
import android.content.Context
|
|
import androidx.paging.PagedList
|
|
import androidx.room.Room
|
|
import androidx.room.RoomDatabase
|
|
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
|
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
|
import cash.z.ecc.android.sdk.internal.SdkDispatchers
|
|
import cash.z.ecc.android.sdk.internal.SdkExecutors
|
|
import cash.z.ecc.android.sdk.internal.db.DerivedDataDb
|
|
import cash.z.ecc.android.sdk.internal.ext.android.toFlowPagedList
|
|
import cash.z.ecc.android.sdk.internal.ext.android.toRefreshable
|
|
import cash.z.ecc.android.sdk.internal.ext.tryWarn
|
|
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
|
import cash.z.ecc.android.sdk.internal.twig
|
|
import cash.z.ecc.android.sdk.jni.RustBackend
|
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
|
|
import kotlinx.coroutines.flow.emitAll
|
|
import kotlinx.coroutines.flow.flow
|
|
import kotlinx.coroutines.withContext
|
|
|
|
/**
|
|
* Example of a repository that leverages the Room paging library to return a [PagedList] of
|
|
* transactions. Consumers can register as a page listener and receive an interface that allows for
|
|
* efficiently paging data.
|
|
*
|
|
* @param pageSize transactions per page. This influences pre-fetch and memory configuration.
|
|
*/
|
|
internal class PagedTransactionRepository private constructor(
|
|
private val zcashNetwork: ZcashNetwork,
|
|
private val db: DerivedDataDb,
|
|
private val pageSize: Int
|
|
) : TransactionRepository {
|
|
|
|
// DAOs
|
|
private val blocks = db.blockDao()
|
|
private val accounts = db.accountDao()
|
|
private val transactions = db.transactionDao()
|
|
|
|
// Transaction Flows
|
|
private val allTransactionsFactory = transactions.getAllTransactions().toRefreshable()
|
|
|
|
override val receivedTransactions
|
|
get() = flow<List<ConfirmedTransaction>> {
|
|
emitAll(
|
|
transactions.getReceivedTransactions().toRefreshable().toFlowPagedList(pageSize)
|
|
)
|
|
}
|
|
override val sentTransactions
|
|
get() = flow<List<ConfirmedTransaction>> {
|
|
emitAll(transactions.getSentTransactions().toRefreshable().toFlowPagedList(pageSize))
|
|
}
|
|
override val allTransactions
|
|
get() = flow<List<ConfirmedTransaction>> {
|
|
emitAll(allTransactionsFactory.toFlowPagedList(pageSize))
|
|
}
|
|
|
|
//
|
|
// TransactionRepository API
|
|
//
|
|
|
|
override fun invalidate() = allTransactionsFactory.refresh()
|
|
|
|
override suspend fun lastScannedHeight() = BlockHeight.new(zcashNetwork, blocks.lastScannedHeight())
|
|
|
|
override suspend fun firstScannedHeight() = BlockHeight.new(zcashNetwork, blocks.firstScannedHeight())
|
|
|
|
override suspend fun isInitialized() = blocks.count() > 0
|
|
|
|
override suspend fun findEncodedTransactionById(txId: Long) =
|
|
transactions.findEncodedTransactionById(txId)
|
|
|
|
override suspend fun findNewTransactions(blockHeightRange: ClosedRange<BlockHeight>): List<ConfirmedTransaction> =
|
|
transactions.findAllTransactionsByRange(blockHeightRange.start.value, blockHeightRange.endInclusive.value)
|
|
|
|
override suspend fun findMinedHeight(rawTransactionId: ByteArray) =
|
|
transactions.findMinedHeight(rawTransactionId)?.let { BlockHeight.new(zcashNetwork, it) }
|
|
|
|
override suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long? =
|
|
transactions.findMatchingTransactionId(rawTransactionId)
|
|
|
|
override suspend fun cleanupCancelledTx(rawTransactionId: ByteArray) =
|
|
transactions.cleanupCancelledTx(rawTransactionId)
|
|
|
|
// let expired transactions linger in the UI for a little while
|
|
override suspend fun deleteExpired(lastScannedHeight: BlockHeight) =
|
|
transactions.deleteExpired(lastScannedHeight.value - (ZcashSdk.EXPIRY_OFFSET / 2))
|
|
|
|
override suspend fun count() = transactions.count()
|
|
|
|
override suspend fun getAccount(accountId: Int) = accounts.findAccountById(accountId)
|
|
|
|
override suspend fun getAccountCount() = accounts.count()
|
|
|
|
/**
|
|
* Close the underlying database.
|
|
*/
|
|
suspend fun close() {
|
|
withContext(SdkDispatchers.DATABASE_IO) {
|
|
db.close()
|
|
}
|
|
}
|
|
|
|
// TODO: begin converting these into Data Access API. For now, just collect the desired operations and iterate/refactor, later
|
|
suspend fun findBlockHash(height: BlockHeight): ByteArray? = blocks.findHashByHeight(height.value)
|
|
suspend fun getTransactionCount(): Int = transactions.count()
|
|
|
|
// TODO: convert this into a wallet repository rather than "transaction repository"
|
|
|
|
companion object {
|
|
internal suspend fun new(
|
|
appContext: Context,
|
|
zcashNetwork: ZcashNetwork,
|
|
pageSize: Int = 10,
|
|
rustBackend: RustBackend,
|
|
birthday: Checkpoint,
|
|
viewingKeys: List<UnifiedViewingKey>,
|
|
overwriteVks: Boolean = false
|
|
): PagedTransactionRepository {
|
|
initMissingDatabases(rustBackend, birthday, viewingKeys)
|
|
|
|
val db = buildDatabase(appContext.applicationContext, rustBackend.pathDataDb)
|
|
applyKeyMigrations(rustBackend, overwriteVks, viewingKeys)
|
|
|
|
return PagedTransactionRepository(zcashNetwork, db, pageSize)
|
|
}
|
|
|
|
/**
|
|
* Build the database and apply migrations.
|
|
*/
|
|
private suspend fun buildDatabase(context: Context, databasePath: String): DerivedDataDb {
|
|
twig("Building dataDb and applying migrations")
|
|
return Room.databaseBuilder(context, DerivedDataDb::class.java, databasePath)
|
|
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
|
|
.setQueryExecutor(SdkExecutors.DATABASE_IO)
|
|
.setTransactionExecutor(SdkExecutors.DATABASE_IO)
|
|
.addMigrations(DerivedDataDb.MIGRATION_3_4)
|
|
.addMigrations(DerivedDataDb.MIGRATION_4_3)
|
|
.addMigrations(DerivedDataDb.MIGRATION_4_5)
|
|
.addMigrations(DerivedDataDb.MIGRATION_5_6)
|
|
.addMigrations(DerivedDataDb.MIGRATION_6_7)
|
|
.build().also {
|
|
// TODO: document why we do this. My guess is to catch database issues early or to trigger migrations--I forget why it was added but there was a good reason?
|
|
withContext(SdkDispatchers.DATABASE_IO) {
|
|
it.openHelper.writableDatabase.beginTransaction()
|
|
it.openHelper.writableDatabase.endTransaction()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create any databases that don't already exist via Rust. Originally, this was done on the Rust
|
|
* side because Rust was intended to own the "dataDb" and Kotlin just reads from it. Since then,
|
|
* it has been more clear that Kotlin should own the data and just let Rust use it.
|
|
*/
|
|
private suspend fun initMissingDatabases(
|
|
rustBackend: RustBackend,
|
|
birthday: Checkpoint,
|
|
viewingKeys: List<UnifiedViewingKey>
|
|
) {
|
|
maybeCreateDataDb(rustBackend)
|
|
maybeInitBlocksTable(rustBackend, birthday)
|
|
maybeInitAccountsTable(rustBackend, viewingKeys)
|
|
}
|
|
|
|
/**
|
|
* Create the dataDb and its table, if it doesn't exist.
|
|
*/
|
|
private suspend fun maybeCreateDataDb(rustBackend: RustBackend) {
|
|
tryWarn("Warning: did not create dataDb. It probably already exists.") {
|
|
rustBackend.initDataDb()
|
|
twig("Initialized wallet for first run file: ${rustBackend.pathDataDb}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the blocks table with the given birthday, if needed.
|
|
*/
|
|
private suspend fun maybeInitBlocksTable(
|
|
rustBackend: RustBackend,
|
|
checkpoint: Checkpoint
|
|
) {
|
|
// TODO: consider converting these to typed exceptions in the welding layer
|
|
tryWarn(
|
|
"Warning: did not initialize the blocks table. It probably was already initialized.",
|
|
ifContains = "table is not empty"
|
|
) {
|
|
rustBackend.initBlocksTable(checkpoint)
|
|
twig("seeded the database with sapling tree at height ${checkpoint.height}")
|
|
}
|
|
twig("database file: ${rustBackend.pathDataDb}")
|
|
}
|
|
|
|
/**
|
|
* Initialize the accounts table with the given viewing keys.
|
|
*/
|
|
private suspend fun maybeInitAccountsTable(
|
|
rustBackend: RustBackend,
|
|
viewingKeys: List<UnifiedViewingKey>
|
|
) {
|
|
// TODO: consider converting these to typed exceptions in the welding layer
|
|
tryWarn(
|
|
"Warning: did not initialize the accounts table. It probably was already initialized.",
|
|
ifContains = "table is not empty"
|
|
) {
|
|
rustBackend.initAccountsTable(*viewingKeys.toTypedArray())
|
|
twig("Initialized the accounts table with ${viewingKeys.size} viewingKey(s)")
|
|
}
|
|
}
|
|
|
|
private suspend fun applyKeyMigrations(
|
|
rustBackend: RustBackend,
|
|
overwriteVks: Boolean,
|
|
viewingKeys: List<UnifiedViewingKey>
|
|
) {
|
|
if (overwriteVks) {
|
|
twig("applying key migrations . . .")
|
|
maybeInitAccountsTable(rustBackend, viewingKeys)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|