@solana-payment-channel/core
v0.2.7
Published
Core payment channel logic with x402 protocol integration for Solana
Downloads
54
Maintainers
Readme
@solana-payment-channel/core
Core payment channel management for Solana with seamless x402 protocol integration.
Features
- Off-chain Payments: Process payments instantly without blockchain transactions
- Overdraft/Credit Limit: Allow users to spend more than their deposit with customizable credit limits (up to any amount)
- Auto-Settlement: Automatically pays off debt when funds are added to the channel
- 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 @solana-payment-channel/core @solana/web3.js @coral-xyz/anchorQuick Start
import { ChannelManager, createChannelConfig } from '@solana-payment-channel/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 with optional credit limit
const channelId = await manager.openChannel({
serverPubkey: new PublicKey('SERVER_PUBLIC_KEY'),
initialDeposit: BigInt(10_000_000), // 10 USDC
creditLimit: BigInt(5_000_000), // 5 USDC credit (optional)
});
// Create payment authorization (client-side)
import { createPaymentAuthorization } from '@solana-payment-channel/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);Overdraft/Credit Limit Feature
Payment channels support customizable credit limits, allowing users to spend more than their initial deposit. This is perfect for applications that need to provide temporary credit to users.
How It Works
When opening a channel, you can specify a creditLimit that determines how much the user can overdraft:
const channelId = await manager.openChannel({
serverPubkey: serverPublicKey,
initialDeposit: BigInt(100_000_000), // 100 USDC deposited
creditLimit: BigInt(1000_000_000), // 1000 USDC credit limit
});
// User can now spend up to 1,100 USDC total (100 deposit + 1000 credit)Credit Limit Examples
- No overdraft:
creditLimit: BigInt(0)- User can only spend their deposit - 50% overdraft: Deposit 100 USDC, credit 50 USDC - Can spend 150 USDC total
- High credit: Deposit 100 USDC, credit 1000 USDC - Can spend 1,100 USDC total
- Unlimited (up to u64 max): Set any credit limit your business model allows
Auto-Settlement
When a user goes into debt and later adds funds to the channel, the system automatically:
- Pays off debt first: Debt is transferred directly to the server
- Adds remaining to balance: Net deposit goes to the channel
// Channel state after overdraft:
// - Deposited: 100 USDC
// - Claimed: 120 USDC
// - Debt: 20 USDC
// User adds 50 USDC to channel
await manager.addFunds(channelId, BigInt(50_000_000));
// Auto-settlement:
// - 20 USDC pays off debt (sent directly to server)
// - 30 USDC added to channel balance
// - Debt: 0 USDCDebt Protection
Channels with outstanding debt cannot be closed until the debt is settled:
try {
await manager.closeChannel(channelId); // Has 20 USDC debt
} catch (error) {
// Error: CannotCloseWithDebt
// User must add funds to settle debt first
}Use Cases
- Gaming: Allow players temporary credit for in-game purchases
- Subscriptions: Prevent service interruption during payment processing
- B2B Services: Provide business customers with credit terms
- Trial Periods: Offer limited credit during trial periods
- Loyalty Programs: Reward high-value users with higher credit limits
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 '@solana-payment-channel/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.creditLimit?: Optional credit limit (defaults to 0, no overdraft)options.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 '@solana-payment-channel/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 '@solana-payment-channel/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 '@solana-payment-channel/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
creditLimit: BigInt(5_000_000), // 5 USDC credit (optional)
});
console.log('Channel opened:', channelId);
console.log('Total spendable: 15 USDC (10 deposit + 5 credit)');Client: Creating Payment Authorization
import { createPaymentAuthorization } from '@solana-payment-channel/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 '@solana-payment-channel/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.
