[#477] Typesafe Zatoshi APIs for amounts

This commit is contained in:
Carter Jernigan 2022-06-21 19:34:42 -04:00 committed by GitHub
parent 62e5cee5dd
commit 823e8387cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 255 additions and 138 deletions

View File

@ -3,6 +3,10 @@ Change Log
Upcoming Upcoming
------------------------------------ ------------------------------------
- Added `Zatoshi` typesafe object to represent amounts instead.
Version 1.6.0-beta01
------------------------------------
- Updated checkpoints for Mainnet and Testnet - Updated checkpoints for Mainnet and Testnet
- Fix: SDK can now be used on Intel x86_64 emulators - Fix: SDK can now be used on Intel x86_64 emulators
- Prevent R8 warnings for apps consuming the SDK - Prevent R8 warnings for apps consuming the SDK

View File

@ -1,6 +1,16 @@
Troubleshooting Migrations Troubleshooting Migrations
========== ==========
Upcoming Migration to Version 1.7 from 1.6
--------------------------------------
Various APIs used `Long` value to represent Zatoshi currency amounts. Those APIs now use a typesafe `Zatoshi` class. When passing amounts, simply wrap Long values with the Zatoshi constructor `Zatoshi(Long)`. When receiving values, simply unwrap Long values with `Zatoshi.value`.
`WalletBalance` no longer has uninitialized default values. This means that `Synchronizer` fields that expose a WalletBalance now use `null` to signal an uninitialized value. Specifically this means `Synchronizer.orchardBalances`, `Synchronzier.saplingBalances`, and `Synchronizer.transparentBalances` have nullable values now.
`ZcashSdk.ZATOSHI_PER_ZEC` has been moved to `Zatoshi.ZATOSHI_PER_ZEC`.
`ZcashSdk.MINERS_FEE_ZATOSHI` has been renamed to `ZcashSdk.MINERS_FEE` and the type has changed from `Long` to `Zatoshi`.
Upcoming Migrating to Version 1.4.* from 1.3.* Upcoming Migrating to Version 1.4.* from 1.3.*
-------------------------------------- --------------------------------------
The main entrypoint to the SDK has changed. The main entrypoint to the SDK has changed.

View File

@ -203,19 +203,19 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
fun validateMinBalance(available: Long = -1, total: Long = -1) { fun validateMinBalance(available: Long = -1, total: Long = -1) {
val balance = synchronizer.saplingBalances.value val balance = synchronizer.saplingBalances.value
if (available > 0) { if (available > 0) {
assertTrue("invalid available balance. Expected a minimum of $available but found ${balance.availableZatoshi}", available <= balance.availableZatoshi) assertTrue("invalid available balance. Expected a minimum of $available but found ${balance?.available}", available <= balance?.available?.value!!)
} }
if (total > 0) { if (total > 0) {
assertTrue("invalid total balance. Expected a minimum of $total but found ${balance.totalZatoshi}", total <= balance.totalZatoshi) assertTrue("invalid total balance. Expected a minimum of $total but found ${balance?.total}", total <= balance?.total?.value!!)
} }
} }
suspend fun validateBalance(available: Long = -1, total: Long = -1, accountIndex: Int = 0) { suspend fun validateBalance(available: Long = -1, total: Long = -1, accountIndex: Int = 0) {
val balance = (synchronizer as SdkSynchronizer).processor.getBalanceInfo(accountIndex) val balance = (synchronizer as SdkSynchronizer).processor.getBalanceInfo(accountIndex)
if (available > 0) { if (available > 0) {
assertEquals("invalid available balance", available, balance.availableZatoshi) assertEquals("invalid available balance", available, balance.available)
} }
if (total > 0) { if (total > 0) {
assertEquals("invalid total balance", total, balance.totalZatoshi) assertEquals("invalid total balance", total, balance.total)
} }
} }
} }

View File

@ -10,6 +10,7 @@ import cash.z.ecc.android.sdk.db.entity.isPending
import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.type.ZcashNetwork
@ -70,7 +71,7 @@ class TestWallet(
val synchronizer: SdkSynchronizer = runBlocking { Synchronizer.new(initializer) } as SdkSynchronizer val synchronizer: SdkSynchronizer = runBlocking { Synchronizer.new(initializer) } as SdkSynchronizer
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService) val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
val available get() = synchronizer.saplingBalances.value.availableZatoshi val available get() = synchronizer.saplingBalances.value?.available
val shieldedAddress = val shieldedAddress =
runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) } runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) }
val transparentAddress = val transparentAddress =
@ -105,7 +106,7 @@ class TestWallet(
return this return this
} }
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Long = 500L, fromAccountIndex: Int = 0): TestWallet { suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet {
Twig.sprout("$alias sending") Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex) synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending() } .takeWhile { it.isPending() }
@ -128,9 +129,9 @@ class TestWallet(
} }
synchronizer.getTransparentBalance(transparentAddress).let { walletBalance -> synchronizer.getTransparentBalance(transparentAddress).let { walletBalance ->
twig("FOUND utxo balance of total: ${walletBalance.totalZatoshi} available: ${walletBalance.availableZatoshi}") twig("FOUND utxo balance of total: ${walletBalance.total} available: ${walletBalance.available}")
if (walletBalance.availableZatoshi > 0L) { if (walletBalance.available.value > 0L) {
synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey) synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey)
.onCompletion { twig("done shielding funds") } .onCompletion { twig("done shielding funds") }
.catch { twig("Failed with $it") } .catch { twig("Failed with $it") }

View File

@ -17,6 +17,7 @@ import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
/** /**
@ -68,22 +69,23 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
synchronizer.status.collectWith(lifecycleScope, ::onStatus) synchronizer.status.collectWith(lifecycleScope, ::onStatus)
synchronizer.progress.collectWith(lifecycleScope, ::onProgress) synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated) synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
synchronizer.saplingBalances.collectWith(lifecycleScope, ::onBalance) synchronizer.saplingBalances.filterNotNull().collectWith(lifecycleScope, ::onBalance)
} }
private fun onBalance(balance: WalletBalance) { private fun onBalance(balance: WalletBalance) {
binding.textBalance.text = """ binding.textBalance.text = """
Available balance: ${balance.availableZatoshi.convertZatoshiToZecString(12)} Available balance: ${balance.available.convertZatoshiToZecString(12)}
Total balance: ${balance.totalZatoshi.convertZatoshiToZecString(12)} Total balance: ${balance.total.convertZatoshiToZecString(12)}
""".trimIndent() """.trimIndent()
} }
private fun onStatus(status: Synchronizer.Status) { private fun onStatus(status: Synchronizer.Status) {
binding.textStatus.text = "Status: $status" binding.textStatus.text = "Status: $status"
if (WalletBalance().none()) { val balance = synchronizer.saplingBalances.value
if (null == balance) {
binding.textBalance.text = "Calculating balance..." binding.textBalance.text = "Calculating balance..."
} else { } else {
onBalance(synchronizer.saplingBalances.value) onBalance(balance)
} }
} }
@ -93,16 +95,6 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
} }
} }
/**
* Extension function which checks if the balance has been updated or its -1
*/
private fun WalletBalance.none(): Boolean {
if (synchronizer.saplingBalances.value.totalZatoshi == -1L &&
synchronizer.saplingBalances.value.availableZatoshi == -1L
) return true
return false
}
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) { private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%" if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
} }

