2021-10-04 04:18:37 -07:00
package cash.z.ecc.android.sdk.internal.transaction
2019-07-10 11:12:32 -07:00
import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
2021-03-10 10:10:03 -08:00
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.PendingTransactionEntity
import cash.z.ecc.android.sdk.db.entity.isCancelled
import cash.z.ecc.android.sdk.db.entity.isFailedEncoding
import cash.z.ecc.android.sdk.db.entity.isSubmitted
2021-11-18 04:10:30 -08:00
import cash.z.ecc.android.sdk.internal.db.PendingTransactionDao
import cash.z.ecc.android.sdk.internal.db.PendingTransactionDb
2021-10-04 04:18:37 -07:00
import cash.z.ecc.android.sdk.internal.service.LightWalletService
2021-11-18 04:10:30 -08:00
import cash.z.ecc.android.sdk.internal.twig
2022-07-12 05:40:09 -07:00
import cash.z.ecc.android.sdk.model.BlockHeight
2022-06-21 16:34:42 -07:00
import cash.z.ecc.android.sdk.model.Zatoshi
2019-11-22 23:18:20 -08:00
import kotlinx.coroutines.Dispatchers
2020-07-31 23:13:39 -07:00
import kotlinx.coroutines.Dispatchers.IO
2019-11-01 13:25:28 -07:00
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
2019-11-22 23:18:20 -08:00
import kotlinx.coroutines.withContext
2020-07-31 23:13:39 -07:00
import java.io.PrintWriter
import java.io.StringWriter
2019-11-01 13:25:28 -07:00
import kotlin.math.max
2019-07-10 11:12:32 -07:00
/ * *
2020-02-27 09:28:10 -08:00
* Facilitates persistent attempts to ensure that an outbound transaction is completed .
*
* @param db the database where the wallet can freely write information related to pending
* transactions . This database effectively serves as the mempool for transactions created by this
* wallet .
* @property encoder responsible for encoding a transaction by taking all the inputs and returning
2020-06-10 00:08:19 -07:00
* an [ cash . z . ecc . android . sdk . entity . EncodedTransaction ] object containing the raw bytes and transaction
2020-02-27 09:28:10 -08:00
* id .
* @property service the lightwallet service used to submit transactions .
2019-07-10 11:12:32 -07:00
* /
2019-11-01 13:25:28 -07:00
class PersistentTransactionManager (
db : PendingTransactionDb ,
2020-06-09 19:14:22 -07:00
internal val encoder : TransactionEncoder ,
2019-11-01 13:25:28 -07:00
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 ( )
2019-07-10 11:12:32 -07:00
/ * *
* 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 "
2019-07-10 11:12:32 -07:00
) : 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-07-10 11:12:32 -07:00
2020-02-27 09:28:10 -08:00
//
// OutboundTransactionManager implementation
//
2019-11-22 23:18:20 -08:00
override suspend fun initSpend (
2022-06-21 16:34:42 -07:00
value : Zatoshi ,
2019-07-10 11:12:32 -07:00
toAddress : String ,
2019-11-01 13:25:28 -07:00
memo : String ,
fromAccountIndex : Int
2019-11-22 23:18:20 -08:00
) : PendingTransaction = withContext ( Dispatchers . IO ) {
2019-07-10 11:12:32 -07:00
twig ( " constructing a placeholder transaction " )
2019-11-01 13:25:28 -07:00
var tx = PendingTransactionEntity (
2022-06-21 16:34:42 -07:00
value = value . value ,
2019-11-01 13:25:28 -07:00
toAddress = toAddress ,
2019-11-12 08:58:15 -08:00
memo = memo . toByteArray ( ) ,
2019-11-01 13:25:28 -07:00
accountIndex = fromAccountIndex
)
2019-07-10 11:12:32 -07:00
try {
2020-07-31 23:13:39 -07:00
safeUpdate ( " creating tx in DB " ) {
tx = findById ( create ( tx ) ) !!
twig ( " successfully created TX in DB with id: ${tx.id} " )
2019-11-01 13:25:28 -07:00
}
2019-07-10 11:12:32 -07:00
} catch ( t : Throwable ) {
2020-07-31 23:13:39 -07:00
twig (
" Unknown error while attempting to create and fetch pending transaction: " +
" ${t.message} caused by: ${t.cause} "
)
2019-07-10 11:12:32 -07:00
}
2019-11-01 13:25:28 -07:00
2019-11-22 23:18:20 -08:00
tx
2019-07-10 11:12:32 -07:00
}
2022-07-12 05:40:09 -07:00
override suspend fun applyMinedHeight ( pendingTx : PendingTransaction , minedHeight : BlockHeight ) {
2020-07-31 23:13:39 -07:00
twig ( " a pending transaction has been mined! " )
safeUpdate ( " updating mined height for pending tx id: ${pendingTx.id} to $minedHeight " ) {
2022-07-12 05:40:09 -07:00
updateMinedHeight ( pendingTx . id , minedHeight . value )
2019-11-22 23:18:20 -08:00
}
2019-11-01 13:25:28 -07:00
}
2019-07-10 11:12:32 -07:00
2019-11-22 23:18:20 -08:00
override suspend fun encode (
2019-11-01 13:25:28 -07:00
spendingKey : String ,
pendingTx : PendingTransaction
2019-11-22 23:18:20 -08:00
) : PendingTransaction = withContext ( Dispatchers . IO ) {
2019-07-10 11:12:32 -07:00
twig ( " managing the creation of a transaction " )
2019-11-01 13:25:28 -07:00
var tx = pendingTx as PendingTransactionEntity
2019-07-10 11:12:32 -07:00
try {
twig ( " beginning to encode transaction with : $encoder " )
2019-11-01 13:25:28 -07:00
val encodedTx = encoder . createTransaction (
spendingKey ,
2022-07-07 05:52:07 -07:00
tx . valueZatoshi ,
2019-11-01 13:25:28 -07:00
tx . toAddress ,
2019-11-12 08:58:15 -08:00
tx . memo ,
2019-11-01 13:25:28 -07:00
tx . accountIndex
)
2020-07-31 23:13:39 -07:00
twig ( " successfully encoded transaction! " )
2021-07-29 10:18:55 -07:00
safeUpdate ( " updating transaction encoding " , - 1 ) {
2020-07-31 23:13:39 -07:00
updateEncoding ( tx . id , encodedTx . raw , encodedTx . txId , encodedTx . expiryHeight )
}
2019-07-10 11:12:32 -07:00
} catch ( t : Throwable ) {
2021-05-25 13:02:27 -07:00
var message = " failed to encode transaction due to : ${t.message} "
t . cause ?. let { message += " caused by: $it " }
2019-07-10 11:12:32 -07:00
twig ( message )
2020-07-31 23:13:39 -07:00
safeUpdate ( " updating transaction error info " ) {
updateError ( tx . id , message , ERROR _ENCODING )
}
2019-07-10 11:12:32 -07:00
} finally {
2021-07-29 10:18:55 -07:00
safeUpdate ( " incrementing transaction encodeAttempts (from: ${tx.encodeAttempts} ) " , - 1 ) {
2020-07-31 23:13:39 -07:00
updateEncodeAttempts ( tx . id , max ( 1 , tx . encodeAttempts + 1 ) )
tx = findById ( tx . id ) !!
}
2019-07-10 11:12:32 -07:00
}
2019-11-01 13:25:28 -07:00
2019-11-22 23:18:20 -08:00
tx
2019-07-10 11:12:32 -07:00
}
2021-02-17 13:07:57 -08:00
override suspend fun encode (
spendingKey : String ,
transparentSecretKey : String ,
pendingTx : PendingTransaction
) : PendingTransaction {
twig ( " managing the creation of a shielding transaction " )
var tx = pendingTx as PendingTransactionEntity
try {
twig ( " beginning to encode shielding transaction with : $encoder " )
val encodedTx = encoder . createShieldingTransaction (
spendingKey ,
transparentSecretKey ,
tx . memo
)
twig ( " successfully encoded shielding transaction! " )
safeUpdate ( " updating shielding transaction encoding " ) {
updateEncoding ( tx . id , encodedTx . raw , encodedTx . txId , encodedTx . expiryHeight )
}
} catch ( t : Throwable ) {
2021-05-25 13:02:27 -07:00
var message = " failed to encode auto-shielding transaction due to : ${t.message} "
t . cause ?. let { message += " caused by: $it " }
2021-02-17 13:07:57 -08:00
twig ( message )
safeUpdate ( " updating shielding transaction error info " ) {
updateError ( tx . id , message , ERROR _ENCODING )
}
} finally {
safeUpdate ( " incrementing shielding transaction encodeAttempts (from: ${tx.encodeAttempts} ) " ) {
updateEncodeAttempts ( tx . id , max ( 1 , tx . encodeAttempts + 1 ) )
tx = findById ( tx . id ) !!
}
}
return tx
}
2019-11-22 23:18:20 -08:00
override suspend fun submit ( pendingTx : PendingTransaction ) : PendingTransaction = withContext ( Dispatchers . IO ) {
2019-11-23 15:07:28 -08:00
// reload the tx to check for cancellation
2020-07-31 23:13:39 -07:00
var tx = pendingTransactionDao { findById ( pendingTx . id ) }
2021-03-10 10:10:03 -08:00
?: throw IllegalStateException (
" Error while submitting transaction. No pending " +
2020-02-27 09:28:10 -08:00
" transaction found that matches the one being submitted. Verify that the " +
2021-03-10 10:10:03 -08:00
" transaction still exists among the set of pending transactions. "
)
2019-07-10 11:12:32 -07:00
try {
2020-07-31 23:13:39 -07:00
// do nothing if failed or cancelled
when {
tx . isFailedEncoding ( ) -> twig ( " Warning: this transaction will not be submitted because it failed to be encoded. " )
tx . isCancelled ( ) -> twig ( " Warning: ignoring cancelled transaction with id ${tx.id} . We will not submit it to the network because it has been cancelled. " )
else -> {
2021-07-29 10:18:55 -07:00
twig ( " submitting transaction with memo: ${tx.memo} amount: ${tx.value} " , - 1 )
2020-07-31 23:13:39 -07:00
val response = service . submitTransaction ( tx . raw )
val error = response . errorCode < 0
2021-03-10 10:10:03 -08:00
twig (
" ${if (error) "FAILURE! " else "SUCCESS!"} submit transaction completed with " +
" response: ${response.errorCode} : ${response.errorMessage} "
)
2020-07-31 23:13:39 -07:00
2021-07-29 10:18:55 -07:00
safeUpdate ( " updating submitted transaction (hadError: $error ) " , - 1 ) {
2020-07-31 23:13:39 -07:00
updateError ( tx . id , if ( error ) response . errorMessage else null , response . errorCode )
updateSubmitAttempts ( tx . id , max ( 1 , tx . submitAttempts + 1 ) )
}
}
2019-11-01 13:25:28 -07:00
}
2019-07-10 11:12:32 -07:00
} catch ( t : Throwable ) {
2019-11-01 13:25:28 -07:00
// a non-server error has occurred
2021-05-25 13:02:27 -07:00
var message =
" Unknown error while submitting transaction: ${t.message} "
t . cause ?. let { message += " caused by: $it " }
2019-11-01 13:25:28 -07:00
twig ( message )
2020-07-31 23:13:39 -07:00
safeUpdate ( " updating submission failure " ) {
updateError ( tx . id , t . message , ERROR _SUBMITTING )
updateSubmitAttempts ( tx . id , max ( 1 , tx . submitAttempts + 1 ) )
}
} finally {
2021-07-29 10:18:55 -07:00
safeUpdate ( " fetching latest tx info " , - 1 ) {
2020-07-31 23:13:39 -07:00
tx = findById ( tx . id ) !!
}
2019-07-10 11:12:32 -07:00
}
2019-11-22 23:18:20 -08:00
tx
}
override suspend fun monitorById ( id : Long ) : Flow < PendingTransaction > {
return pendingTransactionDao { monitorById ( id ) }
2019-07-10 11:12:32 -07:00
}
2020-01-08 00:57:42 -08:00
override suspend fun isValidShieldedAddress ( address : String ) =
encoder . isValidShieldedAddress ( address )
override suspend fun isValidTransparentAddress ( address : String ) =
encoder . isValidTransparentAddress ( address )
2020-07-31 23:13:39 -07:00
override suspend fun cancel ( pendingId : Long ) : Boolean {
2019-11-01 13:25:28 -07:00
return pendingTransactionDao {
2020-07-31 23:13:39 -07:00
val tx = findById ( pendingId )
2019-11-01 13:25:28 -07:00
if ( tx ?. isSubmitted ( ) == true ) {
2020-07-31 23:13:39 -07:00
twig ( " Attempt to cancel transaction failed because it has already been submitted! " )
2019-11-01 13:25:28 -07:00
false
} else {
2020-07-31 23:13:39 -07:00
twig ( " Cancelling unsubmitted transaction id: $pendingId " )
cancel ( pendingId )
2019-11-01 13:25:28 -07:00
true
}
}
2019-07-10 11:12:32 -07:00
}
2020-07-31 23:13:39 -07:00
override suspend fun findById ( id : Long ) = pendingTransactionDao {
findById ( id )
}
2020-02-27 09:28:10 -08:00
2020-07-31 23:13:39 -07:00
override suspend fun markForDeletion ( id : Long ) = pendingTransactionDao {
withContext ( IO ) {
twig ( " [cleanup] marking pendingTx $id for deletion " )
removeRawTransactionId ( id )
updateError ( id , " safe to delete " , - 9090 )
}
}
2020-02-27 09:28:10 -08:00
/ * *
* Remove a transaction and pretend it never existed .
2020-07-31 23:13:39 -07:00
*
* @return the final number of transactions that were removed from the database .
2020-02-27 09:28:10 -08:00
* /
2020-07-31 23:13:39 -07:00
override suspend fun abort ( existingTransaction : PendingTransaction ) : Int {
return pendingTransactionDao {
twig ( " [cleanup] Deleting pendingTxId: ${existingTransaction.id} " )
2020-02-27 09:28:10 -08:00
delete ( existingTransaction as PendingTransactionEntity )
}
}
2020-07-31 23:13:39 -07:00
override fun getAll ( ) = _dao . getAll ( )
//
// Helper functions
//
2019-07-10 11:12:32 -07:00
/ * *
2019-11-01 13:25:28 -07:00
* Updating the pending transaction is often done at the end of a function but still should
2020-07-31 23:13:39 -07:00
* happen within a try / catch block , surrounded by logging . So this helps with that while also
* ensuring that no other coroutines are concurrently interacting with the DAO .
2019-07-10 11:12:32 -07:00
* /
2021-07-29 10:18:55 -07:00
private suspend fun < R > safeUpdate ( logMessage : String = " " , priority : Int = 0 , block : suspend PendingTransactionDao . ( ) -> R ) : R ? {
2019-11-01 13:25:28 -07:00
return try {
2020-07-31 23:13:39 -07:00
twig ( logMessage )
pendingTransactionDao { block ( ) }
2019-11-01 13:25:28 -07:00
} catch ( t : Throwable ) {
2020-07-31 23:13:39 -07:00
val stacktrace = StringWriter ( ) . also { t . printStackTrace ( PrintWriter ( it ) ) } . toString ( )
twig (
" Unknown error while attempting to ' $logMessage ': " +
" ${t.message} caused by: ${t.cause} stacktrace: $stacktrace "
)
null
2019-11-01 13:25:28 -07:00
}
}
private suspend fun < T > pendingTransactionDao ( block : suspend PendingTransactionDao . ( ) -> T ) : T {
return daoMutex . withLock {
2020-07-31 23:13:39 -07:00
withContext ( IO ) {
_dao . block ( )
}
2019-11-01 13:25:28 -07:00
}
2019-07-10 11:12:32 -07:00
}
2020-02-27 09:28:10 -08:00
companion object {
/** Error code for an error while encoding a transaction */
const val ERROR _ENCODING = 2000
2022-06-23 05:31:02 -07:00
2020-02-27 09:28:10 -08:00
/** Error code for an error while submitting a transaction */
const val ERROR _SUBMITTING = 3000
}
2019-11-01 13:25:28 -07:00
}