Multi-Party Applications
Build sophisticated applications with multiple participants using Yellow SDK's flexible channel architecture.
Three-Party Applications
Escrow Pattern
Perfect for marketplace transactions with buyer, seller, and mediator:
Escrow Application Session
import { createAppSessionMessage } from '@erc7824/nitrolite';
async function createEscrowSession(buyer, seller, mediator, amount) {
const appDefinition = {
protocol: 'escrow-v1',
participants: [buyer, seller, mediator],
weights: [33, 33, 34], // Equal voting with mediator tiebreaker
quorum: 67, // Requires 2 of 3 consensus
challenge: 0,
nonce: Date.now()
};
const allocations = [
{ participant: buyer, asset: 'usdc', amount: amount.toString() },
{ participant: seller, asset: 'usdc', amount: '0' },
{ participant: mediator, asset: 'usdc', amount: '0' }
];
return createAppSessionMessage(messageSigner, [{
definition: appDefinition,
allocations
}]);
}
Tournament Structure
Multi-player competitive applications with prize distribution:
async function createTournament(players, entryFee, messageSigner) {
// Create application session for tournament logic
const appDefinition = {
protocol: 'tournament-v1',
participants: [...players, houseAddress],
weights: [...players.map(() => 0), 100], // House controls tournament
quorum: 100,
challenge: 0,
nonce: Date.now()
};
const allocations = players.map(player => ({
participant: player,
asset: 'usdc',
amount: entryFee.toString()
})).concat([{
participant: houseAddress,
asset: 'usdc',
amount: '0'
}]);
const tournamentMessage = await createAppSessionMessage(messageSigner, [{
definition: appDefinition,
allocations
}]);
// Send to ClearNode
ws.send(tournamentMessage);
console.log('🎮 Tournament session created!');
return appDefinition;
}
Consensus Mechanisms
Weighted Voting
Different participants can have different voting power:
const governanceSession = {
protocol: 'governance-v1',
participants: [admin, moderator1, moderator2, user1, user2],
weights: [40, 20, 20, 10, 10], // Admin has 40% vote
quorum: 60, // Requires 60% consensus
challenge: 0,
nonce: Date.now()
};
Multi-Signature Requirements
const multiSigSession = {
protocol: 'multisig-v1',
participants: [signer1, signer2, signer3, beneficiary],
weights: [33, 33, 34, 0], // 3 signers, 1 beneficiary
quorum: 67, // Requires 2 of 3 signatures
challenge: 0,
nonce: Date.now()
};
Application Patterns
Payment Routing
Route payments through multiple sessions for optimal liquidity:
class PaymentRouter {
constructor() {
this.sessions = new Map(); // route -> sessionId
this.liquidity = new Map(); // sessionId -> available amounts
this.ws = null;
this.messageSigner = null;
}
async routePayment(amount, recipient) {
// Find optimal path considering liquidity and fees
const path = await this.findOptimalPath(this.userAddress, recipient, amount);
if (path.length === 1) {
// Direct session exists
return this.sendDirectPayment(path[0], amount, recipient);
} else {
// Multi-hop payment required
return this.executeMultiHopPayment(path, amount, recipient);
}
}
async sendDirectPayment(sessionId, amount, recipient) {
const paymentMessage = {
sessionId,
type: 'payment',
amount: amount.toString(),
recipient,
timestamp: Date.now()
};
const signature = await this.messageSigner(JSON.stringify(paymentMessage));
this.ws.send(JSON.stringify({
...paymentMessage,
signature
}));
return paymentMessage;
}
}
State Synchronization
Keep multiple channels synchronized for complex applications:
class ChannelSynchronizer {
constructor() {
this.channels = new Map();
this.syncGroups = new Map();
}
addToSyncGroup(groupId, channelId) {
if (!this.syncGroups.has(groupId)) {
this.syncGroups.set(groupId, new Set());
}
this.syncGroups.get(groupId).add(channelId);
}
async syncStateUpdate(groupId, stateUpdate) {
const channels = this.syncGroups.get(groupId);
// Apply update to all channels in sync group
const updatePromises = Array.from(channels).map(channelId =>
this.applyStateUpdate(channelId, stateUpdate)
);
const results = await Promise.allSettled(updatePromises);
// Handle any failures
const failures = results.filter(r => r.status === 'rejected');
if (failures.length > 0) {
await this.handleSyncFailures(groupId, failures);
}
return results;
}
}
ClearNode Integration
Multi-Session Management
class MultiSessionManager {
constructor() {
this.connections = new Map(); // sessionId -> WebSocket
this.sessions = new Map(); // sessionId -> sessionData
this.messageSigner = null;
}
async connectToSession(sessionConfig) {
const ws = new WebSocket('wss://clearnet.yellow.com/ws');
return new Promise((resolve, reject) => {
ws.onopen = async () => {
// Create application session
const sessionMessage = await createAppSessionMessage(
this.messageSigner,
[sessionConfig]
);
ws.send(sessionMessage);
const sessionId = this.generateSessionId();
this.connections.set(sessionId, ws);
this.sessions.set(sessionId, sessionConfig);
resolve({ ws, sessionId });
};
ws.onerror = reject;
ws.onmessage = (event) => {
this.handleSessionMessage(sessionId, parseRPCResponse(event.data));
};
});
}
async broadcastToAllSessions(message) {
const broadcasts = Array.from(this.connections.entries()).map(([sessionId, ws]) => {
if (ws.readyState === WebSocket.OPEN) {
const signature = await this.messageSigner(JSON.stringify(message));
return ws.send(JSON.stringify({ ...message, signature }));
}
});
return Promise.allSettled(broadcasts);
}
handleSessionMessage(sessionId, message) {
// Route messages based on session and message type
switch (message.type) {
case 'session_created':
this.handleSessionCreated(sessionId, message.data);
break;
case 'participant_message':
this.handleParticipantMessage(sessionId, message.data);
break;
case 'error':
this.handleSessionError(sessionId, message.error);
break;
}
}
}
Session Management
class SessionManager {
constructor() {
this.activeSessions = new Map();
this.sessionParticipants = new Map();
}
async createMultiPartySession(participants, sessionConfig) {
const appDefinition = {
protocol: sessionConfig.protocol,
participants,
weights: sessionConfig.weights || participants.map(() => 100 / participants.length),
quorum: sessionConfig.quorum || 51,
challenge: 0,
nonce: Date.now()
};
const allocations = participants.map((participant, index) => ({
participant,
asset: sessionConfig.asset || 'usdc',
amount: sessionConfig.initialAmounts?.[index]?.toString() || '0'
}));
const sessionMessage = await createAppSessionMessage(
this.messageSigner,
[{ definition: appDefinition, allocations }]
);
// Send to all participants' connections
await this.broadcastSessionCreation(participants, sessionMessage);
return this.waitForSessionConfirmation();
}
async coordinateStateUpdate(sessionId, updateData) {
const session = this.activeSessions.get(sessionId);
const participants = this.sessionParticipants.get(sessionId);
// Create coordinated update message
const updateMessage = {
type: 'coordinate_update',
sessionId,
data: updateData,
requiredSignatures: this.calculateRequiredSignatures(session),
timestamp: Date.now()
};
// Send to all participants
await this.broadcastToParticipants(participants, updateMessage);
// Wait for consensus
return this.waitForConsensus(sessionId, updateMessage.timestamp);
}
}
Advanced Patterns
Channel Hierarchies
Create parent-child relationships between channels:
class HierarchicalChannels {
constructor(client) {
this.client = client;
this.parentChannels = new Map();
this.childChannels = new Map();
}
async createParentChannel(participants, totalCapacity) {
const parentChannel = await this.client.createChannel({
participants: [...participants, this.coordinatorAddress],
initialAllocationAmounts: [...totalCapacity, 0n],
stateData: '0x'
});
this.parentChannels.set(parentChannel.channelId, {
participants,
totalCapacity,
childChannels: new Set()
});
return parentChannel;
}
async createChildChannel(parentChannelId, subset, allocation) {
const parent = this.parentChannels.get(parentChannelId);
// Validate subset is from parent participants
if (!subset.every(addr => parent.participants.includes(addr))) {
throw new Error('Child participants must be subset of parent');
}
const childChannel = await this.client.createChannel({
participants: subset,
initialAllocationAmounts: allocation,
stateData: this.encodeParentReference(parentChannelId)
});
// Link to parent
parent.childChannels.add(childChannel.channelId);
this.childChannels.set(childChannel.channelId, parentChannelId);
return childChannel;
}
}
Cross-Channel Coordination
Coordinate state updates across multiple related channels:
class CrossChannelCoordinator {
constructor() {
this.channelGroups = new Map();
this.pendingUpdates = new Map();
}
async coordinateAcrossChannels(groupId, operation) {
const channels = this.channelGroups.get(groupId);
// Prepare updates for all channels
const updatePromises = channels.map(channelId =>
this.prepareChannelUpdate(channelId, operation)
);
const preparedUpdates = await Promise.all(updatePromises);
// Execute all updates atomically
try {
const results = await this.executeAtomicUpdates(preparedUpdates);
return results;
} catch (error) {
// Rollback all updates on failure
await this.rollbackUpdates(preparedUpdates);
throw error;
}
}
async executeAtomicUpdates(updates) {
// Use two-phase commit protocol
// Phase 1: Prepare all updates
const preparePromises = updates.map(update =>
this.prepareUpdate(update)
);
const prepared = await Promise.all(preparePromises);
// Phase 2: Commit all updates
const commitPromises = prepared.map(prep =>
this.commitUpdate(prep)
);
return Promise.all(commitPromises);
}
}
Real-World Applications
Gaming Lobby System
class GamingLobby {
constructor() {
this.gameRooms = new Map();
this.playerQueues = new Map();
this.ws = null;
this.messageSigner = null;
}
async createGameRoom(gameType, maxPlayers, buyIn) {
const roomId = this.generateRoomId();
this.gameRooms.set(roomId, {
gameType,
maxPlayers,
buyIn,
players: [],
status: 'WAITING'
});
// Broadcast room creation
const roomMessage = {
type: 'room_created',
roomId,
gameType,
maxPlayers,
buyIn,
timestamp: Date.now()
};
const signature = await this.messageSigner(JSON.stringify(roomMessage));
this.ws.send(JSON.stringify({ ...roomMessage, signature }));
return roomId;
}
async joinGameRoom(roomId, playerAddress) {
const room = this.gameRooms.get(roomId);
if (room.players.length >= room.maxPlayers) {
throw new Error('Room is full');
}
room.players.push(playerAddress);
// Start game when room is full
if (room.players.length === room.maxPlayers) {
await this.startGame(roomId);
}
return room;
}
async startGame(roomId) {
const room = this.gameRooms.get(roomId);
// Create game application session
const appDefinition = {
protocol: `${room.gameType}-v1`,
participants: [...room.players, this.serverAddress],
weights: [...room.players.map(() => 0), 100], // Server controls game
quorum: 100,
challenge: 0,
nonce: Date.now()
};
const allocations = room.players.map(player => ({
participant: player,
asset: 'usdc',
amount: room.buyIn.toString()
})).concat([{
participant: this.serverAddress,
asset: 'usdc',
amount: '0'
}]);
const gameSession = await createAppSessionMessage(this.messageSigner, [{
definition: appDefinition,
allocations
}]);
this.ws.send(gameSession);
room.status = 'ACTIVE';
room.sessionId = this.generateSessionId();
return room;
}
}
Subscription Service
class SubscriptionService {
constructor() {
this.subscriptions = new Map();
this.ws = null;
this.messageSigner = null;
}
async createSubscription(subscriber, provider, monthlyFee) {
const appDefinition = {
protocol: 'subscription-v1',
participants: [subscriber, provider, this.serviceAddress],
weights: [0, 100, 0], // Provider controls service delivery
quorum: 100,
challenge: 0,
nonce: Date.now()
};
const allocations = [
{ participant: subscriber, asset: 'usdc', amount: (monthlyFee * 12).toString() },
{ participant: provider, asset: 'usdc', amount: '0' },
{ participant: this.serviceAddress, asset: 'usdc', amount: '0' }
];
const sessionMessage = await createAppSessionMessage(this.messageSigner, [{
definition: appDefinition,
allocations
}]);
// Send to ClearNode
this.ws.send(sessionMessage);
const subscriptionId = this.generateSubscriptionId();
this.subscriptions.set(subscriptionId, {
subscriber,
provider,
monthlyFee,
createdAt: Date.now()
});
return subscriptionId;
}
async processMonthlyPayment(subscriptionId) {
const subscription = this.subscriptions.get(subscriptionId);
// Create payment message for this month
const paymentMessage = {
type: 'monthly_payment',
subscriptionId,
amount: subscription.monthlyFee,
month: this.getCurrentMonth(),
timestamp: Date.now()
};
const signature = await this.messageSigner(JSON.stringify(paymentMessage));
this.ws.send(JSON.stringify({
...paymentMessage,
signature
}));
return this.waitForPaymentConfirmation(subscriptionId);
}
}
Best Practices
Participant Management
class ParticipantManager {
constructor() {
this.participants = new Map();
this.roles = new Map();
}
addParticipant(address, role, permissions) {
this.participants.set(address, {
role,
permissions: new Set(permissions),
joinedAt: Date.now(),
status: 'ACTIVE'
});
}
validateParticipantAction(address, action) {
const participant = this.participants.get(address);
if (!participant) {
throw new Error('Unknown participant');
}
if (!participant.permissions.has(action)) {
throw new Error(`Insufficient permissions for action: ${action}`);
}
return true;
}
async rotateParticipant(oldAddress, newAddress) {
const participant = this.participants.get(oldAddress);
// Transfer permissions to new address
this.participants.set(newAddress, {
...participant,
previousAddress: oldAddress,
rotatedAt: Date.now()
});
// Mark old address as rotated
participant.status = 'ROTATED';
participant.rotatedTo = newAddress;
}
}
Message Broadcasting
class MessageBroadcaster {
constructor() {
this.connections = new Map();
this.messageQueue = new Map();
}
async broadcastToParticipants(participants, message) {
const deliveryPromises = participants.map(async (participant) => {
const connection = this.connections.get(participant);
if (!connection || connection.readyState !== WebSocket.OPEN) {
// Queue message for later delivery
this.queueMessage(participant, message);
return { participant, status: 'QUEUED' };
}
try {
connection.send(JSON.stringify(message));
return { participant, status: 'DELIVERED' };
} catch (error) {
this.queueMessage(participant, message);
return { participant, status: 'FAILED', error: error.message };
}
});
return Promise.all(deliveryPromises);
}
queueMessage(participant, message) {
if (!this.messageQueue.has(participant)) {
this.messageQueue.set(participant, []);
}
this.messageQueue.get(participant).push({
message,
timestamp: Date.now(),
retries: 0
});
}
async deliverQueuedMessages(participant) {
const queue = this.messageQueue.get(participant);
if (!queue || queue.length === 0) return;
const connection = this.connections.get(participant);
if (!connection || connection.readyState !== WebSocket.OPEN) return;
// Deliver all queued messages
for (const queued of queue) {
try {
connection.send(JSON.stringify(queued.message));
} catch (error) {
queued.retries++;
if (queued.retries < 3) {
continue; // Keep in queue for retry
}
}
}
// Clear delivered messages
this.messageQueue.set(participant,
queue.filter(msg => msg.retries >= 3)
);
}
}
Multi-party applications enable sophisticated logic while maintaining the performance benefits of state channels. Focus on application logic and participant coordination rather than low-level protocol details.