Repository navigation
platform-wallet: expose which tracked asset locks a running funding call is consuming #5477
Description
Expected Behavior
A host can ask platform-wallet whether a tracked asset lock is being consumed by a funding call that is still running, and is notified when that changes. A second call that would consume the same lock is refused with a typed error, so the host doesn't have to keep that state itself.
Current Behavior
Nothing in the SDK tells the host "this outpoint is in use". The wallet keeps the state internally:
- shield-class operations are serialised per wallet by
shield_guard(shielded/fund_from_asset_lock.rs#L219); - a resume about to broadcast holds a per-outpoint
resume_dispatch_claimsentry (asset_lock/manager.rs#L123,sync/tracking.rs#L82).
Neither is visible over the FFI. While a call runs, its lock's persisted row looks exactly like a stuck one (Built/Broadcast/InstantSendLocked/ChainLocked, never Consumed). A Core-funded shield from many small coins can wait minutes for a ChainLock after #5460.
So dashwallet-ios keeps its own registry, ShieldedAssetLocksInFlight (ShieldedTransferCoordinator.swift#L1657-L1762, dashpay/dashwallet-ios#1212). It is a ref-counted set of claimed outpoints plus a count of fresh-build calls:
- a fresh build claims the outpoint reported by
onAssetLockBuilt(feat(platform-wallet)!: report a freshly built asset lock's outpoint to the caller #5476); - every resume entry point claims its outpoint before calling the SDK;
- the Home row, the transaction details and the bulk recovery pass read it, so they hide "Finish now" and removal on a lock that is still being consumed;
- resume entry points read it to refuse a second attempt;
- "Remove if Not on Network" and Drop & Rescan are refused while any call is active, because their runtime reload would stop it.
This logic sits on the wrong side of the boundary:
- Untested. The app's unit-test target doesn't build, so the claim/release accounting has no running tests (review thread fix(wallet): show a completing shielded transfer as an ordinary internal transfer dashwallet-ios#1212 r4285499127).
- Duplicated on Android. A Kotlin host would have to re-implement it.
- A guess, not the wallet's own state. The registry reflects what the app believes it started, not what the wallet is actually consuming.
Possible Solution
-
Add a per-outpoint active-operation set to
AssetLockManager. A fresh build enters it where theon_builtobserver fires, right aftertrack_asset_lock(#5476). A resume enters it whenresolve_funding_with_is_timeout_fallbacktakes aFromExistingAssetLock. An RAII guard leaves it when the call returns, unwinds or is dropped, so a cancelled future cannot leave a stale entry. -
Resolve a
FromExistingAssetLockwhose outpoint is already in the set to a typed error (for exampleAssetLockInUse(OutPoint)with its own FFI code). Don't let it queue behindshield_guard. -
Expose over the FFI:
asset_lock_manager_is_in_use(outpoint);- a listing of the in-use outpoints;
- whether any fresh-build call is running, which the host needs for runtime-reload actions;
- a change event through the existing
PlatformWalletEventHandler.
Mirror them in Swift (
ManagedAssetLockManager) and JNI. -
Rust tests for: enter/leave on success, failure, panic and drop; concurrent resume refusal; independent outpoints; a fresh build before its outpoint exists.
The app would then drop ShieldedAssetLocksInFlight and its claim bookkeeping, keep only the UI reads, and map the new typed error to its "still in progress" message.
Alternatives Considered
- Keep the registry in the app (current state). It works, but it is untested and has to be ported to every host.
- Persist an "in use" marker on the asset-lock row. No: the state lives exactly as long as one call in one process, so after a restart it would be stale by definition.
Additional Context
- Follow-up to fix(platform-wallet): retry an oversized InstantSend asset-lock transition with a ChainLock proof #5460 (IS→CL fallback for oversized asset-lock proofs) and feat(platform-wallet)!: report a freshly built asset lock's outpoint to the caller #5476 (
on_asset_lock_built), both from "Shield from Asset Lock" fails with "State Transition exceeds maximum size of 20480 bytes" — funds locked, unable to complete shielded transfer dashwallet-ios#1042. - dashwallet-ios#1212 merges with the app-side registry; this issue is the path to remove it.
Activity