Replace Sapling balance and scan progress FFIs with wallet summary FFI

* Replace Sapling balance and scan progress FFIs with wallet summary FFI

Closes zcash/zcash-android-wallet-sdk#1282.

* Using test fixture for JniAccountBalance

* Minor documentation changes

* Fix typo

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
str4d 2023-11-08 09:57:23 +00:00 committed by GitHub
parent b82278e747
commit ae2d7152aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 311 additions and 284 deletions

View File

@ -1,10 +1,10 @@
package cash.z.ecc.android.sdk.internal
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.internal.model.JniScanProgress
import cash.z.ecc.android.sdk.internal.model.JniScanRange
import cash.z.ecc.android.sdk.internal.model.JniSubtreeRoot
import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey
import cash.z.ecc.android.sdk.internal.model.JniWalletSummary
/**
* Contract defining the exposed capabilities of the Rust backend.
@ -69,12 +69,6 @@ interface Backend {
suspend fun listTransparentReceivers(account: Int): List<String>
/**
* @throws RuntimeException as a common indicator of the operation failure
*/
@Throws(RuntimeException::class)
suspend fun getBalance(account: Int): Long
fun getBranchIdForHeight(height: Long): Long
/**
@ -83,12 +77,6 @@ interface Backend {
@Throws(RuntimeException::class)
suspend fun getMemoAsUtf8(txId: ByteArray, outputIndex: Int): String?
/**
* @throws RuntimeException as a common indicator of the operation failure
*/
@Throws(RuntimeException::class)
suspend fun getVerifiedBalance(account: Int): Long
suspend fun getNearestRewindHeight(height: Long): Long
/**
@ -139,7 +127,7 @@ interface Backend {
* @throws RuntimeException as a common indicator of the operation failure
*/
@Throws(RuntimeException::class)
suspend fun getScanProgress(): JniScanProgress?
suspend fun getWalletSummary(): JniWalletSummary?
/**
* @throws RuntimeException as a common indicator of the operation failure

View File

@ -5,10 +5,10 @@ import cash.z.ecc.android.sdk.internal.SdkDispatchers
import cash.z.ecc.android.sdk.internal.ext.deleteRecursivelySuspend
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.internal.model.JniScanProgress
import cash.z.ecc.android.sdk.internal.model.JniScanRange
import cash.z.ecc.android.sdk.internal.model.JniSubtreeRoot
import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey
import cash.z.ecc.android.sdk.internal.model.JniWalletSummary
import kotlinx.coroutines.withContext
import java.io.File
@ -108,30 +108,6 @@ class RustBackend private constructor(
}
}
override suspend fun getBalance(account: Int): Long {
val longValue = withContext(SdkDispatchers.DATABASE_IO) {
getBalance(
dataDbFile.absolutePath,
account,
networkId = networkId
)
}
return longValue
}
override suspend fun getVerifiedBalance(account: Int): Long {
val longValue = withContext(SdkDispatchers.DATABASE_IO) {
getVerifiedBalance(
dbDataPath = dataDbFile.absolutePath,
account = account,
networkId = networkId
)
}
return longValue
}
override suspend fun getMemoAsUtf8(txId: ByteArray, outputIndex: Int) =
withContext(SdkDispatchers.DATABASE_IO) {
getMemoAsUtf8(
@ -267,9 +243,9 @@ class RustBackend private constructor(
}
}
override suspend fun getScanProgress(): JniScanProgress? =
override suspend fun getWalletSummary(): JniWalletSummary? =
withContext(SdkDispatchers.DATABASE_IO) {
getScanProgress(
getWalletSummary(
dataDbFile.absolutePath,
networkId = networkId
)
@ -462,16 +438,6 @@ class RustBackend private constructor(
@JvmStatic
private external fun isValidUnifiedAddress(addr: String, networkId: Int): Boolean
@JvmStatic
private external fun getBalance(dbDataPath: String, account: Int, networkId: Int): Long
@JvmStatic
private external fun getVerifiedBalance(
dbDataPath: String,
account: Int,
networkId: Int
): Long
@JvmStatic
private external fun getMemoAsUtf8(
dbDataPath: String,
@ -543,10 +509,10 @@ class RustBackend private constructor(
): Long
@JvmStatic
private external fun getScanProgress(
private external fun getWalletSummary(
dbDataPath: String,
networkId: Int
): JniScanProgress?
): JniWalletSummary?
@JvmStatic
private external fun suggestScanRanges(

View File

@ -0,0 +1,25 @@
package cash.z.ecc.android.sdk.internal.model
import androidx.annotation.Keep
/**
* Serves as cross layer (Kotlin, Rust) communication class.
*
* @param account the account ID
* @param saplingTotalBalance The total account balance in the Sapling pool, including unconfirmed funds.
* @param saplingVerifiedBalance The verified account balance in the Sapling pool.
* @throws IllegalArgumentException if the values are inconsistent.
*/
@Keep
class JniAccountBalance(
val account: Int,
val saplingTotalBalance: Long,
val saplingVerifiedBalance: Long,
) {
init {
require(saplingTotalBalance >= saplingVerifiedBalance) {
"Total Sapling balance $saplingTotalBalance must not be " +
"less than verified Sapling balance $saplingVerifiedBalance."
}
}
}

View File

@ -1,32 +0,0 @@
package cash.z.ecc.android.sdk.internal.model
import androidx.annotation.Keep
/**
* Serves as cross layer (Kotlin, Rust) communication class.
*
* @throws IllegalArgumentException unless (numerator is nonnegative, denominator is
* positive, and the represented ratio is in the range 0.0 to 1.0 inclusive).
* @param numerator the numerator of the progress ratio
* @param denominator the denominator of the progress ratio
*/
@Keep
class JniScanProgress(
val numerator: Long,
val denominator: Long
) {
init {
require(numerator >= 0L) {
"Numerator $numerator is outside of allowed range [0, Long.MAX_VALUE]"
}
require(denominator >= 1L) {
"Denominator $denominator is outside of allowed range [1, Long.MAX_VALUE]"
}
require(numerator.toFloat().div(denominator) >= 0f) {
"Result of ${numerator.toFloat()}/$denominator is outside of allowed range"
}
require(numerator.toFloat().div(denominator) <= 1f) {
"Result of ${numerator.toFloat()}/$denominator is outside of allowed range"
}
}
}

View File

@ -0,0 +1,35 @@
package cash.z.ecc.android.sdk.internal.model
import androidx.annotation.Keep
/**
* Serves as cross layer (Kotlin, Rust) communication class.
*
* @param accountBalances the balances of the wallet accounts
* @param progressNumerator the numerator of the progress ratio
* @param progressDenominator the denominator of the progress ratio
* @throws IllegalArgumentException unless (progressNumerator is nonnegative,
* progressDenominator is positive, and the represented ratio is in the
* range 0.0 to 1.0 inclusive).
*/
@Keep
class JniWalletSummary(
val accountBalances: Array<JniAccountBalance>,
val progressNumerator: Long,
val progressDenominator: Long
) {
init {
require(progressNumerator >= 0L) {
"Numerator $progressNumerator is outside of allowed range [0, Long.MAX_VALUE]"
}
require(progressDenominator >= 1L) {
"Denominator $progressDenominator is outside of allowed range [1, Long.MAX_VALUE]"
}
require(progressNumerator.toFloat().div(progressDenominator) >= 0f) {
"Result of ${progressNumerator.toFloat()}/$progressDenominator is outside of allowed range"
}
require(progressNumerator.toFloat().div(progressDenominator) <= 1f) {
"Result of ${progressNumerator.toFloat()}/$progressDenominator is outside of allowed range"
}
}
}

View File

