[#477] Typesafe Zatoshi APIs for amounts
This commit is contained in:
parent
62e5cee5dd
commit
823e8387cb
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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") }
|
||||
|
|
|
@ -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<FragmentGetBalanceBinding>() {
|
|||
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<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) {
|
||||
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
|
||||
}
|
||||
|
|
|
@ -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<T : ConfirmedTransaction>(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)
|
||||
|
|
|
@ -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<T : ConfirmedTransaction>(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)
|
||||
|
|
|
@ -79,7 +79,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
// 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<FragmentSendBinding>() {
|
|||
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<FragmentSendBinding>() {
|
|||
text = "⌛ syncing"
|
||||
isEnabled = false
|
||||
}
|
||||
balance.availableZatoshi <= 0 -> isEnabled = false
|
||||
(balance?.available?.value ?: 0) <= 0 -> isEnabled = false
|
||||
else -> {
|
||||
text = "send"
|
||||
isEnabled = true
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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") }
|
||||
|
|
|
@ -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<WalletBalance?>(null)
|
||||
private val _saplingBalances = MutableStateFlow<WalletBalance?>(null)
|
||||
private val _transparentBalances = MutableStateFlow<WalletBalance?>(null)
|
||||
|
||||
private val _status = ConflatedBroadcastChannel<Synchronizer.Status>(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<PendingTransaction> = 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.
|
||||
|
|
|
@ -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<WalletBalance>
|
||||
val orchardBalances: StateFlow<WalletBalance?>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
val transparentBalances: StateFlow<WalletBalance>
|
||||
val transparentBalances: StateFlow<WalletBalance?>
|
||||
|
||||
/* Transactions */
|
||||
|
||||
|
@ -203,7 +204,7 @@ interface Synchronizer {
|
|||
*/
|
||||
fun sendToAddress(
|
||||
spendingKey: String,
|
||||
zatoshi: Long,
|
||||
amount: Zatoshi,
|
||||
toAddress: String,
|
||||
memo: String = "",
|
||||
fromAccountIndex: Int = 0
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,7 +101,8 @@ class RustBackend private constructor() : RustBackendWelding {
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getShieldedAddress(account: Int) = withContext(SdkDispatchers.DATABASE_IO) {
|
||||
override suspend fun getShieldedAddress(account: Int) =
|
||||
withContext(SdkDispatchers.DATABASE_IO) {
|
||||
getShieldedAddress(
|
||||
pathDataDb,
|
||||
account,
|
||||
|
@ -112,7 +114,8 @@ class RustBackend private constructor() : RustBackendWelding {
|
|||
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 {
|
||||
val longValue = withContext(SdkDispatchers.DATABASE_IO) {
|
||||
getBalance(
|
||||
pathDataDb,
|
||||
account,
|
||||
|
@ -120,7 +123,11 @@ class RustBackend private constructor() : RustBackendWelding {
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun getVerifiedBalance(account: Int) = withContext(SdkDispatchers.DATABASE_IO) {
|
||||
return Zatoshi(longValue)
|
||||
}
|
||||
|
||||
override suspend fun getVerifiedBalance(account: Int): Zatoshi {
|
||||
val longValue = withContext(SdkDispatchers.DATABASE_IO) {
|
||||
getVerifiedBalance(
|
||||
pathDataDb,
|
||||
account,
|
||||
|
@ -128,8 +135,17 @@ class RustBackend private constructor() : RustBackendWelding {
|
|||
)
|
||||
}
|
||||
|
||||
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,7 +163,8 @@ class RustBackend private constructor() : RustBackendWelding {
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun getNearestRewindHeight(height: Int): Int = withContext(SdkDispatchers.DATABASE_IO) {
|
||||
override suspend fun getNearestRewindHeight(height: Int): Int =
|
||||
withContext(SdkDispatchers.DATABASE_IO) {
|
||||
getNearestRewindHeight(
|
||||
pathDataDb,
|
||||
height,
|
||||
|
@ -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,7 +207,8 @@ class RustBackend private constructor() : RustBackendWelding {
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun decryptAndStoreTransaction(tx: ByteArray) = withContext(SdkDispatchers.DATABASE_IO) {
|
||||
override suspend fun decryptAndStoreTransaction(tx: ByteArray) =
|
||||
withContext(SdkDispatchers.DATABASE_IO) {
|
||||
decryptAndStoreTransaction(
|
||||
pathDataDb,
|
||||
tx,
|
||||
|
@ -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) =
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
init {
|
||||
require(total.value >= available.value) { "Wallet total balance must be >= available balance" }
|
||||
}
|
||||
|
||||
val pending = total - available
|
||||
|
||||
operator fun plus(other: WalletBalance): WalletBalance =
|
||||
WalletBalance(
|
||||
totalZatoshi = totalZatoshi.coerceAtLeast(0) + other.totalZatoshi.coerceAtLeast(0),
|
||||
availableZatoshi = availableZatoshi.coerceAtLeast(0) + other.availableZatoshi.coerceAtLeast(
|
||||
0
|
||||
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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue