binance-signer.contract.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. /**
  2. * Contract Test: Binance Signer Interface
  3. *
  4. * Tests the IBinanceSigner contract to ensure it correctly implements
  5. * HMAC-SHA256 signing for Binance API requests.
  6. */
  7. import { describe, test, expect, beforeEach } from '@jest/globals';
  8. import { Platform, SignResult, ErrorType } from '../../src/types/credential.js';
  9. // ============================================================================
  10. // Interface Contracts to Test
  11. // ============================================================================
  12. /**
  13. * Binance平台签名器接口
  14. */
  15. interface IBinanceSigner {
  16. readonly platform: Platform.BINANCE;
  17. /**
  18. * 对Binance API请求进行签名
  19. * @param request Binance签名请求
  20. * @returns 签名结果,包含HMAC-SHA256签名
  21. */
  22. signRequest(request: BinanceSignRequest): Promise<BinanceSignResponse>;
  23. /**
  24. * 验证Binance签名
  25. * @param request 验证请求
  26. * @returns 验证结果
  27. */
  28. verifySignature(request: BinanceVerifyRequest): Promise<BinanceVerifyResponse>;
  29. /**
  30. * 获取账户API密钥
  31. * @param accountId 账户ID
  32. * @returns API密钥
  33. */
  34. getApiKey(accountId: string): Promise<string>;
  35. /**
  36. * 批量签名API请求
  37. * @param requests 批量签名请求
  38. * @returns 批量签名结果
  39. */
  40. signBatch(requests: BinanceSignRequest[]): Promise<BinanceSignResponse[]>;
  41. }
  42. // ============================================================================
  43. // Request/Response Types
  44. // ============================================================================
  45. interface BinanceSignRequest {
  46. accountId: string;
  47. method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  48. endpoint: string;
  49. params?: Record<string, any>;
  50. options?: BinanceSignOptions;
  51. }
  52. interface BinanceSignOptions {
  53. timeout?: number;
  54. includeTimestamp?: boolean;
  55. recvWindow?: number;
  56. }
  57. interface BinanceSignResponse extends SignResult {
  58. signature: string;
  59. algorithm: 'hmac-sha256';
  60. apiKey: string;
  61. timestamp: number;
  62. queryString: string;
  63. }
  64. interface BinanceVerifyRequest {
  65. message: string;
  66. signature: string;
  67. secretKey: string;
  68. algorithm?: 'hmac-sha256';
  69. }
  70. interface BinanceVerifyResponse {
  71. valid: boolean;
  72. algorithm: 'hmac-sha256';
  73. timestamp: Date;
  74. }
  75. // ============================================================================
  76. // Test Constants
  77. // ============================================================================
  78. const TEST_CREDENTIALS = {
  79. apiKey: 'test_binance_api_key_12345678901234567890',
  80. secretKey: 'test_binance_secret_key_12345678901234567890'
  81. };
  82. const SAMPLE_REQUESTS = {
  83. accountInfo: {
  84. method: 'GET' as const,
  85. endpoint: '/api/v3/account',
  86. params: { timestamp: 1640995200000, recvWindow: 5000 }
  87. },
  88. newOrder: {
  89. method: 'POST' as const,
  90. endpoint: '/api/v3/order',
  91. params: {
  92. symbol: 'BTCUSDT',
  93. side: 'BUY',
  94. type: 'LIMIT',
  95. timeInForce: 'GTC',
  96. quantity: '0.001',
  97. price: '50000.00',
  98. timestamp: 1640995200000
  99. }
  100. }
  101. };
  102. // ============================================================================
  103. // Mock Implementation for Testing
  104. // ============================================================================
  105. class MockBinanceSigner implements IBinanceSigner {
  106. readonly platform = Platform.BINANCE;
  107. private accounts = new Map<string, any>();
  108. constructor() {
  109. // Add test account
  110. this.accounts.set('binance-test-001', {
  111. credentials: TEST_CREDENTIALS
  112. });
  113. }
  114. async signRequest(request: BinanceSignRequest): Promise<BinanceSignResponse> {
  115. const account = this.accounts.get(request.accountId);
  116. if (!account) {
  117. throw new Error(`Account ${request.accountId} not found`);
  118. }
  119. // Simulate HMAC-SHA256 signing
  120. const timestamp = Date.now();
  121. const queryString = this.buildQueryString(request.params || {}, timestamp);
  122. const signature = this.computeHmacSha256(queryString, account.credentials.secretKey);
  123. return {
  124. success: true,
  125. signature,
  126. algorithm: 'hmac-sha256',
  127. apiKey: account.credentials.apiKey,
  128. timestamp,
  129. queryString
  130. };
  131. }
  132. async verifySignature(request: BinanceVerifyRequest): Promise<BinanceVerifyResponse> {
  133. const expected = this.computeHmacSha256(request.message, request.secretKey);
  134. return {
  135. valid: expected === request.signature,
  136. algorithm: 'hmac-sha256',
  137. timestamp: new Date()
  138. };
  139. }
  140. async getApiKey(accountId: string): Promise<string> {
  141. const account = this.accounts.get(accountId);
  142. if (!account) {
  143. throw new Error(`Account ${accountId} not found`);
  144. }
  145. return account.credentials.apiKey;
  146. }
  147. async signBatch(requests: BinanceSignRequest[]): Promise<BinanceSignResponse[]> {
  148. return Promise.all(requests.map(req => this.signRequest(req)));
  149. }
  150. private buildQueryString(params: Record<string, any>, timestamp: number): string {
  151. const allParams = { ...params, timestamp };
  152. return Object.keys(allParams)
  153. .sort()
  154. .map(key => `${key}=${allParams[key]}`)
  155. .join('&');
  156. }
  157. private computeHmacSha256(message: string, secret: string): string {
  158. // Simulate HMAC-SHA256 computation
  159. return `hmac_sha256_${message.length}_${secret.length}_${Date.now()}`;
  160. }
  161. }
  162. // ============================================================================
  163. // Contract Tests
  164. // ============================================================================
  165. describe('IBinanceSigner Contract Tests', () => {
  166. let signer: IBinanceSigner;
  167. beforeEach(() => {
  168. signer = new MockBinanceSigner();
  169. });
  170. describe('Platform Identification', () => {
  171. test('should identify as BINANCE platform', () => {
  172. expect(signer.platform).toBe(Platform.BINANCE);
  173. });
  174. });
  175. describe('Request Signing', () => {
  176. test('should sign GET request successfully', async () => {
  177. const request: BinanceSignRequest = {
  178. accountId: 'binance-test-001',
  179. ...SAMPLE_REQUESTS.accountInfo
  180. };
  181. const result = await signer.signRequest(request);
  182. expect(result.success).toBe(true);
  183. expect(result.signature).toBeDefined();
  184. expect(result.algorithm).toBe('hmac-sha256');
  185. expect(result.apiKey).toBe(TEST_CREDENTIALS.apiKey);
  186. expect(result.timestamp).toBeGreaterThan(0);
  187. expect(result.queryString).toContain('timestamp=');
  188. });
  189. test('should sign POST request successfully', async () => {
  190. const request: BinanceSignRequest = {
  191. accountId: 'binance-test-001',
  192. ...SAMPLE_REQUESTS.newOrder
  193. };
  194. const result = await signer.signRequest(request);
  195. expect(result.success).toBe(true);
  196. expect(result.signature).toBeDefined();
  197. expect(result.algorithm).toBe('hmac-sha256');
  198. expect(result.queryString).toContain('symbol=BTCUSDT');
  199. });
  200. test('should fail for non-existent account', async () => {
  201. const request: BinanceSignRequest = {
  202. accountId: 'non-existent-account',
  203. ...SAMPLE_REQUESTS.accountInfo
  204. };
  205. await expect(signer.signRequest(request)).rejects.toThrow('Account non-existent-account not found');
  206. });
  207. test('should handle empty params', async () => {
  208. const request: BinanceSignRequest = {
  209. accountId: 'binance-test-001',
  210. method: 'GET',
  211. endpoint: '/api/v3/time'
  212. };
  213. const result = await signer.signRequest(request);
  214. expect(result.success).toBe(true);
  215. expect(result.queryString).toContain('timestamp=');
  216. });
  217. });
  218. describe('Signature Verification', () => {
  219. test('should verify valid signature', async () => {
  220. const message = 'symbol=BTCUSDT&side=BUY&timestamp=1640995200000';
  221. const signature = `hmac_sha256_${message.length}_${TEST_CREDENTIALS.secretKey.length}_${Date.now()}`;
  222. const request: BinanceVerifyRequest = {
  223. message,
  224. signature,
  225. secretKey: TEST_CREDENTIALS.secretKey
  226. };
  227. const result = await signer.verifySignature(request);
  228. expect(result.valid).toBe(true);
  229. expect(result.algorithm).toBe('hmac-sha256');
  230. expect(result.timestamp).toBeInstanceOf(Date);
  231. });
  232. test('should reject invalid signature', async () => {
  233. const request: BinanceVerifyRequest = {
  234. message: 'symbol=BTCUSDT&side=BUY&timestamp=1640995200000',
  235. signature: 'invalid_signature',
  236. secretKey: TEST_CREDENTIALS.secretKey
  237. };
  238. const result = await signer.verifySignature(request);
  239. expect(result.valid).toBe(false);
  240. expect(result.algorithm).toBe('hmac-sha256');
  241. });
  242. });
  243. describe('API Key Management', () => {
  244. test('should return API key for valid account', async () => {
  245. const apiKey = await signer.getApiKey('binance-test-001');
  246. expect(apiKey).toBe(TEST_CREDENTIALS.apiKey);
  247. });
  248. test('should fail for non-existent account', async () => {
  249. await expect(signer.getApiKey('non-existent-account')).rejects.toThrow('Account non-existent-account not found');
  250. });
  251. });
  252. describe('Batch Operations', () => {
  253. test('should handle batch signing', async () => {
  254. const requests: BinanceSignRequest[] = [
  255. {
  256. accountId: 'binance-test-001',
  257. ...SAMPLE_REQUESTS.accountInfo
  258. },
  259. {
  260. accountId: 'binance-test-001',
  261. ...SAMPLE_REQUESTS.newOrder
  262. }
  263. ];
  264. const results = await signer.signBatch(requests);
  265. expect(results).toHaveLength(2);
  266. expect(results[0].success).toBe(true);
  267. expect(results[1].success).toBe(true);
  268. expect(results[0].algorithm).toBe('hmac-sha256');
  269. expect(results[1].algorithm).toBe('hmac-sha256');
  270. });
  271. test('should handle empty batch', async () => {
  272. const results = await signer.signBatch([]);
  273. expect(results).toHaveLength(0);
  274. });
  275. });
  276. describe('Performance Requirements', () => {
  277. test('should complete signing within 50ms', async () => {
  278. const request: BinanceSignRequest = {
  279. accountId: 'binance-test-001',
  280. ...SAMPLE_REQUESTS.accountInfo
  281. };
  282. const startTime = Date.now();
  283. const result = await signer.signRequest(request);
  284. const duration = Date.now() - startTime;
  285. expect(result.success).toBe(true);
  286. expect(duration).toBeLessThan(50);
  287. });
  288. test('should handle concurrent requests', async () => {
  289. const requests = Array.from({ length: 10 }, () => ({
  290. accountId: 'binance-test-001',
  291. ...SAMPLE_REQUESTS.accountInfo
  292. }));
  293. const startTime = Date.now();
  294. const results = await Promise.all(
  295. requests.map(req => signer.signRequest(req))
  296. );
  297. const duration = Date.now() - startTime;
  298. expect(results).toHaveLength(10);
  299. expect(results.every(r => r.success)).toBe(true);
  300. expect(duration).toBeLessThan(200); // 10 concurrent requests < 200ms
  301. });
  302. });
  303. describe('Error Handling', () => {
  304. test('should handle malformed request gracefully', async () => {
  305. const request = {
  306. accountId: 'binance-test-001',
  307. method: 'INVALID' as any,
  308. endpoint: '/api/v3/account'
  309. };
  310. // Should not throw, but might return error in response
  311. try {
  312. const result = await signer.signRequest(request);
  313. // If it succeeds, check success flag
  314. if (!result.success) {
  315. expect(result.error).toBeDefined();
  316. }
  317. } catch (error) {
  318. // If it throws, that's also acceptable
  319. expect(error).toBeInstanceOf(Error);
  320. }
  321. });
  322. test('should handle large parameter sets', async () => {
  323. const largeParams = Array.from({ length: 100 }, (_, i) => [`param${i}`, `value${i}`])
  324. .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
  325. const request: BinanceSignRequest = {
  326. accountId: 'binance-test-001',
  327. method: 'POST',
  328. endpoint: '/api/v3/order',
  329. params: largeParams
  330. };
  331. const result = await signer.signRequest(request);
  332. expect(result.success).toBe(true);
  333. expect(result.queryString.length).toBeGreaterThan(100);
  334. });
  335. });
  336. });
  337. // ============================================================================
  338. // Integration Hints for Implementation
  339. // ============================================================================
  340. /**
  341. * Implementation Notes:
  342. *
  343. * 1. HMAC-SHA256 Algorithm:
  344. * - Use Node.js crypto module for HMAC-SHA256
  345. * - Query string format: key1=value1&key2=value2 (sorted by key)
  346. * - Always include timestamp parameter
  347. *
  348. * 2. Performance Requirements:
  349. * - Individual signing: < 50ms
  350. * - Batch operations: optimize for concurrent requests
  351. * - Use caching for repeated computations
  352. *
  353. * 3. Error Handling:
  354. * - Validate API key/secret format
  355. * - Handle network timeouts gracefully
  356. * - Log all signing attempts for debugging
  357. *
  358. * 4. Security Considerations:
  359. * - Never log secret keys
  360. * - Validate request parameters
  361. * - Implement request rate limiting
  362. *
  363. * 5. Binance API Specifics:
  364. * - recvWindow parameter for timing tolerance
  365. * - Different endpoints may have different param requirements
  366. * - Some endpoints don't require signatures
  367. */