[#776] Enable ZIP 317 fees support
* [#776] Enable ZIP 317 fees support * Deprecate ZcashSdk.MINERS_FEE * Replace MINERS_FEE with Proposal API in Demo app * Changelog update * Bump SDK to 2.0.8 to produce snapshot version
This commit is contained in:
parent
5b04cbc579
commit
078e76a941
|
@ -6,6 +6,10 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- The SDK uses ZIP-317 fee system internally
|
||||
- `ZcashSdk.MINERS_FEE` has been deprecated, and will be removed in 2.1.0
|
||||
|
||||
## [2.0.7] - 2024-03-08
|
||||
|
||||
### Fixed
|
||||
|
|
|
@ -376,7 +376,7 @@ class RustBackend private constructor(
|
|||
*/
|
||||
companion object {
|
||||
internal val rustLibraryLoader = NativeLibraryLoader("zcashwalletsdk")
|
||||
private const val IS_USE_ZIP_317_FEES = false
|
||||
private const val IS_USE_ZIP_317_FEES = true
|
||||
|
||||
suspend fun loadLibrary() {
|
||||
rustLibraryLoader.load {
|
||||
|
|
|
@ -40,6 +40,7 @@ import cash.z.ecc.android.sdk.demoapp.ui.screen.transactions.view.Transactions
|
|||
import cash.z.ecc.android.sdk.demoapp.util.AndroidApiVersion
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.model.Proposal
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.type.ServerValidation
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -131,6 +132,9 @@ internal fun ComposeActivity.Navigation() {
|
|||
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
|
||||
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
|
||||
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
|
||||
|
||||
val sendTransactionProposal = remember { mutableStateOf<Proposal?>(null) }
|
||||
|
||||
if (null == synchronizer || null == walletSnapshot || null == spendingKey) {
|
||||
// Display loading indicator
|
||||
} else {
|
||||
|
@ -140,10 +144,14 @@ internal fun ComposeActivity.Navigation() {
|
|||
onSend = {
|
||||
walletViewModel.send(it)
|
||||
},
|
||||
onGetProposal = {
|
||||
sendTransactionProposal.value = walletViewModel.getSendProposal(it)
|
||||
},
|
||||
onBack = {
|
||||
walletViewModel.clearSendOrShieldState()
|
||||
navController.popBackStackJustOnce(SEND)
|
||||
}
|
||||
},
|
||||
sendTransactionProposal = sendTransactionProposal.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBalanceBinding
|
|||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.SyncBlockchainBenchmarkTrace
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
|
@ -138,10 +137,10 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
}
|
||||
|
||||
binding.shield.apply {
|
||||
// TODO [#776]: Support variable fees
|
||||
// TODO [#776]: https://github.com/zcash/zcash-android-wallet-sdk/issues/776
|
||||
// This check is not entirely correct - it does not calculate the resulting fee with the new Proposal API
|
||||
// Note that the entire fragment-based old Demo app will be removed as part of [#973]
|
||||
visibility =
|
||||
if ((transparentBalance ?: Zatoshi(0)) > ZcashSdk.MINERS_FEE) {
|
||||
if ((transparentBalance ?: Zatoshi(0)).value > 0L) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
|
|
|
@ -26,12 +26,11 @@ import cash.z.ecc.android.sdk.demoapp.R
|
|||
import cash.z.ecc.android.sdk.demoapp.fixture.WalletSnapshotFixture
|
||||
import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.SendState
|
||||
import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.WalletSnapshot
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
|
||||
@Preview(name = "Balance")
|
||||
@Composable
|
||||
@Suppress("ktlint:standard:function-naming")
|
||||
@Composable
|
||||
private fun ComposablePreview() {
|
||||
MaterialTheme {
|
||||
Balance(
|
||||
|
@ -152,10 +151,8 @@ private fun BalanceMainContent(
|
|||
)
|
||||
)
|
||||
|
||||
// TODO [#776]: Support variable fees
|
||||
// TODO [#776]: https://github.com/zcash/zcash-android-wallet-sdk/issues/776
|
||||
// This check will not be correct with variable fees
|
||||
if (walletSnapshot.transparentBalance > ZcashSdk.MINERS_FEE) {
|
||||
// This check is not entirely correct - it does not calculate the resulting fee with the new Proposal API
|
||||
if (walletSnapshot.transparentBalance.value > 0L) {
|
||||
// Note this implementation does not guard against multiple clicks
|
||||
Button(onClick = onShieldFunds) {
|
||||
Text(stringResource(id = R.string.action_shield))
|
||||
|
|
|
@ -2,7 +2,6 @@ package cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel
|
|||
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
|
@ -16,13 +15,11 @@ data class WalletSnapshot(
|
|||
val progress: PercentDecimal,
|
||||
val synchronizerError: SynchronizerError?
|
||||
) {
|
||||
// TODO [#776]: Support variable fees
|
||||
// TODO [#776]: https://github.com/zcash/zcash-android-wallet-sdk/issues/776
|
||||
// Note: the wallet is effectively empty if it cannot cover the miner's fee
|
||||
val hasFunds =
|
||||
saplingBalance.available.value >
|
||||
(ZcashSdk.MINERS_FEE.value.toDouble() / Zatoshi.ZATOSHI_PER_ZEC) // 0.0001
|
||||
val hasSaplingBalance = saplingBalance.total.value > 0
|
||||
// This check is not entirely correct - it does not calculate the resulting fee with the new Proposal API
|
||||
val hasFunds = saplingBalance.available.value > 0L
|
||||
|
||||
val hasSaplingBalance = saplingBalance.total.value > 0L
|
||||
|
||||
val isSendEnabled: Boolean get() = status == Synchronizer.Status.SYNCED && hasFunds
|
||||
}
|
||||
|
|
|
@ -22,12 +22,14 @@ import cash.z.ecc.android.sdk.model.Account
|
|||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.android.sdk.model.Proposal
|
||||
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
|
||||
import cash.z.ecc.android.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.model.proposeSend
|
||||
import cash.z.ecc.android.sdk.model.send
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||
|
@ -50,6 +52,7 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -57,6 +60,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
|
||||
// To make this more multiplatform compatible, we need to remove the dependency on Context
|
||||
// for loading the preferences.
|
||||
@Suppress("TooManyFunctions")
|
||||
class WalletViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val walletCoordinator = WalletCoordinator.getInstance(application)
|
||||
|
||||
|
@ -212,12 +216,36 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously provides proposal object for the given [spendingKey] and [zecSend] objects
|
||||
*/
|
||||
fun getSendProposal(zecSend: ZecSend): Proposal? {
|
||||
if (sendState.value is SendState.Sending) {
|
||||
return null
|
||||
}
|
||||
|
||||
val synchronizer = synchronizer.value
|
||||
|
||||
return if (null != synchronizer) {
|
||||
// Calling the proposal API within a blocking coroutine should be fine for the showcase purpose
|
||||
runBlocking {
|
||||
val spendingKey = spendingKey.filterNotNull().first()
|
||||
kotlin.runCatching {
|
||||
synchronizer.proposeSend(spendingKey, zecSend)
|
||||
}.onFailure {
|
||||
Twig.error(it) { "Failed to get transaction proposal" }
|
||||
}.getOrNull()
|
||||
}
|
||||
} else {
|
||||
error("Unable to send funds because synchronizer is not loaded.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously shields transparent funds. Note that two shielding operations cannot occur at the same time.
|
||||
*
|
||||
* Observe the result via [sendState].
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun shieldFunds() {
|
||||
if (sendState.value is SendState.Sending) {
|
||||
return
|
||||
|
@ -230,6 +258,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
viewModelScope.launch {
|
||||
val spendingKey = spendingKey.filterNotNull().first()
|
||||
kotlin.runCatching {
|
||||
@Suppress("MagicNumber")
|
||||
synchronizer.proposeShielding(spendingKey.account, Zatoshi(100000))?.let {
|
||||
synchronizer.createProposedTransactions(
|
||||
it,
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
@ -43,6 +44,7 @@ import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
|||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import cash.z.ecc.android.sdk.model.Memo
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.Proposal
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.model.ZecSendExt
|
||||
|
@ -60,18 +62,22 @@ private fun ComposablePreview() {
|
|||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
sendState = SendState.None,
|
||||
onSend = {},
|
||||
onBack = {}
|
||||
onGetProposal = {},
|
||||
onBack = {},
|
||||
sendTransactionProposal = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("ktlint:standard:function-naming")
|
||||
@Suppress("ktlint:standard:function-naming", "LongParameterList")
|
||||
fun Send(
|
||||
walletSnapshot: WalletSnapshot,
|
||||
sendState: SendState,
|
||||
onSend: (ZecSend) -> Unit,
|
||||
onBack: () -> Unit
|
||||
onGetProposal: (ZecSend) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
sendTransactionProposal: Proposal?,
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
SendTopAppBar(onBack)
|
||||
|
@ -80,7 +86,9 @@ fun Send(
|
|||
paddingValues = paddingValues,
|
||||
walletSnapshot = walletSnapshot,
|
||||
sendState = sendState,
|
||||
onSend = onSend
|
||||
onSend = onSend,
|
||||
onGetProposal = onGetProposal,
|
||||
sendTransactionProposal = sendTransactionProposal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -105,12 +113,14 @@ private fun SendTopAppBar(onBack: () -> Unit) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod", "ktlint:standard:function-naming")
|
||||
@Suppress("LongMethod", "ktlint:standard:function-naming", "LongParameterList")
|
||||
private fun SendMainContent(
|
||||
paddingValues: PaddingValues,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
sendState: SendState,
|
||||
onSend: (ZecSend) -> Unit
|
||||
onSend: (ZecSend) -> Unit,
|
||||
onGetProposal: (ZecSend) -> Unit,
|
||||
sendTransactionProposal: Proposal?,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val monetarySeparators = MonetarySeparators.current(locale = Locale.US)
|
||||
|
@ -229,6 +239,36 @@ private fun SendMainContent(
|
|||
Text(validation.joinToString(", "))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
val zecSendValidation =
|
||||
ZecSendExt.new(
|
||||
context,
|
||||
recipientAddressString,
|
||||
amountZecString,
|
||||
memoString,
|
||||
monetarySeparators
|
||||
)
|
||||
|
||||
when (zecSendValidation) {
|
||||
is ZecSendExt.ZecSendValidation.Valid -> onGetProposal(zecSendValidation.zecSend)
|
||||
is ZecSendExt.ZecSendValidation.Invalid -> validation = zecSendValidation.validationErrors
|
||||
}
|
||||
},
|
||||
// Needs actual validation
|
||||
enabled = amountZecString.isNotBlank() && recipientAddressString.isNotBlank()
|
||||
) {
|
||||
Text(stringResource(id = R.string.send_proposal_button))
|
||||
}
|
||||
|
||||
if (sendTransactionProposal != null) {
|
||||
Text(stringResource(id = R.string.send_proposal_status, sendTransactionProposal.toPrettyString()))
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
val zecSendValidation =
|
||||
|
|
|
@ -73,6 +73,9 @@
|
|||
<string name="send_memo">Memo</string>
|
||||
<string name="send_button">Send</string>
|
||||
|
||||
<string name="send_proposal_button">Get Proposal</string>
|
||||
<string name="send_proposal_status">Proposal:\n<xliff:g id="proposal" example="Fee:0.001...">%1$s</xliff:g></string>
|
||||
|
||||
<string name="server_textfield_value"><xliff:g id="host" example="example.com">%1$s</xliff:g>:<xliff:g id="port" example="508">%2$d</xliff:g></string>
|
||||
<string name="server_textfield_hint"><host>:<port></string>
|
||||
<string name="server_textfield_error">Invalid server</string>
|
||||
|
|
|
@ -26,7 +26,7 @@ ZCASH_ASCII_GPG_KEY=
|
|||
# Configures whether release is an unstable snapshot, therefore published to the snapshot repository.
|
||||
IS_SNAPSHOT=true
|
||||
|
||||
LIBRARY_VERSION=2.0.7
|
||||
LIBRARY_VERSION=2.0.8
|
||||
|
||||
# Kotlin compiler warnings can be considered errors, failing the build.
|
||||
ZCASH_IS_TREAT_WARNINGS_AS_ERRORS=true
|
||||
|
|
|
@ -18,3 +18,13 @@ suspend fun Synchronizer.send(
|
|||
),
|
||||
spendingKey
|
||||
)
|
||||
|
||||
suspend fun Synchronizer.proposeSend(
|
||||
spendingKey: UnifiedSpendingKey,
|
||||
send: ZecSend
|
||||
) = proposeTransfer(
|
||||
spendingKey.account,
|
||||
send.destination.address,
|
||||
send.amount,
|
||||
send.memo.value
|
||||
)
|
||||
|
|
|
@ -5,7 +5,6 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
|
||||
import cash.z.ecc.android.sdk.WalletInitMode
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.ext.onFirst
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
|
@ -116,7 +115,7 @@ class TestnetIntegrationTest : ScopedTest() {
|
|||
synchronizer.proposeTransfer(
|
||||
spendingKey.account,
|
||||
toAddress,
|
||||
ZcashSdk.MINERS_FEE,
|
||||
Zatoshi(10_000L),
|
||||
"first mainnet tx from the SDK"
|
||||
),
|
||||
spendingKey
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
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.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
|
@ -20,7 +19,7 @@ import org.junit.Test
|
|||
* the same data.
|
||||
*/
|
||||
class TransparentRestoreSample {
|
||||
val txValue = Zatoshi(ZcashSdk.MINERS_FEE.value / 2)
|
||||
val txValue = Zatoshi(Zatoshi(10_000L).value / 2)
|
||||
|
||||
// val walletA = SimpleWallet(SEED_PHRASE, "WalletA")
|
||||
|
||||
|
|
|
@ -8,11 +8,19 @@ import cash.z.ecc.android.sdk.model.Zatoshi
|
|||
* becomes easier to reduce privacy by segmenting the anonymity set of users, particularly as it
|
||||
* relates to network requests.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
object ZcashSdk {
|
||||
/**
|
||||
* Miner's fee in zatoshi.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@Deprecated(
|
||||
message = "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.",
|
||||
replaceWith =
|
||||
ReplaceWith(
|
||||
"proposeTransfer(usk.account, toAddress, amount, memo) OR " +
|
||||
"proposeShielding(usk.account, shieldingThreshold, memo)"
|
||||
)
|
||||
)
|
||||
val MINERS_FEE = Zatoshi(10_000L)
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package cash.z.ecc.android.sdk.fixture
|
||||
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.TransactionState
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
object TransactionOverviewFixture {
|
||||
const val ID: Long = 1
|
||||
val RAW_ID: FirstClassByteArray get() = FirstClassByteArray("rawId".toByteArray())
|
||||
|
@ -16,9 +16,8 @@ object TransactionOverviewFixture {
|
|||
val RAW: FirstClassByteArray get() = FirstClassByteArray("raw".toByteArray())
|
||||
const val IS_SENT_TRANSACTION: Boolean = false
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val NET_VALUE: Zatoshi = Zatoshi(10_000)
|
||||
val FEE_PAID: Zatoshi = ZcashSdk.MINERS_FEE
|
||||
val FEE_PAID: Zatoshi = Zatoshi(10_000)
|
||||
const val IS_CHANGE: Boolean = false
|
||||
const val RECEIVED_NOTE_COUNT: Int = 1
|
||||
const val SENT_NOTE_COUNT: Int = 0
|
||||
|
|
|
@ -51,4 +51,8 @@ class Proposal(
|
|||
fun totalFeeRequired(): Zatoshi {
|
||||
return Zatoshi(inner.totalFeeRequired())
|
||||
}
|
||||
|
||||
fun toPrettyString(): String {
|
||||
return "Transaction count: ${transactionCount()}, Total fee required: ${totalFeeRequired()}"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue