nitroguard
v0.1.1
Published
Production-grade state channel lifecycle SDK for Yellow Network / ERC-7824
Maintainers
Readme
@erc7824/nitrolite gives you the raw primitives. NitroGuard gives you a production-ready channel: state machine enforcement, automatic persistence, dispute protection, and typed payloads — all in one composable API.
npm install nitroguard viemQuickstart
import { NitroGuard } from 'nitroguard';
import { createWalletClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const wallet = createWalletClient({ account, chain: mainnet, transport: http() });
const signer = {
address: account.address,
signTypedData: (p) => wallet.signTypedData(p),
signMessage: (p) => wallet.signMessage(p),
};
const channel = await NitroGuard.open({
clearnode: 'wss://clearnet.yellow.com/ws',
signer,
chain: mainnet,
rpcUrl: 'https://eth.llamarpc.com',
assets: [{ token: USDC, amount: 100n * 10n ** 6n }],
});
await channel.send({ type: 'payment', to: bob, amount: 10n * 10n ** 6n });
await channel.send({ type: 'payment', to: bob, amount: 5n * 10n ** 6n });
await channel.close();ClearNode goes offline mid-session? NitroGuard submits a challenge automatically and recovers your funds with no intervention required.
Core API
NitroGuard.open(config)
Opens a channel and returns it in ACTIVE state.
| Option | Type | Required | |
|---|---|:---:|---|
| clearnode | string | ✓ | ClearNode WebSocket URL |
| signer | EIP712Signer | ✓ | Any EIP-712 signer — viem WalletClient works |
| chain | Chain | ✓ | viem Chain object |
| rpcUrl | string | ✓ | RPC endpoint |
| assets | AssetAllocation[] | ✓ | Tokens and amounts to deposit |
| persistence | PersistenceAdapter | | Defaults to IndexedDB / LevelDB |
| custodyClient | CustodyClient | | Required for autoDispute and forceClose |
| autoDispute | boolean | | Auto-respond to stale challenges |
| clearnodeSilenceTimeout | number | | ms of silence before forceClose() triggers |
| protocol | Protocol<T> | | Typed payload schema — see Protocol Schemas |
channel
channel.id // string — keccak256 of channel params
channel.status // 'VOID' | 'INITIAL' | 'ACTIVE' | 'DISPUTE' | 'FINAL'
channel.version // number — increments on every confirmed send()
await channel.send(payload) // off-chain state update, sub-second
await channel.close() // mutual close, ClearNode co-signs final state
await channel.forceClose() // unilateral — challenges on-chain
await channel.checkpoint() // anchors current version on-chain
await channel.withdraw() // release funds after FINAL
channel.on('statusChange', (to, from) => {})
channel.on('stateUpdate', (version, state) => {})
channel.on('error', (err) => {})NitroGuard.restore(channelId, config)
Resumes a channel after process restart or page refresh. Reconnects to ClearNode and picks up at the last persisted version.
const channel = await NitroGuard.restore(channelId, {
clearnode, signer, chain, rpcUrl, persistence,
});
// channel.version === whatever you left it atFund protection
Two independent layers of protection:
DisputeWatcher (autoDispute: true) — subscribes to on-chain ChallengeRegistered events. If a stale version is challenged, NitroGuard submits your latest co-signed state before the window closes.
ClearNodeMonitor (clearnodeSilenceTimeout) — tracks the last message timestamp. Triggers forceClose() automatically if ClearNode goes quiet.
const channel = await NitroGuard.open({
...config,
persistence,
custodyClient,
autoDispute: true,
clearnodeSilenceTimeout: 60_000,
onChallengeDetected: (id) => notify(`Challenge on ${id}`),
onFundsReclaimed: (id, amts) => log('Recovered', amts),
});Both require a persistence adapter (to have a state to submit) and custodyClient (to submit it).
→ Dispute Guide
Typed protocols
Define your payload schema once; send() becomes fully type-checked at compile time and validated at runtime.
import { defineProtocol } from 'nitroguard';
import { z } from 'zod'; // npm install zod
const OptionsProtocol = defineProtocol({
name: 'options-v1',
version: 1,
schema: z.object({
type: z.enum(['open', 'exercise', 'expire']),
strikePrice: z.bigint().positive(),
expiry: z.number(),
premium: z.bigint().nonnegative(),
}),
transitions: {
exerciseBeforeExpiry: (_prev, next) =>
next.type !== 'exercise' || Date.now() <= next.expiry,
},
});
const channel = await NitroGuard.open({ ...config, protocol: OptionsProtocol });
await channel.send({ type: 'open', strikePrice: 3000n, expiry: Date.now() + 86_400_000, premium: 50n });
// TypeScript error if fields are missing or wrong typeReact
// npm install react react-dom
import { NitroGuardProvider, useChannel, useChannelBalance } from 'nitroguard/react';
// Provider is SSR-safe — transport is initialized lazily inside useEffect
function App() {
return (
<NitroGuardProvider
config={{ clearnode: 'wss://...', signer, chain: mainnet, rpcUrl }}
createTransport={() => new MyTransport()}
>
<PaymentUI />
</NitroGuardProvider>
);
}
function PaymentUI() {
const { channel, status, isLoading, open, send, close } = useChannel();
const { myBalance } = useChannelBalance(channel);
return (
<>
<p>{(myBalance / 10n ** 6n).toString()} USDC — {status}</p>
{status === 'VOID' && (
<button disabled={isLoading} onClick={() => open([{ token: USDC, amount: 100n * 10n ** 6n }])}>
Open channel
</button>
)}
{status === 'ACTIVE' && (
<>
<button onClick={() => send({ amount: 10n * 10n ** 6n })}>Pay 10 USDC</button>
<button onClick={close}>Close</button>
</>
)}
</>
);
}→ React Guide — includes Next.js App Router setup
Persistence
| Adapter | Environment | Install |
|---|---|---|
| IndexedDBAdapter | Browser | built-in |
| LevelDBAdapter | Node.js | npm install level |
| MemoryAdapter | Tests | built-in |
All adapters implement the same four-method interface: save, loadLatest, listChannels, clear — making custom adapters (Redis, Postgres, SQLite) straightforward.
State machine
Every method maps to a valid FSM transition. Calling the wrong method in the wrong state throws InvalidTransitionError immediately — no silent failures.
VOID ──open()──▶ INITIAL ──▶ ACTIVE
│
send() ──▶ ACTIVE (loops)
checkpoint() ──▶ ACTIVE
│
close() ◀──┤──▶ DISPUTE ──▶ (auto-respond)
│ │
FINAL ◀──────────────────┘
│
withdraw()
│
VOIDErrors
All errors extend NitroGuardError and carry a .code string for programmatic handling.
import {
InvalidTransitionError, // method called in wrong state
CoSignatureTimeoutError, // ClearNode didn't respond in time
NoPersistenceError, // forceClose() with no persisted state
ProtocolValidationError, // payload failed Zod schema
ProtocolTransitionError, // transition guard rejected the state
ChannelNotFoundError, // restore() with unknown channelId
ClearNodeUnreachableError, // WebSocket connection failed
} from 'nitroguard';Documentation
| | | |---|---| | Quick Start | Zero to a running channel in 5 minutes | | State Machine | All states, transitions, and error handling | | Dispute Guide | autoDispute, silence timeout, manual forceClose | | Persistence Guide | Adapter selection and writing custom adapters | | Protocol Schemas | defineProtocol(), typed sends, transition guards | | React Guide | Hooks reference, Next.js App Router setup |
MIT © Aayush Giri
