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

160 lines
5.8 KiB
Kotlin
Raw Normal View History

2019-10-23 22:21:52 -07:00
package cash.z.wallet.sdk.transaction
2019-07-14 15:13:12 -07:00
import cash.z.wallet.sdk.entity.EncodedTransaction
2019-11-01 13:25:28 -07:00
import cash.z.wallet.sdk.exception.TransactionEncoderException
import cash.z.wallet.sdk.ext.*
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.jni.RustBackendWelding
import com.squareup.okhttp.OkHttpClient
import com.squareup.okhttp.Request
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
2019-11-01 13:25:28 -07:00
import okio.Okio
import java.io.File
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).
*/
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(IO) {
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
/**
* 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 value the zatoshi value to send
* @param toAddress the destination address
* @param memo the memo, which is not augmented in any way
*
* @return the row id in the transactions table that contains the spend transaction
* or -1 if it failed
*/
suspend fun createSpend(
spendingKey: String,
value: Long,
toAddress: String,
memo: ByteArray? = byteArrayOf(),
2019-11-01 13:25:28 -07:00
fromAccountIndex: Int = 0
): Long = withContext(IO) {
twigTask("creating transaction to spend $value zatoshi to" +
" ${toAddress.masked()} with memo $memo") {
try {
ensureParams((rustBackend as RustBackend).paramDestinationDir)
twig("params exist! attempting to send...")
rustBackend.createToAddress(
fromAccountIndex,
spendingKey,
toAddress,
value,
memo
)
} catch (t: Throwable) {
twig("${t.message}")
throw t
}
}.also { result ->
twig("result of sendToAddress: $result")
}
}
/**
* Checks the given directory for the output and spending params and calls [fetchParams] if
* they're missing.
*
* @param destinationDir the directory where the params should be stored.
*/
private suspend fun ensureParams(destinationDir: String) {
var hadError = false
arrayOf(
ZcashSdk.SPEND_PARAM_FILE_NAME,
ZcashSdk.OUTPUT_PARAM_FILE_NAME
).forEach { paramFileName ->
if (!File(destinationDir, paramFileName).exists()) {
twig("ERROR: $paramFileName not found at location: $destinationDir")
hadError = true
}
}
if (hadError) {
try {
Bush.trunk.twigTask("attempting to download missing params") {
fetchParams(destinationDir)
}
} catch (e: Throwable) {
twig("failed to fetch params due to: $e")
throw TransactionEncoderException.MissingParamsException
}
}
}
/**
* Download and store the params into the given directory.
*
* @param destinationDir the directory where the params will be stored. It's assumed that we
* have write access to this directory. Typically, this should be the app's cache directory
* because it is not harmful if these files are cleared by the user since they are downloaded
* on-demand.
*/
suspend fun fetchParams(destinationDir: String) = withContext(IO) {
val client = createHttpClient()
var failureMessage = ""
arrayOf(
ZcashSdk.SPEND_PARAM_FILE_NAME,
ZcashSdk.OUTPUT_PARAM_FILE_NAME
).forEach { paramFileName ->
val url = "${ZcashSdk.CLOUD_PARAM_DIR_URL}/$paramFileName"
val request = Request.Builder().url(url).build()
val response = client.newCall(request).execute()
if (response.isSuccessful) {
twig("fetch succeeded")
val file = File(destinationDir, paramFileName)
if(file.parentFile.exists()) {
twig("directory exists!")
} else {
twig("directory did not exist attempting to make it")
file.parentFile.mkdirs()
}
Okio.buffer(Okio.sink(file)).use {
twig("writing to $file")
it.writeAll(response.body().source())
}
twig("fetch succeeded, done writing $paramFileName")
} else {
failureMessage += "Error while fetching $paramFileName : $response\n"
twig(failureMessage)
}
}
if (failureMessage.isNotEmpty()) throw TransactionEncoderException.FetchParamsException(failureMessage)
}
//
// Helpers
//
/**
* Http client is only used for downloading sapling spend and output params data, which are
* necessary for the wallet to scan blocks.
*/
private fun createHttpClient(): OkHttpClient {
//TODO: add logging and timeouts
return OkHttpClient()
}
}