2019-10-23 22:21:52 -07:00
package cash.z.wallet.sdk.transaction
2019-07-10 11:12:32 -07:00
import android.content.Context
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
2019-07-10 11:12:32 -07:00
import cash.z.wallet.sdk.service.LightWalletService
2019-11-22 23:18:20 -08:00
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
2019-11-22 23:18:20 -08:00
import kotlinx.coroutines.withContext
2019-11-23 15:07:28 -08:00
import java.lang.IllegalStateException
2019-11-01 13:25:28 -07:00
import kotlin.math.max
2019-07-10 11:12:32 -07:00
/ * *
* 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 ( )
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
2019-11-01 13:25:28 -07:00
/ * *
* Initialize a [ PendingTransaction ] and then insert it in the database for monitoring and
* follow - up .
* /
2019-11-22 23:18:20 -08:00
override suspend fun initSpend (
2019-07-10 11:12:32 -07:00
zatoshiValue : Long ,
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 (
value = zatoshiValue ,
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 {
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 " )
}
2019-07-10 11:12:32 -07:00
} 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-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
}
2019-11-22 23:18:20 -08:00
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-07-10 11:12:32 -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 )
}
}
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 = transaction.copy(expiryHeight = if (currentHeight == -1) -1 else currentHeight + EXPIRY_OFFSET)
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 ,
tx . value ,
tx . toAddress ,
2019-11-12 08:58:15 -08:00
tx . memo ,
2019-11-01 13:25:28 -07:00
tx . accountIndex
)
2019-07-10 11:12:32 -07:00
twig ( " successfully encoded transaction for ${tx.memo} !! " )
2019-07-14 15:13:12 -07:00
tx = tx . copy ( raw = encodedTx . raw , rawTransactionId = encodedTx . txId )
2019-07-10 11:12:32 -07:00
} 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
2019-07-10 11:12:32 -07:00
} finally {
2019-11-01 13:25:28 -07:00
tx = tx . copy ( encodeAttempts = max ( 1 , tx . encodeAttempts + 1 ) )
2019-07-10 11:12:32 -07:00
}
2019-11-01 13:25:28 -07:00
safeUpdate ( tx )
2019-11-22 23:18:20 -08:00
tx
2019-07-10 11:12:32 -07:00
}
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
var storedTx = pendingTransactionDao { findById ( pendingTx . id ) } ?: throw IllegalStateException ( " Error while submitting transaction. No pending transaction found that matches the one being submitted. Verify that the transaction still exists among the set of pending transactions. " )
var tx = storedTx
2019-07-10 11:12:32 -07:00
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} " )
2019-11-23 15:07:28 -08:00
val response = service . submitTransaction ( tx . raw )
2019-11-01 13:25:28 -07:00
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} " )
}
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
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 )
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 )
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
}
}
2019-07-10 11:12:32 -07:00
}
2019-11-22 23:18:20 -08:00
override fun getAll ( ) = _dao . getAll ( )
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
* happen within a try / catch block , surrounded by logging . So this helps with that .
2019-07-10 11:12:32 -07:00
* /
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-07-10 11:12:32 -07:00
}
2019-11-01 13:25:28 -07:00
}
2019-07-10 11:12:32 -07:00