[#1285] Adopt proposal API
* Adopt Zcash SDK v2.0.8-SNAPSHOT * [#1285] Adopt proposal API - Closes #1285 - Manually tested and the updated send and shield features work as expected
This commit is contained in:
parent
8e495a542f
commit
fe5236fdae
|
@ -189,7 +189,7 @@ ZCASH_BIP39_VERSION=1.0.7
|
|||
ZXING_VERSION=3.5.2
|
||||
|
||||
# WARNING: Ensure a non-snapshot version is used before releasing to production.
|
||||
ZCASH_SDK_VERSION=2.0.7
|
||||
ZCASH_SDK_VERSION=2.0.8-SNAPSHOT
|
||||
|
||||
# Toolchain is the Java version used to build the application, which is separate from the
|
||||
# Java version used to run the application.
|
||||
|
|
|
@ -2,6 +2,7 @@ package cash.z.ecc.sdk.fixture
|
|||
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.Memo
|
||||
import cash.z.ecc.android.sdk.model.Proposal
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
|
@ -13,9 +14,13 @@ object ZecSendFixture {
|
|||
val AMOUNT = Zatoshi(123)
|
||||
val MEMO = MemoFixture.new()
|
||||
|
||||
// Null until we figure out how to proper test this
|
||||
val PROPOSAL = null
|
||||
|
||||
suspend fun new(
|
||||
address: String = ADDRESS,
|
||||
amount: Zatoshi = AMOUNT,
|
||||
memo: Memo = MEMO
|
||||
) = ZecSend(WalletAddress.Unified.new(address), amount, memo)
|
||||
memo: Memo = MEMO,
|
||||
proposal: Proposal? = PROPOSAL
|
||||
) = ZecSend(WalletAddress.Unified.new(address), amount, memo, proposal)
|
||||
}
|
||||
|
|
|
@ -120,8 +120,8 @@ class SendViewTestSetup(
|
|||
),
|
||||
sendStage = sendStage,
|
||||
onSendStageChange = setSendStage,
|
||||
onCreateZecSend = setZecSend,
|
||||
zecSend = zecSend,
|
||||
onZecSendChange = setZecSend,
|
||||
focusManager = LocalFocusManager.current,
|
||||
onBack = onBackAction,
|
||||
onSettings = { onSettingsCount.incrementAndGet() },
|
||||
|
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.runBlocking
|
|||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.Locale
|
||||
import kotlin.test.Ignore
|
||||
|
||||
class SendViewIntegrationTest {
|
||||
@get:Rule
|
||||
|
@ -52,8 +53,11 @@ class SendViewIntegrationTest {
|
|||
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
|
||||
private val monetarySeparators = MonetarySeparators.current(Locale.US)
|
||||
|
||||
// TODO [#1260]: Cover Send screens UI with tests
|
||||
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("Disabled as the entire Send flow will be reworked and the test align after it")
|
||||
fun send_screens_values_state_restoration() {
|
||||
val restorationTester = StateRestorationTester(composeTestRule)
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package co.electriccoin.zcash.ui.common.model
|
|||
|
||||
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
|
||||
|
@ -19,16 +18,17 @@ data class WalletSnapshot(
|
|||
val progress: PercentDecimal,
|
||||
val synchronizerError: SynchronizerError?
|
||||
) {
|
||||
// Note that both [hasSaplingFunds] and [hasTransparentFunds] checks are not entirely correct - they do not
|
||||
// calculate the resulting fee using the new Proposal API. It's fine for now, but it's subject to improvement
|
||||
// later once we figure out how to handle it in such cases.
|
||||
|
||||
// Note: the wallet is effectively empty if it cannot cover the miner's fee
|
||||
val hasSaplingFunds =
|
||||
saplingBalance.available.value >
|
||||
(ZcashSdk.MINERS_FEE.value.toDouble() / Zatoshi.ZATOSHI_PER_ZEC) // 0.00001
|
||||
val hasSaplingBalance = saplingBalance.total.value > 0
|
||||
val hasSaplingFunds = saplingBalance.available.value > 0L
|
||||
|
||||
val hasSaplingBalance = saplingBalance.total.value > 0L
|
||||
|
||||
// Note: the wallet's transparent balance is effectively empty if it cannot cover the miner's fee
|
||||
val hasTransparentFunds =
|
||||
transparentBalance.value >
|
||||
(ZcashSdk.MINERS_FEE.value.toDouble() / Zatoshi.ZATOSHI_PER_ZEC) // 0.00001
|
||||
val hasTransparentFunds = transparentBalance.value > 0L
|
||||
|
||||
val isSendEnabled: Boolean get() = status == Synchronizer.Status.SYNCED && hasSaplingFunds
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
|
||||
|
@ -59,9 +60,11 @@ internal fun WrapBalances(
|
|||
)
|
||||
}
|
||||
|
||||
const val DEFAULT_SHIELDING_THRESHOLD = 100000L
|
||||
|
||||
@Composable
|
||||
@VisibleForTesting
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
internal fun WrapBalances(
|
||||
goSettings: () -> Unit,
|
||||
checkUpdateViewModel: CheckUpdateViewModel,
|
||||
|
@ -95,6 +98,17 @@ internal fun WrapBalances(
|
|||
|
||||
val (isShowingErrorDialog, setShowErrorDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
suspend fun showShieldingError(error: Throwable?) {
|
||||
Twig.error { "Shielding proposal failed with: $error" }
|
||||
|
||||
// Adding the extra delay before notifying UI for a better UX
|
||||
@Suppress("MagicNumber")
|
||||
delay(1500)
|
||||
|
||||
setShieldState(ShieldState.Failed(error?.message ?: ""))
|
||||
setShowErrorDialog(true)
|
||||
}
|
||||
|
||||
if (null == synchronizer || null == walletSnapshot || null == spendingKey) {
|
||||
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
|
||||
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
|
||||
|
@ -113,25 +127,41 @@ internal fun WrapBalances(
|
|||
setShieldState(ShieldState.Running)
|
||||
|
||||
Twig.debug { "Shielding transparent funds" }
|
||||
// Using empty string for memo to clear the default memo prefix value defined in the SDK
|
||||
|
||||
runCatching {
|
||||
// TODO [#1285]: Adopt proposal API
|
||||
// TODO [#1285]: https://github.com/Electric-Coin-Company/zashi-android/issues/1285
|
||||
@Suppress("deprecation")
|
||||
synchronizer.shieldFunds(spendingKey, "")
|
||||
synchronizer.proposeShielding(
|
||||
account = spendingKey.account,
|
||||
shieldingThreshold = Zatoshi(DEFAULT_SHIELDING_THRESHOLD),
|
||||
// Using empty string for memo to clear the default memo prefix value defined in the SDK
|
||||
memo = "",
|
||||
// Using null will select whichever of the account's trans. receivers has funds to shield
|
||||
transparentReceiver = null
|
||||
)
|
||||
}.onSuccess { newProposal ->
|
||||
Twig.debug { "Shielding proposal result: ${newProposal?.toPrettyString()}" }
|
||||
|
||||
if (newProposal == null) {
|
||||
showShieldingError(null)
|
||||
} else {
|
||||
// TODO [#1294]: Add Send.Multiple-Trx-Failed screen
|
||||
// TODO [#1294]: Note that the following processing is not entirely correct and will be
|
||||
// reworked
|
||||
// TODO [#1294]: https://github.com/Electric-Coin-Company/zashi-android/issues/1294
|
||||
runCatching {
|
||||
synchronizer.createProposedTransactions(
|
||||
proposal = newProposal,
|
||||
usk = spendingKey
|
||||
)
|
||||
}.onSuccess {
|
||||
Twig.debug { "Shielding transaction event" }
|
||||
setShieldState(ShieldState.None)
|
||||
}.onFailure {
|
||||
showShieldingError(null)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
showShieldingError(it)
|
||||
}
|
||||
.onSuccess {
|
||||
Twig.info { "Shielding transaction id:$it submitted successfully" }
|
||||
setShieldState(ShieldState.None)
|
||||
}
|
||||
.onFailure {
|
||||
Twig.error(it) { "Shielding transaction submission failed with: ${it.message}" }
|
||||
// Adding extra delay before notifying UI for a better UX
|
||||
@Suppress("MagicNumber")
|
||||
delay(1500)
|
||||
setShieldState(ShieldState.Failed(it.message ?: ""))
|
||||
setShowErrorDialog(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
shieldState = shieldState,
|
||||
|
|
|
@ -19,8 +19,8 @@ import cash.z.ecc.android.sdk.Synchronizer
|
|||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.model.proposeSend
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import cash.z.ecc.sdk.extension.send
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
|
||||
|
@ -193,7 +193,25 @@ internal fun WrapSend(
|
|||
sendStage = sendStage,
|
||||
onSendStageChange = setSendStage,
|
||||
zecSend = zecSend,
|
||||
onZecSendChange = setZecSend,
|
||||
onCreateZecSend = { newZecSend ->
|
||||
scope.launch {
|
||||
Twig.debug { "Getting send transaction proposal" }
|
||||
runCatching {
|
||||
synchronizer.proposeSend(spendingKey.account, newZecSend)
|
||||
}
|
||||
.onSuccess { proposal ->
|
||||
Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" }
|
||||
setSendStage(SendStage.Confirmation)
|
||||
setZecSend(newZecSend.copy(proposal = proposal))
|
||||
}
|
||||
.onFailure {
|
||||
Twig.error(it) { "Transaction proposal failed" }
|
||||
// TODO [#1161]: Remove Send-Success and rework Send-Failure
|
||||
// TODO [#1161]: https://github.com/Electric-Coin-Company/zashi-android/issues/1161
|
||||
setSendStage(SendStage.SendFailure(it.message ?: ""))
|
||||
}
|
||||
}
|
||||
},
|
||||
focusManager = focusManager,
|
||||
onBack = onBackAction,
|
||||
onSettings = goSettings,
|
||||
|
@ -210,16 +228,26 @@ internal fun WrapSend(
|
|||
)
|
||||
}
|
||||
},
|
||||
onCreateAndSend = {
|
||||
onCreateAndSend = { newZecSend ->
|
||||
scope.launch {
|
||||
Twig.debug { "Sending transaction" }
|
||||
runCatching { synchronizer.send(spendingKey, it) }
|
||||
// TODO [#1294]: Add Send.Multiple-Trx-Failed screen
|
||||
// TODO [#1294]: Note that the following processing is not entirely correct and will be reworked
|
||||
// TODO [#1294]: https://github.com/Electric-Coin-Company/zashi-android/issues/1294
|
||||
runCatching {
|
||||
// The not-null assertion operator is necessary here even if we check its nullability before
|
||||
// due to: "Smart cast to 'Proposal' is impossible, because 'zecSend.proposal' is a public API
|
||||
// property declared in different module
|
||||
// See more details on the Kotlin forum
|
||||
checkNotNull(newZecSend.proposal)
|
||||
synchronizer.createProposedTransactions(newZecSend.proposal!!, spendingKey)
|
||||
}
|
||||
.onSuccess {
|
||||
setSendStage(SendStage.SendSuccessful)
|
||||
Twig.debug { "Transaction id:$it submitted successfully" }
|
||||
}
|
||||
.onFailure {
|
||||
Twig.debug { "Transaction submission failed with: $it." }
|
||||
Twig.error(it) { "Transaction submission failed" }
|
||||
setSendStage(SendStage.SendFailure(it.message ?: ""))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.screen.send.ext
|
|||
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import cash.z.ecc.android.sdk.model.Memo
|
||||
import cash.z.ecc.android.sdk.model.Proposal
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
|
@ -10,6 +11,7 @@ import kotlinx.coroutines.runBlocking
|
|||
private const val KEY_ADDRESS = "address" // $NON-NLS
|
||||
private const val KEY_AMOUNT = "amount" // $NON-NLS
|
||||
private const val KEY_MEMO = "memo" // $NON-NLS
|
||||
private const val KEY_PROPOSAL = "proposal" // $NON-NLS
|
||||
|
||||
// Using a custom saver instead of Parcelize, to avoid adding an Android-specific API to
|
||||
// the ZecSend class
|
||||
|
@ -27,7 +29,11 @@ internal val ZecSend.Companion.Saver
|
|||
val address = runBlocking { WalletAddress.Unified.new(it[KEY_ADDRESS] as String) }
|
||||
val amount = Zatoshi(it[KEY_AMOUNT] as Long)
|
||||
val memo = Memo(it[KEY_MEMO] as String)
|
||||
ZecSend(address, amount, memo)
|
||||
val proposal =
|
||||
it[KEY_PROPOSAL]?.let { data ->
|
||||
Proposal.fromByteArray(data as ByteArray)
|
||||
}
|
||||
ZecSend(address, amount, memo, proposal)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -38,4 +44,7 @@ private fun ZecSend.toSaverMap() =
|
|||
put(KEY_ADDRESS, destination.address)
|
||||
put(KEY_AMOUNT, amount.value)
|
||||
put(KEY_MEMO, memo.value)
|
||||
proposal?.let {
|
||||
put(KEY_PROPOSAL, it.toByteArray())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,6 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.model.Memo
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
|
@ -88,7 +87,7 @@ private fun PreviewSendForm() {
|
|||
sendStage = SendStage.Form,
|
||||
onSendStageChange = {},
|
||||
zecSend = null,
|
||||
onZecSendChange = {},
|
||||
onCreateZecSend = {},
|
||||
focusManager = LocalFocusManager.current,
|
||||
onBack = {},
|
||||
onSettings = {},
|
||||
|
@ -117,7 +116,8 @@ private fun PreviewSendSuccessful() {
|
|||
ZecSend(
|
||||
destination = runBlocking { WalletAddressFixture.sapling() },
|
||||
amount = ZatoshiFixture.new(),
|
||||
memo = MemoFixture.new()
|
||||
memo = MemoFixture.new(),
|
||||
proposal = null,
|
||||
),
|
||||
onDone = {}
|
||||
)
|
||||
|
@ -135,7 +135,8 @@ private fun PreviewSendFailure() {
|
|||
ZecSend(
|
||||
destination = runBlocking { WalletAddressFixture.sapling() },
|
||||
amount = ZatoshiFixture.new(),
|
||||
memo = MemoFixture.new()
|
||||
memo = MemoFixture.new(),
|
||||
proposal = null,
|
||||
),
|
||||
onDone = {},
|
||||
reason = "Insufficient balance"
|
||||
|
@ -154,7 +155,8 @@ private fun PreviewSendConfirmation() {
|
|||
ZecSend(
|
||||
destination = runBlocking { WalletAddressFixture.sapling() },
|
||||
amount = ZatoshiFixture.new(),
|
||||
memo = MemoFixture.new()
|
||||
memo = MemoFixture.new(),
|
||||
proposal = null,
|
||||
),
|
||||
onConfirmation = {}
|
||||
)
|
||||
|
@ -169,7 +171,7 @@ fun Send(
|
|||
sendStage: SendStage,
|
||||
onSendStageChange: (SendStage) -> Unit,
|
||||
zecSend: ZecSend?,
|
||||
onZecSendChange: (ZecSend) -> Unit,
|
||||
onCreateZecSend: (ZecSend) -> Unit,
|
||||
focusManager: FocusManager,
|
||||
onBack: () -> Unit,
|
||||
onSettings: () -> Unit,
|
||||
|
@ -198,7 +200,7 @@ fun Send(
|
|||
sendStage = sendStage,
|
||||
onSendStageChange = onSendStageChange,
|
||||
zecSend = zecSend,
|
||||
onZecSendChange = onZecSendChange,
|
||||
onCreateZecSend = onCreateZecSend,
|
||||
recipientAddressState = recipientAddressState,
|
||||
onRecipientAddressChange = onRecipientAddressChange,
|
||||
amountState = amountState,
|
||||
|
@ -259,7 +261,7 @@ private fun SendMainContent(
|
|||
onBack: () -> Unit,
|
||||
goBalances: () -> Unit,
|
||||
zecSend: ZecSend?,
|
||||
onZecSendChange: (ZecSend) -> Unit,
|
||||
onCreateZecSend: (ZecSend) -> Unit,
|
||||
sendStage: SendStage,
|
||||
onSendStageChange: (SendStage) -> Unit,
|
||||
onSendSubmit: (ZecSend) -> Unit,
|
||||
|
@ -283,10 +285,7 @@ private fun SendMainContent(
|
|||
setAmountState = setAmountState,
|
||||
memoState = memoState,
|
||||
setMemoState = setMemoState,
|
||||
onCreateZecSend = {
|
||||
onSendStageChange(SendStage.Confirmation)
|
||||
onZecSendChange(it)
|
||||
},
|
||||
onCreateZecSend = onCreateZecSend,
|
||||
focusManager = focusManager,
|
||||
onQrScannerOpen = onQrScannerOpen,
|
||||
goBalances = goBalances,
|
||||
|
@ -334,7 +333,6 @@ private fun SendMainContent(
|
|||
// TODO [#1257]: Send.Form TextFields not persisted on a configuration change when the underlying ViewPager is on the
|
||||
// Balances page
|
||||
// TODO [#1257]: https://github.com/Electric-Coin-Company/zashi-android/issues/1257
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongMethod", "LongParameterList")
|
||||
@Composable
|
||||
private fun SendForm(
|
||||
|
@ -436,7 +434,7 @@ private fun SendForm(
|
|||
recipientAddressState.address.isNotEmpty() &&
|
||||
amountState is AmountState.Valid &&
|
||||
amountState.value.isNotBlank() &&
|
||||
walletSnapshot.spendableBalance() >= (amountState.zatoshi + ZcashSdk.MINERS_FEE) &&
|
||||
walletSnapshot.spendableBalance() >= amountState.zatoshi &&
|
||||
// A valid memo is necessary only for non-transparent recipient
|
||||
(recipientAddressState.type == AddressType.Transparent || memoState is MemoState.Correct)
|
||||
|
||||
|
@ -601,7 +599,7 @@ fun SendFormAmountTextField(
|
|||
}
|
||||
}
|
||||
is AmountState.Valid -> {
|
||||
if (walletSnapshot.spendableBalance() < (amountSate.zatoshi + ZcashSdk.MINERS_FEE)) {
|
||||
if (walletSnapshot.spendableBalance() < amountSate.zatoshi) {
|
||||
stringResource(id = R.string.send_amount_insufficient_balance)
|
||||
} else {
|
||||
null
|
||||
|
@ -775,8 +773,13 @@ private fun SendConfirmation(
|
|||
) {
|
||||
Body(
|
||||
stringResource(
|
||||
R.string.send_confirmation_amount_and_address_format,
|
||||
R.string.send_confirmation_amount_format,
|
||||
zecSend.amount.toZecString(),
|
||||
)
|
||||
)
|
||||
Body(
|
||||
stringResource(
|
||||
R.string.send_confirmation_address_format,
|
||||
zecSend.destination.abbreviated()
|
||||
)
|
||||
)
|
||||
|
@ -788,6 +791,19 @@ private fun SendConfirmation(
|
|||
)
|
||||
)
|
||||
}
|
||||
if (zecSend.proposal != null) {
|
||||
// The not-null assertion operator is necessary here even if we check its nullability before
|
||||
// due to: "Smart cast to 'Proposal' is impossible, because 'zecSend.proposal' is a public API
|
||||
// property declared in different module
|
||||
// See more details on the Kotlin forum
|
||||
checkNotNull(zecSend.proposal)
|
||||
Body(
|
||||
stringResource(
|
||||
R.string.send_confirmation_fee_format,
|
||||
zecSend.proposal!!.totalFeeRequired().toZecString()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier =
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
<string name="send_create">Review</string>
|
||||
<string name="send_fee">(Typical Fee < <xliff:g id="fee_amount" example="0.001">%1$s</xliff:g>)</string>
|
||||
|
||||
<string name="send_confirmation_amount_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string>
|
||||
<string name="send_confirmation_amount_format" formatted="true">Send: <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC</string>
|
||||
<string name="send_confirmation_address_format" formatted="true">To: <xliff:g id="address" example="zs1g7cqw … mvyzgm">%1$s</xliff:g>?</string>
|
||||
<string name="send_confirmation_memo_format" formatted="true">Memo: <xliff:g id="memo" example="for Veronika">%1$s</xliff:g></string>
|
||||
<string name="send_confirmation_fee_format" formatted="true">Fee: <xliff:g id="fee" example="0.0001">%1$s</xliff:g></string>
|
||||
<string name="send_confirmation_abbreviated_address_format" formatted="true"><xliff:g id="first_five" example="zs1g7">%1$s</xliff:g>…<xliff:g id="last_five" example="mvyzg">%2$s</xliff:g></string>
|
||||
<string name="send_confirmation_button">Press to send ZEC</string>
|
||||
|
||||
|
|
Loading…
Reference in New Issue