[#477] Typesafe Zatoshi APIs for amounts
This commit is contained in:
parent
62e5cee5dd
commit
823e8387cb
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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") }
|
||||||
|
|
|
@ -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}%"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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") }
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) =
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
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);
|
||||||
|
|
||||||
|
|
|
@ -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-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")
|
||||||
|
|
Loading…
Reference in New Issue