View File

@ -6,6 +6,7 @@ import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -23,7 +24,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
fun bindTo(transaction: T?) { fun bindTo(transaction: T?) {
val isInbound = transaction?.toAddress.isNullOrEmpty() val isInbound = transaction?.toAddress.isNullOrEmpty()
amountText.text = transaction?.value.convertZatoshiToZecString() amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString()
timeText.text = timeText.text =
if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending" if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending"
else formatter.format(transaction.blockTimeInSeconds * 1000L) else formatter.format(transaction.blockTimeInSeconds * 1000L)

View File

@ -4,6 +4,7 @@ import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -19,7 +20,7 @@ class UtxoViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.Vi
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
fun bindTo(transaction: T?) { fun bindTo(transaction: T?) {
amountText.text = transaction?.value.convertZatoshiToZecString() amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString()
timeText.text = timeText.text =
if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending" if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending"
else formatter.format(transaction.blockTimeInSeconds * 1000L) else formatter.format(transaction.blockTimeInSeconds * 1000L)

View File

@ -79,7 +79,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
// Observable properties (done without livedata or flows for simplicity) // Observable properties (done without livedata or flows for simplicity)
// //
private var balance = WalletBalance() private var balance: WalletBalance? = null
set(value) { set(value) {
field = value field = value
onUpdateSendButton() onUpdateSendButton()
@ -144,12 +144,12 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%" if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
} }
private fun onBalance(balance: WalletBalance) { private fun onBalance(balance: WalletBalance?) {
this.balance = balance this.balance = balance
if (!isSyncing) { if (!isSyncing) {
binding.textBalance.text = """ binding.textBalance.text = """
Available balance: ${balance.availableZatoshi.convertZatoshiToZecString(12)} Available balance: ${balance?.available.convertZatoshiToZecString(12)}
Total balance: ${balance.totalZatoshi.convertZatoshiToZecString(12)} Total balance: ${balance?.total.convertZatoshiToZecString(12)}
""".trimIndent() """.trimIndent()
} }
} }
@ -196,7 +196,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
text = "⌛ syncing" text = "⌛ syncing"
isEnabled = false isEnabled = false
} }
balance.availableZatoshi <= 0 -> isEnabled = false (balance?.available?.value ?: 0) <= 0 -> isEnabled = false
else -> { else -> {
text = "send" text = "send"
isEnabled = true isEnabled = true

View File

@ -22,7 +22,7 @@ RELEASE_SIGNING_ENABLED=false
# Required by the maven publishing plugin # Required by the maven publishing plugin
SONATYPE_HOST=DEFAULT SONATYPE_HOST=DEFAULT
LIBRARY_VERSION=1.6.0-beta01 LIBRARY_VERSION=1.7.0-beta01
# Kotlin compiler warnings can be considered errors, failing the build. # Kotlin compiler warnings can be considered errors, failing the build.
# Currently set to false, because this project has a lot of warnings to fix first. # Currently set to false, because this project has a lot of warnings to fix first.

View File

@ -247,6 +247,7 @@ dependencies {
// Tests // Tests
testImplementation(libs.kotlin.reflect) testImplementation(libs.kotlin.reflect)
testImplementation(libs.kotlin.test)
testImplementation(libs.mockito.junit) testImplementation(libs.mockito.junit)
testImplementation(libs.mockito.kotlin) testImplementation(libs.mockito.kotlin)
testImplementation(libs.bundles.junit) testImplementation(libs.bundles.junit)

View File

@ -12,12 +12,14 @@ import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.test.ScopedTest import cash.z.ecc.android.sdk.test.ScopedTest
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -67,9 +69,9 @@ class TestnetIntegrationTest : ScopedTest() {
@LargeTest @LargeTest
@Ignore("This test is extremely slow") @Ignore("This test is extremely slow")
fun testBalance() = runBlocking { fun testBalance() = runBlocking {
var availableBalance: Long = 0L var availableBalance: Zatoshi? = null
synchronizer.saplingBalances.onFirst { synchronizer.saplingBalances.onFirst {
availableBalance = it.availableZatoshi availableBalance = it?.available
} }
synchronizer.status.filter { it == SYNCED }.onFirst { synchronizer.status.filter { it == SYNCED }.onFirst {
@ -78,7 +80,7 @@ class TestnetIntegrationTest : ScopedTest() {
assertTrue( assertTrue(
"No funds available when we expected a balance greater than zero!", "No funds available when we expected a balance greater than zero!",
availableBalance > 0 availableBalance!!.value > 0
) )
} }
@ -86,7 +88,7 @@ class TestnetIntegrationTest : ScopedTest() {
@Ignore("This test is broken") @Ignore("This test is broken")
fun testSpend() = runBlocking { fun testSpend() = runBlocking {
var success = false var success = false
synchronizer.saplingBalances.filter { it.availableZatoshi > 0 }.onEach { synchronizer.saplingBalances.filterNotNull().onEach {
success = sendFunds() success = sendFunds()
}.first() }.first()
log("asserting $success") log("asserting $success")
@ -98,7 +100,7 @@ class TestnetIntegrationTest : ScopedTest() {
log("sending to address") log("sending to address")
synchronizer.sendToAddress( synchronizer.sendToAddress(
spendingKey, spendingKey,
ZcashSdk.MINERS_FEE_ZATOSHI, ZcashSdk.MINERS_FEE,
toAddress, toAddress,
"first mainnet tx from the SDK" "first mainnet tx from the SDK"
).filter { it?.isSubmitSuccess() == true }.onFirst { ).filter { it?.isSubmitSuccess() == true }.onFirst {

View File

@ -11,6 +11,7 @@ import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.test.ScopedTest import cash.z.ecc.android.sdk.test.ScopedTest
import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.stub import com.nhaarman.mockitokotlin2.stub
@ -71,7 +72,7 @@ class PersistentTransactionManagerTest : ScopedTest() {
@Test @Test
fun testCancellation_RaceCondition() = runBlocking { fun testCancellation_RaceCondition() = runBlocking {
val tx = manager.initSpend(1234, "taddr", "memo-good", 0) val tx = manager.initSpend(Zatoshi(1234), "taddr", "memo-good", 0)
val txFlow = manager.monitorById(tx.id) val txFlow = manager.monitorById(tx.id)
// encode TX // encode TX
@ -95,7 +96,7 @@ class PersistentTransactionManagerTest : ScopedTest() {
@Test @Test
fun testCancel() = runBlocking { fun testCancel() = runBlocking {
var tx = manager.initSpend(1234, "a", "b", 0) var tx = manager.initSpend(Zatoshi(1234), "a", "b", 0)
assertFalse(tx.isCancelled()) assertFalse(tx.isCancelled())
manager.cancel(tx.id) manager.cancel(tx.id)
tx = manager.findById(tx.id)!! tx = manager.findById(tx.id)!!
@ -104,7 +105,7 @@ class PersistentTransactionManagerTest : ScopedTest() {
@Test @Test
fun testAbort() = runBlocking { fun testAbort() = runBlocking {
var tx: PendingTransaction? = manager.initSpend(1234, "a", "b", 0) var tx: PendingTransaction? = manager.initSpend(Zatoshi(1234), "a", "b", 0)
assertNotNull(tx) assertNotNull(tx)
manager.abort(tx!!) manager.abort(tx!!)
tx = manager.findById(tx.id) tx = manager.findById(tx.id)

View File

@ -1,6 +1,7 @@
package cash.z.ecc.android.sdk.sample package cash.z.ecc.android.sdk.sample
import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.android.sdk.util.TestWallet import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -33,6 +34,6 @@ class ShieldFundsSample {
// wallet.shieldFunds() // wallet.shieldFunds()
Twig.clip("ShieldFundsSample") Twig.clip("ShieldFundsSample")
Assert.assertEquals(5, wallet.synchronizer.saplingBalances.value.availableZatoshi) Assert.assertEquals(Zatoshi(5), wallet.synchronizer.saplingBalances.value?.available)
} }
} }

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.sample
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.ZcashNetwork.Testnet import cash.z.ecc.android.sdk.type.ZcashNetwork.Testnet
import cash.z.ecc.android.sdk.util.TestWallet import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -18,7 +19,7 @@ import org.junit.Test
*/ */
class TransparentRestoreSample { class TransparentRestoreSample {
val TX_VALUE = ZcashSdk.MINERS_FEE_ZATOSHI / 2 val TX_VALUE = Zatoshi(ZcashSdk.MINERS_FEE.value / 2)
// val walletA = SimpleWallet(SEED_PHRASE, "WalletA") // val walletA = SimpleWallet(SEED_PHRASE, "WalletA")
@ -58,8 +59,8 @@ class TransparentRestoreSample {
val tbalance = wallet.transparentBalance() val tbalance = wallet.transparentBalance()
val address = wallet.transparentAddress val address = wallet.transparentAddress
twig("t-avail: ${tbalance.availableZatoshi} t-total: ${tbalance.totalZatoshi}") twig("t-avail: ${tbalance.available} t-total: ${tbalance.total}")
Assert.assertTrue("Not enough funds to run sample. Expected some Zatoshi but found ${tbalance.availableZatoshi}. Try adding funds to $address", tbalance.availableZatoshi > 0) Assert.assertTrue("Not enough funds to run sample. Expected some Zatoshi but found ${tbalance.available}. Try adding funds to $address", tbalance.available.value > 0)
twig("Shielding available transparent funds!") twig("Shielding available transparent funds!")
// wallet.shieldFunds() // wallet.shieldFunds()

View File

@ -10,6 +10,7 @@ import cash.z.ecc.android.sdk.db.entity.isPending
import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.type.ZcashNetwork
@ -70,7 +71,7 @@ class TestWallet(
val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(initializer) as SdkSynchronizer val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(initializer) as SdkSynchronizer
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService) val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
val available get() = synchronizer.saplingBalances.value.availableZatoshi val available get() = synchronizer.saplingBalances.value?.available
val shieldedAddress = val shieldedAddress =
runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) } runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) }
val transparentAddress = val transparentAddress =
@ -105,7 +106,7 @@ class TestWallet(
return this return this
} }
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Long = 500L, fromAccountIndex: Int = 0): TestWallet { suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet {
Twig.sprout("$alias sending") Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex) synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending() } .takeWhile { it.isPending() }
@ -128,9 +129,9 @@ class TestWallet(
} }
synchronizer.getTransparentBalance(transparentAddress).let { walletBalance -> synchronizer.getTransparentBalance(transparentAddress).let { walletBalance ->
twig("FOUND utxo balance of total: ${walletBalance.totalZatoshi} available: ${walletBalance.availableZatoshi}") twig("FOUND utxo balance of total: ${walletBalance.total} available: ${walletBalance.available}")
if (walletBalance.availableZatoshi > 0L) { if (walletBalance.available.value > 0L) {
synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey) synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey)
.onCompletion { twig("done shielding funds") } .onCompletion { twig("done shielding funds") }
.catch { twig("Failed with $it") } .catch { twig("Failed with $it") }

View File

@ -46,6 +46,7 @@ import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository
import cash.z.ecc.android.sdk.internal.transaction.WalletTransactionEncoder import cash.z.ecc.android.sdk.internal.transaction.WalletTransactionEncoder
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask import cash.z.ecc.android.sdk.internal.twigTask
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.AddressType import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.android.sdk.type.AddressType.Shielded import cash.z.ecc.android.sdk.type.AddressType.Shielded
@ -101,9 +102,9 @@ class SdkSynchronizer internal constructor(
) : Synchronizer { ) : Synchronizer {
// pools // pools
private val _orchardBalances = MutableStateFlow(WalletBalance()) private val _orchardBalances = MutableStateFlow<WalletBalance?>(null)
private val _saplingBalances = MutableStateFlow(WalletBalance()) private val _saplingBalances = MutableStateFlow<WalletBalance?>(null)
private val _transparentBalances = MutableStateFlow(WalletBalance()) private val _transparentBalances = MutableStateFlow<WalletBalance?>(null)
private val _status = ConflatedBroadcastChannel<Synchronizer.Status>(DISCONNECTED) private val _status = ConflatedBroadcastChannel<Synchronizer.Status>(DISCONNECTED)
@ -636,14 +637,14 @@ class SdkSynchronizer internal constructor(
override fun sendToAddress( override fun sendToAddress(
spendingKey: String, spendingKey: String,
zatoshi: Long, amount: Zatoshi,
toAddress: String, toAddress: String,
memo: String, memo: String,
fromAccountIndex: Int fromAccountIndex: Int
): Flow<PendingTransaction> = flow { ): Flow<PendingTransaction> = flow {
twig("Initializing pending transaction") twig("Initializing pending transaction")
// Emit the placeholder transaction, then switch to monitoring the database // Emit the placeholder transaction, then switch to monitoring the database
txManager.initSpend(zatoshi, toAddress, memo, fromAccountIndex).let { placeHolderTx -> txManager.initSpend(amount, toAddress, memo, fromAccountIndex).let { placeHolderTx ->
emit(placeHolderTx) emit(placeHolderTx)
txManager.encode(spendingKey, placeHolderTx).let { encodedTx -> txManager.encode(spendingKey, placeHolderTx).let { encodedTx ->
// only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX. // only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.
@ -675,7 +676,7 @@ class SdkSynchronizer internal constructor(
val zAddr = getAddress(0) val zAddr = getAddress(0)
// Emit the placeholder transaction, then switch to monitoring the database // Emit the placeholder transaction, then switch to monitoring the database
txManager.initSpend(tBalance.availableZatoshi, zAddr, memo, 0).let { placeHolderTx -> txManager.initSpend(tBalance.available, zAddr, memo, 0).let { placeHolderTx ->
emit(placeHolderTx) emit(placeHolderTx)
txManager.encode(spendingKey, transparentSecretKey, placeHolderTx).let { encodedTx -> txManager.encode(spendingKey, transparentSecretKey, placeHolderTx).let { encodedTx ->
// only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX. // only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.

View File

@ -4,6 +4,7 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.AddressType import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.android.sdk.type.ConsensusMatchType import cash.z.ecc.android.sdk.type.ConsensusMatchType
import cash.z.ecc.android.sdk.type.WalletBalance import cash.z.ecc.android.sdk.type.WalletBalance
@ -102,17 +103,17 @@ interface Synchronizer {
/** /**
* A stream of balance values for the orchard pool. Includes the available and total balance. * A stream of balance values for the orchard pool. Includes the available and total balance.
*/ */
val orchardBalances: StateFlow<WalletBalance> val orchardBalances: StateFlow<WalletBalance?>
/** /**
* A stream of balance values for the sapling pool. Includes the available and total balance. * A stream of balance values for the sapling pool. Includes the available and total balance.
*/ */
val saplingBalances: StateFlow<WalletBalance> val saplingBalances: StateFlow<WalletBalance?>
/** /**
* A stream of balance values for the transparent pool. Includes the available and total balance. * A stream of balance values for the transparent pool. Includes the available and total balance.
*/ */
val transparentBalances: StateFlow<WalletBalance> val transparentBalances: StateFlow<WalletBalance?>
/* Transactions */ /* Transactions */
@ -203,7 +204,7 @@ interface Synchronizer {
*/ */
fun sendToAddress( fun sendToAddress(
spendingKey: String, spendingKey: String,
zatoshi: Long, amount: Zatoshi,
toAddress: String, toAddress: String,
memo: String = "", memo: String = "",
fromAccountIndex: Int = 0 fromAccountIndex: Int = 0

View File

@ -5,6 +5,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import cash.z.ecc.android.sdk.model.Zatoshi
// //
// Entities // Entities
@ -166,6 +167,7 @@ data class ConfirmedTransaction(
val expiryHeight: Int? = null, val expiryHeight: Int? = null,
override val raw: ByteArray? = byteArrayOf() override val raw: ByteArray? = byteArrayOf()
) : MinedTransaction, SignedTransaction { ) : MinedTransaction, SignedTransaction {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is ConfirmedTransaction) return false if (other !is ConfirmedTransaction) return false
@ -207,6 +209,9 @@ data class ConfirmedTransaction(
} }
} }
val ConfirmedTransaction.valueInZatoshi
get() = Zatoshi(value)
data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray, val expiryHeight: Int?) : data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray, val expiryHeight: Int?) :
SignedTransaction { SignedTransaction {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -283,8 +288,7 @@ interface PendingTransaction : SignedTransaction, Transaction {
// //
fun PendingTransaction.isSameTxId(other: MinedTransaction): Boolean { fun PendingTransaction.isSameTxId(other: MinedTransaction): Boolean {
return rawTransactionId != null && other.rawTransactionId != null && return rawTransactionId != null && rawTransactionId!!.contentEquals(other.rawTransactionId)
rawTransactionId!!.contentEquals(other.rawTransactionId)
} }
fun PendingTransaction.isSameTxId(other: PendingTransaction): Boolean { fun PendingTransaction.isSameTxId(other: PendingTransaction): Boolean {

View File

@ -4,7 +4,7 @@ package cash.z.ecc.android.sdk.ext
import cash.z.ecc.android.sdk.ext.Conversions.USD_FORMATTER import cash.z.ecc.android.sdk.ext.Conversions.USD_FORMATTER
import cash.z.ecc.android.sdk.ext.Conversions.ZEC_FORMATTER import cash.z.ecc.android.sdk.ext.Conversions.ZEC_FORMATTER
import cash.z.ecc.android.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC import cash.z.ecc.android.sdk.model.Zatoshi
import java.math.BigDecimal import java.math.BigDecimal
import java.math.MathContext import java.math.MathContext
import java.math.RoundingMode import java.math.RoundingMode
@ -21,7 +21,7 @@ import java.util.Locale
// TODO: provide a dynamic way to configure this globally for the SDK // TODO: provide a dynamic way to configure this globally for the SDK
// For now, just make these vars so at least they could be modified in one place // For now, just make these vars so at least they could be modified in one place
object Conversions { object Conversions {
var ONE_ZEC_IN_ZATOSHI = BigDecimal(ZATOSHI_PER_ZEC, MathContext.DECIMAL128) var ONE_ZEC_IN_ZATOSHI = BigDecimal(Zatoshi.ZATOSHI_PER_ZEC, MathContext.DECIMAL128)
var ZEC_FORMATTER = NumberFormat.getInstance(Locale.getDefault()).apply { var ZEC_FORMATTER = NumberFormat.getInstance(Locale.getDefault()).apply {
roundingMode = RoundingMode.HALF_EVEN roundingMode = RoundingMode.HALF_EVEN
maximumFractionDigits = 6 maximumFractionDigits = 6
@ -47,11 +47,11 @@ object Conversions {
* @return this Zatoshi value represented as ZEC, in a string with at least [minDecimals] and at * @return this Zatoshi value represented as ZEC, in a string with at least [minDecimals] and at
* most [maxDecimals] * most [maxDecimals]
*/ */
inline fun Long?.convertZatoshiToZecString( inline fun Zatoshi?.convertZatoshiToZecString(
maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits, maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits,
minDecimals: Int = ZEC_FORMATTER.minimumFractionDigits minDecimals: Int = ZEC_FORMATTER.minimumFractionDigits
): String { ): String {
return currencyFormatter(maxDecimals, minDecimals).format(this.convertZatoshiToZec(maxDecimals)) return currencyFormatter(maxDecimals, minDecimals).format(convertZatoshiToZec(maxDecimals))
} }
/** /**
@ -162,8 +162,8 @@ inline fun currencyFormatter(maxDecimals: Int, minDecimals: Int): NumberFormat {
* @return this Long Zatoshi value represented as ZEC using a BigDecimal with the given scale, * @return this Long Zatoshi value represented as ZEC using a BigDecimal with the given scale,
* rounded accurately out to 128 digits. * rounded accurately out to 128 digits.
*/ */
inline fun Long?.convertZatoshiToZec(scale: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal { inline fun Zatoshi?.convertZatoshiToZec(scale: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
return BigDecimal(this ?: 0L, MathContext.DECIMAL128).divide( return BigDecimal(this?.value ?: 0L, MathContext.DECIMAL128).divide(
Conversions.ONE_ZEC_IN_ZATOSHI, Conversions.ONE_ZEC_IN_ZATOSHI,
MathContext.DECIMAL128 MathContext.DECIMAL128
).setScale(scale, ZEC_FORMATTER.roundingMode) ).setScale(scale, ZEC_FORMATTER.roundingMode)
@ -176,15 +176,15 @@ inline fun Long?.convertZatoshiToZec(scale: Int = ZEC_FORMATTER.maximumFractionD
* @return this ZEC value represented as Zatoshi, rounded accurately out to 128 digits, in order to * @return this ZEC value represented as Zatoshi, rounded accurately out to 128 digits, in order to
* minimize cumulative errors when applied repeatedly over a sequence of calculations. * minimize cumulative errors when applied repeatedly over a sequence of calculations.
*/ */
inline fun BigDecimal?.convertZecToZatoshi(): Long { inline fun BigDecimal?.convertZecToZatoshi(): Zatoshi {
if (this == null) return 0L if (this == null) return Zatoshi(0L)
if (this < BigDecimal.ZERO) { if (this < BigDecimal.ZERO) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Invalid ZEC value: $this. ZEC is represented by notes and" + "Invalid ZEC value: $this. ZEC is represented by notes and" +
" cannot be negative" " cannot be negative"
) )
} }
return this.multiply(Conversions.ONE_ZEC_IN_ZATOSHI, MathContext.DECIMAL128).toLong() return Zatoshi(this.multiply(Conversions.ONE_ZEC_IN_ZATOSHI, MathContext.DECIMAL128).toLong())
} }
/** /**
@ -214,7 +214,7 @@ inline fun Double?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): B
* @return this Double ZEC value converted into Zatoshi, with proper rounding and precision by * @return this Double ZEC value converted into Zatoshi, with proper rounding and precision by
* leveraging an intermediate BigDecimal object. * leveraging an intermediate BigDecimal object.
*/ */
inline fun Double?.convertZecToZatoshi(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): Long { inline fun Double?.convertZecToZatoshi(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): Zatoshi {
return this.toZec(decimals).convertZecToZatoshi() return this.toZec(decimals).convertZecToZatoshi()
} }

View File

@ -1,5 +1,7 @@
package cash.z.ecc.android.sdk.ext package cash.z.ecc.android.sdk.ext
import cash.z.ecc.android.sdk.model.Zatoshi
/** /**
* Wrapper for all the constant values in the SDK. It is important that these values stay fixed for * Wrapper for all the constant values in the SDK. It is important that these values stay fixed for
* all users of the SDK. Otherwise, if individual wallet makers are using different values, it * all users of the SDK. Otherwise, if individual wallet makers are using different values, it
@ -11,12 +13,7 @@ object ZcashSdk {
/** /**
* Miner's fee in zatoshi. * Miner's fee in zatoshi.
*/ */
val MINERS_FEE_ZATOSHI = 1_000L val MINERS_FEE = Zatoshi(1_000L)
/**
* The number of zatoshi that equal 1 ZEC.
*/
val ZATOSHI_PER_ZEC = 100_000_000L
/** /**
* The theoretical maximum number of blocks in a reorg, due to other bottlenecks in the protocol design. * The theoretical maximum number of blocks in a reorg, due to other bottlenecks in the protocol design.

View File

@ -12,6 +12,7 @@ import cash.z.ecc.android.sdk.internal.db.PendingTransactionDao
import cash.z.ecc.android.sdk.internal.db.PendingTransactionDb import cash.z.ecc.android.sdk.internal.db.PendingTransactionDb
import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -70,14 +71,14 @@ class PersistentTransactionManager(
// //
override suspend fun initSpend( override suspend fun initSpend(
zatoshiValue: Long, value: Zatoshi,
toAddress: String, toAddress: String,
memo: String, memo: String,
fromAccountIndex: Int fromAccountIndex: Int
): PendingTransaction = withContext(Dispatchers.IO) { ): PendingTransaction = withContext(Dispatchers.IO) {
twig("constructing a placeholder transaction") twig("constructing a placeholder transaction")
var tx = PendingTransactionEntity( var tx = PendingTransactionEntity(
value = zatoshiValue, value = value.value,
toAddress = toAddress, toAddress = toAddress,
memo = memo.toByteArray(), memo = memo.toByteArray(),
accountIndex = fromAccountIndex accountIndex = fromAccountIndex

View File

@ -1,6 +1,7 @@
package cash.z.ecc.android.sdk.internal.transaction package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.model.Zatoshi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
/** /**
@ -21,7 +22,7 @@ interface OutboundTransactionManager {
* @return the associated pending transaction whose ID can be used to monitor for changes. * @return the associated pending transaction whose ID can be used to monitor for changes.
*/ */
suspend fun initSpend( suspend fun initSpend(
zatoshi: Long, zatoshi: Zatoshi,
toAddress: String, toAddress: String,
memo: String, memo: String,
fromAccountIndex: Int fromAccountIndex: Int

View File

@ -6,6 +6,7 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
import cash.z.ecc.android.sdk.internal.SdkDispatchers import cash.z.ecc.android.sdk.internal.SdkDispatchers
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.WalletBalance import cash.z.ecc.android.sdk.type.WalletBalance
@ -100,36 +101,51 @@ class RustBackend private constructor() : RustBackendWelding {
} }
} }
override suspend fun getShieldedAddress(account: Int) = withContext(SdkDispatchers.DATABASE_IO) { override suspend fun getShieldedAddress(account: Int) =
getShieldedAddress( withContext(SdkDispatchers.DATABASE_IO) {
pathDataDb, getShieldedAddress(
account, pathDataDb,
networkId = network.id account,
) networkId = network.id
} )
}
override suspend fun getTransparentAddress(account: Int, index: Int): String { override suspend 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") throw NotImplementedError("TODO: implement this at the zcash_client_sqlite level. But for now, use DerivationTool, instead to derive addresses from seeds")
} }
override suspend fun getBalance(account: Int) = withContext(SdkDispatchers.DATABASE_IO) { override suspend fun getBalance(account: Int): Zatoshi {
getBalance( val longValue = withContext(SdkDispatchers.DATABASE_IO) {
pathDataDb, getBalance(
account, pathDataDb,
networkId = network.id account,
) networkId = network.id
)
}
return Zatoshi(longValue)
} }
override suspend fun getVerifiedBalance(account: Int) = withContext(SdkDispatchers.DATABASE_IO) { override suspend fun getVerifiedBalance(account: Int): Zatoshi {
getVerifiedBalance( val longValue = withContext(SdkDispatchers.DATABASE_IO) {
pathDataDb, getVerifiedBalance(
account, pathDataDb,
networkId = network.id account,
) networkId = network.id
)
}
return Zatoshi(longValue)
} }
override suspend fun getReceivedMemoAsUtf8(idNote: Long) = override suspend fun getReceivedMemoAsUtf8(idNote: Long) =
withContext(SdkDispatchers.DATABASE_IO) { getReceivedMemoAsUtf8(pathDataDb, idNote, networkId = network.id) } withContext(SdkDispatchers.DATABASE_IO) {
getReceivedMemoAsUtf8(
pathDataDb,
idNote,
networkId = network.id
)
}
override suspend fun getSentMemoAsUtf8(idNote: Long) = withContext(SdkDispatchers.DATABASE_IO) { override suspend fun getSentMemoAsUtf8(idNote: Long) = withContext(SdkDispatchers.DATABASE_IO) {
getSentMemoAsUtf8( getSentMemoAsUtf8(
@ -147,13 +163,14 @@ class RustBackend private constructor() : RustBackendWelding {
) )
} }
override suspend fun getNearestRewindHeight(height: Int): Int = withContext(SdkDispatchers.DATABASE_IO) { override suspend fun getNearestRewindHeight(height: Int): Int =
getNearestRewindHeight( withContext(SdkDispatchers.DATABASE_IO) {
pathDataDb, getNearestRewindHeight(
height, pathDataDb,
networkId = network.id height,
) networkId = network.id
} )
}
/** /**
* Deletes data for all blocks above the given height. Boils down to: * Deletes data for all blocks above the given height. Boils down to:
@ -161,7 +178,13 @@ class RustBackend private constructor() : RustBackendWelding {
* DELETE FROM blocks WHERE height > ? * DELETE FROM blocks WHERE height > ?
*/ */
override suspend fun rewindToHeight(height: Int) = override suspend fun rewindToHeight(height: Int) =
withContext(SdkDispatchers.DATABASE_IO) { rewindToHeight(pathDataDb, height, networkId = network.id) } withContext(SdkDispatchers.DATABASE_IO) {
rewindToHeight(
pathDataDb,
height,
networkId = network.id
)
}
override suspend fun scanBlocks(limit: Int): Boolean { override suspend fun scanBlocks(limit: Int): Boolean {
return if (limit > 0) { return if (limit > 0) {
@ -184,13 +207,14 @@ class RustBackend private constructor() : RustBackendWelding {
} }
} }
override suspend fun decryptAndStoreTransaction(tx: ByteArray) = withContext(SdkDispatchers.DATABASE_IO) { override suspend fun decryptAndStoreTransaction(tx: ByteArray) =
decryptAndStoreTransaction( withContext(SdkDispatchers.DATABASE_IO) {
pathDataDb, decryptAndStoreTransaction(
tx, pathDataDb,
networkId = network.id tx,
) networkId = network.id
} )
}
override suspend fun createToAddress( override suspend fun createToAddress(
consensusBranchId: Long, consensusBranchId: Long,
@ -281,7 +305,7 @@ class RustBackend private constructor() : RustBackendWelding {
networkId = network.id networkId = network.id
) )
} }
return WalletBalance(total, verified) return WalletBalance(Zatoshi(total), Zatoshi(verified))
} }
override fun isValidShieldedAddr(addr: String) = override fun isValidShieldedAddr(addr: String) =

View File

@ -1,5 +1,6 @@
package cash.z.ecc.android.sdk.jni package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.UnifiedViewingKey import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.WalletBalance import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.type.ZcashNetwork
@ -47,7 +48,7 @@ interface RustBackendWelding {
suspend fun getTransparentAddress(account: Int = 0, index: Int = 0): String suspend fun getTransparentAddress(account: Int = 0, index: Int = 0): String
suspend fun getBalance(account: Int = 0): Long suspend fun getBalance(account: Int = 0): Zatoshi
fun getBranchIdForHeight(height: Int): Long fun getBranchIdForHeight(height: Int): Long
@ -55,7 +56,7 @@ interface RustBackendWelding {
suspend fun getSentMemoAsUtf8(idNote: Long): String suspend fun getSentMemoAsUtf8(idNote: Long): String
suspend fun getVerifiedBalance(account: Int = 0): Long suspend fun getVerifiedBalance(account: Int = 0): Zatoshi
// fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList // fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList

View File

@ -0,0 +1,31 @@
package cash.z.ecc.android.sdk.model
/**
* A unit of currency used throughout the SDK.
*
* End users (e.g. app users) generally are not shown Zatoshi values. Instead they are presented
* with ZEC, which is a decimal value represented only as a String. ZEC are not used internally,
* to avoid floating point imprecision.
*/
data class Zatoshi(val value: Long) {
init {
require(value >= MIN_INCLUSIVE) { "Zatoshi must be in the range [$MIN_INCLUSIVE, $MAX_INCLUSIVE]" }
require(value <= MAX_INCLUSIVE) { "Zatoshi must be in the range [$MIN_INCLUSIVE, $MAX_INCLUSIVE]" }
}
operator fun plus(other: Zatoshi) = Zatoshi(value + other.value)
operator fun minus(other: Zatoshi) = Zatoshi(value - other.value)
companion object {
/**
* The number of Zatoshi that equal 1 ZEC.
*/
const val ZATOSHI_PER_ZEC = 100_000_000L
private const val MAX_ZEC_SUPPLY = 21_000_000
const val MIN_INCLUSIVE = 0
const val MAX_INCLUSIVE = ZATOSHI_PER_ZEC * MAX_ZEC_SUPPLY
}
}

View File

@ -1,37 +1,32 @@
package cash.z.ecc.android.sdk.type package cash.z.ecc.android.sdk.type
import cash.z.ecc.android.sdk.model.Zatoshi
/** /**
* Data structure to hold the total and available balance of the wallet. This is what is * Data structure to hold the total and available balance of the wallet. This is what is
* received on the balance channel. * received on the balance channel.
* *
* @param totalZatoshi the total balance, ignoring funds that cannot be used. * @param total the total balance, ignoring funds that cannot be used.
* @param availableZatoshi the amount of funds that are available for use. Typical reasons that funds * @param available the amount of funds that are available for use. Typical reasons that funds
* may be unavailable include fairly new transactions that do not have enough confirmations or * may be unavailable include fairly new transactions that do not have enough confirmations or
* notes that are tied up because we are awaiting change from a transaction. When a note has * notes that are tied up because we are awaiting change from a transaction. When a note has
* been spent, its change cannot be used until there are enough confirmations. * been spent, its change cannot be used until there are enough confirmations.
*/ */
data class WalletBalance( data class WalletBalance(
val totalZatoshi: Long = -1, val total: Zatoshi,
val availableZatoshi: Long = -1 val available: Zatoshi
) { ) {
val pendingZatoshi = totalZatoshi.coerceAtLeast(0) - availableZatoshi.coerceAtLeast(0) init {
operator fun plus(other: WalletBalance): WalletBalance { require(total.value >= available.value) { "Wallet total balance must be >= available balance" }
return if (
totalZatoshi == -1L && other.totalZatoshi == -1L &&
availableZatoshi == -1L && other.availableZatoshi == -1L
) {
// if everything is uninitialized, then return the same
WalletBalance(-1L, -1L)
} else {
// otherwise, ignore any uninitialized values
WalletBalance(
totalZatoshi = totalZatoshi.coerceAtLeast(0) + other.totalZatoshi.coerceAtLeast(0),
availableZatoshi = availableZatoshi.coerceAtLeast(0) + other.availableZatoshi.coerceAtLeast(
0
)
)
}
} }
val pending = total - available
operator fun plus(other: WalletBalance): WalletBalance =
WalletBalance(
total + other.total,
available + other.available
)
} }
/** /**
@ -76,7 +71,13 @@ interface UnifiedAddress {
val rawTransparentAddress: String val rawTransparentAddress: String
} }
enum class ZcashNetwork(val id: Int, val networkName: String, val saplingActivationHeight: Int, val defaultHost: String, val defaultPort: Int) { enum class ZcashNetwork(
val id: Int,
val networkName: String,
val saplingActivationHeight: Int,
val defaultHost: String,
val defaultPort: Int
) {
Testnet(0, "testnet", 280_000, "testnet.lightwalletd.com", 9067), Testnet(0, "testnet", 280_000, "testnet.lightwalletd.com", 9067),
Mainnet(1, "mainnet", 419_200, "mainnet.lightwalletd.com", 9067); Mainnet(1, "mainnet", 419_200, "mainnet.lightwalletd.com", 9067);

View File

@ -0,0 +1,38 @@
package cash.z.ecc.android.sdk.model
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class ZatoshiTest {
@Test
fun minValue() {
assertFailsWith<IllegalArgumentException> {
Zatoshi(Zatoshi.MIN_INCLUSIVE - 1L)
}
}
@Test
fun maxValue() {
assertFailsWith<IllegalArgumentException> {
Zatoshi(Zatoshi.MAX_INCLUSIVE + 1)
}
}
@Test
fun plus() {
assertEquals(Zatoshi(4), Zatoshi(1) + Zatoshi(3))
}
@Test
fun minus() {
assertEquals(Zatoshi(3), Zatoshi(4) - Zatoshi(1))
}
@Test
fun minus_fail() {
assertFailsWith<IllegalArgumentException> {
Zatoshi(5) - Zatoshi(6)
}
}
}

View File

@ -161,6 +161,7 @@ dependencyResolutionManagement {
library("junit-api", "org.junit.jupiter:junit-jupiter-api:$junitVersion") library("junit-api", "org.junit.jupiter:junit-jupiter-api:$junitVersion")
library("junit-engine", "org.junit.jupiter:junit-jupiter-engine:$junitVersion") library("junit-engine", "org.junit.jupiter:junit-jupiter-engine:$junitVersion")
library("junit-migration", "org.junit.jupiter:junit-jupiter-migrationsupport:$junitVersion") library("junit-migration", "org.junit.jupiter:junit-jupiter-migrationsupport:$junitVersion")
library("kotlin-test", "org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
library("kotlinx-coroutines-test", "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion") library("kotlinx-coroutines-test", "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion")
library("mockito-android", "org.mockito:mockito-android:$mockitoVersion") library("mockito-android", "org.mockito:mockito-android:$mockitoVersion")
library("mockito-junit", "org.mockito:mockito-junit-jupiter:$mockitoVersion") library("mockito-junit", "org.mockito:mockito-junit-jupiter:$mockitoVersion")