Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions backend/src/disputes/ArbitratorService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { randomUUID } from 'node:crypto';

export interface Arbitrator {
id: string;
name: string;
address: string;
email?: string;
specializations: string[];
activeDisputes: number;
totalResolved: number;
rating: number;
isAvailable: boolean;
joinedAt: number;
}

export class ArbitratorService {
private arbitrators = new Map<string, Arbitrator>();
private disputeAssignments = new Map<string, string>();

constructor() {
this.registerDefaults();
}

private registerDefaults(): void {
const now = Date.now();
this.arbitrators.set('arb-1', { id: 'arb-1', name: 'Alice Johnson', address: 'GA7...', specializations: ['escrow', 'payment'], activeDisputes: 2, totalResolved: 45, rating: 4.8, isAvailable: true, joinedAt: now - 365 * 86400000 });
this.arbitrators.set('arb-2', { id: 'arb-2', name: 'Bob Chen', address: 'GB8...', specializations: ['smart-contract', 'defi'], activeDisputes: 1, totalResolved: 32, rating: 4.6, isAvailable: true, joinedAt: now - 180 * 86400000 });
this.arbitrators.set('arb-3', { id: 'arb-3', name: 'Carol Martinez', address: 'GC9...', specializations: ['payment', 'identity'], activeDisputes: 3, totalResolved: 28, rating: 4.9, isAvailable: true, joinedAt: now - 90 * 86400000 });
}

registerArbitrator(params: Omit<Arbitrator, 'id' | 'activeDisputes' | 'totalResolved' | 'rating' | 'isAvailable' | 'joinedAt'>): Arbitrator {
const arbitrator: Arbitrator = {
id: `arb-${randomUUID().slice(0, 8)}`,
activeDisputes: 0,
totalResolved: 0,
rating: 5.0,
isAvailable: true,
joinedAt: Date.now(),
...params,
};
this.arbitrators.set(arbitrator.id, arbitrator);
return arbitrator;
}

assignArbitrator(disputeId: string): Arbitrator | null {
const available = Array.from(this.arbitrators.values())
.filter(a => a.isAvailable)
.sort((a, b) => a.activeDisputes - b.activeDisputes || b.rating - a.rating);

if (available.length === 0) return null;

const chosen = available[0];
chosen.activeDisputes++;
this.arbitrators.set(chosen.id, chosen);
this.disputeAssignments.set(disputeId, chosen.id);
return chosen;
}

releaseArbitrator(disputeId: string): void {
const arbId = this.disputeAssignments.get(disputeId);
if (!arbId) return;
const arb = this.arbitrators.get(arbId);
if (arb) {
arb.activeDisputes = Math.max(0, arb.activeDisputes - 1);
arb.totalResolved++;
this.arbitrators.set(arbId, arb);
}
this.disputeAssignments.delete(disputeId);
}

getArbitrator(id: string): Arbitrator | undefined {
return this.arbitrators.get(id);
}

listArbitrators(availableOnly?: boolean): Arbitrator[] {
const all = Array.from(this.arbitrators.values());
return availableOnly ? all.filter(a => a.isAvailable) : all;
}

getWorkloadStats(): { total: number; available: number; avgActiveDisputes: number } {
const all = Array.from(this.arbitrators.values());
return {
total: all.length,
available: all.filter(a => a.isAvailable).length,
avgActiveDisputes: all.reduce((s, a) => s + a.activeDisputes, 0) / all.length,
};
}
}
162 changes: 162 additions & 0 deletions backend/src/disputes/DisputeService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { randomUUID } from 'node:crypto';
import { auditService } from '../services/auditService.js';
import { ArbitratorService } from './ArbitratorService.js';

export type DisputeStatus = 'opened' | 'evidence_gathering' | 'under_review' | 'resolved' | 'appealed' | 'closed';

export type ResolutionType = 'refund' | 'release' | 'split';

export interface EvidenceItem {
id: string;
type: 'document' | 'image' | 'message' | 'other';
title: string;
description?: string;
url: string;
uploadedBy: string;
uploadedAt: number;
}

export interface DisputeRecord {
id: string;
projectId: string;
escrowId: string;
raisedBy: string;
raisedAgainst: string;
reason: string;
status: DisputeStatus;
evidence: EvidenceItem[];
arbitratorId?: string;
resolution?: {
type: ResolutionType;
description: string;
approvedBy: string;
approvedAt: number;
refundAmount?: string;
releaseAmount?: string;
splitRatio?: { partyA: number; partyB: number };
};
appealTarget?: string;
appealDeadline?: number;
createdAt: number;
updatedAt: number;
resolvedAt?: number;
auditTimeline: Array<{ action: string; by: string; at: number; detail?: string }>;
}

