Surface pending reversible transfers, with a live countdown #1

Closed
opened 2026-09-10 05:37:37 +00:00 by grenade · 1 comment
Owner

What

A page showing money that is currently in flight and still cancellable on
ReversibleTransfers, counting down to the block it executes at.

No other explorer can have this, because no other chain has the pallet. It is
the single most distinctive thing Quantus does and today the observer says
nothing about it.

Why it needs state reads, not the event index

The event index can see a transfer being scheduled, cancelled or executed
after the fact. It cannot answer "what is pending right now" without replaying
every event since genesis and reconstructing the set — and getting the
reconstruction wrong is silent.

The chain already holds the answer:

PendingTransfers          H256 → PendingTransfer<AccountId32, u128, u32>   [optional]
PendingTransfersBySender  AccountId32 → BoundedVec<H256>                   [default]
HighSecurityAccounts      AccountId32 → HighSecurityAccountData<…>         [optional]
NextTransactionId         u64                                              [default]

State reads landed in 6b0e02e, but only for entries needing no key. This
needs the other half: enumerating a storage map.

Scope

  1. Core — enumerate a map from its prefix. Runtime::storage_prefix(pallet, item) for the 32-byte prefix, and decode each value against the entry's
    declared value type. Generic, not specific to this pallet.
  2. Datastate_getKeysPaged to list keys under a prefix, and a batched
    read for their values.
  3. API/v1/chains/:chain/reversible: each pending transfer with from,
    to, amount, tx_id, execute_at, and blocks remaining.
  4. Web/:chain/reversible, a countdown per row, linked from the section
    nav.

What the page will say today

Nothing, and correctly. ReversibleTransfers has 0 of 6 calls dispatched on
mainnet and none of its 7 events has fired, so PendingTransfers is empty. The
page has to read as "nothing is in flight" rather than as an error or a blank —
and it has to be there before the first one, because catching the first
scheduled transfer is the point.

Notes for whoever picks this up

  • DefaultDelay is BlockNumber(7200) and MinDelayPeriodBlocks is 2, so a
    countdown can span anything from seconds to a day. Show blocks and an
    estimate in time, and say which is which — block count is the fact, wall
    time is derived from a measured interval that moves.
  • execute_at is a DispatchTime<BlockNumber, Moment> — an enum, so it may be
    either a block or a timestamp. Do not assume the block arm.
  • A wrong storage hasher reads as absent, not as an error (CLAUDE.md). The
    prefix and hashers must come from the metadata, and the enumeration wants a
    test pinned against real keys the same way storage_key has.
  • HighSecurityAccounts and its guardians are a natural follow-up on the same
    machinery, but out of scope here.
## What A page showing money that is currently **in flight and still cancellable** on `ReversibleTransfers`, counting down to the block it executes at. No other explorer can have this, because no other chain has the pallet. It is the single most distinctive thing Quantus does and today the observer says nothing about it. ## Why it needs state reads, not the event index The event index can see a transfer being *scheduled*, *cancelled* or *executed* after the fact. It cannot answer "what is pending right now" without replaying every event since genesis and reconstructing the set — and getting the reconstruction wrong is silent. The chain already holds the answer: ``` PendingTransfers H256 → PendingTransfer<AccountId32, u128, u32> [optional] PendingTransfersBySender AccountId32 → BoundedVec<H256> [default] HighSecurityAccounts AccountId32 → HighSecurityAccountData<…> [optional] NextTransactionId u64 [default] ``` State reads landed in 6b0e02e, but only for entries needing **no key**. This needs the other half: enumerating a storage map. ## Scope 1. **Core** — enumerate a map from its prefix. `Runtime::storage_prefix(pallet, item)` for the 32-byte prefix, and decode each value against the entry's declared value type. Generic, not specific to this pallet. 2. **Data** — `state_getKeysPaged` to list keys under a prefix, and a batched read for their values. 3. **API** — `/v1/chains/:chain/reversible`: each pending transfer with `from`, `to`, `amount`, `tx_id`, `execute_at`, and blocks remaining. 4. **Web** — `/:chain/reversible`, a countdown per row, linked from the section nav. ## What the page will say today Nothing, and correctly. `ReversibleTransfers` has **0 of 6 calls dispatched** on mainnet and none of its 7 events has fired, so `PendingTransfers` is empty. The page has to read as "nothing is in flight" rather than as an error or a blank — and it has to be there *before* the first one, because catching the first scheduled transfer is the point. ## Notes for whoever picks this up - `DefaultDelay` is `BlockNumber(7200)` and `MinDelayPeriodBlocks` is 2, so a countdown can span anything from seconds to a day. Show blocks *and* an estimate in time, and say which is which — block count is the fact, wall time is derived from a measured interval that moves. - `execute_at` is a `DispatchTime<BlockNumber, Moment>` — an enum, so it may be either a block or a timestamp. Do not assume the block arm. - A wrong storage hasher reads as **absent, not as an error** (CLAUDE.md). The prefix and hashers must come from the metadata, and the enumeration wants a test pinned against real keys the same way `storage_key` has. - `HighSecurityAccounts` and its guardians are a natural follow-up on the same machinery, but out of scope here.
Author
Owner

Correction to the scope above, found while reading the pallet.

PendingTransfer does not carry execute_at. The stored struct is:

pub struct PendingTransfer<AccountId, Balance, AssetId> {
    pub from: AccountId,
    pub to: AccountId,
    pub guardian: AccountId,
    pub asset_id: Option<AssetId>,
    pub amount: Balance,
}

The execution time lives with the Scheduler, not with the pending transfer.
ReversibleTransfers::TransactionScheduled carries execute_at as an event
field, and nothing in PendingTransfers does.

So the countdown is a join of both halves, which is a better shape than
either alone:

  • StatePendingTransfers — is authoritative about what is still
    pending
    . A transfer that was cancelled or executed is gone from the map, and
    no amount of event replay is as trustworthy as its absence.
  • EventsTransactionScheduled, keyed by tx_id — supply when it is due
    and when it was scheduled.

A row that appears in state with no matching event is possible (the event index
reaches back only as far as it has read) and must render as pending with an
unknown deadline rather than being dropped.

This also means the DispatchTime note above still stands but applies to the
event field rather than to a storage value.

Correction to the scope above, found while reading the pallet. **`PendingTransfer` does not carry `execute_at`.** The stored struct is: ```rust pub struct PendingTransfer<AccountId, Balance, AssetId> { pub from: AccountId, pub to: AccountId, pub guardian: AccountId, pub asset_id: Option<AssetId>, pub amount: Balance, } ``` The execution time lives with the `Scheduler`, not with the pending transfer. `ReversibleTransfers::TransactionScheduled` carries `execute_at` as an event field, and nothing in `PendingTransfers` does. So the countdown is a **join of both halves**, which is a better shape than either alone: - **State** — `PendingTransfers` — is authoritative about *what is still pending*. A transfer that was cancelled or executed is gone from the map, and no amount of event replay is as trustworthy as its absence. - **Events** — `TransactionScheduled`, keyed by `tx_id` — supply *when it is due* and when it was scheduled. A row that appears in state with no matching event is possible (the event index reaches back only as far as it has read) and must render as pending with an unknown deadline rather than being dropped. This also means the `DispatchTime` note above still stands but applies to the event field rather than to a storage value.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: blackbeard/observer#1