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
1 change: 1 addition & 0 deletions src/analytics/throughput/stellar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './stellar-throughput-analyzer';
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { StellarThroughputAnalyzer } from './stellar-throughput-analyzer';

describe('StellarThroughputAnalyzer', () => {
let analyzer: StellarThroughputAnalyzer;

beforeEach(() => {
analyzer = new StellarThroughputAnalyzer({ keepMaxSamples: 100 });
});

it('should record transfer samples', () => {
const sample = analyzer.recordTransfer({
providerId: 'stellar-provider-1',
durationMs: 150,
success: true,
amount: '10.5',
});

expect(sample.providerId).toBe('stellar-provider-1');
expect(sample.processedAt).toBeInstanceOf(Date);
expect(analyzer.getSamples()).toHaveLength(1);
});

it('should calculate throughput and success rate', () => {
analyzer.recordTransfer({ providerId: 'stellar-provider-1', durationMs: 150, success: true });
analyzer.recordTransfer({ providerId: 'stellar-provider-1', durationMs: 200, success: false });

const metrics = analyzer.getProviderMetrics('stellar-provider-1', 10_000);

expect(metrics.totalTransfers).toBe(2);
expect(metrics.successRate).toBe(0.5);
expect(metrics.throughputOpsPerSecond).toBeGreaterThan(0);
expect(metrics.averageDurationMs).toBeGreaterThanOrEqual(150);
});

it('should ignore samples outside the defined window', () => {
const oldSample = analyzer.recordTransfer({
providerId: 'stellar-provider-1',
durationMs: 100,
success: true,
});

oldSample.processedAt = new Date(Date.now() - 120_000);

const metrics = analyzer.getProviderMetrics('stellar-provider-1', 30_000);

expect(metrics.totalTransfers).toBe(0);
expect(metrics.successRate).toBe(0);
});

it('should compare providers by throughput', () => {
analyzer.recordTransfer({ providerId: 'provider-a', durationMs: 80, success: true });
analyzer.recordTransfer({ providerId: 'provider-b', durationMs: 120, success: true });
analyzer.recordTransfer({ providerId: 'provider-b', durationMs: 130, success: true });

const comparisons = analyzer.compareProviders(['provider-a', 'provider-b'], 60_000);

expect(comparisons[0].providerId).toBe('provider-b');
expect(comparisons[0].rank).toBe(1);
expect(comparisons[1].rank).toBe(2);
});
});
100 changes: 100 additions & 0 deletions src/analytics/throughput/stellar/stellar-throughput-analyzer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
export interface StellarThroughputSample {
providerId: string;
routeId?: string;
amount?: string;
durationMs: number;
processedAt: Date;
success: boolean;
}

export interface StellarThroughputAnalyzerOptions {
keepMaxSamples?: number;
}

export interface StellarProviderThroughputMetrics {
providerId: string;
throughputOpsPerSecond: number;
averageDurationMs: number;
successRate: number;
totalTransfers: number;
totalAmountProcessed: number;
}

export interface StellarProviderComparison extends StellarProviderThroughputMetrics {
rank: number;
}

export class StellarThroughputAnalyzer {
private readonly samples: StellarThroughputSample[] = [];
private readonly maxSamples: number;

constructor(options: StellarThroughputAnalyzerOptions = {}) {
this.maxSamples = options.keepMaxSamples ?? 10_000;
}

recordTransfer(sample: Omit<StellarThroughputSample, 'processedAt'>): StellarThroughputSample {
if (!sample.providerId?.trim()) {
throw new Error('providerId must be a non-empty string');
}
if (sample.durationMs < 0) {
throw new Error('durationMs must be a non-negative number');
}

const entry: StellarThroughputSample = {
...sample,
providerId: sample.providerId.trim(),
routeId: sample.routeId?.trim(),
processedAt: new Date(),
};

this.samples.push(entry);
if (this.samples.length > this.maxSamples) {
this.samples.splice(0, this.samples.length - this.maxSamples);
}
return entry;
}

getProviderMetrics(
providerId: string,
windowMs: number = 60_000
): StellarProviderThroughputMetrics {
const now = Date.now();
const samples = this.samples.filter(
(sample) =>
sample.providerId === providerId &&
now - sample.processedAt.getTime() <= windowMs
);

const totalTransfers = samples.length;
const totalSuccess = samples.filter((sample) => sample.success).length;
const averageDurationMs =
totalTransfers === 0
? 0
: samples.reduce((sum, sample) => sum + sample.durationMs, 0) / totalTransfers;
const throughputOpsPerSecond = windowMs === 0 ? 0 : totalTransfers / (windowMs / 1000);

return {
providerId,
throughputOpsPerSecond,
averageDurationMs,
successRate: totalTransfers === 0 ? 0 : totalSuccess / totalTransfers,
totalTransfers,
totalAmountProcessed: samples.reduce(
(sum, sample) => sum + (parseFloat(sample.amount ?? '0') || 0),
0,
),
};
}

compareProviders(providerIds: string[], windowMs: number = 60_000): StellarProviderComparison[] {
const metrics = providerIds
.map((providerId) => this.getProviderMetrics(providerId, windowMs))
.sort((a, b) => b.throughputOpsPerSecond - a.throughputOpsPerSecond);

return metrics.map((metric, index) => ({ ...metric, rank: index + 1 }));
}

getSamples(): StellarThroughputSample[] {
return [...this.samples];
}
}
1 change: 1 addition & 0 deletions src/batching/stellar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './stellar-transaction-batcher';
102 changes: 102 additions & 0 deletions src/batching/stellar/stellar-transaction-batcher.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { StellarTransactionBatcher } from './stellar-transaction-batcher';

