@bober3r/solana-payment-channels-core
v0.1.0
Published
Core payment channel logic with x402 protocol integration for Solana
Maintainers
Readme
@bober3r/solana-payment-channels-core
Core payment channel management for Solana with seamless x402 protocol integration.
Features
- Off-chain Payments: Process payments instantly without blockchain transactions
- Cryptographic Security: Ed25519 signatures for payment authorization
- State Management: Efficient in-memory caching with TTL
- Automatic Fallback: Seamlessly falls back to x402 protocol when channels unavailable
- Type-Safe: Full TypeScript support with comprehensive types
- Event-Driven: Subscribe to channel state changes in real-time
- Production-Ready: Comprehensive error handling and validation
Installation
npm install @bober3r/solana-payment-channels-core @solana/web3.js @coral-xyz/anchorQuick Start
import { ChannelManager, createChannelConfig } from '@bober3r/solana-payment-channels-core';
import { Keypair, PublicKey } from '@solana/web3.js';
// Initialize configuration
const config = createChannelConfig('devnet', 'YOUR_PROGRAM_ID');
// Create channel manager
const manager = new ChannelManager(config, clientKeypair);
// Open a payment channel
const channelId = await manager.openChannel({
serverPubkey: new PublicKey('SERVER_PUBLIC_KEY'),
initialDeposit: BigInt(10_000_000), // 10 USDC
});
// Create payment authorization (client-side)
import { createPaymentAuthorization } from '@bober3r/solana-payment-channels-core';
const authorization = await createPaymentAuthorization(
Buffer.from(channelId, 'hex'),
BigInt(1_000_000), // 1 USDC
BigInt(1), // nonce
clientKeypair
);
// Claim payment (server-side)
const result = await manager.claimPayment(channelId, {
amount: BigInt(1_000_000),
authorization,
});
console.log('Payment claimed:', result.success);
console.log('Remaining balance:', result.remainingBalance);Architecture
Payment Flow
┌─────────────┐ ┌─────────────┐
│ Client │ │ Server │
└──────┬──────┘ └──────┬──────┘
│ │
│ 1. Open Channel (on-chain) │
├─────────────────────────────────>│
│ │
│ 2. Create Payment Auth │
│ (sign off-chain) │
│ │
│ 3. Send Authorization │
├─────────────────────────────────>│
│ │
│ 4. Verify & Process
│ (instant, free)
│ │
│ 5. Payment Confirmed │
│<─────────────────────────────────┤
│ │Integration with x402
The package seamlessly integrates with the x402 protocol for fallback payments:
import { ChannelManager, FallbackManager } from '@bober3r/solana-payment-channels-core';
const manager = new ChannelManager(config, wallet);
const fallback = manager.getFallbackManager();
// Automatically determine best payment method
const { method, reason } = await fallback.determinePaymentMethod(
channelState,
amount,
serverUrl
);
if (method === 'channel') {
// Use off-chain channel payment (instant, free)
const result = await manager.claimPayment(channelId, options);
} else {
// Fall back to x402 on-chain payment
const receipt = await fallback.payWithX402({
amount,
recipient: serverPubkey,
});
}API Reference
ChannelManager
Main class for managing payment channels.
Constructor
new ChannelManager(config: ChannelConfig, wallet: Keypair)Methods
openChannel
Opens a new payment channel on-chain.
async openChannel(options: OpenChannelOptions): Promise<string>Parameters:
options.serverPubkey: Server's public keyoptions.initialDeposit: Initial deposit amount in smallest unitsoptions.expiry?: Optional expiry date (defaults to 7 days)
Returns: Channel ID as hex string
Throws:
InsufficientFundsError: If wallet lacks sufficient USDCTransactionError: If transaction fails
addFunds
Adds funds to an existing channel.
async addFunds(channelId: string, amount: bigint): Promise<string>Returns: Transaction signature
claimPayment
Claims a payment from a channel (server-side).
async claimPayment(
channelId: string,
options: ClaimPaymentOptions
): Promise<PaymentResult>Parameters:
channelId: Channel identifieroptions.amount: Payment amountoptions.authorization: Signed payment authorization
Returns: Payment result with success status and updated balances
closeChannel
Closes a channel and returns remaining funds.
async closeChannel(channelId: string): Promise<string>getChannelState
Retrieves current channel state from blockchain.
async getChannelState(channelId: string): Promise<ChannelState>getAllChannels
Gets all channels for a public key.
async getAllChannels(pubkey: PublicKey): Promise<ChannelState[]>subscribeToChannel
Subscribes to channel state changes.
subscribeToChannel(
channelId: string,
callback: (state: ChannelState) => void
): () => voidReturns: Unsubscribe function
ChannelStateManager
Manages channel state with in-memory caching.
const stateManager = new ChannelStateManager({ ttl: 60000 });
// Update state
stateManager.updateState(channelId, newState);
// Get cached state
const state = stateManager.getState(channelId);
// Subscribe to changes
const unsubscribe = stateManager.subscribe(channelId, (state) => {
console.log('State updated:', state);
});
// Invalidate cache
stateManager.invalidate(channelId);Signature Utilities
createPaymentAuthorization
Creates a signed payment authorization (client-side).
async function createPaymentAuthorization(
channelId: Buffer,
amount: bigint,
nonce: bigint,
signer: Keypair
): Promise<PaymentAuthorization>verifyPaymentAuthorization
Verifies a payment authorization signature (server-side).
async function verifyPaymentAuthorization(
authorization: PaymentAuthorization,
expectedPublicKey: PublicKey
): Promise<boolean>serializePaymentData
Serializes payment data for signing.
function serializePaymentData(
channelId: Buffer,
amount: bigint,
nonce: bigint
): BufferFallbackManager
Manages fallback to x402 protocol.
const fallback = new FallbackManager({ connection });
// Check if server supports channels
const supportsChannels = await fallback.shouldUseChannel(serverUrl);
// Get server capabilities
const capabilities = await fallback.getServerCapabilities(serverUrl);
// Determine best payment method
const { method, reason } = await fallback.determinePaymentMethod(
channelState,
amount,
serverUrl
);Error Handling
The package provides comprehensive error classes:
import {
ChannelError,
InsufficientFundsError,
ChannelNotFoundError,
ChannelClosedError,
ChannelExpiredError,
InvalidSignatureError,
InvalidNonceError,
TransactionError,
ConfigurationError,
} from '@bober3r/solana-payment-channels-core';
try {
await manager.claimPayment(channelId, options);
} catch (error) {
if (error instanceof InsufficientFundsError) {
console.log('Required:', error.required);
console.log('Available:', error.available);
} else if (error instanceof InvalidNonceError) {
console.log('Expected:', error.expected);
console.log('Received:', error.received);
}
}Configuration
ChannelConfig
interface ChannelConfig {
rpcUrl: string;
network: 'devnet' | 'mainnet-beta';
programId: PublicKey;
usdcMint: PublicKey;
defaultExpiry?: number; // seconds
minBalance?: bigint;
autoRefillAmount?: bigint;
}Helper Function
import { createChannelConfig, NETWORKS } from '@bober3r/solana-payment-channels-core';
const config = createChannelConfig('devnet', programId, {
defaultExpiry: 14 * 24 * 60 * 60, // 14 days
minBalance: BigInt(500_000), // 0.5 USDC
});Examples
Client: Opening a Channel
import { ChannelManager } from '@bober3r/solana-payment-channels-core';
import { Keypair } from '@solana/web3.js';
const client = Keypair.generate();
const manager = new ChannelManager(config, client);
const channelId = await manager.openChannel({
serverPubkey: serverPublicKey,
initialDeposit: BigInt(10_000_000), // 10 USDC
});
console.log('Channel opened:', channelId);Client: Creating Payment Authorization
import { createPaymentAuthorization } from '@bober3r/solana-payment-channels-core';
const state = await manager.getChannelState(channelId);
const nextNonce = state.nonce + BigInt(1);
const authorization = await createPaymentAuthorization(
Buffer.from(channelId, 'hex'),
BigInt(1_000_000), // 1 USDC
nextNonce,
clientKeypair
);
// Send authorization to server
await fetch(serverUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ authorization }),
});Server: Processing Payment
import { ChannelManager, verifyPaymentAuthorization } from '@bober3r/solana-payment-channels-core';
// Receive authorization from client
const { authorization } = await request.json();
// Verify and process payment
const result = await manager.claimPayment(channelId, {
amount: BigInt(1_000_000),
authorization,
});
if (result.success) {
console.log('Payment received!');
console.log('Remaining balance:', result.remainingBalance);
// Provide service to client
} else {
console.error('Payment failed:', result.error);
}Monitoring Channel State
const unsubscribe = manager.subscribeToChannel(channelId, (state) => {
console.log('Channel updated:');
console.log(' Balance:', state.currentBalance);
console.log(' Nonce:', state.nonce);
console.log(' Claimed:', state.claimedAmount);
// Auto-refill if balance is low
if (state.currentBalance < config.minBalance) {
manager.addFunds(channelId, config.autoRefillAmount);
}
});Best Practices
Security
- Always validate nonces: Ensure nonces increment to prevent replay attacks
- Verify signatures: Use
verifyPaymentAuthorizationbefore processing payments - Check expiry: Validate channel hasn't expired before accepting payments
- Secure key storage: Never expose private keys in client-side code
Performance
- Use caching: State manager caches reduce RPC calls
- Batch operations: Open channels for multiple services together
- Monitor balance: Subscribe to state changes to prevent insufficient funds
Integration
- Fallback strategy: Always have x402 fallback for when channels unavailable
- Server capabilities: Check server support before requiring channels
- Graceful degradation: Handle channel failures smoothly
License
MIT
Contributing
Contributions welcome! Please see CONTRIBUTING.md for guidelines.
