zcash-android-wallet-sdk/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt

165 lines
6.8 KiB
Kotlin
Raw Normal View History

package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
import cash.z.ecc.android.sdk.ext.masked
2021-10-13 07:20:13 -07:00
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.internal.SdkDispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Class responsible for encoding a transaction in a consistent way. This bridges the gap by
* behaving like a stateless API so that callers can request [createTransaction] and receive a
* result, even though there are intermediate database interactions.
*
* @property rustBackend the instance of RustBackendWelding to use for creating and validating.
* @property repository the repository that stores information about the transactions being created
* such as the raw bytes and raw txId.
*/
class WalletTransactionEncoder(
2019-11-01 13:25:28 -07:00
private val rustBackend: RustBackendWelding,
private val repository: TransactionRepository
2019-07-14 15:13:12 -07:00
) : TransactionEncoder {
/**
* Creates a transaction, throwing an exception whenever things are missing. When the provided
* wallet implementation doesn't throw an exception, we wrap the issue into a descriptive
* exception ourselves (rather than using double-bangs for things).
*
* @param spendingKey the key associated with the notes that will be spent.
* @param zatoshi the amount of zatoshi to send.
* @param toAddress the recipient's address.
* @param memo the optional memo to include as part of the transaction.
* @param fromAccountIndex the optional account id to use. By default, the 1st account is used.
*
* @return the successfully encoded transaction or an exception
*/
2019-11-01 13:25:28 -07:00
override suspend fun createTransaction(
spendingKey: String,
zatoshi: Long,
toAddress: String,
memo: ByteArray?,
2019-11-01 13:25:28 -07:00
fromAccountIndex: Int
): EncodedTransaction = withContext(SdkDispatchers.IO) {
2019-11-01 13:25:28 -07:00
val transactionId = createSpend(spendingKey, zatoshi, toAddress, memo)
repository.findEncodedTransactionById(transactionId)
2019-11-01 13:25:28 -07:00
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
}
2019-11-01 13:25:28 -07:00
override suspend fun createShieldingTransaction(
spendingKey: String,
transparentSecretKey: String,
memo: ByteArray?
): EncodedTransaction = withContext(SdkDispatchers.IO) {
val transactionId = createShieldingSpend(spendingKey, transparentSecretKey, memo)
repository.findEncodedTransactionById(transactionId)
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
}
/**
* Utility function to help with validation. This is not called during [createTransaction]
* because this class asserts that all validation is done externally by the UI, for now.
*
* @param address the address to validate
*
* @return true when the given address is a valid z-addr
*/
override suspend fun isValidShieldedAddress(address: String): Boolean = withContext(Dispatchers.IO) {
rustBackend.isValidShieldedAddr(address)
}
/**
* Utility function to help with validation. This is not called during [createTransaction]
* because this class asserts that all validation is done externally by the UI, for now.
*
* @param address the address to validate
*
* @return true when the given address is a valid t-addr
*/
override suspend fun isValidTransparentAddress(address: String): Boolean = withContext(Dispatchers.IO) {
rustBackend.isValidTransparentAddr(address)
}
override suspend fun getConsensusBranchId(): Long {
val height = repository.lastScannedHeight()
if (height < rustBackend.network.saplingActivationHeight)
throw TransactionEncoderException.IncompleteScanException(height)
return rustBackend.getBranchIdForHeight(height)
}
2019-11-01 13:25:28 -07:00
/**
* Does the proofs and processing required to create a transaction to spend funds and inserts
* the result in the database. On average, this call takes over 10 seconds.
*
* @param spendingKey the key associated with the notes that will be spent.
* @param zatoshi the amount of zatoshi to send.
* @param toAddress the recipient's address.
* @param memo the optional memo to include as part of the transaction.
* @param fromAccountIndex the optional account id to use. By default, the 1st account is used.
2019-11-01 13:25:28 -07:00
*
* @return the row id in the transactions table that contains the spend transaction or -1 if it
* failed.
2019-11-01 13:25:28 -07:00
*/
private suspend fun createSpend(
2019-11-01 13:25:28 -07:00
spendingKey: String,
zatoshi: Long,
2019-11-01 13:25:28 -07:00
toAddress: String,
memo: ByteArray? = byteArrayOf(),
2019-11-01 13:25:28 -07:00
fromAccountIndex: Int = 0
): Long = withContext(Dispatchers.IO) {
twigTask(
"creating transaction to spend $zatoshi zatoshi to" +
" ${toAddress.masked()} with memo $memo"
) {
2019-11-01 13:25:28 -07:00
try {
val branchId = getConsensusBranchId()
SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir)
twig("params exist! attempting to send with consensus branchId $branchId...")
2019-11-01 13:25:28 -07:00
rustBackend.createToAddress(
branchId,
2019-11-01 13:25:28 -07:00
fromAccountIndex,
spendingKey,
toAddress,
zatoshi,
2019-11-01 13:25:28 -07:00
memo
)
} catch (t: Throwable) {
twig("${t.message}")
throw t
}
}.also { result ->
twig("result of sendToAddress: $result")
}
}
private suspend fun createShieldingSpend(
spendingKey: String,
transparentSecretKey: String,
memo: ByteArray? = byteArrayOf()
): Long = withContext(Dispatchers.IO) {
twigTask("creating transaction to shield all UTXOs") {
try {
SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir)
twig("params exist! attempting to shield...")
rustBackend.shieldToAddress(
spendingKey,
transparentSecretKey,
memo
)
} catch (t: Throwable) {
// TODO: if this error matches: Insufficient balance (have 0, need 1000 including fee)
2021-03-10 19:04:39 -08:00
// then consider custom error that says no UTXOs existed to shield
twig("Shield failed due to: ${t.message}")
throw t
}
}.also { result ->
twig("result of shieldToAddress: $result")
}
}
}