Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
115a081
fix(wallet): drain Core balance with authenticated shielded recovery
llbartekll Oct 12, 2026
f0a115e
fix(platform-wallet): reconcile drain recovery safely after upstream …
llbartekll Oct 13, 2026
186673e
fix(platform-wallet): reject conflicting persisted drain operation IDs
llbartekll Oct 13, 2026
ac79e15
chore(platform-wallet): integrate beta.6 persistence fixtures
llbartekll Oct 13, 2026
214b20b
fix(platform-wallet): make drain recovery and history durable
llbartekll Oct 13, 2026
1cca55e
chore(platform-wallet): integrate current v5.0 development branch
llbartekll Oct 13, 2026
52d6c56
fix(platform-wallet): close drain cancellation and restore gaps
llbartekll Oct 13, 2026
e3354a6
fix(platform-wallet): publish drain outcomes without cancellation gaps
llbartekll Oct 13, 2026
24d9e32
fix(platform-wallet): hydrate durable drains and honor inline commits
llbartekll Oct 13, 2026
0ae730d
fix(platform-wallet): release committed rejections and bound drain re…
llbartekll Oct 13, 2026
7201b08
fix(platform-wallet): retry unpublished drain checkpoints
llbartekll Oct 14, 2026
89a9423
fix(platform-wallet): gate drain completion on a dedicated proven status
llbartekll Oct 14, 2026
999c2b4
fix(platform-wallet): finish rejected-drain cleanup and republish def…
llbartekll Oct 14, 2026
9fad6e3
fix(platform-wallet): confirm rejected-drain absence freshly and repu…
llbartekll Oct 14, 2026
d64e7db
revert(platform-wallet): drop buffered-backend-only drain recovery ha…
llbartekll Oct 14, 2026
9040417
refactor(platform-wallet)!: complete drains from Platform's verdict i…
llbartekll Oct 15, 2026
3eb509e
refactor(swift-sdk)!: drop the signed-attempt archive and activity re…
llbartekll Oct 15, 2026
74a75db
docs(sdk): drop signed-attempt persistence from the drain parity entry
llbartekll Oct 15, 2026
f4d485a
fix(platform-wallet): complete a drain only when the proof attests fu…
llbartekll Oct 15, 2026
319a1b1
fix(platform-wallet): check the drain's consumption verdict before re…
llbartekll Oct 15, 2026
942194e
Merge branch 'v5.1-dev' into codex/core-shielded-max
llbartekll Oct 16, 2026
5a6d662
fix(platform-wallet): use the non-deprecated secp256k1 secret-key con…
llbartekll Oct 16, 2026
54ff774
fix(platform-wallet): admit wallet drains on inline-committing persis…
llbartekll Oct 16, 2026
6177182
fix(platform-wallet): apply the drain persistence admission on every …
llbartekll Oct 16, 2026
73f1bc9
test(platform-wallet): keep the missing-wallet drain regression reach…
llbartekll Oct 16, 2026
849f036
fix(platform-wallet): cap a wallet drain at the asset-lock input limi…
llbartekll Oct 16, 2026
6f65ff5
fix(swift-sdk): keep drain status authoritative in Rust while a call …
llbartekll Oct 16, 2026
1651bd3
Merge branch 'v5.1-dev' into codex/core-shielded-max
llbartekll Oct 18, 2026
7a83ef8
refactor(platform-wallet): localize drain settlement and narrow recip…
llbartekll Oct 19, 2026
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
86 changes: 86 additions & 0 deletions docs/sdk/sdk-parity-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
"description": "Executable current Kotlin/Swift parity truth for the correctness, persistence, recovery, shared-policy, and immediate example-app capabilities audited from the PR #3999 baseline.",
"declared_persistence_capabilities": [
"asset_lock_funding_indices",
"asset_lock_operation_ids",
"atomic_changesets",
"invitations",
"pending_contact_crypto",
"provider_transactions",
"shielded_viewing_keys",
"tracked_asset_locks",
"unsigned_token_storage",
"wallet_restore"
],
Expand Down Expand Up @@ -40,6 +42,9 @@
"platform_wallet_drain_pending_contact_crypto": "packages/rs-platform-wallet-ffi/src/dashpay.rs",
"platform_wallet_manager_create_with_persistence_capabilities": "packages/rs-platform-wallet-ffi/src/manager.rs",
"platform_wallet_manager_persistence_capabilities": "packages/rs-platform-wallet-ffi/src/manager.rs",
"platform_wallet_manager_shielded_fund_from_wallet_drain": "packages/rs-platform-wallet-ffi/src/shielded_wallet_drain.rs",
"platform_wallet_manager_shielded_wallet_drain_preview": "packages/rs-platform-wallet-ffi/src/shielded_wallet_drain.rs",
"platform_wallet_manager_shielded_wallet_drain_status": "packages/rs-platform-wallet-ffi/src/shielded_wallet_drain.rs",
"platform_wallet_manager_spv_rescan_filters": "packages/rs-platform-wallet-ffi/src/spv.rs",
"platform_wallet_manager_sync_fault_detected": "packages/rs-platform-wallet-ffi/src/manager.rs",
"platform_wallet_resume_identity_with_existing_asset_lock_signer": "packages/rs-platform-wallet-ffi/src/identity_registration_funded_with_signer.rs",
Expand Down Expand Up @@ -1604,6 +1609,87 @@
"covers_restart": false
}
]
},
{
"id": "shielded.core_wallet_drain",
"title": "Pooled Core-to-shielded Max with restart-safe recovery",
"area": "recovery",
"shared_apis": [
"platform_wallet_manager_shielded_wallet_drain_preview",
"platform_wallet_manager_shielded_fund_from_wallet_drain",
"platform_wallet_manager_shielded_wallet_drain_status"
],
"required_persistence_capabilities": [
"asset_lock_funding_indices",
"asset_lock_operation_ids",
"atomic_changesets",
"tracked_asset_locks",
"wallet_restore"
],
"hosts": {
"swift": {
"sdk": "supported",
"example_app": "unsupported",
"restart": "tested",
"reason": "Swift exposes preview, execution, and status with durable operation UUIDs. Restart recovery resumes the exact asset-lock outpoint: a fresh Type18 that Platform executes, or that Platform rejects because the outpoint was already consumed by an earlier attempt, durably completes the operation. Unit tests cover store reopen, failed persistence and the terminal DrainCompleted rule. An iOS wallet testnet process-death test confirmed recovery after dispatch and enabled a subsequent Max transfer. SwiftExampleApp does not yet expose this flow."
},
"kotlin": {
"sdk": "unsupported",
"example_app": "unsupported",
"restart": "not_applicable",
"reason": "The typed input-limit error is mapped, but JNI drain wrappers and operation-ID callbacks are not implemented."
}
},
"verification": [
{
"host": "swift",
"kind": "unit",
"file": "packages/swift-sdk/SwiftTests/SwiftDashSDKTests/WalletDrainTests.swift",
"id": "testShouldPreserveExactOutpointAndErrorForPendingOperation",
"command": "cd packages/swift-sdk && xcodebuild test -scheme SwiftDashSDK -sdk iphonesimulator -destination \"platform=iOS Simulator,name=iPhone 17\" ARCHS=arm64 -only-testing:SwiftDashSDKTests/WalletDrainTests",
"covers_restart": false
},
{
"host": "swift",
"kind": "unit",
"file": "packages/swift-sdk/SwiftTests/SwiftDashSDKTests/AssetLockOperationPersistenceTests.swift",
"id": "testOperationIdSurvivesConsumptionAndStoreReopen",
"command": "cd packages/swift-sdk && xcodebuild test -scheme SwiftDashSDK -sdk iphonesimulator -destination \"platform=iOS Simulator,name=iPhone 17\" ARCHS=arm64 -only-testing:SwiftDashSDKTests/AssetLockOperationPersistenceTests",
"covers_restart": true
},
{
"host": "shared",
"kind": "unit",
"file": "packages/rs-platform-wallet/src/wallet/asset_lock/build.rs",
"id": "should_wallet_drain_panic_keep_exact_outpoint_and_pending_status",
"command": "cargo test -p platform-wallet --features shielded --lib should_wallet_drain_panic_keep_exact_outpoint_and_pending_status",
"covers_restart": false
},
{
"host": "shared",
"kind": "unit",
"file": "packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs",
"id": "settle_completes_drain_on_exact_outpoint_already_consumed_report",
"command": "cargo test -p platform-wallet --features shielded --lib settle_completes_drain_on_exact_outpoint_already_consumed_report",
"covers_restart": true
},
{
"host": "shared",
"kind": "unit",
"file": "packages/rs-platform-wallet/src/wallet/shielded/wallet_drain_tests.rs",
"id": "should_resume_the_same_lock_for_a_reused_uuid_without_a_second_core_lock",
"command": "cargo test -p platform-wallet --features shielded --lib wallet_drain_tests",
"covers_restart": true
},
{
"host": "shared",
"kind": "unit",
"file": "packages/rs-platform-wallet/src/wallet/shielded/wallet_drain_tests.rs",
"id": "should_refuse_drain_on_a_buffered_persistence_backend",
"command": "cargo test -p platform-wallet --features shielded --lib should_refuse_drain_on_a_buffered_persistence_backend",
"covers_restart": false
}
]
}
]
}
15 changes: 8 additions & 7 deletions packages/kotlin-sdk/PARITY_SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@
# Kotlin/Swift executable parity summary

