Life of a SOL state machine · on-chain truth
What happens to a SOL between the screens above. Solid edges are user-triggered ix broadcasts; dashed edges are automatic epoch-boundary transitions (~1–2 days each). Includes both Deactivate and Withdraw ix as distinct steps.
tap — user action in the app
waits ~2 days — automatic on-chain timer, no app interaction needed
⚠ user must come back to the app after the wait to finish
[MONEY MOVER] — money-mover amount screen sits on this edge (3 surfaces in this PR)
stateDiagram-v2
direction TB
SOL: SOL in wallet
Delegating: Delegating - frozen, warming up
Active: Active stake (or Activating) - earning rewards
Deactivating: Deactivating - frozen, cooling
Inactive: Inactive stake - detail page always shows BOTH Withdraw and Convert (PSOL push deep-links straight to Convert review)
PSOL: PSOL - transferable, earns via exchange rate
[*] --> SOL
SOL --> Delegating: [MONEY MOVER] tap Stake
Delegating --> Active: waits ~2 days
Active --> Deactivating: tap Unstake
Active --> Deactivating: tap Convert to PSOL (step 1 of 2 - same on-chain effect as Unstake)
Deactivating --> Inactive: waits ~2 days
Inactive --> SOL: tap Withdraw
Inactive --> PSOL: tap Convert to PSOL (step 2 - mints PSOL)
Inactive --> Delegating: tap Restake
SOL --> PSOL: [MONEY MOVER] tap Mint PSOL (current entry, from SOL detail More menu)
SOL --> PSOL: [MONEY MOVER] tap Stake (PROPOSED - new primary CTA on SOL trade row)
PSOL --> SOL: [MONEY MOVER] tap Unstake (waits ~2 days)
classDef nativeStyle fill:#3a2a14,stroke:#fb923c,color:#fde2c4
classDef liquidStyle fill:#0e2a3a,stroke:#38bdf8,color:#bae6fd
classDef wallet fill:#1a1a20,stroke:#82828e,color:#e8e8ee
classDef warnState fill:#3a1f25,stroke:#fb7185,color:#fda4af
classDef choiceState fill:#3a2f0e,stroke:#fbbf24,color:#fde68a
class SOL wallet
class Delegating,Deactivating,Active nativeStyle
class Inactive choiceState
class PSOL liquidStyle
Read one SOL through the machine. Start as SOL → Stake-in (Journey A) → Delegating → ⏱ epoch → Active stake. From Active, the user taps Unstake (or active-mode Convert): Active → Deactivating → ⏱ epoch → Inactive stake. Inactive is a dead-end until the user comes back: they must re-open the app and either (a) tap Withdraw to return to SOL, or (b) re-enter Convert (Bridge inactive-mode) which atomically withdraws + deposits into the stake pool, minting PSOL. Liquid side: SOL → Mint (Journey B) → PSOL. Liquid exit (Journey C) has two paths — the instant withdrawSol path is gated behind the enable-withdraw-sol flag (prod default off); the prod-default withdrawStake path returns a deactivating stake account, dropping the SOL back into the native cooling-down → withdraw cycle.
PM frame. Both red ⚠ MUST RETURN edges and one full epoch wait sit downstream of any Active → Inactive transition — that's the unavoidable cost of leaving native delegation. The only on-chain difference between Journey D Unstake and Bridge active-mode Convert is which screen broadcasts the same StakeProgram.deactivate ix. Killing Journey D's standalone Unstake doesn't change the on-chain SOL lifecycle — it removes one of two screen entry points into the same edge.
PROPOSED CTA — "Stake" primary button on the SOL asset detail trade row, routing directly to the PSOL money mover (MintLiquidStakeAmountPage with lstCaip19=PSOL_CAIP19). Skips the StakingMethodSelection picker. Not yet implemented; minimal change would be removing menuOnly: true from the mintLST button in packages/fungible-pages/src/ctaRow/useFungiblesCta.ts, overriding singleWordAltText to commandStake, and bumping mintLST higher in the orderList in callToAction.ts so it lands above buy. The onNativeMintLSTPress handler is already wired to the right destination via useStakingNavigation.
Verified against code — useCreateStakeAccountAndDelegateStake.ts (Journey A combines createAccount + delegate in one tx), useDeactivateStake.ts and useWithdrawStake.ts (Journey D ix), useConvertStakeAccountReviewPageProps.ts + getConvertToPSOLTransaction.ts (Bridge — active branch broadcasts deactivate only; inactive branch bundles StakeProgram.withdraw + stakePool.depositSol atomically), getMintPoolTokenTransaction.ts (Journey B = depositSol), getWithdrawLiquidStakeTransaction.ts + useGetWithdrawLiquidStakeTransaction.ts (Journey C tries withdrawSol only if enable-withdraw-sol flag is on, otherwise falls back to withdrawStake).
User-triggered transitions (ix broadcasts)
- Stake-in (Journey A):
createStakeAccount + delegate → Liquid → Delegating
- Native Deactivate (Journey D):
deactivate ix → Active → Deactivating
- Native Withdraw (Journey D):
withdraw ix → Inactive → Liquid
- Mint PSOL (Journey B): stake-pool
deposit (SOL) → Liquid → PSOL
- Bridge active-mode:
deactivate ix → Active → Deactivating (same as native Deactivate)
- Bridge inactive-mode:
depositStake ix → Inactive → PSOL
- Liquid unstake (Journey C):
withdrawStake ix → PSOL → UnstakingPSOL
Automatic transitions (epoch boundaries)
- Delegating → Active: stake warm-up. ~1 epoch (~2 days).
- Deactivating → Inactive: cool-down. ~1 epoch.
- UnstakingPSOL → Liquid: stake-pool queues a withdrawal that the user can claim after the deactivation cycle. ~1–2 days.
PM frame: the only on-chain difference between Journey D's standalone Unstake and the Bridge's active-mode Convert is what screen the user is on. Both emit the same deactivate ix and wait the same epoch.
Non-liquid native SOL · delegated stake accounts
SOL goes in / out via on-chain delegation. Stake accounts are owned-by-user, deactivated per-epoch. Crosses into the liquid domain via Convert to PSOL only.
A — Stake-in SOL → delegated stake account
SOL detail → More → Stake SOL → Native Staking card.
SOL Detail
→ More menu
testID: unifiedFungibleDetail-stakeSol-cta
→
StakingMethodSelection
picker
→
ValidatorList
picker
→
StakeAmount
money-mover · this PR
→
StakeReview
real broadcast
→
CreateAndDelegateStatus
status
D — Native unstake kill candidate
From a delegated stake account. Deactivates the whole account (no amount picker). Withdraw is a separate return trip after the epoch boundary.
StakeAccountsList
picker
→
StakeAccountDetail
Unstake btnConvert btn ↘
→
StakeAccountDeactivateStatus
broadcasts deactivate ix · no PR #23967 screenshot
native unstake
⤳
StakeAccountWithdrawStatus
return trip after epoch · withdraw raw SOL
status
Kill tradeoff for the PM. Convert (bridge below) already absorbs the deactivate step for active accounts, so removing the standalone Unstake + Withdraw buttons is functionally covered — but a user who wants raw SOL back goes from one epoch wait (deactivate → withdraw) to two epoch waits (Convert deactivate → wait → Convert mint PSOL → liquid unstake → wait).
Bridge — Convert to PSOL non-liquid → liquid
The only way for SOL inside a native delegated stake account to enter the liquid domain. Same Convert review page branches on the source account's activation state — see useConvertStakeAccountReviewPageProps.ts.
StakeAccountDetail → Convert
entry from non-liquid
→
ConvertToPSOLInfo
info · first run
→
ConvertStakeAccountList
picker
→
ConvertStakeAccountReview (branches)
real broadcasttwo-mode
Source = active / activating
Broadcasts deactivate ix only. User waits until the next epoch boundary (~1–2 days), then re-enters Convert to finish. Same on-chain effect as Journey D's standalone Unstake — this is why Convert can subsume Journey D.
ConvertStakeAccountStatus
deactivate only
⤳
Re-enter Convert
after epoch ends
deep link supported
Source = inactive (already deactivated)
Broadcasts the stake-pool depositStake ix. The deactivated SOL is consumed by the pool and PSOL is minted to the user's wallet immediately. The user now sits in the liquid domain below.
ConvertStakeAccountStatus
mints PSOL
→
PSOL in wallet
crosses into liquid domain
Deep link entry:
DeepLinkDestination.ConvertStakeAccount →
ConvertStakeAccountDeepLinkPage:
DeepLinkPage
deep link
DeepLinkPage · error
deep link · error
Liquid PSOL · Phantom LST pool
Tokenized stake position — PSOL is a fungible token in the wallet. Entry is direct (Mint PSOL from SOL detail) or via the Bridge above. Exit back to raw SOL goes through liquid unstake.
B — Mint PSOL SOL → PSOL
SOL detail → Mint PSOL CTA, or Stake SOL → Liquid Staking card. Stakes SOL with the Phantom LST pool; mints PSOL in return.
SOL Detail
→ Mint PSOL
testID: unifiedFungibleDetail-mintLST-cta
→
MintPSOLInfo
info · first run
→
MintLiquidStakeAmount
money-mover · this PR
→
MintLiquidStakeReview
real broadcast
→
MintLiquidStakeStatus
status
Info-page variants by region / pool — MintPSOLInfoPage shown; alternates are MintPSOLUKInfoPage, MintJitoSOLInfoPage, PSOLMarketingPage. All funnel into MintLiquidStakeAmountPage.
C — Liquid unstake PSOL → SOL
PSOL detail → More → Unstake. Burns PSOL, queues SOL withdrawal. Only path back to raw SOL from the liquid domain.
PSOL Detail
→ More menu
→ Unstake
testID: unifiedFungibleDetail-unstake-cta
→
UnstakeLiquidStakeAmount
money-mover · this PR
→
UnstakeLiquidStakeReview
real broadcast
→
UnstakeLiquidStakeStatus
status
Money-mover review disclaimer: "Your unstaked PSOL can take up to 2 days to become available in your wallet" (commit 46e30f8).