New: Non-Rust changes to support auto-shielding.
This commit is contained in:
parent
5f90675b87
commit
1d2c7b1a4f
|
@ -40,6 +40,7 @@ import cash.z.ecc.android.sdk.ext.twigTask
|
||||||
import cash.z.ecc.android.sdk.jni.RustBackend
|
import cash.z.ecc.android.sdk.jni.RustBackend
|
||||||
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
|
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
|
||||||
import cash.z.ecc.android.sdk.service.LightWalletService
|
import cash.z.ecc.android.sdk.service.LightWalletService
|
||||||
|
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||||
import cash.z.ecc.android.sdk.transaction.OutboundTransactionManager
|
import cash.z.ecc.android.sdk.transaction.OutboundTransactionManager
|
||||||
import cash.z.ecc.android.sdk.transaction.PagedTransactionRepository
|
import cash.z.ecc.android.sdk.transaction.PagedTransactionRepository
|
||||||
import cash.z.ecc.android.sdk.transaction.PersistentTransactionManager
|
import cash.z.ecc.android.sdk.transaction.PersistentTransactionManager
|
||||||
|
@ -500,7 +501,13 @@ class SdkSynchronizer internal constructor(
|
||||||
|
|
||||||
override suspend fun cancelSpend(pendingId: Long) = txManager.cancel(pendingId)
|
override suspend fun cancelSpend(pendingId: Long) = txManager.cancel(pendingId)
|
||||||
|
|
||||||
override suspend fun getAddress(accountId: Int): String = processor.getAddress(accountId)
|
override suspend fun getAddress(accountId: Int): String = getShieldedAddress(accountId)
|
||||||
|
|
||||||
|
override suspend fun getShieldedAddress(accountId: Int): String = processor.getShieldedAddress(accountId)
|
||||||
|
|
||||||
|
override suspend fun getTransparentAddress(seed: ByteArray, accountId: Int, index: Int): String {
|
||||||
|
return DerivationTool.deriveTransparentAddress(seed, accountId, index)
|
||||||
|
}
|
||||||
|
|
||||||
override fun sendToAddress(
|
override fun sendToAddress(
|
||||||
spendingKey: String,
|
spendingKey: String,
|
||||||
|
@ -529,6 +536,45 @@ class SdkSynchronizer internal constructor(
|
||||||
txManager.monitorById(it.id)
|
txManager.monitorById(it.id)
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
|
|
||||||
|
override fun shieldFunds(
|
||||||
|
spendingKey: String,
|
||||||
|
transparentSecretKey: String,
|
||||||
|
memo: String
|
||||||
|
): Flow<PendingTransaction> = flow {
|
||||||
|
twig("Initializing shielding transaction")
|
||||||
|
val tAddr = DerivationTool.deriveTransparentAddress(transparentSecretKey)
|
||||||
|
val tBalance = processor.getUtxoCacheBalance(tAddr)
|
||||||
|
val zAddr = getAddress(0)
|
||||||
|
|
||||||
|
|
||||||
|
// Emit the placeholder transaction, then switch to monitoring the database
|
||||||
|
txManager.initSpend(tBalance.availableZatoshi, zAddr, memo, 0).let { placeHolderTx ->
|
||||||
|
emit(placeHolderTx)
|
||||||
|
txManager.encode(spendingKey, transparentSecretKey, placeHolderTx).let { encodedTx ->
|
||||||
|
// only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.
|
||||||
|
if (encodedTx.isCancelled()) {
|
||||||
|
twig("[cleanup] this shielding tx has been cancelled so we will cleanup instead of submitting")
|
||||||
|
if (cleanupCancelledTx(encodedTx)) refreshBalance()
|
||||||
|
encodedTx
|
||||||
|
} else {
|
||||||
|
txManager.submit(encodedTx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.flatMapLatest {
|
||||||
|
twig("Monitoring shielding transaction (id: ${it.id}) for updates...")
|
||||||
|
txManager.monitorById(it.id)
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
|
||||||
|
override suspend fun refreshUtxos(address: String, sinceHeight: Int): Int {
|
||||||
|
// TODO: we need to think about how we restrict this to only our taddr
|
||||||
|
return processor.downloadUtxos(address, sinceHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTransparentBalance(tAddr: String): WalletBalance {
|
||||||
|
return processor.getUtxoCacheBalance(tAddr)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun isValidShieldedAddr(address: String) =
|
override suspend fun isValidShieldedAddr(address: String) =
|
||||||
txManager.isValidShieldedAddress(address)
|
txManager.isValidShieldedAddress(address)
|
||||||
|
|
||||||
|
|
|
@ -120,14 +120,38 @@ interface Synchronizer {
|
||||||
//
|
//
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the address for the given account.
|
* Gets the shielded address for the given account. This is syntactic sugar for
|
||||||
|
* [getShieldedAddress] because we use z-addrs by default.
|
||||||
*
|
*
|
||||||
* @param accountId the optional accountId whose address is of interest. By default, the first
|
* @param accountId the optional accountId whose address is of interest. By default, the first
|
||||||
* account is used.
|
* account is used.
|
||||||
*
|
*
|
||||||
* @return the address for the given account.
|
* @return the shielded address for the given account.
|
||||||
*/
|
*/
|
||||||
suspend fun getAddress(accountId: Int = 0): String
|
suspend fun getAddress(accountId: Int = 0) = getShieldedAddress(accountId)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the shielded address for the given account.
|
||||||
|
*
|
||||||
|
* @param accountId the optional accountId whose address is of interest. By default, the first
|
||||||
|
* account is used.
|
||||||
|
*
|
||||||
|
* @return the shielded address for the given account.
|
||||||
|
*/
|
||||||
|
suspend fun getShieldedAddress(accountId: Int = 0): String
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the transparent address for the given account and index.
|
||||||
|
*
|
||||||
|
* @param accountId the optional accountId whose address is of interest. By default, the first
|
||||||
|
* account is used.
|
||||||
|
* @param index the optional index whose address is of interest. By default, the first index is
|
||||||
|
* used.
|
||||||
|
*
|
||||||
|
* @return the address for the given account and index.
|
||||||
|
*/
|
||||||
|
suspend fun getTransparentAddress(seed: ByteArray, accountId: Int = 0, index: Int = 0): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends zatoshi.
|
* Sends zatoshi.
|
||||||
|
@ -151,6 +175,12 @@ interface Synchronizer {
|
||||||
fromAccountIndex: Int = 0
|
fromAccountIndex: Int = 0
|
||||||
): Flow<PendingTransaction>
|
): Flow<PendingTransaction>
|
||||||
|
|
||||||
|
fun shieldFunds(
|
||||||
|
spendingKey: String,
|
||||||
|
transparentSecretKey: String,
|
||||||
|
memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX
|
||||||
|
): Flow<PendingTransaction>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true when the given address is a valid z-addr. Invalid addresses will throw an
|
* Returns true when the given address is a valid z-addr. Invalid addresses will throw an
|
||||||
* exception. Valid z-addresses have these characteristics: //TODO copy info from related ZIP
|
* exception. Valid z-addresses have these characteristics: //TODO copy info from related ZIP
|
||||||
|
@ -230,6 +260,13 @@ interface Synchronizer {
|
||||||
errorHandler: (Throwable) -> Unit = { throw it }
|
errorHandler: (Throwable) -> Unit = { throw it }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
suspend fun refreshUtxos(tAddr: String, sinceHeight: Int): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the balance that the wallet knows about. This should be called after [refreshUtxos].
|
||||||
|
*/
|
||||||
|
suspend fun getTransparentBalance(tAddr: String): WalletBalance
|
||||||
|
|
||||||
//
|
//
|
||||||
// Error Handling
|
// Error Handling
|
||||||
//
|
//
|
||||||
|
|
|
@ -34,6 +34,7 @@ import cash.z.ecc.android.sdk.jni.RustBackend
|
||||||
import cash.z.ecc.android.sdk.jni.RustBackendWelding
|
import cash.z.ecc.android.sdk.jni.RustBackendWelding
|
||||||
import cash.z.ecc.android.sdk.transaction.PagedTransactionRepository
|
import cash.z.ecc.android.sdk.transaction.PagedTransactionRepository
|
||||||
import cash.z.ecc.android.sdk.transaction.TransactionRepository
|
import cash.z.ecc.android.sdk.transaction.TransactionRepository
|
||||||
|
import cash.z.wallet.sdk.rpc.Service
|
||||||
import io.grpc.StatusRuntimeException
|
import io.grpc.StatusRuntimeException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
@ -313,6 +314,33 @@ class CompactBlockProcessor(
|
||||||
if (!repository.isInitialized()) throw CompactBlockProcessorException.Uninitialized
|
if (!repository.isInitialized()) throw CompactBlockProcessorException.Uninitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal suspend fun downloadUtxos(tAddress: String, startHeight: Int): Int = withContext(IO) {
|
||||||
|
var skipped = 0
|
||||||
|
twig("Downloading utxos starting at height $startHeight")
|
||||||
|
downloader.lightWalletService.fetchUtxos(tAddress, startHeight).let { result ->
|
||||||
|
result.forEach { utxo: Service.GetAddressUtxosReply ->
|
||||||
|
twig("Found UTXO at height ${utxo.height.toInt()}")
|
||||||
|
try {
|
||||||
|
rustBackend.putUtxo(
|
||||||
|
tAddress,
|
||||||
|
utxo.txid.toByteArray(),
|
||||||
|
utxo.index,
|
||||||
|
utxo.script.toByteArray(),
|
||||||
|
utxo.valueZat,
|
||||||
|
utxo.height.toInt()
|
||||||
|
)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons)
|
||||||
|
skipped++
|
||||||
|
twig("Warning: Ignoring transaction at height ${utxo.height} @ index ${utxo.index} because it already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return the number of UTXOs that were downloaded
|
||||||
|
result.size - skipped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request all blocks in the given range and persist them locally for processing, later.
|
* Request all blocks in the given range and persist them locally for processing, later.
|
||||||
*
|
*
|
||||||
|
@ -546,8 +574,8 @@ class CompactBlockProcessor(
|
||||||
*
|
*
|
||||||
* @return the address of this wallet.
|
* @return the address of this wallet.
|
||||||
*/
|
*/
|
||||||
suspend fun getAddress(accountId: Int) = withContext(IO) {
|
suspend fun getShieldedAddress(accountId: Int) = withContext(IO) {
|
||||||
rustBackend.getAddress(accountId)
|
rustBackend.getShieldedAddress(accountId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -572,6 +600,11 @@ class CompactBlockProcessor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getUtxoCacheBalance(address: String): WalletBalance = withContext(IO) {
|
||||||
|
rustBackend.getDownloadedUtxoBalance(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transmits the given state for this processor.
|
* Transmits the given state for this processor.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package cash.z.ecc.android.sdk.db.entity
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "utxos")
|
||||||
|
data class UtxoEntity(
|
||||||
|
val address: String ="",
|
||||||
|
|
||||||
|
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||||
|
val txid: ByteArray? = byteArrayOf(),
|
||||||
|
|
||||||
|
@ColumnInfo(name = "tx_index")
|
||||||
|
val transactionIndex: Int? = -1,
|
||||||
|
|
||||||
|
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||||
|
val script: ByteArray? = byteArrayOf(),
|
||||||
|
|
||||||
|
val value: Long = 0L,
|
||||||
|
|
||||||
|
val height: Int? = -1,
|
||||||
|
) {
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
var id: Long = 0L
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is UtxoEntity) return false
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (address != other.address) return false
|
||||||
|
if (txid != null) {
|
||||||
|
if (other.txid == null) return false
|
||||||
|
if (!txid.contentEquals(other.txid)) return false
|
||||||
|
} else if (other.txid != null) return false
|
||||||
|
if (transactionIndex != other.transactionIndex) return false
|
||||||
|
if (script != null) {
|
||||||
|
if (other.script == null) return false
|
||||||
|
if (!script.contentEquals(other.script)) return false
|
||||||
|
} else if (other.script != null) return false
|
||||||
|
if (value != other.value) return false
|
||||||
|
if (height != other.height) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + address.hashCode()
|
||||||
|
result = 31 * result + (txid?.contentHashCode() ?: 0)
|
||||||
|
result = 31 * result + (transactionIndex ?: 0)
|
||||||
|
result = 31 * result + (script?.contentHashCode() ?: 0)
|
||||||
|
result = 31 * result + value.hashCode()
|
||||||
|
result = 31 * result + (height ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -110,4 +110,8 @@ open class ZcashSdkCommon {
|
||||||
* this will do for now, since we're using a cloudfront URL that already redirects.
|
* this will do for now, since we're using a cloudfront URL that already redirects.
|
||||||
*/
|
*/
|
||||||
val CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/"
|
val CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/"
|
||||||
|
/**
|
||||||
|
* The default memo to use when shielding transparent funds.
|
||||||
|
*/
|
||||||
|
open val DEFAULT_SHIELD_FUNDS_MEMO_PREFIX = "shielding:"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package cash.z.ecc.android.sdk.jni
|
package cash.z.ecc.android.sdk.jni
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||||
import cash.z.ecc.android.sdk.exception.BirthdayException
|
import cash.z.ecc.android.sdk.exception.BirthdayException
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME
|
import cash.z.ecc.android.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
|
import cash.z.ecc.android.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
|
||||||
import cash.z.ecc.android.sdk.ext.twig
|
import cash.z.ecc.android.sdk.ext.twig
|
||||||
import cash.z.ecc.android.sdk.rpc.LocalRpcTypes
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -64,7 +64,11 @@ class RustBackend private constructor() : RustBackendWelding {
|
||||||
return initBlocksTable(pathDataDb, height, hash, time, saplingTree)
|
return initBlocksTable(pathDataDb, height, hash, time, saplingTree)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAddress(account: Int) = getAddress(pathDataDb, account)
|
override fun getShieldedAddress(account: Int) = getShieldedAddress(pathDataDb, account)
|
||||||
|
|
||||||
|
override fun getTransparentAddress(account: Int, index: Int): String {
|
||||||
|
throw NotImplementedError("TODO: implement this at the zcash_client_sqlite level. But for now, use DerivationTool, instead to derive addresses from seeds")
|
||||||
|
}
|
||||||
|
|
||||||
override fun getBalance(account: Int) = getBalance(pathDataDb, account)
|
override fun getBalance(account: Int) = getBalance(pathDataDb, account)
|
||||||
|
|
||||||
|
@ -108,30 +112,62 @@ class RustBackend private constructor() : RustBackendWelding {
|
||||||
"$pathParamsDir/$OUTPUT_PARAM_FILE_NAME"
|
"$pathParamsDir/$OUTPUT_PARAM_FILE_NAME"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override fun shieldToAddress(
|
||||||
|
extsk: String,
|
||||||
|
tsk: String,
|
||||||
|
memo: ByteArray?
|
||||||
|
): Long {
|
||||||
|
twig("TMP: shieldToAddress with db path: $pathDataDb, ${memo?.size}")
|
||||||
|
return shieldToAddress(
|
||||||
|
pathDataDb,
|
||||||
|
0,
|
||||||
|
extsk,
|
||||||
|
tsk,
|
||||||
|
memo ?: ByteArray(0),
|
||||||
|
"${pathParamsDir}/$SPEND_PARAM_FILE_NAME",
|
||||||
|
"${pathParamsDir}/$OUTPUT_PARAM_FILE_NAME"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putUtxo(
|
||||||
|
tAddress: String,
|
||||||
|
txId: ByteArray,
|
||||||
|
index: Int,
|
||||||
|
script: ByteArray,
|
||||||
|
value: Long,
|
||||||
|
height: Int
|
||||||
|
): Boolean = putUtxo(pathDataDb, tAddress, txId, index, script, value, height)
|
||||||
|
|
||||||
|
override fun getDownloadedUtxoBalance(address: String): CompactBlockProcessor.WalletBalance {
|
||||||
|
val verified = getVerifiedTransparentBalance(pathDataDb, address)
|
||||||
|
val total = getTotalTransparentBalance(pathDataDb, address)
|
||||||
|
return CompactBlockProcessor.WalletBalance(total, verified)
|
||||||
|
}
|
||||||
|
|
||||||
override fun isValidShieldedAddr(addr: String) = isValidShieldedAddress(addr)
|
override fun isValidShieldedAddr(addr: String) = isValidShieldedAddress(addr)
|
||||||
|
|
||||||
override fun isValidTransparentAddr(addr: String) = isValidTransparentAddress(addr)
|
override fun isValidTransparentAddr(addr: String) = isValidTransparentAddress(addr)
|
||||||
|
|
||||||
override fun getBranchIdForHeight(height: Int): Long = branchIdForHeight(height)
|
override fun getBranchIdForHeight(height: Int): Long = branchIdForHeight(height)
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* This is a proof-of-concept for doing Local RPC, where we are effectively using the JNI
|
// * This is a proof-of-concept for doing Local RPC, where we are effectively using the JNI
|
||||||
* boundary as a grpc server. It is slightly inefficient in terms of both space and time but
|
// * boundary as a grpc server. It is slightly inefficient in terms of both space and time but
|
||||||
* given that it is all done locally, on the heap, it seems to be a worthwhile tradeoff because
|
// * given that it is all done locally, on the heap, it seems to be a worthwhile tradeoff because
|
||||||
* it reduces the complexity and expands the capacity for the two layers to communicate.
|
// * it reduces the complexity and expands the capacity for the two layers to communicate.
|
||||||
*
|
// *
|
||||||
* We're able to keep the "unsafe" byteArray functions private and wrap them in typeSafe
|
// * We're able to keep the "unsafe" byteArray functions private and wrap them in typeSafe
|
||||||
* equivalents and, eventually, surface any parse errors (for now, errors are only logged).
|
// * equivalents and, eventually, surface any parse errors (for now, errors are only logged).
|
||||||
*/
|
// */
|
||||||
override fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList {
|
// override fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList {
|
||||||
return try {
|
// return try {
|
||||||
// serialize the list, send it over to rust and get back a serialized set of results that we parse out and return
|
// // serialize the list, send it over to rust and get back a serialized set of results that we parse out and return
|
||||||
return LocalRpcTypes.TransparentTransactionList.parseFrom(parseTransactionDataList(tdl.toByteArray()))
|
// return LocalRpcTypes.TransparentTransactionList.parseFrom(parseTransactionDataList(tdl.toByteArray()))
|
||||||
} catch (t: Throwable) {
|
// } catch (t: Throwable) {
|
||||||
twig("ERROR: failed to parse transaction data list due to: $t caused by: ${t.cause}")
|
// twig("ERROR: failed to parse transaction data list due to: $t caused by: ${t.cause}")
|
||||||
LocalRpcTypes.TransparentTransactionList.newBuilder().build()
|
// LocalRpcTypes.TransparentTransactionList.newBuilder().build()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exposes all of the librustzcash functions along with helpers for loading the static library.
|
* Exposes all of the librustzcash functions along with helpers for loading the static library.
|
||||||
|
@ -208,7 +244,9 @@ class RustBackend private constructor() : RustBackendWelding {
|
||||||
saplingTree: String
|
saplingTree: String
|
||||||
): Boolean
|
): Boolean
|
||||||
|
|
||||||
@JvmStatic private external fun getAddress(dbDataPath: String, account: Int): String
|
@JvmStatic private external fun getShieldedAddress(dbDataPath: String, account: Int): String
|
||||||
|
// TODO: implement this in the zcash_client_sqlite layer. For now, use DerivationTool, instead.
|
||||||
|
// @JvmStatic private external fun getTransparentAddress(dbDataPath: String, account: Int): String
|
||||||
|
|
||||||
@JvmStatic private external fun isValidShieldedAddress(addr: String): Boolean
|
@JvmStatic private external fun isValidShieldedAddress(addr: String): Boolean
|
||||||
|
|
||||||
|
@ -244,10 +282,38 @@ class RustBackend private constructor() : RustBackendWelding {
|
||||||
outputParamsPath: String
|
outputParamsPath: String
|
||||||
): Long
|
): Long
|
||||||
|
|
||||||
|
@JvmStatic private external fun shieldToAddress(
|
||||||
|
dbDataPath: String,
|
||||||
|
account: Int,
|
||||||
|
extsk: String,
|
||||||
|
tsk: String,
|
||||||
|
memo: ByteArray,
|
||||||
|
spendParamsPath: String,
|
||||||
|
outputParamsPath: String
|
||||||
|
): Long
|
||||||
|
|
||||||
@JvmStatic private external fun initLogs()
|
@JvmStatic private external fun initLogs()
|
||||||
|
|
||||||
@JvmStatic private external fun branchIdForHeight(height: Int): Long
|
@JvmStatic private external fun branchIdForHeight(height: Int): Long
|
||||||
|
|
||||||
@JvmStatic private external fun parseTransactionDataList(serializedList: ByteArray): ByteArray
|
@JvmStatic private external fun putUtxo(
|
||||||
|
dbDataPath: String,
|
||||||
|
tAddress: String,
|
||||||
|
txId: ByteArray,
|
||||||
|
index: Int,
|
||||||
|
script: ByteArray,
|
||||||
|
value: Long,
|
||||||
|
height: Int
|
||||||
|
): Boolean
|
||||||
|
|
||||||
|
@JvmStatic private external fun getVerifiedTransparentBalance(
|
||||||
|
pathDataDb: String,
|
||||||
|
taddr: String
|
||||||
|
): Long
|
||||||
|
|
||||||
|
@JvmStatic private external fun getTotalTransparentBalance(
|
||||||
|
pathDataDb: String,
|
||||||
|
taddr: String
|
||||||
|
): Long
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package cash.z.ecc.android.sdk.jni
|
package cash.z.ecc.android.sdk.jni
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.rpc.LocalRpcTypes
|
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contract defining the exposed capabilities of the Rust backend.
|
* Contract defining the exposed capabilities of the Rust backend.
|
||||||
|
@ -19,6 +19,12 @@ interface RustBackendWelding {
|
||||||
memo: ByteArray? = byteArrayOf()
|
memo: ByteArray? = byteArrayOf()
|
||||||
): Long
|
): Long
|
||||||
|
|
||||||
|
fun shieldToAddress(
|
||||||
|
extsk: String,
|
||||||
|
tsk: String,
|
||||||
|
memo: ByteArray? = byteArrayOf()
|
||||||
|
): Long
|
||||||
|
|
||||||
fun decryptAndStoreTransaction(tx: ByteArray)
|
fun decryptAndStoreTransaction(tx: ByteArray)
|
||||||
|
|
||||||
fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array<String>
|
fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array<String>
|
||||||
|
@ -33,7 +39,9 @@ interface RustBackendWelding {
|
||||||
|
|
||||||
fun isValidTransparentAddr(addr: String): Boolean
|
fun isValidTransparentAddr(addr: String): Boolean
|
||||||
|
|
||||||
fun getAddress(account: Int = 0): String
|
fun getShieldedAddress(account: Int = 0): String
|
||||||
|
|
||||||
|
fun getTransparentAddress(account: Int = 0, index: Int = 0): String
|
||||||
|
|
||||||
fun getBalance(account: Int = 0): Long
|
fun getBalance(account: Int = 0): Long
|
||||||
|
|
||||||
|
@ -45,7 +53,7 @@ interface RustBackendWelding {
|
||||||
|
|
||||||
fun getVerifiedBalance(account: Int = 0): Long
|
fun getVerifiedBalance(account: Int = 0): Long
|
||||||
|
|
||||||
fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList
|
// fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList
|
||||||
|
|
||||||
fun rewindToHeight(height: Int): Boolean
|
fun rewindToHeight(height: Int): Boolean
|
||||||
|
|
||||||
|
@ -53,6 +61,17 @@ interface RustBackendWelding {
|
||||||
|
|
||||||
fun validateCombinedChain(): Int
|
fun validateCombinedChain(): Int
|
||||||
|
|
||||||
|
fun putUtxo(
|
||||||
|
tAddress: String,
|
||||||
|
txId: ByteArray,
|
||||||
|
index: Int,
|
||||||
|
script: ByteArray,
|
||||||
|
value: Long,
|
||||||
|
height: Int
|
||||||
|
): Boolean
|
||||||
|
|
||||||
|
fun getDownloadedUtxoBalance(address: String): CompactBlockProcessor.WalletBalance
|
||||||
|
|
||||||
// Implemented by `DerivationTool`
|
// Implemented by `DerivationTool`
|
||||||
interface Derivation {
|
interface Derivation {
|
||||||
fun deriveShieldedAddress(viewingKey: String): String
|
fun deriveShieldedAddress(viewingKey: String): String
|
||||||
|
@ -61,7 +80,11 @@ interface RustBackendWelding {
|
||||||
|
|
||||||
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
|
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
|
||||||
|
|
||||||
fun deriveTransparentAddress(seed: ByteArray): String
|
fun deriveTransparentAddress(seed: ByteArray, account: Int = 0, index: Int = 0): String
|
||||||
|
|
||||||
|
fun deriveTransparentAddress(transparentSecretKey: String): String
|
||||||
|
|
||||||
|
fun deriveTransparentSecretKey(seed: ByteArray, account: Int = 0, index: Int = 0): String
|
||||||
|
|
||||||
fun deriveViewingKey(spendingKey: String): String
|
fun deriveViewingKey(spendingKey: String): String
|
||||||
|
|
||||||
|
|
|
@ -106,6 +106,18 @@ class LightWalletGrpcService private constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun fetchUtxos(
|
||||||
|
tAddress: String,
|
||||||
|
startHeight: Int
|
||||||
|
): List<Service.GetAddressUtxosReply> {
|
||||||
|
channel.resetConnectBackoff()
|
||||||
|
val result = channel.createStub().getAddressUtxos(
|
||||||
|
Service.GetAddressUtxosArg.newBuilder().setAddress(tAddress)
|
||||||
|
.setStartHeight(startHeight.toLong()).build()
|
||||||
|
)
|
||||||
|
return result.addressUtxosList
|
||||||
|
}
|
||||||
|
|
||||||
override fun getTAddressTransactions(
|
override fun getTAddressTransactions(
|
||||||
tAddress: String,
|
tAddress: String,
|
||||||
blockHeightRange: IntRange
|
blockHeightRange: IntRange
|
||||||
|
|
|
@ -16,6 +16,16 @@ interface LightWalletService {
|
||||||
*/
|
*/
|
||||||
fun fetchTransaction(txId: ByteArray): Service.RawTransaction?
|
fun fetchTransaction(txId: ByteArray): Service.RawTransaction?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all UTXOs for the given address, going back to the start height.
|
||||||
|
*
|
||||||
|
* @param tAddress the transparent address to use.
|
||||||
|
* @param startHeight the starting height to use.
|
||||||
|
*
|
||||||
|
* @return the UTXOs for the given address from the startHeight.
|
||||||
|
*/
|
||||||
|
fun fetchUtxos(tAddress: String, startHeight: Int): List<Service.GetAddressUtxosReply>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the given range of blocks.
|
* Return the given range of blocks.
|
||||||
*
|
*
|
||||||
|
|
|
@ -75,8 +75,16 @@ class DerivationTool {
|
||||||
// WIP probably shouldn't be used just yet. Why?
|
// WIP probably shouldn't be used just yet. Why?
|
||||||
// - because we need the private key associated with this seed and this function doesn't return it.
|
// - because we need the private key associated with this seed and this function doesn't return it.
|
||||||
// - the underlying implementation needs to be split out into a few lower-level calls
|
// - the underlying implementation needs to be split out into a few lower-level calls
|
||||||
override fun deriveTransparentAddress(seed: ByteArray): String = withRustBackendLoaded {
|
override fun deriveTransparentAddress(seed: ByteArray, account: Int, index: Int): String = withRustBackendLoaded {
|
||||||
deriveTransparentAddressFromSeed(seed)
|
deriveTransparentAddressFromSeed(seed, account, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deriveTransparentAddress(transparentSecretKey: String): String = withRustBackendLoaded {
|
||||||
|
deriveTransparentAddressFromSecretKey(transparentSecretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deriveTransparentSecretKey(seed: ByteArray, account: Int, index: Int): String = withRustBackendLoaded {
|
||||||
|
deriveTransparentSecretKeyFromSeed(seed, account, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateViewingKey(viewingKey: String) {
|
fun validateViewingKey(viewingKey: String) {
|
||||||
|
@ -122,6 +130,13 @@ class DerivationTool {
|
||||||
private external fun deriveShieldedAddressFromViewingKey(key: String): String
|
private external fun deriveShieldedAddressFromViewingKey(key: String): String
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun deriveTransparentAddressFromSeed(seed: ByteArray): String
|
private external fun deriveTransparentAddressFromSeed(seed: ByteArray, account: Int, index: Int): String
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private external fun deriveTransparentAddressFromSecretKey(tsk: String): String
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private external fun deriveTransparentSecretKeyFromSeed(seed: ByteArray, account: Int, index: Int): String
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,10 +70,16 @@ class SaplingParamTool {
|
||||||
twig("directory did not exist attempting to make it")
|
twig("directory did not exist attempting to make it")
|
||||||
file.parentFile.mkdirs()
|
file.parentFile.mkdirs()
|
||||||
}
|
}
|
||||||
|
Okio.buffer(Okio.sink(file)).use {
|
||||||
|
twig("writing to $file")
|
||||||
|
it.writeAll(response.body().source())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
failureMessage += "Error while fetching $paramFileName : $response\n"
|
failureMessage += "Error while fetching $paramFileName : $response\n"
|
||||||
twig(failureMessage)
|
twig(failureMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
twig("fetch succeeded, done writing $paramFileName")
|
||||||
}
|
}
|
||||||
if (failureMessage.isNotEmpty()) throw TransactionEncoderException.FetchParamsException(
|
if (failureMessage.isNotEmpty()) throw TransactionEncoderException.FetchParamsException(
|
||||||
failureMessage
|
failureMessage
|
||||||
|
|
|
@ -139,6 +139,40 @@ class PersistentTransactionManager(
|
||||||
tx
|
tx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun encode(
|
||||||
|
spendingKey: String,
|
||||||
|
transparentSecretKey: String,
|
||||||
|
pendingTx: PendingTransaction
|
||||||
|
): PendingTransaction {
|
||||||
|
twig("managing the creation of a shielding transaction")
|
||||||
|
var tx = pendingTx as PendingTransactionEntity
|
||||||
|
try {
|
||||||
|
twig("beginning to encode shielding transaction with : $encoder")
|
||||||
|
val encodedTx = encoder.createShieldingTransaction(
|
||||||
|
spendingKey,
|
||||||
|
transparentSecretKey,
|
||||||
|
tx.memo
|
||||||
|
)
|
||||||
|
twig("successfully encoded shielding transaction!")
|
||||||
|
safeUpdate("updating shielding transaction encoding") {
|
||||||
|
updateEncoding(tx.id, encodedTx.raw, encodedTx.txId, encodedTx.expiryHeight)
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
val message = "failed to encode shielding transaction due to : ${t.message} caused by: ${t.cause}"
|
||||||
|
twig(message)
|
||||||
|
safeUpdate("updating shielding transaction error info") {
|
||||||
|
updateError(tx.id, message, ERROR_ENCODING)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
safeUpdate("incrementing shielding transaction encodeAttempts (from: ${tx.encodeAttempts})") {
|
||||||
|
updateEncodeAttempts(tx.id, max(1, tx.encodeAttempts + 1))
|
||||||
|
tx = findById(tx.id)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun submit(pendingTx: PendingTransaction): PendingTransaction = withContext(Dispatchers.IO) {
|
override suspend fun submit(pendingTx: PendingTransaction): PendingTransaction = withContext(Dispatchers.IO) {
|
||||||
// reload the tx to check for cancellation
|
// reload the tx to check for cancellation
|
||||||
var tx = pendingTransactionDao { findById(pendingTx.id) }
|
var tx = pendingTransactionDao { findById(pendingTx.id) }
|
||||||
|
|
|
@ -24,6 +24,12 @@ interface TransactionEncoder {
|
||||||
fromAccountIndex: Int = 0
|
fromAccountIndex: Int = 0
|
||||||
): EncodedTransaction
|
): EncodedTransaction
|
||||||
|
|
||||||
|
suspend fun createShieldingTransaction(
|
||||||
|
spendingKey: String,
|
||||||
|
transparentSecretKey: String,
|
||||||
|
memo: ByteArray? = byteArrayOf()
|
||||||
|
): EncodedTransaction
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility function to help with validation. This is not called during [createTransaction]
|
* 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.
|
* because this class asserts that all validation is done externally by the UI, for now.
|
||||||
|
|
|
@ -39,6 +39,12 @@ interface OutboundTransactionManager {
|
||||||
*/
|
*/
|
||||||
suspend fun encode(spendingKey: String, pendingTx: PendingTransaction): PendingTransaction
|
suspend fun encode(spendingKey: String, pendingTx: PendingTransaction): PendingTransaction
|
||||||
|
|
||||||
|
suspend fun encode(
|
||||||
|
spendingKey: String,
|
||||||
|
transparentSecretKey: String,
|
||||||
|
pendingTx: PendingTransaction
|
||||||
|
): PendingTransaction
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submits the transaction represented by [pendingTx] to lightwalletd to broadcast to the
|
* Submits the transaction represented by [pendingTx] to lightwalletd to broadcast to the
|
||||||
* network and, hopefully, include in the next block.
|
* network and, hopefully, include in the next block.
|
||||||
|
|
|
@ -51,6 +51,17 @@ class WalletTransactionEncoder(
|
||||||
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
|
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun createShieldingTransaction(
|
||||||
|
spendingKey: String,
|
||||||
|
transparentSecretKey: String,
|
||||||
|
memo: ByteArray?
|
||||||
|
): EncodedTransaction = withContext(IO) {
|
||||||
|
twig("TMP: createShieldingTransaction with $spendingKey and $transparentSecretKey and ${memo?.size}")
|
||||||
|
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]
|
* 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.
|
* because this class asserts that all validation is done externally by the UI, for now.
|
||||||
|
@ -126,4 +137,29 @@ class WalletTransactionEncoder(
|
||||||
twig("result of sendToAddress: $result")
|
twig("result of sendToAddress: $result")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun createShieldingSpend(
|
||||||
|
spendingKey: String,
|
||||||
|
transparentSecretKey: String,
|
||||||
|
memo: ByteArray? = byteArrayOf()
|
||||||
|
): Long = withContext(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)
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ message TxFilter {
|
||||||
// RawTransaction contains the complete transaction data. It also optionally includes
|
// RawTransaction contains the complete transaction data. It also optionally includes
|
||||||
// the block height in which the transaction was included.
|
// the block height in which the transaction was included.
|
||||||
message RawTransaction {
|
message RawTransaction {
|
||||||
bytes data = 1; // exact data returned by zcash 'getrawtransaction'
|
bytes data = 1; // exact data returned by Zcash 'getrawtransaction'
|
||||||
uint64 height = 2; // height that the transaction was mined (or -1)
|
uint64 height = 2; // height that the transaction was mined (or -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +66,9 @@ message LightdInfo {
|
||||||
string branch = 9;
|
string branch = 9;
|
||||||
string buildDate = 10;
|
string buildDate = 10;
|
||||||
string buildUser = 11;
|
string buildUser = 11;
|
||||||
|
uint64 estimatedHeight = 12; // less than tip height if zcashd is syncing
|
||||||
|
string zcashdBuild = 13; // example: "v4.1.1-877212414"
|
||||||
|
string zcashdSubversion = 14; // example: "/MagicBean:4.1.1/"
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransparentAddressBlockFilter restricts the results to the given address
|
// TransparentAddressBlockFilter restricts the results to the given address
|
||||||
|
@ -104,7 +107,7 @@ message Exclude {
|
||||||
repeated bytes txid = 1;
|
repeated bytes txid = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The TreeState is derived from the zcash z_gettreestate rpc.
|
// The TreeState is derived from the Zcash z_gettreestate rpc.
|
||||||
message TreeState {
|
message TreeState {
|
||||||
string network = 1; // "main" or "test"
|
string network = 1; // "main" or "test"
|
||||||
uint64 height = 2;
|
uint64 height = 2;
|
||||||
|
@ -139,7 +142,7 @@ service CompactTxStreamer {
|
||||||
|
|
||||||
// Return the requested full (not compact) transaction (as from zcashd)
|
// Return the requested full (not compact) transaction (as from zcashd)
|
||||||
rpc GetTransaction(TxFilter) returns (RawTransaction) {}
|
rpc GetTransaction(TxFilter) returns (RawTransaction) {}
|
||||||
// Submit the given transaction to the zcash network
|
// Submit the given transaction to the Zcash network
|
||||||
rpc SendTransaction(RawTransaction) returns (SendResponse) {}
|
rpc SendTransaction(RawTransaction) returns (SendResponse) {}
|
||||||
|
|
||||||
// Return the txids corresponding to the given t-address within the given block range
|
// Return the txids corresponding to the given t-address within the given block range
|
||||||
|
@ -159,7 +162,7 @@ service CompactTxStreamer {
|
||||||
rpc GetMempoolTx(Exclude) returns (stream CompactTx) {}
|
rpc GetMempoolTx(Exclude) returns (stream CompactTx) {}
|
||||||
|
|
||||||
// GetTreeState returns the note commitment tree state corresponding to the given block.
|
// GetTreeState returns the note commitment tree state corresponding to the given block.
|
||||||
// See section 3.7 of the zcash protocol specification. It returns several other useful
|
// See section 3.7 of the Zcash protocol specification. It returns several other useful
|
||||||
// values also (even though they can be obtained using GetBlock).
|
// values also (even though they can be obtained using GetBlock).
|
||||||
// The block can be specified by either height or hash.
|
// The block can be specified by either height or hash.
|
||||||
rpc GetTreeState(BlockID) returns (TreeState) {}
|
rpc GetTreeState(BlockID) returns (TreeState) {}
|
||||||
|
|
Loading…
Reference in New Issue