A streamlined TypeScript SDK for building custom state channel applications with the Nitrolite framework. The SDK provides a simple client interface that allows developers to create and manage channels with their own application logic.
Nitrolite SDK provides a framework for developing scalable blockchain applications using state channels. State channels allow transactions to occur off-chain while maintaining the security guarantees of the underlying blockchain, resulting in:
- Instant Finality: Transactions settle immediately between parties
- Reduced Gas Costs: Most interactions happen off-chain, with minimal on-chain footprint
- High Throughput: Support for thousands of transactions per second
- Security Guarantees: Same security as on-chain, with cryptographic proofs
- Chain Agnostic: Works with any EVM-compatible blockchain
npm install @erc7824/nitroliteimport { NitroliteClient, AppDataTypes } from '@erc7824/nitrolite';
import { createPublicClient, createWalletClient, http, encodeAbiParameters, Hex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
// Setup clients
const publicClient = createPublicClient({
chain: mainnet,
transport: http('https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY')
});
const account = privateKeyToAccount('0x...');
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http('https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY')
});
// Initialize Nitrolite client with required configuration
const client = new NitroliteClient({
publicClient,
walletClient,
account,
chainId: 1, // The chain ID your contracts are deployed on
addresses: {
custody: '0xYOUR_CUSTODY_CONTRACT_ADDRESS',
adjudicators: {
base: '0xYOUR_BASE_ADJUDICATOR_ADDRESS',
numeric: '0xYOUR_NUMERIC_ADJUDICATOR_ADDRESS',
sequential: '0xYOUR_SEQUENTIAL_ADJUDICATOR_ADDRESS'
}
}
});
// Create a custom application
interface MyAppState {
value: bigint;
sequence: bigint;
metadata: string;
isComplete: boolean;
}
const channel = client.createCustomChannel<MyAppState>({
participants: ['0xALICE_ADDRESS', '0xBOB_ADDRESS'],
adjudicatorAddress: '0xADJUDICATOR_ADDRESS',
challenge: BigInt(86400), // 1 day challenge period
// Encode app state to bytes
encode: (state: MyAppState): Hex => {
return encodeAbiParameters(
[
{ type: 'uint256', name: 'value' },
{ type: 'uint256', name: 'sequence' },
{ type: 'string', name: 'metadata' },
{ type: 'bool', name: 'isComplete' }
],
[state.value, state.sequence, state.metadata, state.isComplete]
);
},
// Decode bytes back to app state
decode: (encoded: Hex): MyAppState => {
// Implementation would decode the bytes
return { value: BigInt(0), sequence: BigInt(0), metadata: "", isComplete: false };
},
// Define your application logic
validateTransition: (prevState, nextState, signer) => {
// Only allow increasing values and sequence numbers
return nextState.sequence > prevState.sequence &&
nextState.value >= prevState.value;
},
// Define when the application state is final
isFinal: (state) => state.isComplete,
// Initial state
initialState: { value: BigInt(0), sequence: BigInt(0), metadata: "", isComplete: false }
});
// Open the channel with initial funding
await channel.open(
'0xTOKEN_ADDRESS', // ERC20 token address
[BigInt(100), BigInt(100)] // Both participants fund with 100 tokens
);
// Update application state
await channel.updateAppState({
value: BigInt(50),
sequence: BigInt(1),
metadata: "First update",
isComplete: false
});
// Close the channel when done
await channel.close();- Open/close channels with configurable challenge periods
- Challenge resolution for uncooperative counterparties
- Checkpointing to prevent disputes
Generic application interface that you can extend with your own logic:
- Custom Applications: Build any application logic on top of state channels
- Application Logic Interface: Define your own rules for state transitions
- Built-in Helpers: Utility functions for common application patterns
- Example Applications: Counter, MicroPayment, and more examples provided
Support for custom state transition validators (adjudicators):
- Use Standard Adjudicators: Built-in support for common patterns
- Custom Adjudicator ABIs: Provide your own adjudicator contract ABIs
- Adjudicator Registry: Register and reference adjudicators by type
- Type-safe Interface: TypeScript generics for your application states
- Message Types for protocol communication
- Flexible Design - implement your own communication layer
- Type Definitions for state proposals, signatures, and notifications
- State hashing and verification
- Signature generation and validation
- Channel ID computation
For detailed API documentation, see API.md.
A state channel is a relationship between participants that allows them to exchange state updates off-chain, with the blockchain serving as the ultimate arbiter in case of disputes.
+---------+ +---------+
| | Off-chain state | |
| Alice | <-------------→ | Bob |
| | updates | |
+---------+ +---------+
↑ ↑
| On-chain resolution |
+------------+ +---------------+
| |
+----+--+----+
| |
| Blockchain |
| |
+------------+
The SDK provides message type definitions for off-chain communication between participants, but lets you implement the transport layer yourself.
import { MessageType, ProposeStateMessage } from '@erc7824/nitrolite';
// Example: Creating your own message transport
class MyChannelMessenger {
async sendMessage(message: NitroliteMessage) {
// Your implementation - could use WebSockets, HTTP, etc.
await this.socket.send(JSON.stringify(message));
}
async proposeState(channelId, state) {
const stateHash = getStateHash(this.channel, state);
// Create a properly formatted message
const message: ProposeStateMessage = {
type: MessageType.PROPOSE_STATE,
channelId,
timestamp: Date.now(),
state,
stateHash
};
await this.sendMessage(message);
}
}See the examples directory for examples of using the Nitrolite SDK:
- NextJS TypeScript Example - A complete frontend application demonstrating how to use the SDK with a React-based web application.
- Nitrolite RPC Example - A simple example demonstrating how to use the NitroliteRPC protocol with WebSockets.
More examples are coming soon! Check the examples README for details.
Nitrolite includes a lightweight RPC protocol for communication between clients and state channel brokers.
Messages are formatted as fixed JSON arrays with a standard structure:
[request_id, method, params, timestamp]
Request Message:
{
"req": [1001, "subtract", [42, 23], 1741344819012],
"sig": "0xa0ad67f51cc73aee5b874ace9bc2e2053488bde06de257541e05fc58fd8c4f149cca44f1c702fcbdbde0aa09bcd24456f465e5c3002c011a3bc0f317df7777d2"
}Response Message:
{
"res": [1001, "subtract", [19], 1741344819814],
"sig": "0xd73268362b04516451ec52170f5c8ca189d35d9ac5e9041c156c9f0faf9aebd2891309e3b2b5d8788578ab3449c96f7aa81aefb25482b53f02bac42c65f806e5"
}Error Message:
{
"err": [1001, -32601, "Method not found", 1741344819814],
"sig": "0xd73268362b04516451ec52170f5c8ca189d35d9ac5e9041c156c9f0faf9aebd2891309e3b2b5d8788578ab3449c96f7aa81aefb25482b53f02bac42c65f806e5"
}import { NitroliteRPC, prepareMessageForSigning } from '@erc7824/nitrolite';
// Create a request message
const request = NitroliteRPC.createRequest(
'subtract', // Method name
[42, 23], // Method parameters
1001 // Optional: Request ID
);
// Sign the request with your own signer function
const signedRequest = await NitroliteRPC.signMessage(
request,
(message) => yourSigningFunction(message)
);
// Send the message via your own WebSocket connection
ws.send(JSON.stringify(signedRequest));Nitrolite SDK provides utilities to help with consistent message signing across different platforms:
import { prepareMessageForSigning, createPayloadHash } from '@erc7824/nitrolite';
import { ethers } from 'ethers';
// Example: Creating an Ethereum signer with ethers.js
function createEthersSigner(privateKey: string) {
const wallet = new ethers.Wallet(privateKey);
return {
address: wallet.address,
publicKey: wallet.publicKey,
sign: async (message: string): Promise<string> => {
// Prepare the message for signing using the SDK utility
// This handles JSON conversion and keccak256 hashing in a consistent way
const hashBytes = prepareMessageForSigning(message);
// Sign the prepared hash bytes
const signature = await wallet.signMessage(hashBytes);
return signature;
}
};
}
// Example: Creating a signer with viem
function createViemSigner(privateKey: string) {
const account = privateKeyToAccount(privateKey);
return {
address: account.address,
sign: async (message: string): Promise<string> => {
// Get the keccak hash of the message
const hash = createPayloadHash(message);
// Sign the hash with viem
return signMessage({
account,
message: { raw: hash }
});
}
};
}| Method | Description | Parameters |
|---|---|---|
auth |
Authenticate with the broker | [publicKey] |
subscribe |
Subscribe to a channel | [channelName] |
publish |
Publish a message to a channel | [channelName, messageData] |
ping |
Check connection latency | [timestamp] |
state_update |
Update channel state | [channelId, stateData, signature] |
-32700: Parse Error - Invalid JSON-32600: Invalid Request - Not a valid request object-32601: Method Not Found - Method doesn't exist-32602: Invalid Params - Invalid method parameters-32603: Internal Error - Internal JSON-RPC error-32001: Invalid State - Invalid state transition-32002: Channel Not Found - Referenced channel doesn't exist-32003: Invalid Signature - Signature verification failed
Nitrolite SDK works with any EVM-compatible blockchain. Here's how to use it with different chains:
import { NitroliteClient } from '@erc7824/nitrolite';
import { createPublicClient, http } from 'viem';
import { mainnet, optimism, arbitrum, base, polygon } from 'viem/chains';
// Example: Initialize client for Optimism
const optimismClient = new NitroliteClient({
publicClient: createPublicClient({
chain: optimism,
transport: http('https://optimism.example.com')
}),
// Chain ID is automatically detected from the publicClient
addresses: {
custody: '0xOPTIMISM_CUSTODY_ADDRESS',
adjudicators: {
base: '0xOPTIMISM_BASE_ADJUDICATOR',
// Add other adjudicators as needed
}
}
});
// Example: Initialize client for Arbitrum
const arbitrumClient = new NitroliteClient({
publicClient: createPublicClient({
chain: arbitrum,
transport: http('https://arbitrum.example.com')
}),
// Explicitly provide chain ID if needed
chainId: arbitrum.id,
addresses: {
custody: '0xARBITRUM_CUSTODY_ADDRESS',
adjudicators: {
base: '0xARBITRUM_BASE_ADJUDICATOR',
// Add other adjudicators as needed
}
}
});
// The same SDK code works across all chains
// Just initialize with the appropriate client for your target chainimport { NitroliteClient } from '@erc7824/nitrolite';
import { Abi } from 'viem';
// Your custom adjudicator ABI
const myGameAdjudicatorAbi: Abi = [
{
type: 'function',
name: 'adjudicate',
inputs: [
// Channel structure
{
name: 'chan',
type: 'tuple',
components: [
{ name: 'participants', type: 'address[2]' },
{ name: 'adjudicator', type: 'address' },
{ name: 'challenge', type: 'uint64' },
{ name: 'nonce', type: 'uint64' }
]
},
// Candidate state
{
name: 'candidate',
type: 'tuple',
components: [
{ name: 'data', type: 'bytes' },
// Rest of the structure...
]
},
// Proof states
{
name: 'proofs',
type: 'tuple[]',
components: [
// State structure...
]
}
],
outputs: [
// Define outputs...
],
stateMutability: 'view'
}
// Other function definitions...
];
// Initialize client with custom adjudicator ABIs
const client = new NitroliteClient({
publicClient,
walletClient,
account,
chainId: 1, // Required - specify the chain ID your contracts are deployed on
addresses: {
custody: '0xCUSTODY_ADDRESS', // Required
adjudicators: {
// You must provide at least a 'base' adjudicator
base: '0xBASE_ADJUDICATOR_ADDRESS',
// And you can register any custom adjudicators
myGame: '0xMY_GAME_ADJUDICATOR_ADDRESS'
}
},
// Optionally provide custom ABIs for your adjudicators
adjudicatorAbis: {
myGame: myGameAdjudicatorAbi
}
});
// Or register an adjudicator ABI after initialization
client.registerAdjudicatorAbi('myOtherGame', myOtherGameAdjudicatorAbi);
// Create a channel using the custom adjudicator
const channel = client.createCustomChannel<MyGameState>({
participants: [player1Address, player2Address],
adjudicatorType: 'myGame', // References the registered ABI
encode: gameStateEncoder,
decode: gameStateDecoder,
// Rest of config...
});# Install dependencies
npm install
# Build the SDK
npm run build
# Run tests
npm test
# Type checking
npm run typecheck
# Lint code
npm run lint
# Clean build artifacts
npm run cleanContributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.