How sessions work
The lifecycle of a Shain session from the moment it opens to the moment it returns rent. Read this once; everything else in the docs assumes it.
Session states
A Shain session is a PDA. Its lifecycle is a three-state machine plus the absence state:
| State | Condition | Allowed instructions |
|---|---|---|
missing | No PDA at the derived address | start_session |
active | expires_at > now | gated_action |
expired | expires_at ≤ now | close_session, or start_session to reopen |
The program enforces these transitions. Calling start_session on an already-active session returns SessionAlreadyActive. Calling close_session while active returns SessionStillActive.
Opening a session
The holder sends a transaction with a single start_session instruction. The program performs four checks in order:
- Token account ownership — the provided ATA must be owned by the signer.
- Mint match — the ATA must be for the
$SHAINmint declared inShainConfig. - Minimum balance — the ATA balance must be ≥
min_holding. - No active session — if a session PDA already exists for the caller and
now < expires_at, the instruction errors.
If all four pass, the program transfers session_fee from the holder's ATA to the treasury ATA via SPL Token CPI, then writes the session PDA:
session.owner = user.pubkey();
session.started_at = clock.unix_timestamp;
session.expires_at = started_at + session_duration; // = now + 24h
session.actions_count = 0;
session.total_sessions += 1;
cfg.total_sessions += 1;
cfg.total_fees_collected += session_fee;Using a session
Once the PDA is written, the caller holds a valid session until expires_at. Any dapp may call gated_action(tag):
require!(
session.owner != Pubkey::default(),
ShainError::SessionNotFound
);
require!(
session.is_active(clock.unix_timestamp),
ShainError::SessionExpired
);
session.actions_count = session.actions_count.checked_add(1)
.ok_or(ShainError::Overflow)?;Nothing about the downstream private route is hard-coded into Shain. The program exposes the gate; integrating dapps call gated_action and, on success, proceed with their own private call (a CPI into a confidential transfer, a private DEX route, a custom relayer, etc.).
Closing a session
After expires_at, anyone may call close_session. The instruction:
- Verifies the session is actually expired.
- Uses Anchor's
close = userattribute to transfer the account's lamports back to the original session owner. - Wipes the account data.
The holder can immediately open a new session. The total_sessions field on the fresh session PDA preserves the lifetime count from before close.
No admin interposition
There is no instruction in the program that lets the authority close or alter a live session on behalf of a holder. There is also no pausable flag. A holder's session can only end in two ways: expiry (time), or the holder themselves triggering a close after expiry.