ShainShain/docs
Shain/Docs/How sessions work
Product

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:

StateConditionAllowed instructions
missingNo PDA at the derived addressstart_session
activeexpires_at > nowgated_action
expiredexpires_at ≤ nowclose_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:

  1. Token account ownership — the provided ATA must be owned by the signer.
  2. Mint match — the ATA must be for the $SHAIN mint declared in ShainConfig.
  3. Minimum balance — the ATA balance must be ≥ min_holding.
  4. 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:

pseudo-coderust
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):

from programs/shain/src/instructions/gated_action.rsrust
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.