123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- /**
- * 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<BinanceSignResponse>;
- /**
- * 验证Binance签名
- * @param request 验证请求
- * @returns 验证结果
- */
- verifySignature(request: BinanceVerifyRequest): Promise<BinanceVerifyResponse>;
- /**
- * 获取账户API密钥
- * @param accountId 账户ID
- * @returns API密钥
- */
- getApiKey(accountId: string): Promise<string>;
- /**
- * 批量签名API请求
- * @param requests 批量签名请求
- * @returns 批量签名结果
- */
- signBatch(requests: BinanceSignRequest[]): Promise<BinanceSignResponse[]>;
- }
- // ============================================================================
- // Request/Response Types
- // ============================================================================
- interface BinanceSignRequest {
- accountId: string;
- method: 'GET' | 'POST' | 'PUT' | 'DELETE';
- endpoint: string;
- params?: Record<string, any>;
- 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<string, any>();
- constructor() {
- // Add test account
- this.accounts.set('binance-test-001', {
- credentials: TEST_CREDENTIALS
- });
- }
- async signRequest(request: BinanceSignRequest): Promise<BinanceSignResponse> {
- 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<BinanceVerifyResponse> {
- const expected = this.computeHmacSha256(request.message, request.secretKey);
- return {
- valid: expected === request.signature,
- algorithm: 'hmac-sha256',
- timestamp: new Date()
- };
- }
- async getApiKey(accountId: string): Promise<string> {
- const account = this.accounts.get(accountId);
- if (!account) {
- throw new Error(`Account ${accountId} not found`);
- }
- return account.credentials.apiKey;
- }
- async signBatch(requests: BinanceSignRequest[]): Promise<BinanceSignResponse[]> {
- return Promise.all(requests.map(req => this.signRequest(req)));
- }
- private buildQueryString(params: Record<string, any>, 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
- */
|