Cleanup after Zcon1.
This commit is contained in:
parent
78f98c2868
commit
8c7103d0ee
|
@ -183,6 +183,7 @@ cargo {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
|
||||
|
||||
// Architecture Components: Lifecycle
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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") }
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)?
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue