Integration
Monitor sessions
Poll session snapshots, render a countdown, and react to expiry. The pattern every UI uses.
React hook
"use client";
import { useEffect, useState } from "react";
import { PublicKey } from "@solana/web3.js";
import { ShainClient, SessionSnapshot, formatRemaining } from "@shain/sdk";
export function useShainSession(client: ShainClient | null, holder?: PublicKey) {
const [snap, setSnap] = useState<SessionSnapshot | null>(null);
const [remaining, setRemaining] = useState<string>("—");
useEffect(() => {
if (!client) return;
let cancelled = false;
const refresh = async () => {
const next = await client.snapshotSession(holder);
if (!cancelled) setSnap(next);
};
refresh();
const id = window.setInterval(refresh, 30_000);
return () => {
cancelled = true;
window.clearInterval(id);
};
}, [client, holder?.toBase58()]);
useEffect(() => {
if (!snap) return;
const tick = () => setRemaining(formatRemaining(snap));
tick();
const id = window.setInterval(tick, 1_000);
return () => window.clearInterval(id);
}, [snap]);
return { snapshot: snap, remaining };
}Usage in a component
export function SessionBadge() {
const client = useShainClient();
const { snapshot, remaining } = useShainSession(client);
if (!snapshot) {
return <span className="badge muted">no session</span>;
}
if (!snapshot.active) {
return <span className="badge muted">expired</span>;
}
return (
<span className="badge active">
active · {remaining}
</span>
);
}Server-side polling
For a Telegram bot or a Discord webhook, use the REST shape instead of the SDK to skip the bundle:
curl -s "https://shain.fun/api/wallet/balance?address=<WALLET>" | jq
# { "ok": true, "address": "...", "lamports": 4_132_041, "sol": 0.00413, "slot": 339_182_441, "network": "solana-devnet" }The balance endpoint is a thin RPC proxy. For session state, read the PDA directly or run a minimal SDK script in your worker.
WebSockets
If you want push updates instead of polling, subscribe to the session PDA via connection.onAccountChange:
const { session } = client.pdas(holder);
if (!session) return;
const subscriptionId = connection.onAccountChange(session, (info) => {
// info.data is the raw Buffer; parse with your SDK decoder
refreshSnapshot();
}, "confirmed");
// Clean up later: connection.removeAccountChangeListener(subscriptionId)