Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
Expand All @@ -30,10 +31,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import org.dashfoundation.dashsdk.funding.ShieldedProver
import org.dashfoundation.example.di.LocalAppContainer
import org.dashfoundation.example.di.LocalAppState
import org.dashfoundation.example.navigation.ShieldedFundProgress
import org.dashfoundation.example.services.shielded.ShieldedFundFromAssetLockCoordinator.StartFundingResult
import org.dashfoundation.example.ui.components.ErrorAlertDialog
Expand All @@ -47,7 +50,9 @@ import org.dashfoundation.example.util.toHex
* Shield funds from an asset lock — port of `ShieldedFundFromAssetLockView.swift`.
* Gated on shielded support ([ShieldedGate]). Shows the Halo 2 prover
* readiness (via [ShieldedProver.isReady], warming it on entry) and the
* consensus-pinned shield fee ([ShieldedProver.estimateFee]).
* consensus-pinned shield fee
* ([org.dashfoundation.dashsdk.wallet.PlatformWalletManager.estimateShieldedFee],
* computed at the manager's network-tracked platform version).
*
* Submit is wired to the real shield FFI: the recipient defaults to the
* wallet's own bound shielded address ("shield to self", via
Expand All @@ -64,6 +69,7 @@ import org.dashfoundation.example.util.toHex
fun ShieldedFundScreen(walletIdHex: String, navController: NavHostController) {
ShieldedGate(navController) {
val container = LocalAppContainer.current
val appState = LocalAppState.current
val walletId = remember(walletIdHex) { walletIdHex.hexToBytes() }
val manager by container.walletManagerStore.activeManager.collectAsStateWithLifecycle()

Expand All @@ -77,10 +83,34 @@ fun ShieldedFundScreen(walletIdHex: String, navController: NavHostController) {
runCatching { ShieldedProver.warmUp() }
value = runCatching { ShieldedProver.isReady() }.getOrDefault(false)
}
val feeEstimate by produceState<Long?>(initialValue = null) {
value = runCatching {
ShieldedProver.estimateFee(ShieldedProver.FeeKind.TransferOrShield, 2)
}.getOrNull()
// Re-keyed on the published protocol version: the SDK learns the
// network's version on a background refresh after the manager
// exists, so an estimate produced before the ratchet completed was
// computed at the seed version (← iOS SendTransactionView
// re-resolves on `platformState.platformProtocolVersion`).
val protocolVersion by appState.platformProtocolVersion.collectAsStateWithLifecycle()
// ...and re-read on every resume, because the flow alone can go
// stale: that refresh republishes the unchanged seed when its
// proven fetch fails, and the SDK independently ratchets its
// version from ANY proof-verified query's response metadata
// without publishing at all. The estimate is a pure handle
// lookup, so re-reading is free.
var feeReadEpoch by remember { mutableIntStateOf(0) }
LifecycleResumeEffect(Unit) {
feeReadEpoch++
onPauseOrDispose {}
}
val feeEstimate by produceState<Long?>(
initialValue = null,
manager,
protocolVersion,
feeReadEpoch,
) {
value = manager?.let { m ->
runCatching {
m.estimateShieldedFee(ShieldedProver.FeeKind.TransferOrShield, 2)
}.getOrNull()
}
}

// Default "shield to self" recipient — the wallet's bound shielded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
Expand All @@ -42,6 +43,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import kotlinx.coroutines.delay
Expand Down Expand Up @@ -127,7 +129,8 @@ private const val MEMO_BYTE_LIMIT = 32
*
* The shielded flows settle in credits (1 DASH = 1e11) and block through a
* ~30s Halo 2 proof; their consensus-pinned fee comes from
* [ShieldedProver.estimateFee]. A `ShieldedSpendUnconfirmed` outcome is
* `PlatformWalletManager.estimateShieldedFee` (computed at the manager's
* network-tracked platform version). A `ShieldedSpendUnconfirmed` outcome is
* surfaced through the SUCCESS path ("may have gone through — do not
* retry"), mirroring iOS SendViewModel.swift:790.
*/
Expand Down Expand Up @@ -288,25 +291,45 @@ fun SendTransactionScreen(
}
}
}
val shieldedFeeEstimate by produceState<Long?>(initialValue = null, flow) {
value = when (flow) {
// Re-key on the published protocol version too: the SDK learns the
// network's version on a background refresh after the manager exists,
// so an estimate produced before the ratchet completed was computed at
// the seed version (← iOS SendTransactionView re-resolves on
// `platformState.platformProtocolVersion` the same way).
val protocolVersion by appState.platformProtocolVersion.collectAsStateWithLifecycle()
// ...and re-read on every resume, because the flow alone can go stale:
// that refresh republishes the unchanged seed when its proven fetch
// fails, and the SDK independently ratchets its version from ANY
// proof-verified query's response metadata without publishing at all.
// The estimate is a pure handle lookup, so re-reading is free.
var feeReadEpoch by remember { mutableIntStateOf(0) }
LifecycleResumeEffect(Unit) {
feeReadEpoch++
onPauseOrDispose {}
}
val shieldedFeeEstimate by produceState<Long?>(
initialValue = null,
flow,
manager,
protocolVersion,
feeReadEpoch,
) {
val activeManager = manager
val kind = when (flow) {
// Type 15 Shield reserves the same compute_minimum_shielded_fee(2)
// base as a shielded→shielded transfer (← iOS estimateFee: .transfer
// for .platformToShielded), so they share the TransferOrShield kind.
SendFlow.SHIELDED_TO_SHIELDED, SendFlow.PLATFORM_TO_SHIELDED ->
runCatching {
ShieldedProver.estimateFee(ShieldedProver.FeeKind.TransferOrShield, 2)
}.getOrNull()
SendFlow.SHIELDED_TO_PLATFORM ->
runCatching {
ShieldedProver.estimateFee(ShieldedProver.FeeKind.Unshield, 2)
}.getOrNull()
SendFlow.SHIELDED_TO_CORE ->
runCatching {
ShieldedProver.estimateFee(ShieldedProver.FeeKind.Withdrawal, 2)
}.getOrNull()
ShieldedProver.FeeKind.TransferOrShield
SendFlow.SHIELDED_TO_PLATFORM -> ShieldedProver.FeeKind.Unshield
SendFlow.SHIELDED_TO_CORE -> ShieldedProver.FeeKind.Withdrawal
SendFlow.CORE_TO_CORE, null -> null
}
value = if (activeManager != null && kind != null) {
runCatching { activeManager.estimateShieldedFee(kind, 2) }.getOrNull()
} else {
null
}
}

val recipientKnown = addressType != DashAddressType.Unknown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ package org.dashfoundation.dashsdk.ffi
* mirrors `rs-unified-sdk-jni/src/funding.rs`.
*
* Internal: the public API is
* [org.dashfoundation.dashsdk.funding.ShieldedProver]. These entry points
* are process-global (no wallet handle) and back the shielded funding
* [org.dashfoundation.dashsdk.funding.ShieldedProver] for the
* process-global prover probes and
* [org.dashfoundation.dashsdk.wallet.PlatformWalletManager.estimateShieldedFee]
* for the (manager-handle) fee estimator. They back the shielded funding
* screens' prover-status indicator and fee preview. Available only when
* the native library was built with shielded support
* ([org.dashfoundation.dashsdk.Sdk.hasShielded]); calling them on a
Expand All @@ -24,10 +26,11 @@ internal object FundingNative {
/**
* The flat shielded fee in credits for a transition of [kind]
* (0 = ShieldedTransfer/Shield, 1 = Unshield, 2 = ShieldedWithdrawal)
* and Orchard action count [numActions]. Pure computation; throws on
* an unknown kind or overflow.
* and Orchard action count [numActions], computed at [managerHandle]'s
* network-tracked platform version. No network round-trip; throws on
* an unknown kind, an invalid manager handle, or overflow.
*/
external fun estimateShieldedFee(kind: Int, numActions: Int): Long
external fun estimateShieldedFee(managerHandle: Long, kind: Int, numActions: Int): Long

// ── Shielded funding submits (manager-handle calls) ──────────────

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@ import org.dashfoundation.dashsdk.errors.mapNativeErrors
import org.dashfoundation.dashsdk.ffi.FundingNative

/**
* Thin wrapper over the shielded-funding support JNI surface
* (`FundingNative`) — the Halo 2 prover warm-up / readiness probe and the
* shielded fee estimator that back the shielded funding screens'
* prover-status indicator and fee preview.
* Thin wrapper over the process-global shielded-funding support JNI
* surface (`FundingNative`) — the Halo 2 prover warm-up / readiness probe
* that backs the shielded funding screens' prover-status indicator.
*
* Process-global (no wallet handle). Only meaningful on a shielded build
* The shielded fee estimator lives on
* [org.dashfoundation.dashsdk.wallet.PlatformWalletManager.estimateShieldedFee]
* (it resolves the manager's network-tracked platform version, so it needs
* the manager handle); only its [FeeKind] selector is declared here.
*
* Only meaningful on a shielded build
* ([org.dashfoundation.dashsdk.Sdk.hasShielded]); the caller must gate on
* that before use.
*/
object ShieldedProver {

/** Fee-kind selector for [estimateFee]. */
/**
* Fee-kind selector for
* [org.dashfoundation.dashsdk.wallet.PlatformWalletManager.estimateShieldedFee].
*/
enum class FeeKind(val raw: Int) {
/** ShieldedTransfer / Shield (the base flat fee). */
TransferOrShield(0),
Expand All @@ -38,13 +45,4 @@ object ShieldedProver {
suspend fun isReady(): Boolean = withContext(Dispatchers.IO) {
mapNativeErrors { FundingNative.proverIsReady() }
}

/**
* The flat shielded fee in credits for a transition of [kind] and
* Orchard action count [numActions] (a single-note spend with change
* is 2 actions).
*/
suspend fun estimateFee(kind: FeeKind, numActions: Int): Long = withContext(Dispatchers.IO) {
mapNativeErrors { FundingNative.estimateShieldedFee(kind.raw, numActions) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.dashfoundation.dashsdk.ffi.DpnsMarketplaceNative
import org.dashfoundation.dashsdk.ffi.FundingNative
import org.dashfoundation.dashsdk.ffi.NativeWalletEventBridge
import org.dashfoundation.dashsdk.ffi.WalletManagerNative
import org.dashfoundation.dashsdk.funding.ShieldedProver
import org.dashfoundation.dashsdk.persistence.DashDatabase
import org.dashfoundation.dashsdk.persistence.PlatformWalletPersistenceHandler
import org.dashfoundation.dashsdk.persistence.entities.DashpayPaymentEntity
Expand Down Expand Up @@ -1444,6 +1445,27 @@ class PlatformWalletManager(
}
}

/**
* Consensus-pinned flat shielded fee (in credits) for a pool-paid
* shielded transition of [kind] with [numActions] Orchard actions —
* port of Swift's `PlatformWalletManager.estimateShieldedFee`
* (`PlatformWalletManagerShieldedSync.swift`). Computed at this
* manager's network-tracked platform version (the same version the
* shielded builders carve fees with), so the preview can't drift from
* the fee the consensus gate validates even when the connected network
* hasn't activated the client's latest protocol version yet. No
* network round-trip. A single-note spend with change is
* `numActions = 2`.
*/
suspend fun estimateShieldedFee(
kind: ShieldedProver.FeeKind,
numActions: Int = 2,
): Long = withContext(Dispatchers.IO) {
mapNativeErrors {
FundingNative.estimateShieldedFee(managerHandle, kind.raw, numActions)
}
}

/**
* Fund a wallet's shielded (Orchard) pool from a fresh Core L1 asset
* lock — port of Swift's `shieldedFundFromAssetLock`. Blocks for the
Expand Down
Loading
Loading