Skip to content

Commit e69d377

Browse files
authored
Merge pull request Gildado#89 from ryzen-xp/feature/Contract-Address-Registry-API
feature : Contract-Address-Registry-API
2 parents 096afad + 9d471e2 commit e69d377

6 files changed

Lines changed: 246 additions & 0 deletions

File tree

backend/environments.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[testnet.bulk_payment]
2+
contractId = "CBULK_TEST_123"
3+
version = "1.0.0"
4+
deployedAt = 1456789
5+
6+
[testnet.vesting_escrow]
7+
contractId = "CVEST_TEST_456"
8+
version = "1.1.0"
9+
deployedAt = 1456795
10+
11+
[mainnet.bulk_payment]
12+
contractId = "CBULK_MAIN_789"
13+
version = "1.0.0"
14+
deployedAt = 9876543

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"passport-github2": "^0.1.12",
2828
"passport-google-oauth20": "^2.0.0",
2929
"pg": "^8.18.0",
30+
"toml": "^3.0.0",
3031
"zod": "^4.3.6"
3132
},
3233
"devDependencies": {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import request from 'supertest';
2+
import express from 'express';
3+
import contractRegistryRoutes from '../../routes/contractRegistryRoutes.js';
4+
import { ContractRegistryService } from '../../services/contractRegistryService.js';
5+
import logger from '../../utils/logger.js';
6+
7+
jest.mock('../../services/contractRegistryService');
8+
jest.mock('../../utils/logger');
9+
10+
const app = express();
11+
app.use(express.json());
12+
app.use('/api', contractRegistryRoutes);
13+
14+
describe('Contract Registry API Integration', () => {
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
});
18+
19+
describe('GET /api/contracts', () => {
20+
it('returns all networks when no query param provided', async () => {
21+
(ContractRegistryService.getAllContracts as jest.Mock)
22+
.mockReturnValue({
23+
testnet: {
24+
bulk_payment: {
25+
contractId: 'CBULK_TEST',
26+
version: '1.0.0',
27+
deployedAt: 123456,
28+
},
29+
},
30+
});
31+
32+
const response = await request(app).get('/api/contracts');
33+
34+
expect(response.status).toBe(200);
35+
expect(response.body).toHaveProperty('networks');
36+
expect(response.body.count).toBe(1);
37+
expect(response.body.networks.testnet).toBeDefined();
38+
expect(response.body).toHaveProperty('timestamp');
39+
});
40+
41+
it('returns specific network when query param is provided', async () => {
42+
(ContractRegistryService.getContractsByNetwork as jest.Mock)
43+
.mockReturnValue({
44+
bulk_payment: {
45+
contractId: 'CBULK_TEST',
46+
version: '1.0.0',
47+
deployedAt: 123456,
48+
},
49+
});
50+
51+
const response = await request(app)
52+
.get('/api/contracts')
53+
.query({ network: 'testnet' });
54+
55+
expect(response.status).toBe(200);
56+
expect(response.body.network).toBe('testnet');
57+
expect(response.body.contracts.bulk_payment).toBeDefined();
58+
expect(response.body.count).toBe(1);
59+
});
60+
61+
it('returns 500 when service throws error', async () => {
62+
(ContractRegistryService.getAllContracts as jest.Mock)
63+
.mockImplementation(() => {
64+
throw new Error('Registry load failed');
65+
});
66+
67+
const response = await request(app).get('/api/contracts');
68+
69+
expect(response.status).toBe(500);
70+
expect(response.body.error).toBe('Internal Server Error');
71+
expect(response.body.message).toBe('Registry load failed');
72+
expect(logger.error).toHaveBeenCalled();
73+
});
74+
75+
it('logs warning if response time exceeds 500ms', async () => {
76+
(ContractRegistryService.getAllContracts as jest.Mock)
77+
.mockReturnValue({});
78+
79+
// Mock slow response
80+
const originalNow = Date.now;
81+
let time = 0;
82+
Date.now = jest.fn(() => {
83+
time += 600;
84+
return time;
85+
});
86+
87+
await request(app).get('/api/contracts');
88+
89+
expect(logger.warn).toHaveBeenCalledWith(
90+
expect.stringContaining('Contract registry response slow')
91+
);
92+
93+
Date.now = originalNow;
94+
});
95+
});
96+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Request, Response } from 'express';
2+
import { ContractRegistryService } from '../services/contractRegistryService.js';
3+
import logger from '../utils/logger.js';
4+
5+
export class ContractRegistryController {
6+
/**
7+
* GET /api/contracts
8+
* Returns all contracts across all networks
9+
* Optional query param: ?network=testnet
10+
*/
11+
static async getContracts(req: Request, res: Response): Promise<void> {
12+
const startTime = Date.now();
13+
14+
try {
15+
const { network } = req.query;
16+
17+
let responseData;
18+
19+
if (typeof network === 'string' && network.length > 0) {
20+
const contracts =
21+
ContractRegistryService.getContractsByNetwork(network);
22+
23+
responseData = {
24+
network,
25+
contracts,
26+
count: Object.keys(contracts).length,
27+
};
28+
} else {
29+
const networks = ContractRegistryService.getAllContracts();
30+
31+
responseData = {
32+
networks,
33+
count: Object.keys(networks).length,
34+
};
35+
}
36+
37+
// Set response headers
38+
res.setHeader('Content-Type', 'application/json');
39+
res.setHeader('Cache-Control', 'public, max-age=3600'); // cache 1 hour
40+
41+
const responseTime = Date.now() - startTime;
42+
43+
if (responseTime > 500) {
44+
logger.warn(
45+
`Contract registry response slow: ${responseTime}ms`
46+
);
47+
}
48+
49+
res.status(200).json({
50+
...responseData,
51+
timestamp: new Date().toISOString(),
52+
});
53+
} catch (error) {
54+
logger.error('Error retrieving contract registry', error);
55+
56+
res.status(500).json({
57+
error: 'Internal Server Error',
58+
message:
59+
error instanceof Error
60+
? error.message
61+
: 'Failed to load contract registry',
62+
timestamp: new Date().toISOString(),
63+
});
64+
}
65+
}
66+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
import { Router } from 'express';
3+
import { ContractRegistryController } from '../controllers/contractRegistryController.js';
4+
5+
const router = Router();
6+
7+
/**
8+
* GET /api/contracts
9+
* Optional: ?network=testnet
10+
*/
11+
router.get('/contracts', ContractRegistryController.getContracts);
12+
13+
export default router;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import toml from 'toml';
4+
5+
export interface ContractInfo {
6+
contractId: string;
7+
version: string;
8+
deployedAt: number;
9+
}
10+
11+
export interface NetworkRegistry {
12+
[contractName: string]: ContractInfo;
13+
}
14+
15+
export interface ContractRegistry {
16+
[network: string]: NetworkRegistry;
17+
}
18+
19+
export class ContractRegistryService {
20+
private static cache: ContractRegistry | null = null;
21+
22+
/**
23+
* Load registry from environments.toml
24+
* Cached after first read.
25+
*/
26+
static loadRegistry(): ContractRegistry {
27+
if (this.cache) return this.cache;
28+
29+
const filePath = path.join(process.cwd(), 'environments.toml');
30+
31+
if (!fs.existsSync(filePath)) {
32+
throw new Error('environments.toml not found');
33+
}
34+
35+
const raw = fs.readFileSync(filePath, 'utf-8');
36+
const parsed = toml.parse(raw);
37+
38+
this.cache = parsed as ContractRegistry;
39+
return this.cache;
40+
}
41+
42+
/**
43+
* Get all contracts across all networks
44+
*/
45+
static getAllContracts(): ContractRegistry {
46+
return this.loadRegistry();
47+
}
48+
49+
/**
50+
* Get contracts for a specific network
51+
*/
52+
static getContractsByNetwork(network: string): NetworkRegistry {
53+
const registry = this.loadRegistry();
54+
return registry[network] ?? {};
55+
}
56+
}

0 commit comments

Comments
 (0)