diff --git a/CHANGELOG.md b/CHANGELOG.md index 433bdefd..8a34faa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Change Log Upcoming ------------------------------------ +- Added `Zatoshi` typesafe object to represent amounts instead. + +Version 1.6.0-beta01 +------------------------------------ - Updated checkpoints for Mainnet and Testnet - Fix: SDK can now be used on Intel x86_64 emulators - Prevent R8 warnings for apps consuming the SDK diff --git a/MIGRATIONS.md b/MIGRATIONS.md index e29ee029..d5ce8119 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -1,6 +1,16 @@ 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.* -------------------------------------- The main entrypoint to the SDK has changed. diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt index c54ab58b..9bdfd6ad 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt +++ b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt @@ -203,19 +203,19 @@ class DarksideTestCoordinator(val wallet: TestWallet) { fun validateMinBalance(available: Long = -1, total: Long = -1) { val balance = synchronizer.saplingBalances.value 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) { - 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) { val balance = (synchronizer as SdkSynchronizer).processor.getBalanceInfo(accountIndex) if (available > 0) { - assertEquals("invalid available balance", available, balance.availableZatoshi) + assertEquals("invalid available balance", available, balance.available) } if (total > 0) { - assertEquals("invalid total balance", total, balance.totalZatoshi) + assertEquals("invalid total balance", total, balance.total) } } } diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt index 3b536a41..d9200b79 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt +++ b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt @@ -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.service.LightWalletGrpcService 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.type.WalletBalance import cash.z.ecc.android.sdk.type.ZcashNetwork @@ -70,7 +71,7 @@ class TestWallet( val synchronizer: SdkSynchronizer = runBlocking { Synchronizer.new(initializer) } as SdkSynchronizer val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService) - val available get() = synchronizer.saplingBalances.value.availableZatoshi + val available get() = synchronizer.saplingBalances.value?.available val shieldedAddress = runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) } val transparentAddress = @@ -105,7 +106,7 @@ class TestWallet( 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") synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex) .takeWhile { it.isPending() } @@ -128,9 +129,9 @@ class TestWallet( } 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) .onCompletion { twig("done shielding funds") } .catch { twig("Failed with $it") } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt index bac95254..cb41bf8f 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt @@ -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.type.WalletBalance import cash.z.ecc.android.sdk.type.ZcashNetwork +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.runBlocking /** @@ -68,22 +69,23 @@ class GetBalanceFragment : BaseDemoFragment() { synchronizer.status.collectWith(lifecycleScope, ::onStatus) synchronizer.progress.collectWith(lifecycleScope, ::onProgress) synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated) - synchronizer.saplingBalances.collectWith(lifecycleScope, ::onBalance) + synchronizer.saplingBalances.filterNotNull().collectWith(lifecycleScope, ::onBalance) } private fun onBalance(balance: WalletBalance) { binding.textBalance.text = """ - Available balance: ${balance.availableZatoshi.convertZatoshiToZecString(12)} - Total balance: ${balance.totalZatoshi.convertZatoshiToZecString(12)} + Available balance: ${balance.available.convertZatoshiToZecString(12)} + Total balance: ${balance.total.convertZatoshiToZecString(12)} """.trimIndent() } private fun onStatus(status: Synchronizer.Status) { binding.textStatus.text = "Status: $status" - if (WalletBalance().none()) { + val balance = synchronizer.saplingBalances.value + if (null == balance) { binding.textBalance.text = "Calculating balance..." } else { - onBalance(synchronizer.saplingBalances.value) + onBalance(balance) } } @@ -93,16 +95,6 @@ class GetBalanceFragment : BaseDemoFragment() { } } - /** - * 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) { if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%" } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt index 2d1da2fb..74b9fd0b 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt @@ -6,6 +6,7 @@ import android.widget.TextView import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView 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.ext.convertZatoshiToZecString import java.text.SimpleDateFormat @@ -23,7 +24,7 @@ class TransactionViewHolder(itemView: View) : Recycler fun bindTo(transaction: T?) { val isInbound = transaction?.toAddress.isNullOrEmpty() - amountText.text = transaction?.value.convertZatoshiToZecString() + amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString() timeText.text = if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending" else formatter.format(transaction.blockTimeInSeconds * 1000L) diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoViewHolder.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoViewHolder.kt index b6166e58..78006d90 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoViewHolder.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoViewHolder.kt @@ -4,6 +4,7 @@ import android.view.View import android.widget.TextView import androidx.recyclerview.widget.RecyclerView 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.ext.convertZatoshiToZecString import java.text.SimpleDateFormat @@ -19,7 +20,7 @@ class UtxoViewHolder(itemView: View) : RecyclerView.Vi private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) fun bindTo(transaction: T?) { - amountText.text = transaction?.value.convertZatoshiToZecString() + amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString() timeText.text = if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending" else formatter.format(transaction.blockTimeInSeconds * 1000L) diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt index 2b266051..4a005c47 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt @@ -79,7 +79,7 @@ class SendFragment : BaseDemoFragment() { // Observable properties (done without livedata or flows for simplicity) // - private var balance = WalletBalance() + private var balance: WalletBalance? = null set(value) { field = value onUpdateSendButton() @@ -144,12 +144,12 @@ class SendFragment : BaseDemoFragment() { if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%" } - private fun onBalance(balance: WalletBalance) { + private fun onBalance(balance: WalletBalance?) { this.balance = balance if (!isSyncing) { binding.textBalance.text = """ - Available balance: ${balance.availableZatoshi.convertZatoshiToZecString(12)} - Total balance: ${balance.totalZatoshi.convertZatoshiToZecString(12)} + Available balance: ${balance?.available.convertZatoshiToZecString(12)} + Total balance: ${balance?.total.convertZatoshiToZecString(12)} """.trimIndent() } } @@ -196,7 +196,7 @@ class SendFragment : BaseDemoFragment() { text = "⌛ syncing" isEnabled = false } - balance.availableZatoshi <= 0 -> isEnabled = false + (balance?.available?.value ?: 0) <= 0 -> isEnabled = false else -> { text = "send" isEnabled = true diff --git a/gradle.properties b/gradle.properties index afc85aa6..78d7142c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ RELEASE_SIGNING_ENABLED=false # Required by the maven publishing plugin 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. # Currently set to false, because this project has a lot of warnings to fix first. diff --git a/sdk-lib/build.gradle.kts b/sdk-lib/build.gradle.kts index 2ad4b882..9f229aab 100644 --- a/sdk-lib/build.gradle.kts +++ b/sdk-lib/build.gradle.kts @@ -247,6 +247,7 @@ dependencies { // Tests testImplementation(libs.kotlin.reflect) + testImplementation(libs.kotlin.test) testImplementation(libs.mockito.junit) testImplementation(libs.mockito.kotlin) testImplementation(libs.bundles.junit) diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt index 3bb9cf9d..e055e252 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt @@ -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.service.LightWalletGrpcService 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.tool.DerivationTool import cash.z.ecc.android.sdk.tool.WalletBirthdayTool import cash.z.ecc.android.sdk.type.ZcashNetwork import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking @@ -67,9 +69,9 @@ class TestnetIntegrationTest : ScopedTest() { @LargeTest @Ignore("This test is extremely slow") fun testBalance() = runBlocking { - var availableBalance: Long = 0L + var availableBalance: Zatoshi? = null synchronizer.saplingBalances.onFirst { - availableBalance = it.availableZatoshi + availableBalance = it?.available } synchronizer.status.filter { it == SYNCED }.onFirst { @@ -78,7 +80,7 @@ class TestnetIntegrationTest : ScopedTest() { assertTrue( "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") fun testSpend() = runBlocking { var success = false - synchronizer.saplingBalances.filter { it.availableZatoshi > 0 }.onEach { + synchronizer.saplingBalances.filterNotNull().onEach { success = sendFunds() }.first() log("asserting $success") @@ -98,7 +100,7 @@ class TestnetIntegrationTest : ScopedTest() { log("sending to address") synchronizer.sendToAddress( spendingKey, - ZcashSdk.MINERS_FEE_ZATOSHI, + ZcashSdk.MINERS_FEE, toAddress, "first mainnet tx from the SDK" ).filter { it?.isSubmitSuccess() == true }.onFirst { diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManagerTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManagerTest.kt index 737315d4..890ba2e5 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManagerTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManagerTest.kt @@ -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.service.LightWalletService 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 com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.stub @@ -71,7 +72,7 @@ class PersistentTransactionManagerTest : ScopedTest() { @Test 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) // encode TX @@ -95,7 +96,7 @@ class PersistentTransactionManagerTest : ScopedTest() { @Test fun testCancel() = runBlocking { - var tx = manager.initSpend(1234, "a", "b", 0) + var tx = manager.initSpend(Zatoshi(1234), "a", "b", 0) assertFalse(tx.isCancelled()) manager.cancel(tx.id) tx = manager.findById(tx.id)!! @@ -104,7 +105,7 @@ class PersistentTransactionManagerTest : ScopedTest() { @Test fun testAbort() = runBlocking { - var tx: PendingTransaction? = manager.initSpend(1234, "a", "b", 0) + var tx: PendingTransaction? = manager.initSpend(Zatoshi(1234), "a", "b", 0) assertNotNull(tx) manager.abort(tx!!) tx = manager.findById(tx.id) diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt index 63e849d5..a2fc4364 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt @@ -1,6 +1,7 @@ package cash.z.ecc.android.sdk.sample 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.util.TestWallet import kotlinx.coroutines.runBlocking @@ -33,6 +34,6 @@ class ShieldFundsSample { // wallet.shieldFunds() Twig.clip("ShieldFundsSample") - Assert.assertEquals(5, wallet.synchronizer.saplingBalances.value.availableZatoshi) + Assert.assertEquals(Zatoshi(5), wallet.synchronizer.saplingBalances.value?.available) } } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt index 1079412a..0057c64f 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt @@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.sample import androidx.test.filters.LargeTest import cash.z.ecc.android.sdk.ext.ZcashSdk 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.util.TestWallet import kotlinx.coroutines.delay @@ -18,7 +19,7 @@ import org.junit.Test */ 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") @@ -58,8 +59,8 @@ class TransparentRestoreSample { val tbalance = wallet.transparentBalance() val address = wallet.transparentAddress - twig("t-avail: ${tbalance.availableZatoshi} t-total: ${tbalance.totalZatoshi}") - Assert.assertTrue("Not enough funds to run sample. Expected some Zatoshi but found ${tbalance.availableZatoshi}. Try adding funds to $address", tbalance.availableZatoshi > 0) + twig("t-avail: ${tbalance.available} t-total: ${tbalance.total}") + 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!") // wallet.shieldFunds() diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt index 10b75de8..970ad42b 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt @@ -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.service.LightWalletGrpcService 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.type.WalletBalance import cash.z.ecc.android.sdk.type.ZcashNetwork @@ -70,7 +71,7 @@ class TestWallet( val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(initializer) as SdkSynchronizer val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService) - val available get() = synchronizer.saplingBalances.value.availableZatoshi + val available get() = synchronizer.saplingBalances.value?.available val shieldedAddress = runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) } val transparentAddress = @@ -105,7 +106,7 @@ class TestWallet( 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") synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex) .takeWhile { it.isPending() } @@ -128,9 +129,9 @@ class TestWallet( } 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) .onCompletion { twig("done shielding funds") } .catch { twig("Failed with $it") } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index 3f0bdc7d..217275cb 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -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.twig 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.type.AddressType import cash.z.ecc.android.sdk.type.AddressType.Shielded @@ -101,9 +102,9 @@ class SdkSynchronizer internal constructor( ) : Synchronizer { // pools - private val _orchardBalances = MutableStateFlow(WalletBalance()) - private val _saplingBalances = MutableStateFlow(WalletBalance()) - private val _transparentBalances = MutableStateFlow(WalletBalance()) + private val _orchardBalances = MutableStateFlow(null) + private val _saplingBalances = MutableStateFlow(null) + private val _transparentBalances = MutableStateFlow(null) private val _status = ConflatedBroadcastChannel(DISCONNECTED) @@ -636,14 +637,14 @@ class SdkSynchronizer internal constructor( override fun sendToAddress( spendingKey: String, - zatoshi: Long, + amount: Zatoshi, toAddress: String, memo: String, fromAccountIndex: Int ): Flow = flow { twig("Initializing pending transaction") // 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) txManager.encode(spendingKey, placeHolderTx).let { encodedTx -> // 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) // 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) txManager.encode(spendingKey, transparentSecretKey, placeHolderTx).let { encodedTx -> // only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX. diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index f41888b9..6d4f17c7 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt @@ -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.PendingTransaction 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.ConsensusMatchType 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. */ - val orchardBalances: StateFlow + val orchardBalances: StateFlow /** * A stream of balance values for the sapling pool. Includes the available and total balance. */ - val saplingBalances: StateFlow + val saplingBalances: StateFlow /** * A stream of balance values for the transparent pool. Includes the available and total balance. */ - val transparentBalances: StateFlow + val transparentBalances: StateFlow /* Transactions */ @@ -203,7 +204,7 @@ interface Synchronizer { */ fun sendToAddress( spendingKey: String, - zatoshi: Long, + amount: Zatoshi, toAddress: String, memo: String = "", fromAccountIndex: Int = 0 diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt index f7a6bf0f..658fc80f 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt @@ -5,6 +5,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import cash.z.ecc.android.sdk.model.Zatoshi // // Entities @@ -166,6 +167,7 @@ data class ConfirmedTransaction( val expiryHeight: Int? = null, override val raw: ByteArray? = byteArrayOf() ) : MinedTransaction, SignedTransaction { + override fun equals(other: Any?): Boolean { if (this === other) return true 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?) : SignedTransaction { override fun equals(other: Any?): Boolean { @@ -283,8 +288,7 @@ interface PendingTransaction : SignedTransaction, Transaction { // fun PendingTransaction.isSameTxId(other: MinedTransaction): Boolean { - return rawTransactionId != null && other.rawTransactionId != null && - rawTransactionId!!.contentEquals(other.rawTransactionId) + return rawTransactionId != null && rawTransactionId!!.contentEquals(other.rawTransactionId) } fun PendingTransaction.isSameTxId(other: PendingTransaction): Boolean { diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/CurrencyFormatter.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/CurrencyFormatter.kt index fd9edcaf..05fbc116 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/CurrencyFormatter.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/CurrencyFormatter.kt @@ -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.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.MathContext import java.math.RoundingMode @@ -21,7 +21,7 @@ import java.util.Locale // 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 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 { roundingMode = RoundingMode.HALF_EVEN maximumFractionDigits = 6 @@ -47,11 +47,11 @@ object Conversions { * @return this Zatoshi value represented as ZEC, in a string with at least [minDecimals] and at * most [maxDecimals] */ -inline fun Long?.convertZatoshiToZecString( +inline fun Zatoshi?.convertZatoshiToZecString( maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits, minDecimals: Int = ZEC_FORMATTER.minimumFractionDigits ): 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, * rounded accurately out to 128 digits. */ -inline fun Long?.convertZatoshiToZec(scale: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal { - return BigDecimal(this ?: 0L, MathContext.DECIMAL128).divide( +inline fun Zatoshi?.convertZatoshiToZec(scale: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal { + return BigDecimal(this?.value ?: 0L, MathContext.DECIMAL128).divide( Conversions.ONE_ZEC_IN_ZATOSHI, MathContext.DECIMAL128 ).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 * minimize cumulative errors when applied repeatedly over a sequence of calculations. */ -inline fun BigDecimal?.convertZecToZatoshi(): Long { - if (this == null) return 0L +inline fun BigDecimal?.convertZecToZatoshi(): Zatoshi { + if (this == null) return Zatoshi(0L) if (this < BigDecimal.ZERO) { throw IllegalArgumentException( "Invalid ZEC value: $this. ZEC is represented by notes and" + " 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 * 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() } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdk.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdk.kt index 34130b78..e87184cc 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdk.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdk.kt @@ -1,5 +1,7 @@ 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 * 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. */ - val MINERS_FEE_ZATOSHI = 1_000L - - /** - * The number of zatoshi that equal 1 ZEC. - */ - val ZATOSHI_PER_ZEC = 100_000_000L + val MINERS_FEE = Zatoshi(1_000L) /** * The theoretical maximum number of blocks in a reorg, due to other bottlenecks in the protocol design. diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt index 410e8155..5566eecb 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt @@ -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.service.LightWalletService import cash.z.ecc.android.sdk.internal.twig +import cash.z.ecc.android.sdk.model.Zatoshi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.flow.Flow @@ -70,14 +71,14 @@ class PersistentTransactionManager( // override suspend fun initSpend( - zatoshiValue: Long, + value: Zatoshi, toAddress: String, memo: String, fromAccountIndex: Int ): PendingTransaction = withContext(Dispatchers.IO) { twig("constructing a placeholder transaction") var tx = PendingTransactionEntity( - value = zatoshiValue, + value = value.value, toAddress = toAddress, memo = memo.toByteArray(), accountIndex = fromAccountIndex diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt index db555469..896757c4 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt @@ -1,6 +1,7 @@ package cash.z.ecc.android.sdk.internal.transaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction +import cash.z.ecc.android.sdk.model.Zatoshi 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. */ suspend fun initSpend( - zatoshi: Long, + zatoshi: Zatoshi, toAddress: String, memo: String, fromAccountIndex: Int diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt index 76e2e78f..e7634ea2 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt @@ -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.ext.deleteSuspend 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.type.UnifiedViewingKey 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) { - getShieldedAddress( - pathDataDb, - account, - networkId = network.id - ) - } + override suspend fun getShieldedAddress(account: Int) = + withContext(SdkDispatchers.DATABASE_IO) { + getShieldedAddress( + pathDataDb, + account, + networkId = network.id + ) + } 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") } - override suspend fun getBalance(account: Int) = withContext(SdkDispatchers.DATABASE_IO) { - getBalance( - pathDataDb, - account, - networkId = network.id - ) + override suspend fun getBalance(account: Int): Zatoshi { + val longValue = withContext(SdkDispatchers.DATABASE_IO) { + getBalance( + pathDataDb, + account, + networkId = network.id + ) + } + + return Zatoshi(longValue) } - override suspend fun getVerifiedBalance(account: Int) = withContext(SdkDispatchers.DATABASE_IO) { - getVerifiedBalance( - pathDataDb, - account, - networkId = network.id - ) + override suspend fun getVerifiedBalance(account: Int): Zatoshi { + val longValue = withContext(SdkDispatchers.DATABASE_IO) { + getVerifiedBalance( + pathDataDb, + account, + networkId = network.id + ) + } + + return Zatoshi(longValue) } 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) { getSentMemoAsUtf8( @@ -147,13 +163,14 @@ class RustBackend private constructor() : RustBackendWelding { ) } - override suspend fun getNearestRewindHeight(height: Int): Int = withContext(SdkDispatchers.DATABASE_IO) { - getNearestRewindHeight( - pathDataDb, - height, - networkId = network.id - ) - } + override suspend fun getNearestRewindHeight(height: Int): Int = + withContext(SdkDispatchers.DATABASE_IO) { + getNearestRewindHeight( + pathDataDb, + height, + networkId = network.id + ) + } /** * 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 > ? */ 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 { return if (limit > 0) { @@ -184,13 +207,14 @@ class RustBackend private constructor() : RustBackendWelding { } } - override suspend fun decryptAndStoreTransaction(tx: ByteArray) = withContext(SdkDispatchers.DATABASE_IO) { - decryptAndStoreTransaction( - pathDataDb, - tx, - networkId = network.id - ) - } + override suspend fun decryptAndStoreTransaction(tx: ByteArray) = + withContext(SdkDispatchers.DATABASE_IO) { + decryptAndStoreTransaction( + pathDataDb, + tx, + networkId = network.id + ) + } override suspend fun createToAddress( consensusBranchId: Long, @@ -281,7 +305,7 @@ class RustBackend private constructor() : RustBackendWelding { networkId = network.id ) } - return WalletBalance(total, verified) + return WalletBalance(Zatoshi(total), Zatoshi(verified)) } override fun isValidShieldedAddr(addr: String) = diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt index 82268809..80711ad4 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt @@ -1,5 +1,6 @@ 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.WalletBalance 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 getBalance(account: Int = 0): Long + suspend fun getBalance(account: Int = 0): Zatoshi fun getBranchIdForHeight(height: Int): Long @@ -55,7 +56,7 @@ interface RustBackendWelding { 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 diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zatoshi.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zatoshi.kt new file mode 100644 index 00000000..d0216d19 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zatoshi.kt @@ -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 + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt index 4ea674d7..b7ebbbc1 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt @@ -1,37 +1,32 @@ 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 * received on the balance channel. * - * @param totalZatoshi 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 total the total balance, ignoring funds that cannot be used. + * @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 * 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. */ data class WalletBalance( - val totalZatoshi: Long = -1, - val availableZatoshi: Long = -1 + val total: Zatoshi, + val available: Zatoshi ) { - val pendingZatoshi = totalZatoshi.coerceAtLeast(0) - availableZatoshi.coerceAtLeast(0) - operator fun plus(other: WalletBalance): WalletBalance { - 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 - ) - ) - } + init { + require(total.value >= available.value) { "Wallet total balance must be >= available balance" } } + + 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 } -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), Mainnet(1, "mainnet", 419_200, "mainnet.lightwalletd.com", 9067); diff --git a/sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/ZatoshiTest.kt b/sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/ZatoshiTest.kt new file mode 100644 index 00000000..7427bbbb --- /dev/null +++ b/sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/ZatoshiTest.kt @@ -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 { + Zatoshi(Zatoshi.MIN_INCLUSIVE - 1L) + } + } + + @Test + fun maxValue() { + assertFailsWith { + 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 { + Zatoshi(5) - Zatoshi(6) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e820f274..b66cb8a0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -161,6 +161,7 @@ dependencyResolutionManagement { library("junit-api", "org.junit.jupiter:junit-jupiter-api:$junitVersion") library("junit-engine", "org.junit.jupiter:junit-jupiter-engine:$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("mockito-android", "org.mockito:mockito-android:$mockitoVersion") library("mockito-junit", "org.mockito:mockito-junit-jupiter:$mockitoVersion")