Skip to content

Commit 72c012c

Browse files
authored
Merge pull request Stellar-Paymaster#625 from TeeYml/main
feat: implement bulk tenant updates, stellar auto-swap and api chaos monkey
2 parents 2efb48e + d20c598 commit 72c012c

9 files changed

Lines changed: 241 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Bulk Tenant Updates
2+
3+
## Overview
4+
This feature allows administrators to apply rate limits to a large group of tenants simultaneously within the `admin-dashboard` package.
5+
6+
## Requirements
7+
- Target specific groups of tenants based on usage tiers.
8+
- Efficient database updates with error handling.
9+
10+
## Implementation Details
11+
The `BulkTenantUpdateService` loops through the provided tenant IDs and applies the new rate limit values. It returns a summary of successful and failed operations.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { BulkTenantUpdateService, TenantRateLimitUpdate } from './bulkTenantUpdates';
3+
4+
describe('BulkTenantUpdateService', () => {
5+
it('should successfully apply rate limits to multiple tenants', async () => {
6+
const mockDbClient = {
7+
tenant: {
8+
update: vi.fn().mockResolvedValue(true),
9+
},
10+
};
11+
const service = new BulkTenantUpdateService(mockDbClient);
12+
13+
const updates: TenantRateLimitUpdate[] = [
14+
{ tenantId: 'tenant-1', newRateLimit: 100 },
15+
{ tenantId: 'tenant-2', newRateLimit: 200 },
16+
];
17+
18+
const result = await service.applyRateLimits(updates);
19+
expect(result.success).toBe(2);
20+
expect(result.failed).toBe(0);
21+
expect(mockDbClient.tenant.update).toHaveBeenCalledTimes(2);
22+
});
23+
24+
it('should handle partial failures', async () => {
25+
const mockDbClient = {
26+
tenant: {
27+
update: vi.fn()
28+
.mockResolvedValueOnce(true)
29+
.mockRejectedValueOnce(new Error('DB Error')),
30+
},
31+
};
32+
const service = new BulkTenantUpdateService(mockDbClient);
33+
34+
const updates: TenantRateLimitUpdate[] = [
35+
{ tenantId: 'tenant-1', newRateLimit: 100 },
36+
{ tenantId: 'tenant-2', newRateLimit: 200 },
37+
];
38+
39+
const result = await service.applyRateLimits(updates);
40+
expect(result.success).toBe(1);
41+
expect(result.failed).toBe(1);
42+
});
43+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export interface TenantRateLimitUpdate {
2+
tenantId: string;
3+
newRateLimit: number;
4+
}
5+
6+
export class BulkTenantUpdateService {
7+
constructor(private dbClient: any) {}
8+
9+
async applyRateLimits(updates: TenantRateLimitUpdate[]): Promise<{ success: number; failed: number }> {
10+
let success = 0;
11+
let failed = 0;
12+
13+
for (const update of updates) {
14+
try {
15+
await this.dbClient.tenant.update({
16+
where: { id: update.tenantId },
17+
data: { rateLimit: update.newRateLimit },
18+
});
19+
success++;
20+
} catch (error) {
21+
console.error(`Failed to update tenant ${update.tenantId}:`, error);
22+
failed++;
23+
}
24+
}
25+
26+
return { success, failed };
27+
}
28+
}

server/docs/api-chaos-monkey.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# API Chaos Monkey
2+
3+
## Overview
4+
API Chaos Monkey randomly drops connections in staging environments to verify system resilience and ensure our applications can recover gracefully from sudden faults.
5+
6+
## Configuration
7+
It exposes a middleware that can be attached to specific endpoints. The `dropProbability` controls how often requests are artificially failed (returning 503). Ensure `enabled` is `false` in production environments!
8+
9+
## Resilience Benefits
10+
- Validates that the frontend retry logic behaves as expected.
11+
- Helps identify single points of failure in complex workflows.

server/docs/stellar-auto-swap.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Stellar Asset Auto-Swap
2+
3+
## Overview
4+
This feature automatically converts tenant fees from various Stellar assets into XLM on the fly, ensuring a uniform reserve of the native asset.
5+
6+
## Implementation
7+
The `StellarAutoSwapService` intercepts incoming fee payments and checks their asset type. If the asset is not XLM, it interfaces with a decentralized exchange to swap the assets into XLM.
8+
9+
## Edge Cases Handled
10+
- Bypasses the swap mechanism entirely if the incoming asset is already XLM.
11+
- Gracefully handles DEX transaction failures and logs them for review.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { ApiChaosMonkey } from './apiChaosMonkey';
3+
4+
describe('ApiChaosMonkey', () => {
5+
it('should not drop connection if disabled', () => {
6+
const chaosMonkey = new ApiChaosMonkey({ dropProbability: 1.0, enabled: false });
7+
const req = {};
8+
const res = { status: vi.fn().mockReturnThis(), json: vi.fn() };
9+
const next = vi.fn();
10+
11+
chaosMonkey.middleware()(req, res, next);
12+
expect(next).toHaveBeenCalled();
13+
expect(res.status).not.toHaveBeenCalled();
14+
});
15+
16+
it('should drop connection based on probability', () => {
17+
// Math.random will return 0.1, which is < 0.5, so it drops
18+
vi.spyOn(Math, 'random').mockReturnValue(0.1);
19+
const chaosMonkey = new ApiChaosMonkey({ dropProbability: 0.5, enabled: true });
20+
21+
const req = {};
22+
const res = { status: vi.fn().mockReturnThis(), json: vi.fn() };
23+
const next = vi.fn();
24+
25+
chaosMonkey.middleware()(req, res, next);
26+
expect(next).not.toHaveBeenCalled();
27+
expect(res.status).toHaveBeenCalledWith(503);
28+
expect(res.json).toHaveBeenCalledWith({ error: 'Service Unavailable - Chaos Monkey Intervention' });
29+
30+
vi.restoreAllMocks();
31+
});
32+
33+
it('should pass connection if probability not met', () => {
34+
// Math.random will return 0.9, which is > 0.5, so it passes
35+
vi.spyOn(Math, 'random').mockReturnValue(0.9);
36+
const chaosMonkey = new ApiChaosMonkey({ dropProbability: 0.5, enabled: true });
37+
38+
const req = {};
39+
const res = { status: vi.fn().mockReturnThis(), json: vi.fn() };
40+
const next = vi.fn();
41+
42+
chaosMonkey.middleware()(req, res, next);
43+
expect(next).toHaveBeenCalled();
44+
expect(res.status).not.toHaveBeenCalled();
45+
46+
vi.restoreAllMocks();
47+
});
48+
});

server/src/chaos/apiChaosMonkey.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export class ApiChaosMonkey {
2+
constructor(private options: { dropProbability: number; enabled: boolean }) {}
3+
4+
middleware() {
5+
return (req: any, res: any, next: any) => {
6+
if (!this.options.enabled) {
7+
return next();
8+
}
9+
10+
const randomValue = Math.random();
11+
if (randomValue < this.options.dropProbability) {
12+
console.warn('Chaos Monkey: Dropping database connection simulation');
13+
return res.status(503).json({ error: 'Service Unavailable - Chaos Monkey Intervention' });
14+
}
15+
16+
next();
17+
};
18+
}
19+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { StellarAutoSwapService, SwapRequest } from './autoSwap';
3+
4+
describe('StellarAutoSwapService', () => {
5+
it('should skip swap if source asset is already XLM', async () => {
6+
const service = new StellarAutoSwapService({});
7+
const request: SwapRequest = { tenantId: 'tenant-1', amount: 100, sourceAsset: 'XLM' };
8+
9+
const result = await service.autoSwapFees(request);
10+
expect(result.success).toBe(true);
11+
expect(result.txHash).toBeUndefined();
12+
});
13+
14+
it('should successfully execute swap for non-XLM asset', async () => {
15+
const mockDexClient = {
16+
executeSwap: vi.fn().mockResolvedValue('mock-tx-hash'),
17+
};
18+
const service = new StellarAutoSwapService(mockDexClient);
19+
const request: SwapRequest = { tenantId: 'tenant-1', amount: 100, sourceAsset: 'USDC' };
20+
21+
const result = await service.autoSwapFees(request);
22+
expect(result.success).toBe(true);
23+
expect(result.txHash).toBe('mock-tx-hash');
24+
expect(mockDexClient.executeSwap).toHaveBeenCalledWith({
25+
from: 'USDC',
26+
to: 'XLM',
27+
amount: 100,
28+
});
29+
});
30+
31+
it('should handle swap failures', async () => {
32+
const mockDexClient = {
33+
executeSwap: vi.fn().mockRejectedValue(new Error('DEX Error')),
34+
};
35+
const service = new StellarAutoSwapService(mockDexClient);
36+
const request: SwapRequest = { tenantId: 'tenant-1', amount: 100, sourceAsset: 'USDC' };
37+
38+
const result = await service.autoSwapFees(request);
39+
expect(result.success).toBe(false);
40+
});
41+
});

server/src/stellar/autoSwap.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export interface SwapRequest {
2+
tenantId: string;
3+
amount: number;
4+
sourceAsset: string;
5+
}
6+
7+
export class StellarAutoSwapService {
8+
constructor(private dexClient: any) {}
9+
10+
async autoSwapFees(request: SwapRequest): Promise<{ success: boolean; txHash?: string }> {
11+
if (request.sourceAsset === 'XLM') {
12+
return { success: true };
13+
}
14+
15+
try {
16+
// Simulate conversion to XLM on the fly using decentralized exchange
17+
const txHash = await this.dexClient.executeSwap({
18+
from: request.sourceAsset,
19+
to: 'XLM',
20+
amount: request.amount,
21+
});
22+
23+
return { success: true, txHash };
24+
} catch (error) {
25+
console.error(`Auto-swap failed for tenant ${request.tenantId}:`, error);
26+
return { success: false };
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)