Audit baseline: `PR #3999 @ 6dbc72a54df72d26eb9c4a014b425d2b95134e4e`
Capabilities tracked: **26**
Capabilities tracked: **27**

## Status counts

| Host | Surface | Supported | Partial | Unsupported | Not applicable |
| --- | --- | ---: | ---: | ---: | ---: |
| Swift | SDK | 15 | 9 | 1 | 1 |
| Swift | Example app | 4 | 12 | 1 | 9 |
| Kotlin | SDK | 13 | 12 | 0 | 1 |
| Kotlin | Example app | 5 | 12 | 0 | 9 |
| Swift | SDK | 16 | 9 | 1 | 1 |
| Swift | Example app | 4 | 12 | 2 | 9 |
| Kotlin | SDK | 13 | 12 | 1 | 1 |
| Kotlin | Example app | 5 | 12 | 1 | 9 |

## Restart coverage

| Host | Tested | Required | Not applicable |
| --- | ---: | ---: | ---: |
| Swift | 0 | 8 | 18 |
| Kotlin | 4 | 6 | 16 |
| Swift | 1 | 8 | 18 |
| Kotlin | 4 | 6 | 17 |

## Capability status

Expand Down Expand Up @@ -50,6 +50,7 @@ Capabilities tracked: **26**
| `errors.platform_wallet_not_found_typed` | supported / not-applicable / not_applicable | supported / not-applicable / not_applicable |
| `persistence.pending_identity_key_repair` | unsupported / unsupported / not_applicable | supported / not-applicable / tested |
| `errors.signing_key_unavailable_discriminator` | supported / not-applicable / not_applicable | supported / not-applicable / not_applicable |
| `shielded.core_wallet_drain` | supported / unsupported / tested | unsupported / unsupported / not_applicable |

