2021-10-04 04:18:37 -07:00
|
|
|
package cash.z.ecc.android.sdk.internal.transaction
|
2019-07-10 11:12:32 -07:00
|
|
|
|
2020-06-10 00:08:19 -07:00
|
|
|
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
|
|
|
|
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
|
2021-03-10 10:10:03 -08:00
|
|
|
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
|
2020-06-10 00:08:19 -07:00
|
|
|
import cash.z.ecc.android.sdk.jni.RustBackend
|
|
|
|
import cash.z.ecc.android.sdk.jni.RustBackendWelding
|
2021-10-04 04:18:37 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
2021-10-21 13:05:02 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.SdkDispatchers
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
2019-07-10 11:12:32 -07:00
|
|
|
import kotlinx.coroutines.withContext
|
|
|
|
|
2020-02-27 09:28:10 -08:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2019-07-10 11:12:32 -07:00
|
|
|
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 {
|
2019-07-10 11:12:32 -07:00
|
|
|
|
|
|
|
/**
|
2020-02-27 09:28:10 -08:00
|
|
|
* 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-07-10 11:12:32 -07:00
|
|
|
*/
|
2019-11-01 13:25:28 -07:00
|
|
|
override suspend fun createTransaction(
|
|
|
|
spendingKey: String,
|
|
|
|
zatoshi: Long,
|
|
|
|
toAddress: String,
|
2019-11-12 08:58:15 -08:00
|
|
|
memo: ByteArray?,
|
2019-11-01 13:25:28 -07:00
|
|
|
fromAccountIndex: Int
|
2021-10-21 13:05:02 -07:00
|
|
|
): EncodedTransaction = withContext(SdkDispatchers.IO) {
|
2019-11-01 13:25:28 -07:00
|
|
|
val transactionId = createSpend(spendingKey, zatoshi, toAddress, memo)
|
2019-11-22 23:18:20 -08:00
|
|
|
repository.findEncodedTransactionById(transactionId)
|
2019-11-01 13:25:28 -07:00
|
|
|
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
|
2019-07-10 11:12:32 -07:00
|
|
|
}
|
2019-11-01 13:25:28 -07:00
|
|
|
|
2021-02-17 13:07:57 -08:00
|
|
|
override suspend fun createShieldingTransaction(
|
|
|
|
spendingKey: String,
|
|
|
|
transparentSecretKey: String,
|
|
|
|
memo: ByteArray?
|
2021-10-21 13:05:02 -07:00
|
|
|
): EncodedTransaction = withContext(SdkDispatchers.IO) {
|
2021-02-17 13:07:57 -08:00
|
|
|
val transactionId = createShieldingSpend(spendingKey, transparentSecretKey, memo)
|
|
|
|
repository.findEncodedTransactionById(transactionId)
|
|
|
|
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
|
|
|
|
}
|
|
|
|
|
2020-01-08 00:57:42 -08:00
|
|
|
/**
|
|
|
|
* 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.
|
2020-02-27 09:28:10 -08:00
|
|
|
*
|
|
|
|
* @param address the address to validate
|
|
|
|
*
|
|
|
|
* @return true when the given address is a valid z-addr
|
2020-01-08 00:57:42 -08:00
|
|
|
*/
|
2021-10-21 13:05:02 -07:00
|
|
|
override suspend fun isValidShieldedAddress(address: String): Boolean = withContext(Dispatchers.IO) {
|
2020-01-08 00:57:42 -08:00
|
|
|
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.
|
2020-02-27 09:28:10 -08:00
|
|
|
*
|
|
|
|
* @param address the address to validate
|
|
|
|
*
|
|
|
|
* @return true when the given address is a valid t-addr
|
2020-01-08 00:57:42 -08:00
|
|
|
*/
|
2021-10-21 13:05:02 -07:00
|
|
|
override suspend fun isValidTransparentAddress(address: String): Boolean = withContext(Dispatchers.IO) {
|
2020-01-08 00:57:42 -08:00
|
|
|
rustBackend.isValidTransparentAddr(address)
|
|
|
|
}
|
|
|
|
|
2020-06-09 19:14:22 -07:00
|
|
|
override suspend fun getConsensusBranchId(): Long {
|
|
|
|
val height = repository.lastScannedHeight()
|
2021-04-09 18:20:09 -07:00
|
|
|
if (height < rustBackend.network.saplingActivationHeight)
|
2020-06-09 19:14:22 -07:00
|
|
|
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.
|
|
|
|
*
|
2020-02-27 09:28:10 -08:00
|
|
|
* @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
|
|
|
*
|
2020-02-27 09:28:10 -08: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
|
|
|
*/
|
2019-11-23 17:47:50 -08:00
|
|
|
private suspend fun createSpend(
|
2019-11-01 13:25:28 -07:00
|
|
|
spendingKey: String,
|
2020-02-27 09:28:10 -08:00
|
|
|
zatoshi: Long,
|
2019-11-01 13:25:28 -07:00
|
|
|
toAddress: String,
|
2019-11-12 08:58:15 -08:00
|
|
|
memo: ByteArray? = byteArrayOf(),
|
2019-11-01 13:25:28 -07:00
|
|
|
fromAccountIndex: Int = 0
|
2021-10-21 13:05:02 -07:00
|
|
|
): Long = withContext(Dispatchers.IO) {
|
2021-03-10 10:10:03 -08:00
|
|
|
twigTask(
|
|
|
|
"creating transaction to spend $zatoshi zatoshi to" +
|
|
|
|
" ${toAddress.masked()} with memo $memo"
|
|
|
|
) {
|
2019-11-01 13:25:28 -07:00
|
|
|
try {
|
2020-06-09 19:14:22 -07:00
|
|
|
val branchId = getConsensusBranchId()
|
2021-01-22 14:05:11 -08:00
|
|
|
SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir)
|
2020-06-09 19:14:22 -07:00
|
|
|
twig("params exist! attempting to send with consensus branchId $branchId...")
|
2019-11-01 13:25:28 -07:00
|
|
|
rustBackend.createToAddress(
|
2020-06-09 19:14:22 -07:00
|
|
|
branchId,
|
2019-11-01 13:25:28 -07:00
|
|
|
fromAccountIndex,
|
|
|
|
spendingKey,
|
|
|
|
toAddress,
|
2020-02-27 09:28:10 -08:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
2021-02-17 13:07:57 -08:00
|
|
|
|
|
|
|
private suspend fun createShieldingSpend(
|
|
|
|
spendingKey: String,
|
|
|
|
transparentSecretKey: String,
|
|
|
|
memo: ByteArray? = byteArrayOf()
|
2021-10-21 13:05:02 -07:00
|
|
|
): Long = withContext(Dispatchers.IO) {
|
2021-02-17 13:07:57 -08:00
|
|
|
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
|
2021-02-17 13:07:57 -08:00
|
|
|
twig("Shield failed due to: ${t.message}")
|
|
|
|
throw t
|
|
|
|
}
|
|
|
|
}.also { result ->
|
|
|
|
twig("result of shieldToAddress: $result")
|
|
|
|
}
|
|
|
|
}
|
2019-07-10 11:12:32 -07:00
|
|
|
}
|