@ -20,7 +20,7 @@ use tracing_subscriber::reload;
use zcash_address::{ToAddress, ZcashAddress};
use zcash_client_backend::data_api::{
scanning::{ScanPriority, ScanRange},
AccountBirthday, NoteId, Ratio, ShieldedProtocol,
AccountBalance, AccountBirthday, NoteId, ShieldedProtocol, WalletSummary,
};
use zcash_client_backend::keys::{DecodingError, UnifiedSpendingKey};
use zcash_client_backend::{
@ -644,37 +644,6 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_is
unwrap_exc_or(&env, res, JNI_FALSE)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getBalance(
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
accountj: jint,
network_id: jint,
) -> jlong {
let res = panic::catch_unwind(|| {
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(&env, network, db_data)?;
let account = AccountId::from(u32::try_from(accountj)?);
if let Some(wallet_summary) = db_data
.get_wallet_summary(0)
.map_err(|e| format_err!("Error while fetching balance: {}", e))?
{
wallet_summary
.account_balances()
.get(&account)
.ok_or_else(|| format_err!("Unknown account"))
.map(|balances| Amount::from(balances.sapling_balance.total()).into())
} else {
// `None` means that the caller has not yet called `updateChainTip` on a
// brand-new wallet, so we can assume the balance is zero.
Ok(0)
}
});
unwrap_exc_or(&env, res, -1)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getVerifiedTransparentBalance(
env: JNIEnv<'_>,
@ -753,38 +722,6 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_ge
unwrap_exc_or(&env, res, -1)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getVerifiedBalance(
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
account: jint,
network_id: jint,
) -> jlong {
let res = panic::catch_unwind(|| {
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(&env, network, db_data)?;
let account = AccountId::from(u32::try_from(account)?);
if let Some(wallet_summary) = db_data
.get_wallet_summary(ANCHOR_OFFSET_U32)
.map_err(|e| format_err!("Error while fetching verified balance: {}", e))?
{
wallet_summary
.account_balances()
.get(&account)
.ok_or_else(|| format_err!("Unknown account"))
.map(|balances| Amount::from(balances.sapling_balance.spendable_value).into())
} else {
// `None` means that the caller has not yet called `updateChainTip` on a
// brand-new wallet, so we can assume the balance is zero.
Ok(0)
}
});
unwrap_exc_or(&env, res, -1)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getMemoAsUtf8(
env: JNIEnv<'_>,
@ -1147,25 +1084,64 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_ge
unwrap_exc_or(&env, res, -1)
}
/// Returns a `JniScanProgress` object, provided that numerator is nonnegative, denominator
/// is positive, and the represented ratio is in the range 0.0 to 1.0 inclusive.
const JNI_ACCOUNT_BALANCE: &str = "cash/z/ecc/android/sdk/internal/model/JniAccountBalance";
fn encode_account_balance<'a>(
env: &JNIEnv<'a>,
account: &AccountId,
balance: &AccountBalance,
) -> jni::errors::Result<JObject<'a>> {
let sapling_total_balance = Amount::from(balance.sapling_balance.total());
let sapling_verified_balance = Amount::from(balance.sapling_balance.spendable_value);
env.new_object(
JNI_ACCOUNT_BALANCE,
"(IJJ)V",
&[
JValue::Int(u32::from(*account) as i32),
JValue::Long(sapling_total_balance.into()),
JValue::Long(sapling_verified_balance.into()),
],
)
}
/// Returns a `JniWalletSummary` object, provided that `progress_numerator` is
/// nonnegative, `progress_denominator` is positive, and the represented ratio is in the
/// range 0.0 to 1.0 inclusive.
///
/// If these conditions are not met, this fails and leaves an `IllegalArgumentException`
/// pending.
fn encode_scan_progress(env: &JNIEnv<'_>, progress: Ratio<u64>) -> Result<jobject, failure::Error> {
fn encode_wallet_summary(
env: &JNIEnv<'_>,
summary: WalletSummary,
) -> Result<jobject, failure::Error> {
let account_balances = utils::rust_vec_to_java(
&env,
summary.account_balances().into_iter().collect(),
JNI_ACCOUNT_BALANCE,
|env, (account, balance)| encode_account_balance(env, account, balance),
|env| encode_account_balance(env, &AccountId::from(0), &AccountBalance::ZERO),
);
let (progress_numerator, progress_denominator) = summary
.scan_progress()
.map(|progress| (*progress.numerator(), *progress.denominator()))
.unwrap_or((0, 1));
let output = env.new_object(
"cash/z/ecc/android/sdk/internal/model/JniScanProgress",
"(JJ)V",
"cash/z/ecc/android/sdk/internal/model/JniWalletSummary",
&format!("([L{};JJ)V", JNI_ACCOUNT_BALANCE),
&[
JValue::Long(*progress.numerator() as i64),
JValue::Long(*progress.denominator() as i64),
JValue::Object(unsafe { JObject::from_raw(account_balances) }),
JValue::Long(progress_numerator as i64),
JValue::Long(progress_denominator as i64),
],
)?;
Ok(output.into_raw())
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getScanProgress(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getWalletSummary(
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
@ -1176,11 +1152,15 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_ge
let db_data = wallet_db(&env, network, db_data)?;
match db_data
.get_wallet_summary(0)
.get_wallet_summary(ANCHOR_OFFSET_U32)
.map_err(|e| format_err!("Error while fetching scan progress: {}", e))?
.and_then(|summary| summary.scan_progress().filter(|r| r.denominator() > &0))
{
Some(progress) => encode_scan_progress(&env, progress),
.filter(|summary| {
summary
.scan_progress()
.map(|r| r.denominator() > &0)
.unwrap_or(false)
}) {
Some(summary) => encode_wallet_summary(&env, summary),
None => Ok(ptr::null_mut()),
}
});

View File

@ -0,0 +1,21 @@
package cash.z.ecc.android.sdk.internal.fixture
import cash.z.ecc.android.sdk.internal.model.JniAccountBalance
object JniAccountBalanceFixture {
const val ACCOUNT_ID: Int = 0
const val SAPLING_TOTAL_BALANCE: Long = 0L
const val SAPLING_VERIFIED_BALANCE: Long = 0L
fun new(
account: Int = ACCOUNT_ID,
saplingTotalBalance: Long = SAPLING_TOTAL_BALANCE,
saplingVerifiedBalance: Long = SAPLING_VERIFIED_BALANCE,
) = JniAccountBalance(
account = account,
saplingTotalBalance = saplingTotalBalance,
saplingVerifiedBalance = saplingVerifiedBalance
)
}

View File

@ -1,46 +0,0 @@
package cash.z.ecc.android.sdk.internal.model
import kotlin.test.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertIs
class JniScanProgressTest {
@Test
fun both_attribute_within_constraints() {
val instance = JniScanProgress(
numerator = 1L,
denominator = 100L
)
assertIs<JniScanProgress>(instance)
}
@Test
fun numerator_attribute_not_in_constraints() {
assertFailsWith(IllegalArgumentException::class) {
JniScanProgress(
numerator = -1L,
denominator = 100L
)
}
}
@Test
fun denominator_attribute_not_in_constraints() {
assertFailsWith(IllegalArgumentException::class) {
JniScanProgress(
numerator = 1L,
denominator = 0L
)
}
}
@Test
fun ratio_not_in_constraints() {
assertFailsWith(IllegalArgumentException::class) {
JniScanProgress(
numerator = 100L,
denominator = 1L
)
}
}
}

View File

@ -0,0 +1,51 @@
package cash.z.ecc.android.sdk.internal.model
import cash.z.ecc.android.sdk.internal.fixture.JniAccountBalanceFixture
import kotlin.test.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertIs
class JniWalletSummaryTest {
@Test
fun both_attribute_within_constraints() {
val instance = JniWalletSummary(
accountBalances = arrayOf(JniAccountBalanceFixture.new()),
progressNumerator = 1L,
progressDenominator = 100L
)
assertIs<JniWalletSummary>(instance)
}
@Test
fun numerator_attribute_not_in_constraints() {
assertFailsWith(IllegalArgumentException::class) {
JniWalletSummary(
accountBalances = arrayOf(JniAccountBalanceFixture.new()),
progressNumerator = -1L,
progressDenominator = 100L
)
}
}
@Test
fun denominator_attribute_not_in_constraints() {
assertFailsWith(IllegalArgumentException::class) {
JniWalletSummary(
accountBalances = arrayOf(JniAccountBalanceFixture.new()),
progressNumerator = 1L,
progressDenominator = 0L
)
}
}
@Test
fun ratio_not_in_constraints() {
assertFailsWith(IllegalArgumentException::class) {
JniWalletSummary(
accountBalances = arrayOf(JniAccountBalanceFixture.new()),
progressNumerator = 100L,
progressDenominator = 1L
)
}
}
}

View File

@ -2,10 +2,10 @@ package cash.z.ecc.fixture
import cash.z.ecc.android.sdk.internal.Backend
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.internal.model.JniScanProgress
import cash.z.ecc.android.sdk.internal.model.JniScanRange
import cash.z.ecc.android.sdk.internal.model.JniSubtreeRoot
import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey
import cash.z.ecc.android.sdk.internal.model.JniWalletSummary
internal class FakeRustBackend(
override val networkId: Int,
@ -39,7 +39,7 @@ internal class FakeRustBackend(
TODO("Not yet implemented")
}
override suspend fun getScanProgress(): JniScanProgress {
override suspend fun getWalletSummary(): JniWalletSummary {
TODO("Not yet implemented")
}
@ -129,10 +129,6 @@ internal class FakeRustBackend(
TODO("Not yet implemented")
}
override suspend fun getBalance(account: Int): Long {
TODO("Not yet implemented")
}
override fun getBranchIdForHeight(height: Long): Long {
TODO("Not yet implemented")
}
@ -140,10 +136,6 @@ internal class FakeRustBackend(
override suspend fun getMemoAsUtf8(txId: ByteArray, outputIndex: Int): String? =
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
override suspend fun getVerifiedBalance(account: Int): Long {
TODO("Not yet implemented")
}
override suspend fun getNearestRewindHeight(height: Long): Long {
TODO("Not yet implemented")
}

View File

@ -5,8 +5,8 @@ import cash.z.ecc.android.sdk.BuildConfig
import cash.z.ecc.android.sdk.annotation.OpenForTesting
import cash.z.ecc.android.sdk.block.processor.model.BatchSyncProgress
import cash.z.ecc.android.sdk.block.processor.model.GetMaxScannedHeightResult
import cash.z.ecc.android.sdk.block.processor.model.GetScanProgressResult
import cash.z.ecc.android.sdk.block.processor.model.GetSubtreeRootsResult
import cash.z.ecc.android.sdk.block.processor.model.GetWalletSummaryResult
import cash.z.ecc.android.sdk.block.processor.model.PutSaplingSubtreeRootsResult
import cash.z.ecc.android.sdk.block.processor.model.SbSPreparationResult
import cash.z.ecc.android.sdk.block.processor.model.SuggestScanRangesResult
@ -41,6 +41,7 @@ import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.internal.model.ScanRange
import cash.z.ecc.android.sdk.internal.model.SubtreeRoot
import cash.z.ecc.android.sdk.internal.model.SuggestScanRangePriority
import cash.z.ecc.android.sdk.internal.model.WalletSummary
import cash.z.ecc.android.sdk.internal.model.ext.from
import cash.z.ecc.android.sdk.internal.model.ext.toBlockHeight
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
@ -48,6 +49,7 @@ import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
import co.electriccoin.lightwallet.client.model.GetAddressUtxosReplyUnsafe
@ -460,16 +462,16 @@ class CompactBlockProcessor internal constructor(
withDownload = true,
enhanceStartHeight = firstUnenhancedHeight
).collect { batchSyncProgress ->
// Update sync progress
when (val result = getScanProgress(backend)) {
is GetScanProgressResult.Success -> {
val resultProgress = result.toPercentDecimal()
// Update sync progress and wallet balance
when (val result = getWalletSummary(backend)) {
is GetWalletSummaryResult.Success -> {
val resultProgress = result.scanProgressPercentDecimal()
Twig.info { "Progress from rust: ${resultProgress.decimal}" }
setProgress(resultProgress)
updateAllBalances(result.walletSummary)
}
else -> { /* Do not report the progress in case of any error */ }
else -> { /* Do not report the progress and balances in case of any error */ }
}
checkAllBalances()
when (batchSyncProgress.resultState) {
SyncingResult.UpdateBirthday -> {
@ -562,16 +564,16 @@ class CompactBlockProcessor internal constructor(
withDownload = true,
enhanceStartHeight = firstUnenhancedHeight
).map { batchSyncProgress ->
// Update sync progress
when (val result = getScanProgress(backend)) {
is GetScanProgressResult.Success -> {
val resultProgress = result.toPercentDecimal()
// Update sync progress and wallet balance
when (val result = getWalletSummary(backend)) {
is GetWalletSummaryResult.Success -> {
val resultProgress = result.scanProgressPercentDecimal()
Twig.info { "Progress from rust: ${resultProgress.decimal}" }
setProgress(resultProgress)
updateAllBalances(result.walletSummary)
}
else -> { /* Do not report the progress in case of any error */ }
else -> { /* Do not report the progress and balances in case of any error */ }
}
checkAllBalances()
when (batchSyncProgress.resultState) {
SyncingResult.UpdateBirthday -> {
@ -757,6 +759,20 @@ class CompactBlockProcessor internal constructor(
transparentBalances.value = getUtxoCacheBalance(getTransparentAddress(backend, Account.DEFAULT))
}
/**
* Update the latest balances using the given wallet summary, and transmit this information
* into the related internal flows. Note that the Orchard balance is not supported.
*/
internal suspend fun updateAllBalances(summary: WalletSummary) {
summary.accountBalances[Account.DEFAULT]?.let {
Twig.debug { "Updating Sapling balance" }
saplingBalances.value = it.sapling
// TODO [#682]: refresh orchard balance
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
}
checkTransparentBalance()
}
sealed class BlockProcessingResult {
object NoBlocksToProcess : BlockProcessingResult()
object Success : BlockProcessingResult()
@ -1269,28 +1285,28 @@ class CompactBlockProcessor internal constructor(
}
/**
* Get the current block scanning progress.
* Get the current wallet summary.
*
* @return the last scanning progress calculated by the Rust layer and wrapped in [GetScanProgressResult]
* @return the latest wallet summary calculated by the Rust layer and wrapped in [GetWalletSummaryResult]
*/
@VisibleForTesting
internal suspend fun getScanProgress(backend: TypesafeBackend): GetScanProgressResult {
internal suspend fun getWalletSummary(backend: TypesafeBackend): GetWalletSummaryResult {
return runCatching {
backend.getScanProgress()
backend.getWalletSummary()
}.onSuccess {
Twig.verbose { "Successfully called getScanProgress with result: $it" }
Twig.verbose { "Successfully called getWalletSummary with result: $it" }
}.onFailure {
Twig.error { "Failed to call getScanProgress with result: $it" }
Twig.error { "Failed to call getWalletSummary with result: $it" }
}.fold(
onSuccess = {
if (it == null) {
GetScanProgressResult.None
GetWalletSummaryResult.None
} else {
GetScanProgressResult.Success(it)
GetWalletSummaryResult.Success(it)
}
},
onFailure = {
GetScanProgressResult.Failure(it)
GetWalletSummaryResult.Failure(it)
}
)
}
@ -2049,11 +2065,17 @@ class CompactBlockProcessor internal constructor(
*/
suspend fun getBalanceInfo(account: Account): WalletBalance {
return runCatching {
val balanceTotal = backend.getBalance(account)
Twig.info { "Found total balance: $balanceTotal" }
val balanceAvailable = backend.getVerifiedBalance(account)
Twig.info { "Found available balance: $balanceAvailable" }
WalletBalance(balanceTotal, balanceAvailable)
val walletSummary = backend.getWalletSummary()
val accountBalance = walletSummary?.accountBalances?.get(account)
// `None` means that the caller has not yet called `updateChainTip` on a
// brand-new wallet, so we can assume the balance is zero.
val saplingBalance = accountBalance?.sapling ?: WalletBalance(
total = Zatoshi(0L),
available = Zatoshi(0L)
)
Twig.info { "Found total balance: ${saplingBalance.total}" }
Twig.info { "Found available balance: ${saplingBalance.available}" }
saplingBalance
}.onFailure {
Twig.error(it) { "Failed to get balance due to ${it.localizedMessage}" }
}.getOrElse {

View File

@ -1,16 +0,0 @@
package cash.z.ecc.android.sdk.block.processor.model
import cash.z.ecc.android.sdk.internal.model.ScanProgress
import cash.z.ecc.android.sdk.model.PercentDecimal
/**
* Internal class for sharing get scan progress action result.
*/
internal sealed class GetScanProgressResult {
data class Success(val scanProgress: ScanProgress) : GetScanProgressResult() {
fun toPercentDecimal() = PercentDecimal(scanProgress.getSafeRatio())
}
data object None : GetScanProgressResult()
data class Failure(val exception: Throwable) : GetScanProgressResult()
}

View File

@ -0,0 +1,16 @@
package cash.z.ecc.android.sdk.block.processor.model
import cash.z.ecc.android.sdk.internal.model.WalletSummary
import cash.z.ecc.android.sdk.model.PercentDecimal
/**
* Internal class for sharing wallet summary action result.
*/
internal sealed class GetWalletSummaryResult {
data class Success(val walletSummary: WalletSummary) : GetWalletSummaryResult() {
fun scanProgressPercentDecimal() = PercentDecimal(walletSummary.scanProgress.getSafeRatio())
}
data object None : GetWalletSummaryResult()
data class Failure(val exception: Throwable) : GetWalletSummaryResult()
}

View File

@ -1,16 +1,15 @@
package cash.z.ecc.android.sdk.internal
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.internal.model.ScanProgress
import cash.z.ecc.android.sdk.internal.model.ScanRange
import cash.z.ecc.android.sdk.internal.model.SubtreeRoot
import cash.z.ecc.android.sdk.internal.model.TreeState
import cash.z.ecc.android.sdk.internal.model.WalletSummary
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.FirstClassByteArray
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
@Suppress("TooManyFunctions")
@ -41,12 +40,8 @@ internal interface TypesafeBackend {
suspend fun listTransparentReceivers(account: Account): List<String>
suspend fun getBalance(account: Account): Zatoshi
fun getBranchIdForHeight(height: BlockHeight): Long
suspend fun getVerifiedBalance(account: Account): Zatoshi
suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight
suspend fun rewindToHeight(height: BlockHeight)
@ -121,7 +116,7 @@ internal interface TypesafeBackend {
* @throws RuntimeException as a common indicator of the operation failure
*/
@Throws(RuntimeException::class)
suspend fun getScanProgress(): ScanProgress?
suspend fun getWalletSummary(): WalletSummary?
/**
* @throws RuntimeException as a common indicator of the operation failure

View File

@ -2,10 +2,10 @@ package cash.z.ecc.android.sdk.internal
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.internal.model.JniSubtreeRoot
import cash.z.ecc.android.sdk.internal.model.ScanProgress
import cash.z.ecc.android.sdk.internal.model.ScanRange
import cash.z.ecc.android.sdk.internal.model.SubtreeRoot
import cash.z.ecc.android.sdk.internal.model.TreeState
import cash.z.ecc.android.sdk.internal.model.WalletSummary
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.FirstClassByteArray
@ -70,18 +70,10 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
return backend.listTransparentReceivers(account.value)
}
override suspend fun getBalance(account: Account): Zatoshi {
return Zatoshi(backend.getBalance(account.value))
}
override fun getBranchIdForHeight(height: BlockHeight): Long {
return backend.getBranchIdForHeight(height.value)
}
override suspend fun getVerifiedBalance(account: Account): Zatoshi {
return Zatoshi(backend.getVerifiedBalance(account.value))
}
override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight {
return BlockHeight.new(
ZcashNetwork.from(backend.networkId),
@ -182,8 +174,8 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
override suspend fun scanBlocks(fromHeight: BlockHeight, limit: Long) = backend.scanBlocks(fromHeight.value, limit)
override suspend fun getScanProgress(): ScanProgress? = backend.getScanProgress()?.let { jniScanProgress ->
ScanProgress.new(jniScanProgress)
override suspend fun getWalletSummary(): WalletSummary? = backend.getWalletSummary()?.let { jniWalletSummary ->
WalletSummary.new(jniWalletSummary)
}
override suspend fun suggestScanRanges(): List<ScanRange> = backend.suggestScanRanges().map { jniScanRange ->

View File

@ -0,0 +1,19 @@
package cash.z.ecc.android.sdk.internal.model
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
internal data class AccountBalance(
val sapling: WalletBalance
) {
companion object {
fun new(jni: JniAccountBalance): AccountBalance {
return AccountBalance(
sapling = WalletBalance(
Zatoshi(jni.saplingTotalBalance),
Zatoshi(jni.saplingVerifiedBalance)
)
)
}
}
}

View File

@ -9,19 +9,19 @@ internal data class ScanProgress(
/**
* Returns progress ratio in [0, 1] range. Any out-of-range value is treated as 0.
*/
fun getSafeRatio() = numerator.toFloat().div(denominator).let { ration ->
if (ration < 0f || ration > 1f) {
fun getSafeRatio() = numerator.toFloat().div(denominator).let { ratio ->
if (ratio < 0f || ratio > 1f) {
0f
} else {
ration
ratio
}
}
companion object {
fun new(jni: JniScanProgress): ScanProgress {
fun new(jni: JniWalletSummary): ScanProgress {
return ScanProgress(
numerator = jni.numerator,
denominator = jni.denominator
numerator = jni.progressNumerator,
denominator = jni.progressDenominator
)
}
}

View File

@ -0,0 +1,19 @@
package cash.z.ecc.android.sdk.internal.model
import cash.z.ecc.android.sdk.model.Account
internal data class WalletSummary(
val accountBalances: Map<Account, AccountBalance>,
val scanProgress: ScanProgress
) {
companion object {
fun new(jni: JniWalletSummary): WalletSummary {
return WalletSummary(
accountBalances = jni.accountBalances.associateBy({ Account(it.account) }, {
AccountBalance.new(it)
}),
scanProgress = ScanProgress.new(jni)
)
}
}
}