Cleanup after Zcon1.

This commit is contained in:
Kevin Gorham 2019-07-12 04:47:17 -04:00 committed by Kevin Gorham
parent 78f98c2868
commit 8c7103d0ee
22 changed files with 296 additions and 228 deletions

View File

@ -183,6 +183,7 @@ cargo {
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
// Architecture Components: Lifecycle

View File

@ -5,21 +5,21 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.test.core.app.ApplicationProvider
import cash.z.wallet.sdk.dao.BlockDao
import cash.z.wallet.sdk.dao.CompactBlockDao
import cash.z.wallet.sdk.dao.TransactionDao
import cash.z.wallet.sdk.data.SampleSeedProvider
import cash.z.wallet.sdk.entity.CompactBlock
import cash.z.wallet.sdk.ext.toBlockHeight
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.entity.CompactBlock
import cash.z.wallet.sdk.jni.RustBackendWelding
import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import org.junit.*
import org.junit.Assert.*
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc
import cash.z.wallet.sdk.rpc.Service
import cash.z.wallet.sdk.rpc.Service.*
import cash.z.wallet.sdk.rpc.Service.BlockID
import cash.z.wallet.sdk.rpc.Service.BlockRange
import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.TimeUnit
class GlueIntegrationTest {

View File

@ -6,6 +6,8 @@ import cash.z.wallet.sdk.block.CompactBlockDownloader
import cash.z.wallet.sdk.block.CompactBlockProcessor
import cash.z.wallet.sdk.block.ProcessorConfig
import cash.z.wallet.sdk.data.*
import cash.z.wallet.sdk.ext.SampleSeedProvider
import cash.z.wallet.sdk.ext.SampleSpendingKeyProvider
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.secure.Wallet
import cash.z.wallet.sdk.service.LightWalletGrpcService

View File

@ -1,9 +1,9 @@
package cash.z.wallet.sdk.db
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.wallet.sdk.data.SampleSeedProvider
import cash.z.wallet.sdk.data.TroubleshootingTwig
import cash.z.wallet.sdk.data.Twig
import cash.z.wallet.sdk.ext.SampleSeedProvider
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.secure.Wallet
import kotlinx.coroutines.ExperimentalCoroutinesApi

View File

@ -3,10 +3,10 @@ package cash.z.wallet.sdk.db
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.wallet.sdk.block.CompactBlockDbStore
import cash.z.wallet.sdk.block.CompactBlockDownloader
import cash.z.wallet.sdk.data.SampleSeedProvider
import cash.z.wallet.sdk.data.TroubleshootingTwig
import cash.z.wallet.sdk.data.Twig
import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.ext.SampleSeedProvider
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.secure.Wallet
import cash.z.wallet.sdk.service.LightWalletGrpcService

View File

@ -50,10 +50,45 @@ interface TransactionDao {
WHERE received_notes.is_change != 1 or transactions.raw IS NOT NULL
ORDER BY block IS NOT NUll, height DESC, time DESC, txId DESC
""")
fun getAll(): List<WalletTransaction>
fun getAll(): List<ClearedTransaction>
/**
* Query transactions by rawTxId
*/
@Query("""
SELECT transactions.id_tx AS id,
transactions.txid AS rawTransactionId,
transactions.block AS height,
transactions.raw IS NOT NULL AS isSend,
transactions.block IS NOT NULL AS isMined,
blocks.time AS timeInSeconds,
sent_notes.address AS address,
CASE
WHEN transactions.raw IS NOT NULL THEN sent_notes.value
ELSE received_notes.value
END AS value,
CASE
WHEN transactions.raw IS NOT NULL THEN sent_notes.memo IS NOT NULL
ELSE received_notes.memo IS NOT NULL
END AS rawMemoExists,
CASE
WHEN transactions.raw IS NOT NULL THEN sent_notes.id_note
ELSE received_notes.id_note
END AS noteId
FROM transactions
LEFT JOIN sent_notes
ON transactions.id_tx = sent_notes.tx
LEFT JOIN received_notes
ON transactions.id_tx = received_notes.tx
LEFT JOIN blocks
ON transactions.block = blocks.height
WHERE (received_notes.is_change != 1 or transactions.raw IS NOT NULL) AND (height IS NOT NULL) and (rawTransactionId = :rawTxId)
ORDER BY block IS NOT NUll, height DESC, time DESC, txId DESC
""")
fun findByRawId(rawTxId: ByteArray): List<ClearedTransaction>
}
data class WalletTransaction(
data class ClearedTransaction(
val id: Long = 0L,
val noteId: Long = 0L,
val rawTransactionId: ByteArray? = null,
@ -72,7 +107,7 @@ data class WalletTransaction(
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is WalletTransaction) return false
if (other !is ClearedTransaction) return false
if (noteId != other.noteId) return false
if (id != other.id) return false

View File

@ -1,6 +1,6 @@
package cash.z.wallet.sdk.data
import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.dao.ClearedTransaction
import cash.z.wallet.sdk.ext.masked
import cash.z.wallet.sdk.secure.Wallet
import kotlinx.coroutines.*
@ -13,8 +13,6 @@ import cash.z.wallet.sdk.data.TransactionState.*
import cash.z.wallet.sdk.service.LightWalletService
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
/**
* Manages active send/receive transactions. These are transactions that have been initiated but not completed with
@ -34,7 +32,7 @@ class ActiveTransactionManager(
// mutableMapOf gives the same result but we're explicit about preserving insertion order, since we rely on that
private val activeTransactions = LinkedHashMap<ActiveTransaction, TransactionState>()
private val channel = ConflatedBroadcastChannel<Map<ActiveTransaction, TransactionState>>()
private val clearedTransactions = ConflatedBroadcastChannel<List<WalletTransaction>>()
private val clearedTransactions = ConflatedBroadcastChannel<List<ClearedTransaction>>()
// private val latestHeightSubscription = service.latestHeights()
fun subscribe(): ReceiveChannel<Map<ActiveTransaction, TransactionState>> {
@ -150,7 +148,7 @@ class ActiveTransactionManager(
* @param transactions the latest transactions received from our subscription to the transaction repository. That
* channel only publishes transactions when they have changed in some way.
*/
private fun updateSentTransactions(transactions: List<WalletTransaction>) {
private fun updateSentTransactions(transactions: List<ClearedTransaction>) {
twig("transaction modification received! Updating active sent transactions based on new transaction list")
val sentTransactions = transactions.filter { it.isSend }
val activeSentTransactions =

View File

@ -1,9 +0,0 @@
package cash.z.android.wallet.data
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
class ChannelListValueProvider<T>(val channel: ConflatedBroadcastChannel<List<T>>) {
fun getLatestValue(): List<T> {
return if (channel.isClosedForSend) listOf() else channel.value
}
}

View File

@ -1,8 +1,6 @@
package cash.z.wallet.sdk.data
import cash.z.wallet.sdk.block.CompactBlockProcessor
import cash.z.wallet.sdk.block.ProcessorConfig
import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.dao.ClearedTransaction
import cash.z.wallet.sdk.ext.MINERS_FEE_ZATOSHI
import cash.z.wallet.sdk.secure.Wallet
import kotlinx.coroutines.*
@ -50,7 +48,7 @@ open class MockSynchronizer(
get() = Dispatchers.IO + job
/* only accessed through mutual exclusion */
private val transactions = mutableListOf<WalletTransaction>()
private val transactions = mutableListOf<ClearedTransaction>()
private val activeTransactions = mutableMapOf<ActiveTransaction, TransactionState>()
private val transactionMutex = Mutex()
@ -60,7 +58,7 @@ open class MockSynchronizer(
private val balanceChannel = ConflatedBroadcastChannel<Wallet.WalletBalance>()
private val activeTransactionsChannel = ConflatedBroadcastChannel<Map<ActiveTransaction, TransactionState>>(mutableMapOf())
private val transactionsChannel = ConflatedBroadcastChannel<List<WalletTransaction>>(listOf())
private val transactionsChannel = ConflatedBroadcastChannel<List<ClearedTransaction>>(listOf())
private val progressChannel = ConflatedBroadcastChannel<Int>()
/**
@ -257,7 +255,7 @@ open class MockSynchronizer(
}
// other collaborators add to the list, periodically. This simulates, real-world, non-distinct updates.
delay(Random.nextLong(transactionInterval / 2))
var copyList = listOf<WalletTransaction>()
var copyList = listOf<ClearedTransaction>()
transactionMutex.withLock {
// shallow copy
copyList = transactions.map { it }
@ -302,8 +300,8 @@ open class MockSynchronizer(
/**
* Fabricate a receive transaction.
*/
fun createReceiveTransaction(): WalletTransaction {
return WalletTransaction(
fun createReceiveTransaction(): ClearedTransaction {
return ClearedTransaction(
id = transactionId.getAndIncrement(),
value = Random.nextLong(20_000L..1_000_000_000L),
height = latestHeight.getAndIncrement(),
@ -319,8 +317,8 @@ open class MockSynchronizer(
fun createSendTransaction(
amount: Long = Random.nextLong(20_000L..1_000_000_000L),
txId: Long = -1L
): WalletTransaction {
return WalletTransaction(
): ClearedTransaction {
return ClearedTransaction(
id = if (txId == -1L) transactionId.getAndIncrement() else txId,
value = amount,
height = null,
@ -333,7 +331,7 @@ open class MockSynchronizer(
/**
* Fabricate an active send transaction, based on the given wallet transaction instance.
*/
fun createActiveSendTransaction(walletTransaction: WalletTransaction, toAddress: String)
fun createActiveSendTransaction(walletTransaction: ClearedTransaction, toAddress: String)
= createActiveSendTransaction(walletTransaction.value, toAddress, walletTransaction.id)
/**

View File

@ -5,7 +5,7 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import cash.z.wallet.sdk.db.PendingTransactionDao
import cash.z.wallet.sdk.db.PendingTransactionDb
import cash.z.wallet.sdk.db.PendingTransactionEntity
import cash.z.wallet.sdk.db.PendingTransaction
import cash.z.wallet.sdk.ext.EXPIRY_OFFSET
import cash.z.wallet.sdk.service.LightWalletService
import kotlinx.coroutines.Dispatchers.IO
@ -31,11 +31,11 @@ class PersistentTransactionManager(private val db: PendingTransactionDb) : Trans
.build()
) {
dbCallback(db)
dao = db.pendingTransactionDao()
}
override fun start() {
twig("TransactionManager starting")
dao = db.pendingTransactionDao()
}
override fun stop() {
@ -47,7 +47,7 @@ class PersistentTransactionManager(private val db: PendingTransactionDb) : Trans
zatoshiValue: Long,
toAddress: String,
memo: String
): PendingTransactionEntity? = withContext(IO) {
): PendingTransaction? = withContext(IO) {
twig("constructing a placeholder transaction")
val tx = initTransaction(zatoshiValue, toAddress, memo)
twig("done constructing a placeholder transaction")
@ -71,14 +71,14 @@ class PersistentTransactionManager(private val db: PendingTransactionDb) : Trans
toAddress: String,
memo: String,
currentHeight: Int
): PendingTransactionEntity = manageCreation(encoder, initTransaction(zatoshiValue, toAddress, memo), currentHeight)
): PendingTransaction = manageCreation(encoder, initTransaction(zatoshiValue, toAddress, memo), currentHeight)
suspend fun manageCreation(
encoder: RawTransactionEncoder,
transaction: PendingTransactionEntity,
transaction: PendingTransaction,
currentHeight: Int
): PendingTransactionEntity = withContext(IO){
): PendingTransaction = withContext(IO){
twig("managing the creation of a transaction")
var tx = transaction.copy(expiryHeight = if (currentHeight == -1) -1 else currentHeight + EXPIRY_OFFSET)
try {
@ -103,7 +103,7 @@ class PersistentTransactionManager(private val db: PendingTransactionDb) : Trans
}
override suspend fun manageSubmission(service: LightWalletService, pendingTransaction: RawTransaction) {
var tx = pendingTransaction as PendingTransactionEntity
var tx = pendingTransaction as PendingTransaction
try {
twig("managing the preparation to submit transaction memo: ${tx.memo} amount: ${tx.value}")
val response = service.submitTransaction(pendingTransaction.raw!!)
@ -121,7 +121,7 @@ class PersistentTransactionManager(private val db: PendingTransactionDb) : Trans
}
}
override suspend fun getAll(): List<PendingTransactionEntity> = withContext(IO) {
override suspend fun getAll(): List<PendingTransaction> = withContext(IO) {
dao.getAll()
}
@ -130,8 +130,8 @@ class PersistentTransactionManager(private val db: PendingTransactionDb) : Trans
toAddress: String,
memo: String,
currentHeight: Int = -1
): PendingTransactionEntity {
return PendingTransactionEntity(
): PendingTransaction {
return PendingTransaction(
value = value,
address = toAddress,
memo = memo,
@ -139,7 +139,7 @@ class PersistentTransactionManager(private val db: PendingTransactionDb) : Trans
)
}
suspend fun manageMined(pendingTx: PendingTransactionEntity, matchingMinedTx: PendingTransactionEntity) = withContext(IO) {
suspend fun manageMined(pendingTx: PendingTransaction, matchingMinedTx: PendingTransaction) = withContext(IO) {
twig("a pending transaction has been mined!")
val tx = pendingTx.copy(minedHeight = matchingMinedTx.minedHeight)
dao.insert(tx)
@ -148,7 +148,7 @@ class PersistentTransactionManager(private val db: PendingTransactionDb) : Trans
/**
* Remove a transaction and pretend it never existed.
*/
suspend fun abortTransaction(existingTransaction: PendingTransactionEntity) = withContext(IO) {
suspend fun abortTransaction(existingTransaction: PendingTransaction) = withContext(IO) {
dao.delete(existingTransaction)
}

View File

@ -1,26 +1,30 @@
package cash.z.wallet.sdk.data
import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.db.PendingTransactionEntity
import cash.z.wallet.sdk.dao.ClearedTransaction
import cash.z.wallet.sdk.data.TransactionUpdateRequest.RefreshSentTx
import cash.z.wallet.sdk.data.TransactionUpdateRequest.SubmitPendingTx
import cash.z.wallet.sdk.db.PendingTransaction
import cash.z.wallet.sdk.db.isMined
import cash.z.wallet.sdk.db.isPending
import cash.z.wallet.sdk.db.isSameTxId
import cash.z.wallet.sdk.service.LightWalletService
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.actor
/**
* Monitors pending transactions and sends or retries them, when appropriate.
*/
class PersistentTransactionSender (
private val manager: TransactionManager,
private val service: LightWalletService,
private val clearedTxProvider: ClearedTransactionProvider
private val ledger: TransactionRepository
) : TransactionSender {
private lateinit var channel: SendChannel<TransactionUpdateRequest>
private var monitoringJob: Job? = null
private val initialMonitorDelay = 45_000L
private var listenerChannel: SendChannel<List<PendingTransactionEntity>>? = null
private var listenerChannel: SendChannel<List<PendingTransaction>>? = null
override var onSubmissionError: ((Throwable) -> Unit)? = null
fun CoroutineScope.requestUpdate(triggerSend: Boolean) = launch {
@ -75,7 +79,7 @@ class PersistentTransactionSender (
manager.stop()
}
override fun notifyOnChange(channel: SendChannel<List<PendingTransactionEntity>>) {
override fun notifyOnChange(channel: SendChannel<List<PendingTransaction>>) {
if (channel != null) twig("warning: listener channel was not null but it probably should have been. Something else was listening with $channel!")
listenerChannel = channel
}
@ -89,7 +93,7 @@ class PersistentTransactionSender (
toAddress: String,
memo: String,
fromAccountId: Int
): PendingTransactionEntity = withContext(IO) {
): PendingTransaction = withContext(IO) {
val currentHeight = service.safeLatestBlockHeight()
(manager as PersistentTransactionManager).manageCreation(encoder, zatoshi, toAddress, memo, currentHeight).also {
requestUpdate(true)
@ -100,7 +104,7 @@ class PersistentTransactionSender (
zatoshiValue: Long,
address: String,
memo: String
): PendingTransactionEntity? = withContext(IO) {
): PendingTransaction? = withContext(IO) {
(manager as PersistentTransactionManager).initPlaceholder(zatoshiValue, address, memo).also {
// update UI to show what we've just created. No need to submit, it has no raw data yet!
requestUpdate(false)
@ -109,8 +113,8 @@ class PersistentTransactionSender (
override suspend fun sendPreparedTransaction(
encoder: RawTransactionEncoder,
tx: PendingTransactionEntity
): PendingTransactionEntity = withContext(IO) {
tx: PendingTransaction
): PendingTransaction = withContext(IO) {
val currentHeight = service.safeLatestBlockHeight()
(manager as PersistentTransactionManager).manageCreation(encoder, tx, currentHeight).also {
// submit what we've just created
@ -118,16 +122,16 @@ class PersistentTransactionSender (
}
}
override suspend fun cleanupPreparedTransaction(tx: PendingTransactionEntity) {
override suspend fun cleanupPreparedTransaction(tx: PendingTransaction) {
if (tx.raw == null) {
(manager as PersistentTransactionManager).abortTransaction(tx)
}
}
// TODO: get this from the channel instead
var previousSentTxs: List<PendingTransactionEntity>? = null
var previousSentTxs: List<PendingTransaction>? = null
private suspend fun notifyIfChanged(currentSentTxs: List<PendingTransactionEntity>) = withContext(IO) {
private suspend fun notifyIfChanged(currentSentTxs: List<PendingTransaction>) = withContext(IO) {
twig("notifyIfChanged: listener null? ${listenerChannel == null} closed? ${listenerChannel?.isClosedForSend}")
if (hasChanged(previousSentTxs, currentSentTxs) && listenerChannel?.isClosedForSend != true) {
twig("START notifying listenerChannel of changed txs")
@ -139,15 +143,15 @@ class PersistentTransactionSender (
}
}
override suspend fun cancel(existingTransaction: PendingTransactionEntity) = withContext(IO) {
override suspend fun cancel(existingTransaction: PendingTransaction) = withContext(IO) {
(manager as PersistentTransactionManager).abortTransaction(existingTransaction). also {
requestUpdate(false)
}
}
private fun hasChanged(
previousSents: List<PendingTransactionEntity>?,
currentSents: List<PendingTransactionEntity>
previousSents: List<PendingTransaction>?,
currentSents: List<PendingTransaction>
): Boolean {
// shortcuts first
if (currentSents.isEmpty() && previousSents == null) return false.also { twig("checking pending txs: detected nothing happened yet") } // if nothing has happened, that doesn't count as a change
@ -164,7 +168,7 @@ class PersistentTransactionSender (
* Check on all sent transactions and if they've changed, notify listeners. This method can be called proactively
* when anything interesting has occurred with a transaction (via [requestUpdate]).
*/
private suspend fun refreshSentTransactions(): List<PendingTransactionEntity> = withContext(IO) {
private suspend fun refreshSentTransactions(): List<PendingTransaction> = withContext(IO) {
twig("refreshing all sent transactions")
val allSentTransactions = (manager as PersistentTransactionManager).getAll() // TODO: make this crash and catch error gracefully
notifyIfChanged(allSentTransactions)
@ -191,7 +195,7 @@ class PersistentTransactionSender (
}
} else {
findMatchingClearedTx(tx)?.let {
twig("matching cleared transaction found!")
twig("matching cleared transaction found! $tx")
(manager as PersistentTransactionManager).manageMined(tx, it)
refreshSentTransactions()
}
@ -203,25 +207,17 @@ class PersistentTransactionSender (
}
}
private fun findMatchingClearedTx(tx: PendingTransactionEntity): PendingTransactionEntity? {
return clearedTxProvider.getCleared().firstOrNull { clearedTx ->
// TODO: remove this troubleshooting code
if (tx.isSameTxId(clearedTx)) {
twig("found a matching cleared transaction with id: ${clearedTx.id}...")
if (clearedTx.height.let { it ?: 0 } <= 0) {
twig("...but it didn't have a mined height. That probably shouldn't happen so investigate this.")
false
} else {
true
}
} else false
}.toPendingTransactionEntity()
private fun findMatchingClearedTx(tx: PendingTransaction): PendingTransaction? {
return if (tx.txId == null) null else {
(ledger as PollingTransactionRepository)
.findTransactionByRawId(tx.txId)?.firstOrNull()?.toPendingTransactionEntity()
}
}
}
private fun WalletTransaction?.toPendingTransactionEntity(): PendingTransactionEntity? {
private fun ClearedTransaction?.toPendingTransactionEntity(): PendingTransaction? {
if(this == null) return null
return PendingTransactionEntity(
return PendingTransaction(
address = address ?: "",
value = value,
memo = memo ?: "",
@ -239,9 +235,10 @@ private fun LightWalletService.safeLatestBlockHeight(): Int {
}
}
sealed class TransactionUpdateRequest
object SubmitPendingTx : TransactionUpdateRequest()
object RefreshSentTx : TransactionUpdateRequest()
sealed class TransactionUpdateRequest {
object SubmitPendingTx : TransactionUpdateRequest()
object RefreshSentTx : TransactionUpdateRequest()
}

View File

@ -5,7 +5,7 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import cash.z.wallet.sdk.dao.BlockDao
import cash.z.wallet.sdk.dao.TransactionDao
import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.dao.ClearedTransaction
import cash.z.wallet.sdk.db.DerivedDataDb
import cash.z.wallet.sdk.entity.Transaction
import cash.z.wallet.sdk.jni.RustBackendWelding
@ -64,16 +64,20 @@ open class PollingTransactionRepository(
transaction
}
fun findTransactionByRawId(rawTxId: ByteArray): List<ClearedTransaction>? {
return transactions.findByRawId(rawTxId)
}
override suspend fun deleteTransactionById(txId: Long) = withContext(IO) {
twigTask("deleting transaction with id $txId") {
transactions.deleteById(txId)
}
}
suspend fun poll(channel: SendChannel<List<WalletTransaction>>, frequency: Long = pollFrequencyMillis) = withContext(IO) {
suspend fun poll(channel: SendChannel<List<ClearedTransaction>>, frequency: Long = pollFrequencyMillis) = withContext(IO) {
pollingJob?.cancel()
pollingJob = launch {
var previousTransactions: List<WalletTransaction>? = null
var previousTransactions: List<ClearedTransaction>? = null
while (isActive && !channel.isClosedForSend) {
twigTask("polling for cleared transactions every ${frequency}ms") {
val newTransactions = transactions.getAll()
@ -97,7 +101,7 @@ open class PollingTransactionRepository(
derivedDataDb.close()
}
private suspend fun addMemos(newTransactions: List<WalletTransaction>): List<WalletTransaction> = withContext(IO){
private suspend fun addMemos(newTransactions: List<ClearedTransaction>): List<ClearedTransaction> = withContext(IO){
for (tx in newTransactions) {
if (tx.rawMemoExists) {
tx.memo = if(tx.isSend) {
@ -111,8 +115,8 @@ open class PollingTransactionRepository(
}
private fun hasChanged(oldTxs: List<WalletTransaction>?, newTxs: List<WalletTransaction>): Boolean {
fun pr(t: List<WalletTransaction>?): String {
private fun hasChanged(oldTxs: List<ClearedTransaction>?, newTxs: List<ClearedTransaction>): Boolean {
fun pr(t: List<ClearedTransaction>?): String {
if(t == null) return "none"
val str = StringBuilder()
for (tx in t) {
@ -134,4 +138,5 @@ open class PollingTransactionRepository(
return false.also { twig("detected no changes in all new txs") }
}
}

View File

@ -1,10 +0,0 @@
package cash.z.wallet.sdk.data
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class SampleSeedProvider(val seedValue: String) : ReadOnlyProperty<Any?, ByteArray> {
override fun getValue(thisRef: Any?, property: KProperty<*>): ByteArray {
return seedValue.toByteArray()
}
}

View File

@ -1,17 +0,0 @@
package cash.z.wallet.sdk.data
import java.lang.IllegalStateException
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class SampleSpendingKeyProvider(private val seedValue: String) : ReadWriteProperty<Any?, String> {
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
}
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
// dynamically generating keys, based on seed is out of scope for this sample
if(seedValue != "dummyseed") throw IllegalStateException("This sample key provider only supports the dummy seed")
return "secret-extended-key-test1q0f0urnmqqqqpqxlree5urprcmg9pdgvr2c88qhm862etv65eu84r9zwannpz4g88299xyhv7wf9xkecag653jlwwwyxrymfraqsnz8qfgds70qjammscxxyl7s7p9xz9w906epdpy8ztsjd7ez7phcd5vj7syx68sjskqs8j9lef2uuacghsh8puuvsy9u25pfvcdznta33qe6xh5lrlnhdkgymnpdug4jm6tpf803cad6tqa9c0ewq9l03fqxatevm97jmuv8u0ccxjews5"
}
}

View File

@ -1,7 +1,7 @@
package cash.z.wallet.sdk.data
import cash.z.wallet.sdk.block.CompactBlockProcessor
import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.dao.ClearedTransaction
import cash.z.wallet.sdk.exception.SynchronizerException
import cash.z.wallet.sdk.exception.WalletException
import cash.z.wallet.sdk.secure.Wallet
@ -80,7 +80,7 @@ class SdkSynchronizer(
/**
* Channel of transactions from the repository.
*/
private val transactionChannel = ConflatedBroadcastChannel<List<WalletTransaction>>()
private val transactionChannel = ConflatedBroadcastChannel<List<ClearedTransaction>>()
/**
* Channel of balance information.
@ -147,7 +147,7 @@ class SdkSynchronizer(
/**
* A stream of all the wallet transactions, delegated to the [repository].
*/
override fun allTransactions(): ReceiveChannel<List<WalletTransaction>> {
override fun allTransactions(): ReceiveChannel<List<ClearedTransaction>> {
return transactionChannel.openSubscription()
}
@ -244,7 +244,7 @@ class SdkSynchronizer(
/**
* Monitors transactions and recalculates the balance any time transactions have changed.
*/
private suspend fun monitorTransactions(transactionChannel: ReceiveChannel<List<WalletTransaction>>) =
private suspend fun monitorTransactions(transactionChannel: ReceiveChannel<List<ClearedTransaction>>) =
withContext(IO) {
twig("beginning to monitor transactions in order to update the balance")
launch {

View File

@ -1,14 +0,0 @@
package cash.z.wallet.sdk.data
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class SimpleProvider<T>(var value: T) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}

View File

@ -1,9 +1,8 @@
package cash.z.wallet.sdk.data
import cash.z.android.wallet.data.ChannelListValueProvider
import cash.z.wallet.sdk.block.CompactBlockProcessor
import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.db.PendingTransactionEntity
import cash.z.wallet.sdk.dao.ClearedTransaction
import cash.z.wallet.sdk.db.PendingTransaction
import cash.z.wallet.sdk.exception.WalletException
import cash.z.wallet.sdk.secure.Wallet
import kotlinx.coroutines.*
@ -21,40 +20,73 @@ class StableSynchronizer (
private val ledger: PollingTransactionRepository,
private val sender: TransactionSender,
private val processor: CompactBlockProcessor,
private val encoder: RawTransactionEncoder,
private val clearedTransactionProvider: ChannelListValueProvider<WalletTransaction>
) : DataSyncronizer {
private val encoder: RawTransactionEncoder
) : DataSynchronizer {
/** This listener will not be called on the main thread. So it will need to switch to do anything with UI, like dialogs */
override var onCriticalErrorListener: ((Throwable) -> Boolean)? = null
/**
* The lifespan of this Synchronizer. This scope is initialized once the Synchronizer starts because it will be a
* child of the parentScope that gets passed into the [start] function. Everything launched by this Synchronizer
* will be cancelled once the Synchronizer or its parentScope stops. This is a lateinit rather than nullable
* property so that it fails early rather than silently, whenever the scope is used before the Synchronizer has been
* started.
*/
lateinit var coroutineScope: CoroutineScope
private var syncJob: Job? = null
private var clearedJob: Job? = null
private var pendingJob: Job? = null
private var progressJob: Job? = null
//
// Communication Primitives
//
private val balanceChannel = ConflatedBroadcastChannel(Wallet.WalletBalance())
private val progressChannel = ConflatedBroadcastChannel(0)
private val pendingChannel = ConflatedBroadcastChannel<List<PendingTransactionEntity>>(listOf())
private val clearedChannel = clearedTransactionProvider.channel
private val pendingChannel = ConflatedBroadcastChannel<List<PendingTransaction>>(listOf())
private val clearedChannel = ConflatedBroadcastChannel<List<ClearedTransaction>>(listOf())
// TODO: clean these up and turn them into delegates
internal val pendingProvider = ChannelListValueProvider(pendingChannel)
//
// Status
//
override val isConnected: Boolean get() = processor.isConnected
override val isSyncing: Boolean get() = processor.isSyncing
override val isScanning: Boolean get() = processor.isScanning
// TODO: find a better way to expose the lifecycle of this synchronizer (right now this is only used by the zcon1 app's SendReceiver)
lateinit var internalScope: CoroutineScope
override fun start(scope: CoroutineScope) {
internalScope = scope
twig("Starting sender!")
//
// Error Callbacks
//
/** This listener will not be called on the main thread. So it will need to switch to do anything with UI, like dialogs */
override var onCriticalErrorListener: ((Throwable) -> Boolean)? = null
override fun start(parentScope: CoroutineScope) {
// base this scope on the parent so that when the parent's job cancels, everything here cancels as well
// also use a supervisor job so that one failure doesn't bring down the whole synchronizer
coroutineScope = CoroutineScope(SupervisorJob(parentScope.coroutineContext[Job]!!) + Dispatchers.Main)
coroutineScope.launch {
initWallet()
startSender(this)
launchProgressMonitor()
launchPendingMonitor()
launchClearedMonitor()
onReady()
}
}
private fun startSender(parentScope: CoroutineScope) {
sender.onSubmissionError = ::onFailedSend
sender.start(parentScope)
}
private suspend fun initWallet() = withContext(Dispatchers.IO) {
try {
wallet.initialize()
} catch (e: WalletException.AlreadyInitializedException) {
twig("Warning: wallet already initialized but this is safe to ignore " +
"because the SDK now automatically detects where to start downloading.")
"because the SDK automatically detects where to start downloading.")
} catch (f: WalletException.FalseStart) {
if (recoverFrom(f)) {
twig("Warning: had a wallet init error but we recovered!")
@ -62,12 +94,6 @@ class StableSynchronizer (
twig("Error: false start while initializing wallet!")
}
}
sender.onSubmissionError = ::onFailedSend
sender.start(scope)
progressJob = scope.launchProgressMonitor()
pendingJob = scope.launchPendingMonitor()
clearedJob = scope.launchClearedMonitor()
syncJob = scope.onReady()
}
private fun recoverFrom(error: WalletException.FalseStart): Boolean {
@ -76,19 +102,11 @@ class StableSynchronizer (
//TODO: these errors are fatal and we need to delete the database and start over
twig("Database should be deleted and we should start over")
}
return true
return false
}
// TODO: consider removing the need for stopping by wrapping everything in a job that gets cancelled
// probably just returning the job from start
override fun stop() {
sender.stop()
// TODO: consider wrapping these in another object that helps with cleanup like job.toScopedJob()
// it would keep a reference to the job and then clear that reference when the scope ends
syncJob?.cancel().also { syncJob = null }
pendingJob?.cancel().also { pendingJob = null }
clearedJob?.cancel().also { clearedJob = null }
progressJob?.cancel().also { progressJob = null }
coroutineScope.cancel()
}
@ -206,20 +224,20 @@ class StableSynchronizer (
return progressChannel.openSubscription()
}
override fun pendingTransactions(): ReceiveChannel<List<PendingTransactionEntity>> {
override fun pendingTransactions(): ReceiveChannel<List<PendingTransaction>> {
return pendingChannel.openSubscription()
}
override fun clearedTransactions(): ReceiveChannel<List<WalletTransaction>> {
override fun clearedTransactions(): ReceiveChannel<List<ClearedTransaction>> {
return clearedChannel.openSubscription()
}
override fun getPending(): List<PendingTransactionEntity> {
return pendingProvider.getLatestValue()
override fun getPending(): List<PendingTransaction> {
return if (pendingChannel.isClosedForSend) listOf() else pendingChannel.value
}
override fun getCleared(): List<WalletTransaction> {
return clearedTransactionProvider.getLatestValue()
override fun getCleared(): List<ClearedTransaction> {
return if (clearedChannel.isClosedForSend) listOf() else clearedChannel.value
}
override fun getBalance(): Wallet.WalletBalance {
@ -238,38 +256,32 @@ class StableSynchronizer (
toAddress: String,
memo: String,
fromAccountId: Int
): PendingTransactionEntity = withContext(IO) {
): PendingTransaction = withContext(IO) {
sender.sendToAddress(encoder, zatoshi, toAddress, memo, fromAccountId)
}
}
interface DataSyncronizer : ClearedTransactionProvider, PendingTransactionProvider {
fun start(scope: CoroutineScope)
interface DataSynchronizer {
fun start(parentScope: CoroutineScope)
fun stop()
suspend fun getAddress(accountId: Int = 0): String
suspend fun sendToAddress(zatoshi: Long, toAddress: String, memo: String = "", fromAccountId: Int = 0): PendingTransactionEntity
suspend fun sendToAddress(zatoshi: Long, toAddress: String, memo: String = "", fromAccountId: Int = 0): PendingTransaction
fun balances(): ReceiveChannel<Wallet.WalletBalance>
fun progress(): ReceiveChannel<Int>
fun pendingTransactions(): ReceiveChannel<List<PendingTransactionEntity>>
fun clearedTransactions(): ReceiveChannel<List<WalletTransaction>>
fun pendingTransactions(): ReceiveChannel<List<PendingTransaction>>
fun clearedTransactions(): ReceiveChannel<List<ClearedTransaction>>
fun getPending(): List<PendingTransaction>
fun getCleared(): List<ClearedTransaction>
fun getBalance(): Wallet.WalletBalance
val isConnected: Boolean
val isSyncing: Boolean
val isScanning: Boolean
var onCriticalErrorListener: ((Throwable) -> Boolean)?
override fun getPending(): List<PendingTransactionEntity>
override fun getCleared(): List<WalletTransaction>
fun getBalance(): Wallet.WalletBalance
}
interface ClearedTransactionProvider {
fun getCleared(): List<WalletTransaction>
}
interface PendingTransactionProvider {
fun getPending(): List<PendingTransactionEntity>
}

View File

@ -1,6 +1,6 @@
package cash.z.wallet.sdk.data
import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.dao.ClearedTransaction
import cash.z.wallet.sdk.secure.Wallet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.ReceiveChannel
@ -38,7 +38,7 @@ interface Synchronizer {
/**
* A stream of all the wallet transactions.
*/
fun allTransactions(): ReceiveChannel<List<WalletTransaction>>
fun allTransactions(): ReceiveChannel<List<ClearedTransaction>>
/**
* A stream of balance values.

View File

@ -1,19 +1,19 @@
package cash.z.wallet.sdk.data
import cash.z.wallet.sdk.db.PendingTransactionEntity
import cash.z.wallet.sdk.db.PendingTransaction
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.SendChannel
interface TransactionSender {
fun start(scope: CoroutineScope)
fun stop()
fun notifyOnChange(channel: SendChannel<List<PendingTransactionEntity>>)
fun notifyOnChange(channel: SendChannel<List<PendingTransaction>>)
/** only necessary when there is a long delay between starting a transaction and beginning to create it. Like when sweeping a wallet that first needs to be scanned. */
suspend fun prepareTransaction(amount: Long, address: String, memo: String): PendingTransactionEntity?
suspend fun sendPreparedTransaction(encoder: RawTransactionEncoder, tx: PendingTransactionEntity): PendingTransactionEntity
suspend fun cleanupPreparedTransaction(tx: PendingTransactionEntity)
suspend fun sendToAddress(encoder: RawTransactionEncoder, zatoshi: Long, toAddress: String, memo: String = "", fromAccountId: Int = 0): PendingTransactionEntity
suspend fun cancel(existingTransaction: PendingTransactionEntity): Unit?
suspend fun prepareTransaction(amount: Long, address: String, memo: String): PendingTransaction?
suspend fun sendPreparedTransaction(encoder: RawTransactionEncoder, tx: PendingTransaction): PendingTransaction
suspend fun cleanupPreparedTransaction(tx: PendingTransaction)
suspend fun sendToAddress(encoder: RawTransactionEncoder, zatoshi: Long, toAddress: String, memo: String = "", fromAccountId: Int = 0): PendingTransaction
suspend fun cancel(existingTransaction: PendingTransaction): Unit?
var onSubmissionError: ((Throwable) -> Unit)?
}

View File

@ -1,13 +1,13 @@
package cash.z.wallet.sdk.db
import androidx.room.*
import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.dao.ClearedTransaction
import cash.z.wallet.sdk.data.RawTransaction
import cash.z.wallet.sdk.ext.masked
@Database(
entities = [
PendingTransactionEntity::class
PendingTransaction::class
],
version = 1,
exportSchema = false
@ -19,10 +19,10 @@ abstract class PendingTransactionDb : RoomDatabase() {
@Dao
interface PendingTransactionDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(transaction: PendingTransactionEntity): Long
fun insert(transaction: PendingTransaction): Long
@Delete
fun delete(transaction: PendingTransactionEntity)
fun delete(transaction: PendingTransaction)
//
// /**
// * Query all blocks that are not mined and not expired.
@ -48,11 +48,11 @@ interface PendingTransactionDao {
// fun getAllPending(currentHeight: Int): List<PendingTransactionEntity>
@Query("SELECT * from pending_transactions ORDER BY createTime")
fun getAll(): List<PendingTransactionEntity>
fun getAll(): List<PendingTransaction>
}
@Entity(tableName = "pending_transactions")
data class PendingTransactionEntity(
data class PendingTransaction(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val address: String = "",
@ -86,7 +86,7 @@ data class PendingTransactionEntity(
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is PendingTransactionEntity) return false
if (other !is PendingTransaction) return false
if (id != other.id) return false
if (address != other.address) return false
@ -130,50 +130,50 @@ data class PendingTransactionEntity(
}
fun PendingTransactionEntity.isSameTxId(other: WalletTransaction): Boolean {
fun PendingTransaction.isSameTxId(other: ClearedTransaction): Boolean {
return txId != null && other.rawTransactionId != null && txId.contentEquals(other.rawTransactionId!!)
}
fun PendingTransactionEntity.isSameTxId(other: PendingTransactionEntity): Boolean {
fun PendingTransaction.isSameTxId(other: PendingTransaction): Boolean {
return txId != null && other.txId != null && txId.contentEquals(other.txId)
}
fun PendingTransactionEntity.isCreating(): Boolean {
fun PendingTransaction.isCreating(): Boolean {
return raw == null && submitAttempts <= 0 && !isFailedSubmit() && !isFailedEncoding()
}
fun PendingTransactionEntity.isFailedEncoding(): Boolean {
fun PendingTransaction.isFailedEncoding(): Boolean {
return raw == null && encodeAttempts > 0
}
fun PendingTransactionEntity.isFailedSubmit(): Boolean {
fun PendingTransaction.isFailedSubmit(): Boolean {
return errorMessage != null || (errorCode != null && errorCode < 0)
}
fun PendingTransactionEntity.isFailure(): Boolean {
fun PendingTransaction.isFailure(): Boolean {
return isFailedEncoding() || isFailedSubmit()
}
fun PendingTransactionEntity.isSubmitted(): Boolean {
fun PendingTransaction.isSubmitted(): Boolean {
return submitAttempts > 0
}
fun PendingTransactionEntity.isMined(): Boolean {
fun PendingTransaction.isMined(): Boolean {
return minedHeight > 0
}
fun PendingTransactionEntity.isPending(currentHeight: Int = -1): Boolean {
fun PendingTransaction.isPending(currentHeight: Int = -1): Boolean {
// not mined and not expired and successfully created
return !isSubmitSuccess() && minedHeight == -1 && (expiryHeight == -1 || expiryHeight > currentHeight) && raw != null
}
fun PendingTransactionEntity.isSubmitSuccess(): Boolean {
fun PendingTransaction.isSubmitSuccess(): Boolean {
return submitAttempts > 0 && (errorCode != null && errorCode >= 0) && errorMessage == null
}
/**
* The amount of time remaining until this transaction is stale
*/
fun PendingTransactionEntity.ttl(): Long {
fun PendingTransaction.ttl(): Long {
return (60L * 2L) - (System.currentTimeMillis()/1000 - createTime)
}

View File

@ -0,0 +1,70 @@
package cash.z.wallet.sdk.ext
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.provider.Settings
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
@Deprecated(message = InsecureWarning.message)
class SampleSpendingKeyProvider(private val seedValue: String) : ReadWriteProperty<Any?, String> {
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
}
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
// dynamically generating keys, based on seed is out of scope for this sample
if (seedValue != "dummyseed") throw IllegalStateException("This sample provider only supports the dummy seed")
return "secret-extended-key-test1q0f0urnmqqqqpqxlree5urprcmg9pdgvr2c88qhm862etv65eu84r9zwannpz4g88299xyhv7wf9" +
"xkecag653jlwwwyxrymfraqsnz8qfgds70qjammscxxyl7s7p9xz9w906epdpy8ztsjd7ez7phcd5vj7syx68sjskqs8j9lef2uu" +
"acghsh8puuvsy9u25pfvcdznta33qe6xh5lrlnhdkgymnpdug4jm6tpf803cad6tqa9c0ewq9l03fqxatevm97jmuv8u0ccxjews5"
}
}
@Deprecated(message = InsecureWarning.message)
class SampleSeedProvider(val seedValue: String) : ReadOnlyProperty<Any?, ByteArray> {
override fun getValue(thisRef: Any?, property: KProperty<*>): ByteArray {
return seedValue.toByteArray()
}
}
@Deprecated(message = InsecureWarning.message)
class SimpleProvider<T>(var value: T) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
/**
* This is intentionally insecure. Wallet makers have told us storing keys is their specialty so we don't put a lot of
* energy here. A true implementation would create a key using user interaction, perhaps with a password they know that
* is never stored, along with requiring user authentication for key use (i.e. fingerprint/PIN/pattern/etc). From there,
* one of these approaches might be helpful to store the key securely:
*
* https://developer.android.com/training/articles/keystore.html
* https://github.com/scottyab/AESCrypt-Android/blob/master/aescrypt/src/main/java/com/scottyab/aescrypt/AESCrypt.java
* https://github.com/iamMehedi/Secured-Preference-Store
*/
@SuppressLint("HardwareIds")
@Deprecated(message = InsecureWarning.message)
class SeedGenerator {
companion object {
@Deprecated(message = InsecureWarning.message)
fun getDeviceId(appContext: Context): String {
val id =
Build.FINGERPRINT + Settings.Secure.getString(appContext.contentResolver, Settings.Secure.ANDROID_ID)
return id.replace("\\W".toRegex(), "_")
}
}
}
internal object InsecureWarning {
const val message = "Do not use this because it is insecure and only intended for test code and samples. " +
"Instead, use the Android Keystore system or a 3rd party library that leverages it."
}

View File

@ -1,6 +1,6 @@
package cash.z.wallet.sdk.data
import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.dao.ClearedTransaction
import kotlinx.coroutines.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
@ -142,7 +142,7 @@ internal class MockSynchronizerTest {
@Test
fun `balance matches transactions without sends`() = runBlocking {
val balances = fastSynchronizer.start(fastSynchronizer).balances()
var transactions = listOf<WalletTransaction>()
var transactions = listOf<ClearedTransaction>()
while (transactions.count() < 10) {
transactions = fastSynchronizer.allTransactions().receive()
println("got ${transactions.count()} transaction(s)")
@ -152,7 +152,7 @@ internal class MockSynchronizerTest {
@Test
fun `balance matches transactions with sends`() = runBlocking {
var transactions = listOf<WalletTransaction>()
var transactions = listOf<ClearedTransaction>()
val balances = fastSynchronizer.start(fastSynchronizer).balances()
val transactionChannel = fastSynchronizer.allTransactions()
while (transactions.count() < 10) {