export class DisputeService {
private disputes = new Map<string, DisputeRecord>();
private arbitratorService: ArbitratorService;

constructor() {
this.arbitratorService = new ArbitratorService();
}

async createDispute(params: {
projectId: string;
escrowId: string;
raisedBy: string;
raisedAgainst: string;
reason: string;
}): Promise<DisputeRecord> {
const dispute: DisputeRecord = {
id: randomUUID(),
status: 'opened',
evidence: [],
createdAt: Date.now(),
updatedAt: Date.now(),
auditTimeline: [{ action: 'dispute.created', by: params.raisedBy, at: Date.now() }],
...params,
};

this.disputes.set(dispute.id, dispute);

const arbitrator = this.arbitratorService.assignArbitrator(dispute.id);
if (arbitrator) {
dispute.arbitratorId = arbitrator.id;
dispute.status = 'under_review';
dispute.auditTimeline.push({ action: 'arbitrator.assigned', by: 'system', at: Date.now(), detail: arbitrator.id });
this.disputes.set(dispute.id, dispute);
}

await auditService.logAction({ action: 'dispute.created', resource: 'dispute', resourceId: dispute.id, details: { projectId: params.projectId, raisedBy: params.raisedBy, reason: params.reason } });
return dispute;
}

async addEvidence(disputeId: string, evidence: Omit<EvidenceItem, 'id' | 'uploadedAt'>): Promise<DisputeRecord | null> {
const dispute = this.disputes.get(disputeId);
if (!dispute || dispute.status === 'closed' || dispute.status === 'resolved') return null;

const item: EvidenceItem = { ...evidence, id: randomUUID(), uploadedAt: Date.now() };
dispute.evidence.push(item);
if (dispute.status === 'opened') dispute.status = 'evidence_gathering';
dispute.updatedAt = Date.now();
dispute.auditTimeline.push({ action: 'evidence.added', by: evidence.uploadedBy, at: Date.now(), detail: evidence.title });
this.disputes.set(disputeId, dispute);
return dispute;
}

async resolveDispute(disputeId: string, resolution: {
type: ResolutionType;
description: string;
approvedBy: string;
refundAmount?: string;
releaseAmount?: string;
splitRatio?: { partyA: number; partyB: number };
}): Promise<DisputeRecord | null> {
const dispute = this.disputes.get(disputeId);
if (!dispute || dispute.status === 'closed') return null;

dispute.status = 'resolved';
dispute.resolution = { ...resolution, approvedAt: Date.now() };
dispute.resolvedAt = Date.now();
dispute.updatedAt = Date.now();
dispute.auditTimeline.push({ action: 'dispute.resolved', by: resolution.approvedBy, at: Date.now(), detail: resolution.type });
this.disputes.set(disputeId, dispute);

await auditService.logAction({ action: 'dispute.resolved', resource: 'dispute', resourceId: disputeId, details: { type: resolution.type, approvedBy: resolution.approvedBy } });
return dispute;
}

async appealDispute(disputeId: string, appealTarget: string): Promise<DisputeRecord | null> {
const dispute = this.disputes.get(disputeId);
if (!dispute || dispute.status !== 'resolved') return null;

dispute.status = 'appealed';
dispute.appealTarget = appealTarget;
dispute.appealDeadline = Date.now() + 14 * 24 * 60 * 60 * 1000;
dispute.updatedAt = Date.now();
dispute.auditTimeline.push({ action: 'dispute.appealed', by: 'system', at: Date.now(), detail: `Appealed to ${appealTarget}` });
this.disputes.set(disputeId, dispute);
return dispute;
}

async closeDispute(disputeId: string, closedBy: string): Promise<DisputeRecord | null> {
const dispute = this.disputes.get(disputeId);
if (!dispute) return null;

dispute.status = 'closed';
dispute.updatedAt = Date.now();
dispute.auditTimeline.push({ action: 'dispute.closed', by: closedBy, at: Date.now() });
this.disputes.set(disputeId, dispute);
return dispute;
}

getDispute(disputeId: string): DisputeRecord | undefined {
return this.disputes.get(disputeId);
}

listDisputes(status?: DisputeStatus): DisputeRecord[] {
const all = Array.from(this.disputes.values());
return status ? all.filter(d => d.status === status) : all;
}

getDisputesByUser(userId: string): DisputeRecord[] {
return Array.from(this.disputes.values()).filter(d => d.raisedBy === userId || d.raisedAgainst === userId);
}

getArbitratorService(): ArbitratorService {
return this.arbitratorService;
}
}

