Use SDK protocol autodetection and refresh before exposing new wallet runtimes #1228
Description
Status (2026-11-04). Narrowed after #1232 (merged 2026-10-27,
ba3a09547):
- Done in feat(wallet): show a nonblocking protocol update advisory #1232: the testnet pin is gone (
SwiftDashSDKHost.platformVersion(for:)returns0for every network); a proved epoch query runs in the background after runtime publication and on foreground return (WalletProtocolCompatibilityController), with stale-runtime results discarded; the "update available" advisory is shown whenverified > maximum_supported_version.- Still open, the only remaining scope of this issue: proposed change 2 — call
refreshProtocolVersion()for a newly created active-network runtime before it is exposed to version-sensitive writes (fee preflight, asset-lock funding). feat(wallet): show a nonblocking protocol update advisory #1232 explicitly accepted the cold-start window where auto mode runs at the network floor (13 on testnet) until the first verified response, so a Core → Shielded prepared in that window can still buildShieldFromAssetLockV0 and be rejected after the Core lock exists. Today the only explicit refresh in the app isContestedUsernameFee.- Proposed changes 1 and 6 are done; 3, 4, 5 and 7 apply only insofar as they support change 2. The acceptance criteria and validation below are kept for reference.
Goal
Use the protocol-version autodetection and explicit refresh already available in SwiftDashSDK. Remove the app's hardcoded testnet protocol pin and refresh the SDK before exposing a newly created runtime to version-sensitive wallet operations.
Scope: dashwallet-ios only. No Platform SDK / rust-dashcore changes are required by this issue. A complete “update the app” flow for unsupported protocols is deferred.
Why
Core → Shielded failed on testnet when the app constructed ShieldFromAssetLockTransitionV0 using protocol 13 while the network required V1 under protocol 14. The Core asset lock had already been created before the Platform operation failed.
PR #1222 changes the testnet pin from 13 to 14 as an immediate workaround. An explicit pin disables protocol-version learning and makes refreshProtocolVersion() a no-op. It therefore requires another app change when the network next moves to a protocol requiring new transition formats.
Simply replacing 14 with 0 is insufficient: an unpinned SDK starts at its per-network floor and learns newer supported versions from verified responses. A first write can otherwise run before any such read.
Verified baseline
TestFlight 9.1.4 (11) was built from:
- app
7465095d2a0a0bef0888394d271a563502580951, requested refqa/1042-pv14; - Platform
d959e62c5591bae3a107956bffdd7b00e5fab819, exported platform-wallet version 5.0.0-rc.5.
In that app revision, SwiftDashSDKHost.platformVersion(for:) returns 14 for testnet and 0 for mainnet/devnet/regtest. Its comment explicitly notes that the app does not call refreshProtocolVersion().
The older local develop checkout still pins testnet to 13. Implement against the target branch's actual state; do not assume the TF QA ref is identical to develop.
Existing SDK capability
SwiftDashSDK already exposes SDK.refreshProtocolVersion():
- invokes a proven
getEpochsInforead; - learns a newer supported protocol version after proof/signature verification;
- shares the learned version with clones of the same underlying SDK, including the wallet manager;
- does not refresh a pinned SDK.
The relevant Rust implementation is Sdk::refresh_protocol_version.
Important limitations to preserve explicitly
This is a best-effort API, not a reliable compatibility handshake:
- A failed network/proof fetch is logged by Rust; the method can still return the previously stored version as success.
- A newer version unknown to the linked SDK is logged and is not adopted; the stored older version remains.
- A returned version alone does not prove that a fresh check succeeded or that the network does not require a newer version.
Do not label a returned number as “network compatibility confirmed”, infer “update required” from a timeout, or claim that this issue solves unknown-protocol handling. No new SDK status/error contract is part of this scope.
Proposed iOS changes
- Configure testnet with
platformVersion: 0, as mainnet already is. Preserve existing devnet/regtest support restrictions; do not expand the network scope. - For each newly created active-network SDK runtime, call the existing
refreshProtocolVersion()after SDK creation and before making the runtime available to version-sensitive operations, fee preflight, or new asset-lock funding. The intended ordering is SDK creation → refresh attempt completes → runtime becomes usable. - Cover cold startup, network switching, and runtime recreation through a shared lifecycle path. Ensure a stale asynchronous result from the previous network/runtime cannot publish readiness for the new one.
- Run the blocking FFI refresh off the main thread, using the existing SDK initialization execution model or an appropriate equivalent. Avoid freezing navigation or the launch UI.
- Use the existing SDK request timeout/retry policy; do not add an unbounded launch retry loop. Preserve usable offline/error behavior. Handle errors actually surfaced by the Swift API through the existing lifecycle error handling, and document that internal best-effort fetch failures are not distinguishable through this return value.
- Add concise diagnostics: network, autodetect configuration, refresh attempt duration, returned SDK protocol version, and any surfaced error. Do not log secrets or describe the returned version as independently confirmed current network state.
- Update stale pin-related comments and relevant tests/documentation. Keep the linked framework provenance explicit; autodetection cannot supply support for protocol versions absent from that framework.
Refreshing on foreground resume is a useful subsequent lifecycle improvement, but does not need to expand this first issue beyond startup/network-switch/runtime-creation behavior.
Acceptance criteria
- Testnet and mainnet SDKs are unpinned.
- Each newly initialized active-network runtime attempts explicit refresh before it can be used for version-sensitive writes.
- A supported version advertised in verified metadata is used by the same SDK/wallet-manager instance that performs the operation.
- The application remains responsive while refresh runs, with no indefinite startup waiting added by the app.
- Changing networks while initialization is in progress cannot apply the old runtime's completion to the new network.
- Failed/offline refresh is not presented as successful network compatibility verification or as evidence that an app update is required.
- The implementation does not promise to prevent all writes after an internally swallowed refresh failure; that requires a stronger SDK contract and is deferred explicitly.
Validation
Focused automated coverage
- SDK configuration uses autodetect on testnet and mainnet.
- A controllable/delayed refresh verifies that the active runtime is not exposed to spending before the refresh attempt returns.
- Surfaced errors and network switches do not leave stale initialization tasks publishing the wrong runtime.
- Exercise existing lifecycle tests and a clean
dashpayarm64 simulator build using the target branch's supported test setup.
Live testnet smoke test
- Record app SHA, Platform/framework provenance, and framework checksum.
- Cold-start with autodetect and observe the returned protocol version after refresh. At the time of diagnosis the testnet version was 14; validate the network state at test time rather than making 14 a permanent app constant.
- Switch mainnet → testnet and back; verify separate SDK instances refresh for the correct network.
- Start offline / with an unreachable endpoint, then restore connectivity and recreate or restart the runtime. Record actual behavior, including the possibility of an unchanged returned version after an internally logged failure.
- After Core wallet synchronization is ready, execute one dedicated testnet Core → Shielded smoke test and verify Platform success. Do not mix this with the incomplete-Core-sync spending bug in Core spends allowed during Keychain recovery sync can select already-spent inputs #1227.
Preliminary probe already performed (not full app validation)
On 2026-10-20, a standalone FFI probe ran inside an iOS simulator using a local framework reporting 5.0.0-rc.1:
| Configuration | Explicit refresh result | Observation |
|---|---|---|
Testnet, platform_version = 0 |
14 | approximately 1 second; trusted provider quorum cache populated |
Testnet, platform_version = 13 |
13 | immediate no-op; quorum cache remained empty |
No wallet was loaded, no mnemonic accessed, and no transaction was created or broadcast. This verifies the existing refresh mechanism on that local binary only; it is not an end-to-end test of TF 9.1.4(11), startup ordering, offline behavior, or shielding.
Probe framework simulator archive SHA-256: b30588436a8ca6968302d6c382aaf6bb6c2c8ea67026a6eef7bd35ec1eadcceb.
Out of scope / related work
- No changes to Platform SDK, rust-dashcore, consensus, protocol floors, or transition formats.
- No complete “update your app” UI or unsupported-protocol write gate in this first step.
- No replacement for the separate per-wallet historical-sync spending guard: Core spends allowed during Keychain recovery sync can select already-spent inputs #1227.
- Existing SDK discussion: dashpay/platform#5495.
- Testnet pin workaround: fix(wallet): pin testnet to Platform protocol 14 #1222. This issue replaces that workaround with explicit best-effort initialization refresh; it does not claim the deferred SDK limitations are fixed.
No application code has been changed as part of filing this issue.
Activity