/** * Contract Test: Binance Signer Interface * * Tests the IBinanceSigner contract to ensure it correctly implements * HMAC-SHA256 signing for Binance API requests. */ import { describe, test, expect, beforeEach } from '@jest/globals'; import { Platform, SignResult, ErrorType } from '../../src/types/credential.js'; // ============================================================================ // Interface Contracts to Test // ============================================================================ /** * Binance平台签名器接口 */ interface IBinanceSigner { readonly platform: Platform.BINANCE; /** * 对Binance API请求进行签名 * @param request Binance签名请求 * @returns 签名结果,包含HMAC-SHA256签名 */ signRequest(request: BinanceSignRequest): Promise; /** * 验证Binance签名 * @param request 验证请求 * @returns 验证结果 */ verifySignature(request: BinanceVerifyRequest): Promise; /** * 获取账户API密钥 * @param accountId 账户ID * @returns API密钥 */ getApiKey(accountId: string): Promise; /** * 批量签名API请求 * @param requests 批量签名请求 * @returns 批量签名结果 */ signBatch(requests: BinanceSignRequest[]): Promise; } // ============================================================================ // Request/Response Types // ============================================================================ interface BinanceSignRequest { accountId: string; method: 'GET' | 'POST' | 'PUT' | 'DELETE'; endpoint: string; params?: Record; options?: BinanceSignOptions; } interface BinanceSignOptions { timeout?: number; includeTimestamp?: boolean; recvWindow?: number; } interface BinanceSignResponse extends SignResult { signature: string; algorithm: 'hmac-sha256'; apiKey: string; timestamp: number; queryString: string; } interface BinanceVerifyRequest { message: string; signature: string; secretKey: string; algorithm?: 'hmac-sha256'; } interface BinanceVerifyResponse { valid: boolean; algorithm: 'hmac-sha256'; timestamp: Date; } // ============================================================================ // Test Constants // ============================================================================ const TEST_CREDENTIALS = { apiKey: 'test_binance_api_key_12345678901234567890', secretKey: 'test_binance_secret_key_12345678901234567890' }; const SAMPLE_REQUESTS = { accountInfo: { method: 'GET' as const, endpoint: '/api/v3/account', params: { timestamp: 1640995200000, recvWindow: 5000 } }, newOrder: { method: 'POST' as const, endpoint: '/api/v3/order', params: { symbol: 'BTCUSDT', side: 'BUY', type: 'LIMIT', timeInForce: 'GTC', quantity: '0.001', price: '50000.00', timestamp: 1640995200000 } } }; // ============================================================================ // Mock Implementation for Testing // ============================================================================ class MockBinanceSigner implements IBinanceSigner { readonly platform = Platform.BINANCE; private accounts = new Map(); constructor() { // Add test account this.accounts.set('binance-test-001', { credentials: TEST_CREDENTIALS }); } async signRequest(request: BinanceSignRequest): Promise { const account = this.accounts.get(request.accountId); if (!account) { throw new Error(`Account ${request.accountId} not found`); } // Simulate HMAC-SHA256 signing const timestamp = Date.now(); const queryString = this.buildQueryString(request.params || {}, timestamp); const signature = this.computeHmacSha256(queryString, account.credentials.secretKey); return { success: true, signature, algorithm: 'hmac-sha256', apiKey: account.credentials.apiKey, timestamp, queryString }; } async verifySignature(request: BinanceVerifyRequest): Promise { const expected = this.computeHmacSha256(request.message, request.secretKey); return { valid: expected === request.signature, algorithm: 'hmac-sha256', timestamp: new Date() }; } async getApiKey(accountId: string): Promise { const account = this.accounts.get(accountId); if (!account) { throw new Error(`Account ${accountId} not found`); } return account.credentials.apiKey; } async signBatch(requests: BinanceSignRequest[]): Promise { return Promise.all(requests.map(req => this.signRequest(req))); } private buildQueryString(params: Record, timestamp: number): string { const allParams = { ...params, timestamp }; return Object.keys(allParams) .sort() .map(key => `${key}=${allParams[key]}`) .join('&'); } private computeHmacSha256(message: string, secret: string): string { // Simulate HMAC-SHA256 computation return `hmac_sha256_${message.length}_${secret.length}_${Date.now()}`; } } // ============================================================================ // Contract Tests // ============================================================================ describe('IBinanceSigner Contract Tests', () => { let signer: IBinanceSigner; beforeEach(() => { signer = new MockBinanceSigner(); }); describe('Platform Identification', () => { test('should identify as BINANCE platform', () => { expect(signer.platform).toBe(Platform.BINANCE); }); }); describe('Request Signing', () => { test('should sign GET request successfully', async () => { const request: BinanceSignRequest = { accountId: 'binance-test-001', ...SAMPLE_REQUESTS.accountInfo }; const result = await signer.signRequest(request); expect(result.success).toBe(true); expect(result.signature).toBeDefined(); expect(result.algorithm).toBe('hmac-sha256'); expect(result.apiKey).toBe(TEST_CREDENTIALS.apiKey); expect(result.timestamp).toBeGreaterThan(0); expect(result.queryString).toContain('timestamp='); }); test('should sign POST request successfully', async () => { const request: BinanceSignRequest = { accountId: 'binance-test-001', ...SAMPLE_REQUESTS.newOrder }; const result = await signer.signRequest(request); expect(result.success).toBe(true); expect(result.signature).toBeDefined(); expect(result.algorithm).toBe('hmac-sha256'); expect(result.queryString).toContain('symbol=BTCUSDT'); }); test('should fail for non-existent account', async () => { const request: BinanceSignRequest = { accountId: 'non-existent-account', ...SAMPLE_REQUESTS.accountInfo }; await expect(signer.signRequest(request)).rejects.toThrow('Account non-existent-account not found'); }); test('should handle empty params', async () => { const request: BinanceSignRequest = { accountId: 'binance-test-001', method: 'GET', endpoint: '/api/v3/time' }; const result = await signer.signRequest(request); expect(result.success).toBe(true); expect(result.queryString).toContain('timestamp='); }); }); describe('Signature Verification', () => { test('should verify valid signature', async () => { const message = 'symbol=BTCUSDT&side=BUY×tamp=1640995200000'; const signature = `hmac_sha256_${message.length}_${TEST_CREDENTIALS.secretKey.length}_${Date.now()}`; const request: BinanceVerifyRequest = { message, signature, secretKey: TEST_CREDENTIALS.secretKey }; const result = await signer.verifySignature(request); expect(result.valid).toBe(true); expect(result.algorithm).toBe('hmac-sha256'); expect(result.timestamp).toBeInstanceOf(Date); }); test('should reject invalid signature', async () => { const request: BinanceVerifyRequest = { message: 'symbol=BTCUSDT&side=BUY×tamp=1640995200000', signature: 'invalid_signature', secretKey: TEST_CREDENTIALS.secretKey }; const result = await signer.verifySignature(request); expect(result.valid).toBe(false); expect(result.algorithm).toBe('hmac-sha256'); }); }); describe('API Key Management', () => { test('should return API key for valid account', async () => { const apiKey = await signer.getApiKey('binance-test-001'); expect(apiKey).toBe(TEST_CREDENTIALS.apiKey); }); test('should fail for non-existent account', async () => { await expect(signer.getApiKey('non-existent-account')).rejects.toThrow('Account non-existent-account not found'); }); }); describe('Batch Operations', () => { test('should handle batch signing', async () => { const requests: BinanceSignRequest[] = [ { accountId: 'binance-test-001', ...SAMPLE_REQUESTS.accountInfo }, { accountId: 'binance-test-001', ...SAMPLE_REQUESTS.newOrder } ]; const results = await signer.signBatch(requests); expect(results).toHaveLength(2); expect(results[0].success).toBe(true); expect(results[1].success).toBe(true); expect(results[0].algorithm).toBe('hmac-sha256'); expect(results[1].algorithm).toBe('hmac-sha256'); }); test('should handle empty batch', async () => { const results = await signer.signBatch([]); expect(results).toHaveLength(0); }); }); describe('Performance Requirements', () => { test('should complete signing within 50ms', async () => { const request: BinanceSignRequest = { accountId: 'binance-test-001', ...SAMPLE_REQUESTS.accountInfo }; const startTime = Date.now(); const result = await signer.signRequest(request); const duration = Date.now() - startTime; expect(result.success).toBe(true); expect(duration).toBeLessThan(50); }); test('should handle concurrent requests', async () => { const requests = Array.from({ length: 10 }, () => ({ accountId: 'binance-test-001', ...SAMPLE_REQUESTS.accountInfo })); const startTime = Date.now(); const results = await Promise.all( requests.map(req => signer.signRequest(req)) ); const duration = Date.now() - startTime; expect(results).toHaveLength(10); expect(results.every(r => r.success)).toBe(true); expect(duration).toBeLessThan(200); // 10 concurrent requests < 200ms }); }); describe('Error Handling', () => { test('should handle malformed request gracefully', async () => { const request = { accountId: 'binance-test-001', method: 'INVALID' as any, endpoint: '/api/v3/account' }; // Should not throw, but might return error in response try { const result = await signer.signRequest(request); // If it succeeds, check success flag if (!result.success) { expect(result.error).toBeDefined(); } } catch (error) { // If it throws, that's also acceptable expect(error).toBeInstanceOf(Error); } }); test('should handle large parameter sets', async () => { const largeParams = Array.from({ length: 100 }, (_, i) => [`param${i}`, `value${i}`]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); const request: BinanceSignRequest = { accountId: 'binance-test-001', method: 'POST', endpoint: '/api/v3/order', params: largeParams }; const result = await signer.signRequest(request); expect(result.success).toBe(true); expect(result.queryString.length).toBeGreaterThan(100); }); }); }); // ============================================================================ // Integration Hints for Implementation // ============================================================================ /** * Implementation Notes: * * 1. HMAC-SHA256 Algorithm: * - Use Node.js crypto module for HMAC-SHA256 * - Query string format: key1=value1&key2=value2 (sorted by key) * - Always include timestamp parameter * * 2. Performance Requirements: * - Individual signing: < 50ms * - Batch operations: optimize for concurrent requests * - Use caching for repeated computations * * 3. Error Handling: * - Validate API key/secret format * - Handle network timeouts gracefully * - Log all signing attempts for debugging * * 4. Security Considerations: * - Never log secret keys * - Validate request parameters * - Implement request rate limiting * * 5. Binance API Specifics: * - recvWindow parameter for timing tolerance * - Different endpoints may have different param requirements * - Some endpoints don't require signatures */