zcash-android-wallet-sdk/src/main/java/cash/z/wallet/sdk/transaction/PersistentTransactionManage...

209 lines
7.6 KiB
Kotlin
Raw Normal View History

2019-10-23 22:21:52 -07:00
package cash.z.wallet.sdk.transaction
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.room.Room
import androidx.room.RoomDatabase
import cash.z.wallet.sdk.db.PendingTransactionDao
import cash.z.wallet.sdk.db.PendingTransactionDb
2019-11-01 13:25:28 -07:00
import cash.z.wallet.sdk.entity.*
2019-10-23 22:21:52 -07:00
import cash.z.wallet.sdk.ext.twig
import cash.z.wallet.sdk.service.LightWalletService
import kotlinx.coroutines.Dispatchers
2019-11-01 13:25:28 -07:00
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
2019-11-01 13:25:28 -07:00
import kotlin.math.max
/**
* Facilitates persistent attempts to ensure a transaction occurs.
*/
// TODO: consider having the manager register the fail listeners rather than having that responsibility spread elsewhere (synchronizer and the broom)
2019-11-01 13:25:28 -07:00
class PersistentTransactionManager(
db: PendingTransactionDb,
private val encoder: TransactionEncoder,
private val service: LightWalletService
) : OutboundTransactionManager {
2019-07-14 15:13:12 -07:00
2019-11-01 13:25:28 -07:00
private val daoMutex = Mutex()
/**
* Internal reference to the dao that is only accessed after locking the [daoMutex] in order
* to enforce DB access in both a threadsafe and coroutinesafe way.
*/
private val _dao: PendingTransactionDao = db.pendingTransactionDao()
/**
* Constructor that creates the database and then executes a callback on it.
*/
constructor(
appContext: Context,
2019-11-01 13:25:28 -07:00
encoder: TransactionEncoder,
service: LightWalletService,
2019-07-14 15:13:12 -07:00
dataDbName: String = "PendingTransactions.db"
) : this(
2019-07-14 15:13:12 -07:00
Room.databaseBuilder(
appContext,
PendingTransactionDb::class.java,
dataDbName
2019-11-01 13:25:28 -07:00
).setJournalMode(RoomDatabase.JournalMode.TRUNCATE).build(),
encoder,
service
2019-07-14 15:13:12 -07:00
)
2019-11-01 13:25:28 -07:00
/**
* Initialize a [PendingTransaction] and then insert it in the database for monitoring and
* follow-up.
*/
override suspend fun initSpend(
zatoshiValue: Long,
toAddress: String,
2019-11-01 13:25:28 -07:00
memo: String,
fromAccountIndex: Int
): PendingTransaction = withContext(Dispatchers.IO) {
twig("constructing a placeholder transaction")
2019-11-01 13:25:28 -07:00
var tx = PendingTransactionEntity(
value = zatoshiValue,
toAddress = toAddress,
memo = memo.toByteArray(),
2019-11-01 13:25:28 -07:00
accountIndex = fromAccountIndex
)
try {
2019-11-01 13:25:28 -07:00
twig("creating tx in DB: $tx")
pendingTransactionDao {
val insertedTx = findById(create(tx))
twig("pending transaction created with id: ${insertedTx?.id}")
tx = tx.copy(id = insertedTx!!.id)
}.also {
twig("successfully created TX in DB")
}
} catch (t: Throwable) {
2019-11-01 13:25:28 -07:00
twig("Unknown error while attempting to create pending transaction: ${t.message} caused by: ${t.cause}")
}
2019-11-01 13:25:28 -07:00
tx
}
override suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: Int) {
(pendingTx as? PendingTransactionEntity)?.let {
twig("a pending transaction has been mined!")
safeUpdate(pendingTx.copy(minedHeight = minedHeight))
}
2019-11-01 13:25:28 -07:00
}
2019-11-01 13:25:28 -07:00
/**
* Remove a transaction and pretend it never existed.
*/
suspend fun abortTransaction(existingTransaction: PendingTransaction) {
pendingTransactionDao {
delete(existingTransaction as PendingTransactionEntity)
}
}
override suspend fun encode(
2019-11-01 13:25:28 -07:00
spendingKey: String,
pendingTx: PendingTransaction
): PendingTransaction = withContext(Dispatchers.IO) {
twig("managing the creation of a transaction")
2019-11-01 13:25:28 -07:00
//var tx = transaction.copy(expiryHeight = if (currentHeight == -1) -1 else currentHeight + EXPIRY_OFFSET)
var tx = pendingTx as PendingTransactionEntity
try {
twig("beginning to encode transaction with : $encoder")
2019-11-01 13:25:28 -07:00
val encodedTx = encoder.createTransaction(
spendingKey,
tx.value,
tx.toAddress,
tx.memo,
2019-11-01 13:25:28 -07:00
tx.accountIndex
)
twig("successfully encoded transaction for ${tx.memo}!!")
2019-07-14 15:13:12 -07:00
tx = tx.copy(raw = encodedTx.raw, rawTransactionId = encodedTx.txId)
} catch (t: Throwable) {
val message = "failed to encode transaction due to : ${t.message} caused by: ${t.cause}"
twig(message)
message
2019-11-01 13:25:28 -07:00
tx = tx.copy(errorMessage = message, errorCode = 2000) //TODO: find a place for these error codes
} finally {
2019-11-01 13:25:28 -07:00
tx = tx.copy(encodeAttempts = max(1, tx.encodeAttempts + 1))
}
2019-11-01 13:25:28 -07:00
safeUpdate(tx)
tx
}
override suspend fun submit(pendingTx: PendingTransaction): PendingTransaction = withContext(Dispatchers.IO) {
2019-11-01 13:25:28 -07:00
var tx1 = pendingTransactionDao { findById(pendingTx.id) }
if(tx1 == null) twig("unable to find transaction for id: ${pendingTx.id}")
var tx = tx1!!
try {
2019-11-01 13:25:28 -07:00
// do nothing when cancelled
if (!tx.isCancelled()) {
twig("submitting transaction to lightwalletd - memo: ${tx.memo} amount: ${tx.value}")
val response = service.submitTransaction(tx.raw!!)
val error = response.errorCode < 0
twig("${if (error) "FAILURE! " else "SUCCESS!"} submit transaction completed with response: ${response.errorCode}: ${response.errorMessage}")
tx = tx.copy(
errorMessage = if (error) response.errorMessage else null,
errorCode = response.errorCode,
submitAttempts = max(1, tx.submitAttempts + 1)
)
safeUpdate(tx)
} else {
twig("Warning: ignoring cancelled transaction with id ${tx.id}")
}
} catch (t: Throwable) {
2019-11-01 13:25:28 -07:00
// a non-server error has occurred
val message =
"Unknown error while submitting transaction: ${t.message} caused by: ${t.cause}"
twig(message)
tx = tx.copy(errorMessage = t.message, errorCode = 3000, submitAttempts = max(1, tx.submitAttempts + 1)) //TODO: find a place for these error codes
safeUpdate(tx)
}
tx
}
override suspend fun monitorById(id: Long): Flow<PendingTransaction> {
return pendingTransactionDao { monitorById(id) }
}
2019-11-01 13:25:28 -07:00
override suspend fun cancel(pendingTx: PendingTransaction): Boolean {
return pendingTransactionDao {
val tx = findById(pendingTx.id)
if (tx?.isSubmitted() == true) {
false
} else {
cancel(pendingTx.id)
true
}
}
}
override fun getAll() = _dao.getAll()
/**
2019-11-01 13:25:28 -07:00
* Updating the pending transaction is often done at the end of a function but still should
* happen within a try/catch block, surrounded by logging. So this helps with that.
*/
2019-11-01 13:25:28 -07:00
private suspend fun safeUpdate(tx: PendingTransactionEntity): PendingTransaction {
return try {
twig("updating tx into DB: $tx")
pendingTransactionDao { update(tx) }
twig("successfully updated TX into DB")
tx
} catch (t: Throwable) {
twig("Unknown error while attempting to update pending transaction: ${t.message} caused by: ${t.cause}")
tx
}
}
private suspend fun <T> pendingTransactionDao(block: suspend PendingTransactionDao.() -> T): T {
return daoMutex.withLock {
_dao.block()
}
}
2019-11-01 13:25:28 -07:00
}