describe('StellarTransactionBatcher', () => {
let batcher: StellarTransactionBatcher;

beforeEach(() => {
batcher = new StellarTransactionBatcher();
});

it('should group compatible operations into the same batch', () => {
const operations = [
{
operationId: 'op-1',
routeId: 'route-1',
bridgeId: 'bridge-a',
action: 'payment' as const,
sourceAccount: 'GABC...1',
destinationAccount: 'GXYZ...2',
asset: 'XLM',
amount: '10',
},
{
operationId: 'op-2',
routeId: 'route-2',
bridgeId: 'bridge-a',
action: 'payment' as const,
sourceAccount: 'GABC...3',
destinationAccount: 'GXYZ...2',
asset: 'USDC',
amount: '5',
},
{
operationId: 'op-3',
routeId: 'route-3',
bridgeId: 'bridge-a',
action: 'contract_call' as const,
sourceAccount: 'GABC...4',
destinationAccount: 'GXYZ...5',
asset: 'USDC',
amount: '1',
},
];

const groups = batcher.groupOperations(operations);

expect(groups).toHaveLength(2);
expect(groups[0].map((op) => op.operationId)).toEqual(expect.arrayContaining(['op-1', 'op-2']));
expect(groups[1].map((op) => op.operationId)).toEqual(['op-3']);
});

it('should execute a batch successfully', async () => {
const operations = [
{
operationId: 'op-1',
routeId: 'route-1',
bridgeId: 'bridge-a',
action: 'payment' as const,
sourceAccount: 'GABC...1',
destinationAccount: 'GXYZ...2',
asset: 'XLM',
amount: '10',
},
];

const result = await batcher.executeBatch(operations);

expect(result.batchId).toHaveLength(10);
expect(result.successCount).toBe(1);
expect(result.failureCount).toBe(0);
expect(result.results[0].success).toBe(true);
});

it('should execute multiple batches when operations are incompatible', async () => {
const operations = [
{
operationId: 'op-1',
routeId: 'route-1',
bridgeId: 'bridge-a',
action: 'payment' as const,
sourceAccount: 'GABC...1',
destinationAccount: 'GXYZ...2',
asset: 'XLM',
amount: '10',
},
{
operationId: 'op-2',
routeId: 'route-2',
bridgeId: 'bridge-a',
action: 'contract_call' as const,
sourceAccount: 'GABC...1',
destinationAccount: 'GXYZ...2',
asset: 'USDC',
amount: '5',
},
];

const results = await batcher.executeBatches(operations);

expect(results).toHaveLength(2);
expect(results[0].successCount + results[1].successCount).toBe(2);
});
});
82 changes: 82 additions & 0 deletions src/batching/stellar/stellar-transaction-batcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
export type StellarBatchAction = 'payment' | 'path_payment' | 'contract_call';

export interface StellarBatchOperation {
operationId: string;
routeId: string;
bridgeId: string;
action: StellarBatchAction;
sourceAccount: string;
destinationAccount: string;
asset: string;
amount: string;
memo?: string;
}

export interface StellarBatchOperationResult {
operationId: string;
success: boolean;
message: string;
executedAt: Date;
}

export interface StellarBatchExecutionResult {
batchId: string;
executedAt: Date;
results: StellarBatchOperationResult[];
successCount: number;
failureCount: number;
}

export class StellarTransactionBatcher {
groupOperations(
operations: StellarBatchOperation[],
): StellarBatchOperation[][] {
const groups = new Map<string, StellarBatchOperation[]>();

for (const operation of operations) {
const key = this.getGroupingKey(operation);
const bucket = groups.get(key) ?? [];
bucket.push(operation);
groups.set(key, bucket);
}

return Array.from(groups.values());
}

async executeBatch(
operations: StellarBatchOperation[],
): Promise<StellarBatchExecutionResult> {
const batchId = cryptoRandomId();
const executedAt = new Date();

const results = operations.map((operation) => ({
operationId: operation.operationId,
success: true,
message: `Executed batch operation ${operation.operationId}`,
executedAt,
}));

return {
batchId,
executedAt,
results,
successCount: results.filter((result) => result.success).length,
failureCount: results.filter((result) => !result.success).length,
};
}

async executeBatches(
operations: StellarBatchOperation[],
): Promise<StellarBatchExecutionResult[]> {
const groups = this.groupOperations(operations);
return Promise.all(groups.map((group) => this.executeBatch(group)));
}

private getGroupingKey(operation: StellarBatchOperation): string {
return `${operation.bridgeId}|${operation.action}|${operation.destinationAccount}`;
}
}

function cryptoRandomId(): string {
return Math.random().toString(36).substring(2, 12);
}
1 change: 1 addition & 0 deletions src/dry-run/stellar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './stellar-dry-run.service';
Loading
Loading