export const disputeService = new DisputeService();
4 changes: 4 additions & 0 deletions backend/src/disputes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { disputeService, DisputeService } from './DisputeService.js';
export type { DisputeRecord, DisputeStatus, ResolutionType, EvidenceItem } from './DisputeService.js';
export { ArbitratorService } from './ArbitratorService.js';
export type { Arbitrator } from './ArbitratorService.js';
12 changes: 8 additions & 4 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ import { graphQLRouter, graphQLWsRouter } from './graphql/gateway.js';
import { fraudDetectionRouter } from './routes/fraud-detection.js';
import { bridgeRouter } from './routes/bridge.js';
import { tokenizationRouter } from './routes/tokenization.js';
import { routingRouter } from './routes/routing.js';
import { disputesRouter } from './routes/disputes.js';
import { startWebhookWorker, stopWebhookWorker } from './services/webhooks.js';
import { analyticsService } from './services/analytics.js';
import { createAnalyticsRouter } from './routes/analytics.js';
Expand Down Expand Up @@ -234,19 +236,21 @@ apiV1Router.use('/allowances', allowancesRouter);
apiV1Router.use('/forms', formsRouter);
// Webhook management and verification
apiV1Router.use('/webhooks', webhooksRouter);
// Email delivery system
apiV1Router.use('/disputes', disputeRoutes);
apiV1Router.use('/emails', emailRouter);
apiV1Router.use('/portfolio', portfolioRouter);
apiV1Router.use('/backup', backupRouter);
apiV1Router.use('/ip-allowlist', ipAllowlistRouter);
apiV1Router.use('/push', pushRouter);
// NFC / QR payment requests
apiV1Router.use('/nfc', nfcRouter);
// Cache management
apiV1Router.use('/cache', cacheRouter);

apiV1Router.use('/circuit-breaker', circuitBreakerRouter);
apiV1Router.use('/fraud-detection', fraudDetectionRouter);
apiV1Router.use('/bridge', bridgeRouter);
apiV1Router.use('/tokenization', tokenizationRouter);
apiV1Router.use('/routing', routingRouter);
apiV1Router.use('/escrow', escrowRouter);
apiV1Router.use('/disputes', disputesRouter);
apiV1Router.get('/compression/metrics', (_req, res) => {
res.json(getCompressionMetrics());
});
Expand Down
80 changes: 80 additions & 0 deletions backend/src/payments/AtomicSwapBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { NetworkId } from './NetworkRegistry.js';

export interface SwapIntent {
fromNetwork: NetworkId;
toNetwork: NetworkId;
fromAsset: string;
toAsset: string;
amount: string;
recipientAddress: string;
refundAddress: string;
timeoutMinutes: number;
price: string;
expiresAt: number;
}

export interface SwapState {
intentId: string;
status: 'pending' | 'locked' | 'claimed' | 'refunded' | 'expired';
fromTxHash?: string;
toTxHash?: string;
createdAt: number;
updatedAt: number;
secretHash: string;
}

export class AtomicSwapBridge {
private swaps = new Map<string, SwapState>();

async createSwap(intent: Omit<SwapIntent, 'expiresAt'>): Promise<{ intentId: string; secretHash: string }> {
const intentId = `swap_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
const secret = Array.from({ length: 32 }, () => Math.floor(Math.random() * 256).toString(16).padStart(2, '0')).join('');
const secretHash = Array.from({ length: 32 }, () => Math.floor(Math.random() * 256).toString(16).padStart(2, '0')).join('');

this.swaps.set(intentId, {
intentId, status: 'pending', createdAt: Date.now(), updatedAt: Date.now(), secretHash,
});

return { intentId, secretHash };
}

async lockSwap(intentId: string, fromTxHash: string): Promise<SwapState | null> {
const swap = this.swaps.get(intentId);
if (!swap || swap.status !== 'pending') return null;
swap.status = 'locked';
swap.fromTxHash = fromTxHash;
swap.updatedAt = Date.now();
this.swaps.set(intentId, swap);
return swap;
}

async claimSwap(intentId: string, secret: string, toTxHash: string): Promise<SwapState | null> {
const swap = this.swaps.get(intentId);
if (!swap || swap.status !== 'locked') return null;
swap.status = 'claimed';
swap.toTxHash = toTxHash;
swap.updatedAt = Date.now();
this.swaps.set(intentId, swap);
return swap;
}

async refundSwap(intentId: string): Promise<SwapState | null> {
const swap = this.swaps.get(intentId);
if (!swap || swap.status !== 'locked') return null;
swap.status = 'refunded';
swap.updatedAt = Date.now();
this.swaps.set(intentId, swap);
return swap;
}

getSwap(intentId: string): SwapState | undefined {
return this.swaps.get(intentId);
}

listSwaps(status?: SwapState['status']): SwapState[] {
const all = Array.from(this.swaps.values());
return status ? all.filter(s => s.status === status) : all;
}
}

export const atomicSwapBridge = new AtomicSwapBridge();
Loading
Loading