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.exception.TransactionEncoderException
|
2021-03-10 10:10:03 -08:00
|
|
|
import cash.z.ecc.android.sdk.ext.masked
|
2021-11-18 04:10:30 -08:00
|
|
|
import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
2023-02-06 14:36:28 -08:00
|
|
|
import cash.z.ecc.android.sdk.internal.Twig
|
2023-07-17 03:50:53 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.TypesafeBackend
|
2022-10-19 13:52:54 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
|
|
|
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
2024-02-19 14:09:01 -08:00
|
|
|
import cash.z.ecc.android.sdk.model.Account
|
2023-09-11 00:11:01 -07:00
|
|
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
2023-08-07 12:17:28 -07:00
|
|
|
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
2024-02-19 14:09:01 -08:00
|
|
|
import cash.z.ecc.android.sdk.model.Proposal
|
2022-10-26 18:37:40 -07:00
|
|
|
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
2022-09-29 10:04:00 -07:00
|
|
|
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
2022-07-07 05:52:07 -07:00
|
|
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
2019-07-10 11:12:32 -07:00
|
|
|
|
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.
|
|
|
|
*
|
2024-01-04 01:47:46 -08:00
|
|
|
* @property backend the instance of RustBackendWelding to use for creating and validating.
|
2020-02-27 09:28:10 -08:00
|
|
|
* @property repository the repository that stores information about the transactions being created
|
|
|
|
* such as the raw bytes and raw txId.
|
|
|
|
*/
|
2024-02-19 14:09:01 -08:00
|
|
|
@Suppress("TooManyFunctions")
|
2023-05-05 14:46:07 -07:00
|
|
|
internal class TransactionEncoderImpl(
|
2023-07-17 03:50:53 -07:00
|
|
|
private val backend: TypesafeBackend,
|
2022-09-06 03:44:33 -07:00
|
|
|
private val saplingParamTool: SaplingParamTool,
|
2022-10-19 13:52:54 -07:00
|
|
|
private val repository: DerivedDataRepository
|
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).
|
|
|
|
*
|
2022-09-29 10:04:00 -07:00
|
|
|
* @param usk the unified spending key associated with the notes that will be spent.
|
2022-07-07 05:52:07 -07:00
|
|
|
* @param amount the amount of zatoshi to send.
|
2024-01-04 01:47:46 -08:00
|
|
|
* @param recipient the recipient's address.
|
2020-02-27 09:28:10 -08:00
|
|
|
* @param memo the optional memo to include as part of the transaction.
|
|
|
|
*
|
|
|
|
* @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(
|
2022-09-29 10:04:00 -07:00
|
|
|
usk: UnifiedSpendingKey,
|
2022-07-07 05:52:07 -07:00
|
|
|
amount: Zatoshi,
|
2022-10-26 18:37:40 -07:00
|
|
|
recipient: TransactionRecipient,
|
2022-10-06 10:44:34 -07:00
|
|
|
memo: ByteArray?
|
2022-01-19 10:39:07 -08:00
|
|
|
): EncodedTransaction {
|
2022-10-26 18:37:40 -07:00
|
|
|
require(recipient is TransactionRecipient.Address)
|
|
|
|
|
|
|
|
val transactionId = createSpend(usk, amount, recipient.addressValue, memo)
|
2023-08-07 12:17:28 -07:00
|
|
|
return repository.findEncodedTransactionByTxId(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(
|
2022-09-29 10:04:00 -07:00
|
|
|
usk: UnifiedSpendingKey,
|
2022-10-26 18:37:40 -07:00
|
|
|
recipient: TransactionRecipient,
|
2021-02-17 13:07:57 -08:00
|
|
|
memo: ByteArray?
|
2022-01-19 10:39:07 -08:00
|
|
|
): EncodedTransaction {
|
2022-10-26 18:37:40 -07:00
|
|
|
require(recipient is TransactionRecipient.Account)
|
|
|
|
|
2022-09-29 10:04:00 -07:00
|
|
|
val transactionId = createShieldingSpend(usk, memo)
|
2023-08-07 12:17:28 -07:00
|
|
|
return repository.findEncodedTransactionByTxId(transactionId)
|
2021-02-17 13:07:57 -08:00
|
|
|
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
|
|
|
|
}
|
|
|
|
|
2024-02-19 14:09:01 -08:00
|
|
|
override suspend fun proposeTransfer(
|
|
|
|
account: Account,
|
|
|
|
recipient: String,
|
|
|
|
amount: Zatoshi,
|
|
|
|
memo: ByteArray?
|
|
|
|
): Proposal {
|
|
|
|
Twig.debug {
|
|
|
|
"creating proposal to spend $amount zatoshi to" +
|
|
|
|
" ${recipient.masked()} with memo: ${memo?.decodeToString()}"
|
|
|
|
}
|
|
|
|
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
|
|
|
return try {
|
|
|
|
backend.proposeTransfer(
|
|
|
|
account,
|
|
|
|
recipient,
|
|
|
|
amount.value,
|
|
|
|
memo
|
|
|
|
)
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
Twig.debug(t) { "Caught exception while creating proposal." }
|
|
|
|
throw t
|
|
|
|
}.also { result ->
|
|
|
|
Twig.debug { "result of proposeTransfer: $result" }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override suspend fun proposeShielding(
|
|
|
|
account: Account,
|
|
|
|
memo: ByteArray?
|
|
|
|
): Proposal {
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
|
|
|
return try {
|
|
|
|
backend.proposeShielding(account, memo)
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
// TODO [#680]: if this error matches: Insufficient balance (have 0, need 1000 including fee)
|
|
|
|
// then consider custom error that says no UTXOs existed to shield
|
|
|
|
// TODO [#680]: https://github.com/zcash/zcash-android-wallet-sdk/issues/680
|
|
|
|
Twig.debug(t) { "proposeShielding failed" }
|
|
|
|
throw t
|
|
|
|
}.also { result ->
|
|
|
|
Twig.debug { "result of proposeShielding: $result" }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override suspend fun createProposedTransactions(
|
|
|
|
proposal: Proposal,
|
|
|
|
usk: UnifiedSpendingKey
|
|
|
|
): List<EncodedTransaction> {
|
|
|
|
Twig.debug {
|
|
|
|
"creating transactions for proposal"
|
|
|
|
}
|
|
|
|
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
|
|
|
val transactionId =
|
|
|
|
try {
|
|
|
|
saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory)
|
|
|
|
Twig.debug { "params exist! attempting to send..." }
|
|
|
|
backend.createProposedTransaction(proposal, usk)
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
Twig.debug(t) { "Caught exception while creating transaction." }
|
|
|
|
throw t
|
|
|
|
}.also { result ->
|
|
|
|
Twig.debug { "result of createProposedTransactions: $result" }
|
|
|
|
}
|
|
|
|
|
|
|
|
val tx =
|
|
|
|
repository.findEncodedTransactionByTxId(transactionId)
|
|
|
|
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
|
|
|
|
|
|
|
|
return listOf(tx)
|
|
|
|
}
|
|
|
|
|
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
|
|
|
*/
|
2024-01-08 09:17:35 -08:00
|
|
|
override suspend fun isValidShieldedAddress(address: String): Boolean = backend.isValidSaplingAddr(address)
|
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 t-addr
|
2020-01-08 00:57:42 -08:00
|
|
|
*/
|
2024-01-04 12:21:32 -08:00
|
|
|
override suspend fun isValidTransparentAddress(address: String): Boolean = backend.isValidTransparentAddr(address)
|
2020-01-08 00:57:42 -08:00
|
|
|
|
2022-06-17 05:06:21 -07: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.
|
|
|
|
*
|
|
|
|
* @param address the address to validate
|
|
|
|
*
|
|
|
|
* @return true when the given address is a valid ZIP 316 Unified Address
|
|
|
|
*/
|
2024-01-04 12:21:32 -08:00
|
|
|
override suspend fun isValidUnifiedAddress(address: String): Boolean = backend.isValidUnifiedAddr(address)
|
2022-06-17 05:06:21 -07:00
|
|
|
|
2023-09-11 00:11:01 -07:00
|
|
|
/**
|
|
|
|
* Return the consensus branch that the encoder is using when making transactions.
|
|
|
|
*
|
|
|
|
* @param height the height at which we want to get the consensus branch
|
|
|
|
*
|
|
|
|
* @return id of consensus branch
|
|
|
|
*
|
|
|
|
* @throws TransactionEncoderException.IncompleteScanException if the [height] is less than activation height
|
|
|
|
*/
|
|
|
|
override suspend fun getConsensusBranchId(height: BlockHeight): Long {
|
|
|
|
if (height < backend.network.saplingActivationHeight) {
|
2020-06-09 19:14:22 -07:00
|
|
|
throw TransactionEncoderException.IncompleteScanException(height)
|
2022-06-23 05:31:02 -07:00
|
|
|
}
|
2023-05-05 14:46:07 -07:00
|
|
|
return backend.getBranchIdForHeight(height)
|
2020-06-09 19:14:22 -07:00
|
|
|
}
|
|
|
|
|
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.
|
|
|
|
*
|
2022-09-29 10:04:00 -07:00
|
|
|
* @param usk the unified spending key associated with the notes that will be spent.
|
2022-07-07 05:52:07 -07:00
|
|
|
* @param amount the amount of zatoshi to send.
|
2020-02-27 09:28:10 -08:00
|
|
|
* @param toAddress the recipient's address.
|
|
|
|
* @param memo the optional memo to include as part of the transaction.
|
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(
|
2022-09-29 10:04:00 -07:00
|
|
|
usk: UnifiedSpendingKey,
|
2022-07-07 05:52:07 -07:00
|
|
|
amount: Zatoshi,
|
2019-11-01 13:25:28 -07:00
|
|
|
toAddress: String,
|
2022-10-06 10:44:34 -07:00
|
|
|
memo: ByteArray? = byteArrayOf()
|
2023-08-07 12:17:28 -07:00
|
|
|
): FirstClassByteArray {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug {
|
2022-07-07 05:52:07 -07:00
|
|
|
"creating transaction to spend $amount zatoshi to" +
|
2023-08-16 03:59:52 -07:00
|
|
|
" ${toAddress.masked()} with memo: ${memo?.decodeToString()}"
|
2023-02-06 14:36:28 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
|
|
|
return try {
|
2023-05-18 04:36:15 -07:00
|
|
|
saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory)
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug { "params exist! attempting to send..." }
|
2024-01-08 15:02:52 -08:00
|
|
|
val proposal =
|
|
|
|
backend.proposeTransfer(
|
2024-02-19 14:09:01 -08:00
|
|
|
usk.account,
|
2024-01-08 15:02:52 -08:00
|
|
|
toAddress,
|
|
|
|
amount.value,
|
|
|
|
memo
|
|
|
|
)
|
|
|
|
backend.createProposedTransaction(proposal, usk)
|
2023-02-06 14:36:28 -08:00
|
|
|
} catch (t: Throwable) {
|
|
|
|
Twig.debug(t) { "Caught exception while creating transaction." }
|
|
|
|
throw t
|
2019-11-01 13:25:28 -07:00
|
|
|
}.also { result ->
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug { "result of sendToAddress: $result" }
|
2019-11-01 13:25:28 -07:00
|
|
|
}
|
|
|
|
}
|
2021-02-17 13:07:57 -08:00
|
|
|
|
|
|
|
private suspend fun createShieldingSpend(
|
2022-09-29 10:04:00 -07:00
|
|
|
usk: UnifiedSpendingKey,
|
2021-02-17 13:07:57 -08:00
|
|
|
memo: ByteArray? = byteArrayOf()
|
2023-08-07 12:17:28 -07:00
|
|
|
): FirstClassByteArray {
|
2023-02-06 14:36:28 -08:00
|
|
|
@Suppress("TooGenericExceptionCaught")
|
|
|
|
return try {
|
2023-05-18 04:36:15 -07:00
|
|
|
saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory)
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug { "params exist! attempting to shield..." }
|
2024-02-19 14:09:01 -08:00
|
|
|
val proposal = backend.proposeShielding(usk.account, memo)
|
2024-01-08 15:02:52 -08:00
|
|
|
backend.createProposedTransaction(proposal, usk)
|
2023-02-06 14:36:28 -08:00
|
|
|
} catch (t: Throwable) {
|
|
|
|
// TODO [#680]: if this error matches: Insufficient balance (have 0, need 1000 including fee)
|
|
|
|
// then consider custom error that says no UTXOs existed to shield
|
|
|
|
// TODO [#680]: https://github.com/zcash/zcash-android-wallet-sdk/issues/680
|
|
|
|
Twig.debug(t) { "Shield failed" }
|
|
|
|
throw t
|
2021-02-17 13:07:57 -08:00
|
|
|
}.also { result ->
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug { "result of shieldToAddress: $result" }
|
2021-02-17 13:07:57 -08:00
|
|
|
}
|
|
|
|
}
|
2019-07-10 11:12:32 -07:00
|
|
|
}
|