Source: [`docs/sdk/sdk-parity-manifest.json`](../../docs/sdk/sdk-parity-manifest.json).
Detailed legacy view mapping remains in [`PARITY.md`](PARITY.md), but its manual totals are not authoritative.
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ sealed class DashSdkError(
class AssetLockInsufficientFunds(message: String, cause: Throwable? = null) :
PlatformWallet(message, cause)

/** Code 60: the full drain exceeds the input cap; no partial drain was submitted. */
class AssetLockTooManyInputs(message: String, cause: Throwable? = null) :
PlatformWallet(message, cause)

/**
* `ErrorShieldedNoRecordedAnchor` (native code 19). A shielded spend
* could not be built against a Platform-recorded anchor because the
Expand Down Expand Up @@ -763,6 +767,7 @@ sealed class DashSdkError(
26 -> PlatformWallet.TransactionBroadcastRejected(message, cause) // ErrorTransactionBroadcastRejected
27 -> PlatformWallet.ShutdownIncomplete(message, cause) // ErrorShutdownIncomplete
29 -> PlatformWallet.AssetLockInsufficientFunds(message, cause) // ErrorAssetLockInsufficientFunds
60 -> PlatformWallet.AssetLockTooManyInputs(message, cause) // ErrorAssetLockTooManyInputs
// The deferred-token trio sits at the contiguous block 34-36 because
// 27-33 are claimed elsewhere: 27 ErrorShutdownIncomplete
// (dashpay/platform#4268, merged), 29 ErrorAssetLockInsufficientFunds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import org.junit.Test

class DashSdkErrorTest {

@Test
fun shouldPreserveAssetLockInputCapWithoutRetryingAPartialDrain() {
val native = DashSDKException(DashSdkError.PLATFORM_WALLET_CODE_OFFSET + 60, "101 exceeds 100")
val mapped = DashSdkError.fromNative(native)
assertTrue(mapped is DashSdkError.PlatformWallet.AssetLockTooManyInputs)
assertEquals(native.message, mapped.message)
assertEquals(native, mapped.cause)
assertFalse(mapped.isRetryable)
}

@Test
fun mapsNativeCodesToHierarchy() {
// Codes mirror DashSDKErrorCode in rs-sdk-ffi/src/error.rs.
Expand Down
7 changes: 5 additions & 2 deletions packages/rs-platform-wallet-ffi/ERROR_CODE_REGISTRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,16 @@ These are shipped ABI. Do not renumber.
| 98 | `NotFound` | Sentinel — `Option` returned as an error |
| 99 | `ErrorUnknown` | Sentinel — unmapped/flattened errors |

**Next allocatable integer: 60** — 27–59 are all claimed (27, 29, 31, 34–42
**Next allocatable integer: 61** — 27–60 are all claimed (27, 29, 31, 34–42
and 46 merged; 43–45 proposed by active #4313 at head `0302b188ab`; 47 and
48 proposed by active #4356 (47 renumbered from 42, 48 from 43 — see their
rows below); 49–54 proposed by active #4586 (the persister
operation × kind block); 55–57 proposed by #4715 for pending identity-funded
shield debits and durable recovery errors; 58 proposed by #4799 for an unavailable
identity balance response; 59 proposed by #5322 for an SPV
teardown that needs a process restart; 28, 30,
teardown that needs a process restart; 60 proposed by the pooled Core-to-shielded
wallet drain for `ErrorAssetLockTooManyInputs` (Rust, Swift and Kotlin mirrors —
renumbered from 59 when #5322 took that number on `v5.1-dev`); 28, 30,
32 and 33 reserved). **28, 30,
32 and 33 are RESERVED, not free**: 28 and 30 were vacated when the
reservation trio moved to 34–36; 32 and 33 lapsed when their in-repo owners
Expand Down Expand Up @@ -174,6 +176,7 @@ Fork-era numbers remain in the collision history, which is immutable record.
| 57 | `ErrorShieldedRecoveryKeysRequired` | #4715 | Proposed — recovery needs the account and compatible keys; ciphertext damage can produce the same symptom. Rust, Swift and Kotlin preserve this typed error |
| 58 | `ErrorIdentityBalanceUnavailable` | #4799 | Proposed — Platform returned no balance for a managed identity. Retrying the read is safe; this is distinct from missing wallet ownership and must not trigger registration or funding |
| 59 | `ErrorSpvProcessRestartRequired` | #5322 | Proposed — SPV startup or teardown panicked; restart the host process. Not retryable, unlike 27, which SPV start, stop and storage clear now return while an SPV teardown is still running. Typed in Rust, Swift and Kotlin (the PR also types 27 on Kotlin) |
| 60 | `ErrorAssetLockTooManyInputs` | Core-to-shielded pooled Max | Proposed — the full drain needs more inputs than an asset lock may carry (100 on Dash Core relay policy and Platform consensus); no partial drain was broadcast. Rust, Swift and Kotlin retain the typed refusal. Renumbered from 59 after #5322 claimed it |

**Code 31 left this table on 2026-08-04.** `ErrorSigningKeyUnavailable` sat here
as #4183's proposal until #4183 merged (`189a3abb1c`); it is now in the merged
Expand Down
4 changes: 3 additions & 1 deletion packages/rs-platform-wallet-ffi/src/asset_lock/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ pub struct TrackedAssetLockFFI {
pub amount: u64,
/// Status (0=Built, 1=Broadcast, 2=InstantSendLocked, 3=ChainLocked,
/// 4=Consumed, 5=RecoveredFromChain — Core finality proven by restore
/// reconstruction or live reconciliation, Platform-side consumption unknown).
/// reconstruction or live reconciliation, Platform-side consumption unknown;
/// 6=DrainCompleted — proven drain consumption and owned output recorded durably).
pub status: u32,
/// Whether a proof is attached.
pub has_proof: bool,
Expand Down Expand Up @@ -81,6 +82,7 @@ pub unsafe extern "C" fn asset_lock_manager_list_tracked_locks(
AssetLockStatus::ChainLocked => 3,
AssetLockStatus::Consumed => 4,
AssetLockStatus::RecoveredFromChain => 5,
AssetLockStatus::DrainCompleted => 6,
},
has_proof: lock.proof.is_some(),
}
Expand Down
53 changes: 52 additions & 1 deletion packages/rs-platform-wallet-ffi/src/asset_lock_persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ use platform_wallet::AssetLockStatus;
/// bincode-encoded with `dpp::bincode::config::standard()`. Both byte
/// slices are Rust-owned for the lifetime of the callback and live in
/// the matching [`AssetLockEntryStorage`].
/// This unversioned row's layout is frozen. Operation UUIDs travel through
/// the size-negotiated persistence extension, never by changing this stride.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct AssetLockEntryFFI {
Expand All @@ -55,7 +57,8 @@ pub struct AssetLockEntryFFI {
/// Locked amount in duffs (1 DASH = 1e8 duffs).
pub amount_duffs: u64,
/// Discriminant of [`AssetLockStatus`]:
/// 0 = Built, 1 = Broadcast, 2 = InstantSendLocked, 3 = ChainLocked.
/// 0 = Built, 1 = Broadcast, 2 = InstantSendLocked, 3 = ChainLocked,
/// 4 = Consumed, 5 = RecoveredFromChain, 6 = DrainCompleted (proven owned output).
pub status: u8,
/// Bincode-encoded [`AssetLockProof`] (standard config). Rust-owned,
/// `null` + `0` length when the entry has no proof yet (statuses
Expand Down Expand Up @@ -170,5 +173,53 @@ fn status_to_u8(status: &AssetLockStatus) -> u8 {
AssetLockStatus::ChainLocked => 3,
AssetLockStatus::Consumed => 4,
AssetLockStatus::RecoveredFromChain => 5,
AssetLockStatus::DrainCompleted => 6,
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn should_keep_the_published_asset_lock_row_layout() {
// A host compiled before drain UUID support still allocates and indexes
// this exact shape. Sidecars must not move any field or grow its stride.
#[repr(C)]
struct LegacyAssetLockEntry {
out_point: [u8; 36],
transaction_bytes: *const u8,
transaction_bytes_len: usize,
account_index: u32,
funding_type: u8,
identity_index: u32,
amount_duffs: u64,
status: u8,
proof_bytes: *const u8,
proof_bytes_len: usize,
}
assert_eq!(
std::mem::size_of::<AssetLockEntryFFI>(),
std::mem::size_of::<LegacyAssetLockEntry>()
);
assert_eq!(
std::mem::align_of::<AssetLockEntryFFI>(),
std::mem::align_of::<LegacyAssetLockEntry>()
);
macro_rules! same_offset {
($($field:ident),+) => { $(assert_eq!(std::mem::offset_of!(AssetLockEntryFFI, $field), std::mem::offset_of!(LegacyAssetLockEntry, $field));)+ };
}
same_offset!(
out_point,
transaction_bytes,
transaction_bytes_len,
account_index,
funding_type,
identity_index,
amount_duffs,
status,
proof_bytes,
proof_bytes_len
);
}
}
8 changes: 8 additions & 0 deletions packages/rs-platform-wallet-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,11 @@ pub enum PlatformWalletFFIResultCode {
/// [`Self::ErrorShutdownIncomplete`]: restart the host process.
ErrorSpvProcessRestartRequired = 59,

/// An all-input asset lock exceeds the asset-lock input cap (100: Dash
/// Core relay policy and Platform consensus). Nothing was broadcast; the
/// host must not silently substitute a partial drain.
ErrorAssetLockTooManyInputs = 60,

/// The named thing does not exist.
///
/// Originally (and still mostly) the code for every `Option` returned as an
Expand Down Expand Up @@ -1020,6 +1025,9 @@ impl From<PlatformWalletError> for PlatformWalletFFIResult {
PlatformWalletError::AssetLockInsufficientFunds { .. } => {
PlatformWalletFFIResultCode::ErrorAssetLockInsufficientFunds
}
PlatformWalletError::AssetLockTooManyInputs { .. } => {
PlatformWalletFFIResultCode::ErrorAssetLockTooManyInputs
}
// A quiesce/drain barrier that did not complete within budget
// (clear/reset paths). The host must fail closed: keep its
// callback context alive and skip any paired persistence wipe.
Expand Down
4 changes: 4 additions & 0 deletions packages/rs-platform-wallet-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ pub mod shielded_send;
#[cfg(feature = "shielded")]
pub mod shielded_sync;
pub mod shielded_types;
#[cfg(feature = "shielded")]
pub mod shielded_wallet_drain;
pub mod sign_with_mnemonic_resolver;
pub mod spv;
pub mod token_persistence;
Expand Down Expand Up @@ -156,6 +158,8 @@ pub use shielded_send::*;
#[cfg(feature = "shielded")]
pub use shielded_sync::*;
pub use shielded_types::*;
#[cfg(feature = "shielded")]
pub use shielded_wallet_drain::*;
pub use sign_with_mnemonic_resolver::*;
pub use spv::*;
pub use token_persistence::*;
Expand Down
Loading
Loading