You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Placeholder for a design discussion that several subsystems already need and none can proceed without. Not scheduled — this exists so the context is not lost.
The gap
The Rust core has no mechanism for being invoked cyclically. Everything in it acts only when called by a product request. Nothing in truapi-platform provides a timer, a scheduler, or a tick: the trait surface is ProductStorage, Navigation, Notifications, Permissions, CoreAdmin, PairingHostAdmin, Features, ChainProvider, JsonRpcConnection, CoreStorage, AuthPresenter, UserConfirmation, ThemeHost, PreimageHost. None of them is about when.
Two subsystems need this today, for unrelated reasons, which is what makes it platform work rather than either subsystem's problem:
Coinage maintenance (docs/design/coinage-layer.md §6.4, §8.7, and the deferral at line 915). Two periodic passes, both of which protect against permanent loss of funds:
Recycling sweep — a coin past MaximumAge can no longer be transferred or split. Coins approaching it must be converted to recycler entries first, or they become unusable.
Ring-expiration sweep — once a recycler entry's ring becomes immutable and RecyclerExpirationTime (90 days) passes, the chain destroys the entry's backing value. Entries near that deadline must be unloaded back into coins.
Both coinage sweeps are implemented and tested. Nothing calls them on a clock. CoinageLayer::refresh_subscriptions likewise has no caller anywhere in the crate.
Why this is not hypothetical
This is the middle of the three conditions required to fix a live loss-of-funds bug in polkadot-app-ios-v2 (CoinageRecyclingService recycles coins into recycler entries and never unloads them out; a user who tops up and does not open the app within 90 days loses the value). The three conditions are: the core implements the rescue sweep (done), the host schedules it in the background (this issue), and the app routes its coinage through the core (separate work).
The failure mode is precisely "the user did not open the app", so a foreground-only or session-scoped tick narrows the window without closing it. That constraint should drive the design rather than be discovered by it.
What does the core expose — one tick per subsystem, or one global tick that fans out internally?
How does the core say when it next wants waking — return a deadline, a duration, or nothing and let the host pick an interval?
What happens if the host never ticks? The spec must state, per subsystem, whether the consequence is degraded freshness or destroyed data. Coinage is the latter.
Session-scoped or true background? See above — session-scoped does not close the coinage window.
What if the tick needs network and there is none?
Overlap — what if a tick fires while the previous one is still running?
Ownership.CoinageLayer is &mut self throughout. A long-lived driver needs an ownership model the crate has not chosen: an actor loop owning the layer, or Arc<Mutex<CoinageLayer>> driven by the existing crate::subscription::Spawner. An actor avoids a mutex around every operation and is the current recommendation, but nothing is decided.
Questions 4 and 5 are product-visible, not just engineering detail.
Related, and deliberately out of scope here
Reactive observation.coinage-layer.md §6.1 requires continuous subscriptions to the chain storage backing local records — "The layer does not pull-poll" — and this is unbuilt for the same underlying reason: nothing in the core stays alive. It also needs state_subscribeStorage added to RpcClient, which currently exposes only call, get_storage, get_storage_at, finalized_head, submit_and_watch, submit_and_watch_inclusion (the inner subxt subscribe is used only by submit_and_watch_inclusion). Observation shares question 8's ownership decision, so it should be designed alongside this rather than separately.
Expected shape of the outcome
A design document covering the model, then an RFC for the truapi-platform trait addition — since that surface change is something every host (iOS, Android, web, CLI) must then implement.
Not blocked on this
Coinage layer 1 supplies the core-side half — an entry point that answers "it is time T, do what is due" and reports when it next wants waking. That is buildable without settling any question above, and does not prejudge them. Only the mechanism that calls it is blocked.
Placeholder for a design discussion that several subsystems already need and none can proceed without. Not scheduled — this exists so the context is not lost.
The gap
The Rust core has no mechanism for being invoked cyclically. Everything in it acts only when called by a product request. Nothing in
truapi-platformprovides a timer, a scheduler, or a tick: the trait surface isProductStorage,Navigation,Notifications,Permissions,CoreAdmin,PairingHostAdmin,Features,ChainProvider,JsonRpcConnection,CoreStorage,AuthPresenter,UserConfirmation,ThemeHost,PreimageHost. None of them is about when.Two subsystems need this today, for unrelated reasons, which is what makes it platform work rather than either subsystem's problem:
renewal.rsand notickanywhere intruapi-server.docs/design/coinage-layer.md§6.4, §8.7, and the deferral at line 915). Two periodic passes, both of which protect against permanent loss of funds:MaximumAgecan no longer be transferred or split. Coins approaching it must be converted to recycler entries first, or they become unusable.RecyclerExpirationTime(90 days) passes, the chain destroys the entry's backing value. Entries near that deadline must be unloaded back into coins.Both coinage sweeps are implemented and tested. Nothing calls them on a clock.
CoinageLayer::refresh_subscriptionslikewise has no caller anywhere in the crate.Why this is not hypothetical
This is the middle of the three conditions required to fix a live loss-of-funds bug in
polkadot-app-ios-v2(CoinageRecyclingServicerecycles coins into recycler entries and never unloads them out; a user who tops up and does not open the app within 90 days loses the value). The three conditions are: the core implements the rescue sweep (done), the host schedules it in the background (this issue), and the app routes its coinage through the core (separate work).The failure mode is precisely "the user did not open the app", so a foreground-only or session-scoped tick narrows the window without closing it. That constraint should drive the design rather than be discovered by it.
Questions to settle
High level, before any code:
CoinageLayeris&mut selfthroughout. A long-lived driver needs an ownership model the crate has not chosen: an actor loop owning the layer, orArc<Mutex<CoinageLayer>>driven by the existingcrate::subscription::Spawner. An actor avoids a mutex around every operation and is the current recommendation, but nothing is decided.Questions 4 and 5 are product-visible, not just engineering detail.
Related, and deliberately out of scope here
Reactive observation.
coinage-layer.md§6.1 requires continuous subscriptions to the chain storage backing local records — "The layer does not pull-poll" — and this is unbuilt for the same underlying reason: nothing in the core stays alive. It also needsstate_subscribeStorageadded toRpcClient, which currently exposes onlycall,get_storage,get_storage_at,finalized_head,submit_and_watch,submit_and_watch_inclusion(the inner subxtsubscribeis used only bysubmit_and_watch_inclusion). Observation shares question 8's ownership decision, so it should be designed alongside this rather than separately.Expected shape of the outcome
A design document covering the model, then an RFC for the
truapi-platformtrait addition — since that surface change is something every host (iOS, Android, web, CLI) must then implement.Not blocked on this
Coinage layer 1 supplies the core-side half — an entry point that answers "it is time T, do what is due" and reports when it next wants waking. That is buildable without settling any question above, and does not prejudge them. Only the mechanism that calls it is blocked.