Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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 @@ -176,6 +176,55 @@ sealed class DashSdkError(
cause,
)

/**
* A state transition could not be signed because the signer has no
* usable private key for the requested public key — the stored blob
* is missing (never derived, wiped, or written under a different
* Keystore alias/policy) rather than the operation itself failing.
*
* Signing failures originate as free text in
* `KeystoreSigner.completeSign` (the "[MESSAGE_MARKER] <pubkeyHex>"
* string), travel through Rust, and come back under the catch-all
* platform-wallet codes; [fromPlatformWalletNative] recognizes the
* marker on the Kotlin boundary and surfaces this typed error so
* hosts can route users to key repair (e.g.
* `PlatformWalletManager.repairIdentityKey`) instead of treating it
* as an opaque [Generic] failure. Not retryable as-is — the key must
* be (re-)derived first.
*/
class SigningKeyUnavailable(message: String, cause: Throwable? = null) :
PlatformWallet(message, cause) {
companion object {
/**
* Stable prefix of the `KeystoreSigner` "missing key"
* completion error. `KeystoreSigner` builds its message from
* this constant, so the emitter and the matcher cannot
* drift.
*/
const val MESSAGE_MARKER = "no private key stored for"
}
}

/**
* `PlatformWalletFFIResultCode::NotFound` (native code 98,
* [PLATFORM_WALLET_NOT_FOUND_CODE]) — the code the FFI's blanket
* `Option → result` conversion emits for every "requested <thing>
* not found" miss (an unknown wallet id, an identity the wallet
* does not manage, …). Typed inside the wallet-error family —
* parity with Swift's `PlatformWalletError.notFound`, which also
* keeps 98 in the wallet family — so callers can match a
* wallet-level absence without sniffing [Generic] codes, while
* staying distinct from the rs-sdk-ffi top-level
* [DashSdkError.NotFound] that codes 7/8 map to.
*
* Dashpay's managed-identity local reads never see this type:
* `translateManagedIdentityNotFoundToZero` intercepts the RAW
* code (offset + 98) on the [org.dashfoundation.dashsdk.ffi.DashSDKException]
* before [fromNative] runs and turns the miss into an absence.
*/
class NotFound(message: String, cause: Throwable? = null) :
PlatformWallet(message, cause)

/**
* Any other `PlatformWalletFFIResultCode` without a dedicated type.
* Carries the platform-wallet [nativeCode] (already de-offset) and
Expand Down Expand Up @@ -218,12 +267,24 @@ sealed class DashSdkError(
}
}

/**
* `PlatformWalletFFIResultCode::NotFound` (98) — the code the FFI's
* blanket `Option → result` conversion emits for every "requested
* <thing> not found" miss (e.g. an identity id that is not managed
* by the wallet). Mapped to the typed [PlatformWallet.NotFound].
*/
const val PLATFORM_WALLET_NOT_FOUND_CODE = 98

/**
* Map a de-offset `PlatformWalletFFIResultCode` value into the
* [PlatformWallet] subtree — mirror of Swift's
* `PlatformWalletError(result:)` construction. Retry-semantics-bearing
* codes get dedicated types; the rest fall through to
* [PlatformWallet.Generic].
* [PlatformWallet.Generic] — except the `KeystoreSigner` "missing
* key" completion error, which travels as free text through Rust and
* is recognized by its [PlatformWallet.SigningKeyUnavailable]
* message marker here (only on the catch-all codes, so the dedicated
* retry-semantics types are never overridden).
*/
private fun fromPlatformWalletNative(
code: Int,
Expand All @@ -232,11 +293,25 @@ sealed class DashSdkError(
): DashSdkError = when (code) {
// PlatformWalletFFIResultCode variants (platform-wallet-ffi/src/error.rs)
1 -> PlatformWallet.InvalidHandle(message, cause) // ErrorInvalidHandle
6 -> PlatformWallet.WalletOperation(message, cause) // ErrorWalletOperation
6 -> // ErrorWalletOperation
if (isSigningKeyUnavailable(message)) {
PlatformWallet.SigningKeyUnavailable(message, cause)
} else {
PlatformWallet.WalletOperation(message, cause)
}
7, // ErrorIdentityNotFound
8, // ErrorContactNotFound
98, // NotFound (Option returned as an error)
-> NotFound(message, cause)
// 98 (PlatformWalletFFIResultCode::NotFound, the blanket Option →
// result miss) stays inside the wallet-error family as the typed
// PlatformWallet.NotFound — exact Swift parity
// (PlatformWalletError.notFound) — rather than collapsing into the
// top-level NotFound that rs-sdk-ffi codes 7/8 map to. Dashpay's
// managed-identity local reads are unaffected: they intercept the
// RAW code via translateManagedIdentityNotFoundToZero (#4051)
// before this mapping ever runs.
PLATFORM_WALLET_NOT_FOUND_CODE ->
PlatformWallet.NotFound(message, cause)
Comment on lines +313 to +314

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Code 98 still breaks the existing NotFound subtype contract

Before this PR, the shared PlatformWalletFFIResultCode::NotFound value mapped to the public top-level DashSdkError.NotFound. The current mapping instead constructs the unrelated sibling DashSdkError.PlatformWallet.NotFound, and the updated boundary test explicitly confirms that it is not a DashSdkError.NotFound. Existing applications that catch or branch on the established subtype will therefore stop recognizing code-98 absence results from APIs such as rescanSpvFilters. Dashpay does not require this global behavior change because translateManagedIdentityNotFoundToZero intercepts its raw managed-identity code before fromNative runs. Preserve the top-level mapping for code 98 and keep the Dashpay-specific absence translation local.

Suggested change
PLATFORM_WALLET_NOT_FOUND_CODE ->
PlatformWallet.NotFound(message, cause)
PLATFORM_WALLET_NOT_FOUND_CODE ->
NotFound(message, cause)

source: ['codex']

16 -> PlatformWallet.ShieldedBroadcastFailed(message, cause) // ErrorShieldedBroadcastFailed
18 -> PlatformWallet.ShieldedSpendUnconfirmed(message, cause) // ErrorShieldedSpendUnconfirmed
19 -> PlatformWallet.ShieldedNoRecordedAnchor(message, cause) // ErrorShieldedNoRecordedAnchor
Expand All @@ -245,8 +320,16 @@ sealed class DashSdkError(
23 -> PlatformWallet.AssetLockNotTracked(message, cause) // ErrorAssetLockNotTracked
24 -> PlatformWallet.AssetLockAlreadyConsumed(message, cause) // ErrorAssetLockAlreadyConsumed
25 -> PlatformWallet.AssetLockFundingMismatch(message, cause) // ErrorAssetLockFundingMismatch
else -> PlatformWallet.Generic(code, message, cause)
else ->
if (isSigningKeyUnavailable(message)) {
PlatformWallet.SigningKeyUnavailable(message, cause)
} else {
PlatformWallet.Generic(code, message, cause)
}
}

private fun isSigningKeyUnavailable(message: String): Boolean =
message.contains(PlatformWallet.SigningKeyUnavailable.MESSAGE_MARKER)
}
}

Expand Down
Loading
Loading