/** * Contract Test: Aster Signer Interface * * Tests the IAsterSigner contract to ensure it correctly implements * EIP-191 (Ethereum Signed Message Standard) signing for Aster Network. */ import { describe, test, expect, beforeEach } from '@jest/globals'; import { Platform, SignResult, ErrorType } from '../../src/types/credential.js'; // ============================================================================ // Interface Contracts to Test // ============================================================================ /** * Aster平台签名器接口 */ interface IAsterSigner { readonly platform: Platform.ASTER; /** * 对Aster网络交易进行签名 * @param request Aster签名请求 * @returns 签名结果,包含EIP-191格式签名 */ signTransaction(request: AsterSignRequest): Promise; /** * 对任意消息进行签名(EIP-191) * @param request 消息签名请求 * @returns 签名结果 */ signMessage(request: AsterMessageSignRequest): Promise; /** * 验证Aster签名 * @param request 验证请求 * @returns 验证结果 */ verifySignature(request: AsterVerifyRequest): Promise; /** * 获取账户以太坊地址 * @param accountId 账户ID * @returns 以太坊地址 (0x...) */ getAddress(accountId: string): Promise; /** * 批量签名交易 * @param requests 批量签名请求 * @returns 批量签名结果 */ signBatch(requests: AsterSignRequest[]): Promise; } // ============================================================================ // Request/Response Types // ============================================================================ interface AsterSignRequest { accountId: string; transaction: AsterTransaction; options?: AsterSignOptions; } interface AsterMessageSignRequest { accountId: string; message: string | Uint8Array; options?: AsterSignOptions; } interface AsterSignOptions { timeout?: number; chainId?: number; gasLimit?: string; gasPrice?: string; nonce?: number; } interface AsterTransaction { to: string; value?: string; data?: string; gasLimit?: string; gasPrice?: string; nonce?: number; chainId?: number; } interface AsterSignResponse extends SignResult { signature: string; algorithm: 'ecdsa-secp256k1' | 'eip-191'; address: string; chainId?: number; txHash?: string; } interface AsterVerifyRequest { message: string | Uint8Array; signature: string; address: string; algorithm?: 'ecdsa-secp256k1' | 'eip-191'; } interface AsterVerifyResponse { valid: boolean; algorithm: 'ecdsa-secp256k1' | 'eip-191'; address: string; timestamp: Date; } // ============================================================================ // Test Constants // ============================================================================ const TEST_CREDENTIALS = { privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12', address: '0x742d35Cc6634C0532925a3b8D8002a66a30a1234' }; const SAMPLE_TRANSACTIONS = { transfer: { to: '0x742d35Cc6634C0532925a3b8D8002a66a30a5678', value: '1000000000000000000', // 1 ETH gasLimit: '21000', gasPrice: '20000000000', // 20 Gwei chainId: 592 // Astar Network }, contractCall: { to: '0x1234567890123456789012345678901234567890', data: '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d8002a66a30a567800000000000000000000000000000000000000000000000000000000000f4240', gasLimit: '60000', gasPrice: '25000000000', chainId: 592 } }; const SAMPLE_MESSAGES = { simple: 'Hello, Aster Network!', json: JSON.stringify({ action: 'verify', timestamp: Date.now() }), binary: new Uint8Array([72, 101, 108, 108, 111, 32, 65, 115, 116, 101, 114]) }; // ============================================================================ // Mock Implementation for Testing // ============================================================================ class MockAsterSigner implements IAsterSigner { readonly platform = Platform.ASTER; private accounts = new Map(); constructor() { // Add test account this.accounts.set('aster-test-001', { credentials: { type: 'aster', privateKey: TEST_CREDENTIALS.privateKey }, address: TEST_CREDENTIALS.address }); } async signTransaction(request: AsterSignRequest): Promise { const account = this.accounts.get(request.accountId); if (!account) { throw new Error(`Account ${request.accountId} not found`); } // Simulate ECDSA signing const signature = this.computeEcdsaSignature( this.serializeTransaction(request.transaction), account.credentials.privateKey ); return { success: true, signature, algorithm: 'ecdsa-secp256k1', address: account.address, chainId: request.transaction.chainId, timestamp: new Date(), txHash: this.computeTransactionHash(request.transaction) }; } async signMessage(request: AsterMessageSignRequest): Promise { const account = this.accounts.get(request.accountId); if (!account) { throw new Error(`Account ${request.accountId} not found`); } const message = typeof request.message === 'string' ? request.message : new TextDecoder().decode(request.message); // Simulate EIP-191 message signing const eip191Message = `\x19Ethereum Signed Message:\n${message.length}${message}`; const signature = this.computeEcdsaSignature(eip191Message, account.credentials.privateKey); return { success: true, signature, algorithm: 'eip-191', address: account.address, timestamp: new Date() }; } async verifySignature(request: AsterVerifyRequest): Promise { // Simulate signature verification const isValid = this.verifyEcdsaSignature( request.message, request.signature, request.address ); return { valid: isValid, algorithm: request.algorithm || 'eip-191', address: request.address, timestamp: new Date() }; } async getAddress(accountId: string): Promise { const account = this.accounts.get(accountId); if (!account) { throw new Error(`Account ${accountId} not found`); } return account.address; } async signBatch(requests: AsterSignRequest[]): Promise { return Promise.all(requests.map(req => this.signTransaction(req))); } private serializeTransaction(tx: AsterTransaction): string { return JSON.stringify({ to: tx.to, value: tx.value || '0', data: tx.data || '0x', gasLimit: tx.gasLimit, gasPrice: tx.gasPrice, nonce: tx.nonce, chainId: tx.chainId }); } private computeEcdsaSignature(message: string, privateKey: string): string { // Simulate ECDSA signature computation const hash = this.keccak256(message); return `0x${hash.substring(0, 130)}01`; // Mock signature with recovery id } private verifyEcdsaSignature(message: string | Uint8Array, signature: string, address: string): boolean { // Simulate signature verification return signature.length === 132 && address.length === 42 && signature.startsWith('0x'); } private computeTransactionHash(tx: AsterTransaction): string { return this.keccak256(this.serializeTransaction(tx)); } private keccak256(data: string): string { // Simulate Keccak256 hash return `0x${data.length.toString(16).padStart(64, '0')}${Date.now().toString(16).padStart(64, '0')}`; } } // ============================================================================ // Contract Tests // ============================================================================ describe('IAsterSigner Contract Tests', () => { let signer: IAsterSigner; beforeEach(() => { signer = new MockAsterSigner(); }); describe('Platform Identification', () => { test('should identify as ASTER platform', () => { expect(signer.platform).toBe(Platform.ASTER); }); }); describe('Transaction Signing', () => { test('should sign ETH transfer transaction', async () => { const request: AsterSignRequest = { accountId: 'aster-test-001', transaction: SAMPLE_TRANSACTIONS.transfer }; const result = await signer.signTransaction(request); expect(result.success).toBe(true); expect(result.signature).toBeDefined(); expect(result.signature).toMatch(/^0x[0-9a-fA-F]{130}$/); expect(result.algorithm).toBe('ecdsa-secp256k1'); expect(result.address).toBe(TEST_CREDENTIALS.address); expect(result.chainId).toBe(592); expect(result.txHash).toBeDefined(); }); test('should sign contract call transaction', async () => { const request: AsterSignRequest = { accountId: 'aster-test-001', transaction: SAMPLE_TRANSACTIONS.contractCall }; const result = await signer.signTransaction(request); expect(result.success).toBe(true); expect(result.signature).toBeDefined(); expect(result.algorithm).toBe('ecdsa-secp256k1'); expect(result.address).toBe(TEST_CREDENTIALS.address); }); test('should fail for non-existent account', async () => { const request: AsterSignRequest = { accountId: 'non-existent-account', transaction: SAMPLE_TRANSACTIONS.transfer }; await expect(signer.signTransaction(request)).rejects.toThrow('Account non-existent-account not found'); }); test('should handle transaction with options', async () => { const request: AsterSignRequest = { accountId: 'aster-test-001', transaction: SAMPLE_TRANSACTIONS.transfer, options: { timeout: 10000, chainId: 592, gasLimit: '25000' } }; const result = await signer.signTransaction(request); expect(result.success).toBe(true); expect(result.chainId).toBe(592); }); }); describe('Message Signing (EIP-191)', () => { test('should sign string message', async () => { const request: AsterMessageSignRequest = { accountId: 'aster-test-001', message: SAMPLE_MESSAGES.simple }; const result = await signer.signMessage(request); expect(result.success).toBe(true); expect(result.signature).toBeDefined(); expect(result.algorithm).toBe('eip-191'); expect(result.address).toBe(TEST_CREDENTIALS.address); }); test('should sign JSON message', async () => { const request: AsterMessageSignRequest = { accountId: 'aster-test-001', message: SAMPLE_MESSAGES.json }; const result = await signer.signMessage(request); expect(result.success).toBe(true); expect(result.algorithm).toBe('eip-191'); }); test('should sign binary message', async () => { const request: AsterMessageSignRequest = { accountId: 'aster-test-001', message: SAMPLE_MESSAGES.binary }; const result = await signer.signMessage(request); expect(result.success).toBe(true); expect(result.algorithm).toBe('eip-191'); }); test('should fail for non-existent account', async () => { const request: AsterMessageSignRequest = { accountId: 'non-existent-account', message: SAMPLE_MESSAGES.simple }; await expect(signer.signMessage(request)).rejects.toThrow('Account non-existent-account not found'); }); }); describe('Signature Verification', () => { test('should verify valid signature', async () => { const request: AsterVerifyRequest = { message: SAMPLE_MESSAGES.simple, signature: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef121234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1201', address: TEST_CREDENTIALS.address }; const result = await signer.verifySignature(request); expect(result.valid).toBe(true); expect(result.algorithm).toBe('eip-191'); expect(result.address).toBe(TEST_CREDENTIALS.address); expect(result.timestamp).toBeInstanceOf(Date); }); test('should reject invalid signature format', async () => { const request: AsterVerifyRequest = { message: SAMPLE_MESSAGES.simple, signature: 'invalid_signature', address: TEST_CREDENTIALS.address }; const result = await signer.verifySignature(request); expect(result.valid).toBe(false); }); test('should handle binary message verification', async () => { const request: AsterVerifyRequest = { message: SAMPLE_MESSAGES.binary, signature: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef121234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1201', address: TEST_CREDENTIALS.address, algorithm: 'eip-191' }; const result = await signer.verifySignature(request); expect(result.algorithm).toBe('eip-191'); }); }); describe('Address Management', () => { test('should return address for valid account', async () => { const address = await signer.getAddress('aster-test-001'); expect(address).toBe(TEST_CREDENTIALS.address); expect(address).toMatch(/^0x[0-9a-fA-F]{40}$/); }); test('should fail for non-existent account', async () => { await expect(signer.getAddress('non-existent-account')).rejects.toThrow('Account non-existent-account not found'); }); }); describe('Batch Operations', () => { test('should handle batch transaction signing', async () => { const requests: AsterSignRequest[] = [ { accountId: 'aster-test-001', transaction: SAMPLE_TRANSACTIONS.transfer }, { accountId: 'aster-test-001', transaction: SAMPLE_TRANSACTIONS.contractCall } ]; 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('ecdsa-secp256k1'); expect(results[1].algorithm).toBe('ecdsa-secp256k1'); }); test('should handle empty batch', async () => { const results = await signer.signBatch([]); expect(results).toHaveLength(0); }); }); describe('Performance Requirements', () => { test('should complete transaction signing within 50ms', async () => { const request: AsterSignRequest = { accountId: 'aster-test-001', transaction: SAMPLE_TRANSACTIONS.transfer }; const startTime = Date.now(); const result = await signer.signTransaction(request); const duration = Date.now() - startTime; expect(result.success).toBe(true); expect(duration).toBeLessThan(50); }); test('should complete message signing within 50ms', async () => { const request: AsterMessageSignRequest = { accountId: 'aster-test-001', message: SAMPLE_MESSAGES.simple }; const startTime = Date.now(); const result = await signer.signMessage(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: 'aster-test-001', transaction: SAMPLE_TRANSACTIONS.transfer })); const startTime = Date.now(); const results = await Promise.all( requests.map(req => signer.signTransaction(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 transaction gracefully', async () => { const request = { accountId: 'aster-test-001', transaction: { to: 'invalid_address', value: 'invalid_value' } as any }; // Should not throw, but might return error in response try { const result = await signer.signTransaction(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 empty message', async () => { const request: AsterMessageSignRequest = { accountId: 'aster-test-001', message: '' }; const result = await signer.signMessage(request); expect(result.success).toBe(true); expect(result.algorithm).toBe('eip-191'); }); test('should handle large message', async () => { const largeMessage = 'A'.repeat(10000); // 10KB message const request: AsterMessageSignRequest = { accountId: 'aster-test-001', message: largeMessage }; const result = await signer.signMessage(request); expect(result.success).toBe(true); }); }); }); // ============================================================================ // Integration Hints for Implementation // ============================================================================ /** * Implementation Notes: * * 1. ECDSA secp256k1 Algorithm: * - Use ethers.js or noble-secp256k1 library * - Generate recoverable signatures (v, r, s format) * - Support both transaction and message signing * * 2. EIP-191 Message Signing: * - Prefix: "\x19Ethereum Signed Message:\n{message_length}{message}" * - Use Keccak256 for hashing * - Return signature in 0x format * * 3. Transaction Serialization: * - RLP encoding for raw transactions * - Include chainId for EIP-155 protection * - Handle legacy and EIP-1559 transaction types * * 4. Performance Requirements: * - Individual signing: < 50ms * - Batch operations: optimize for concurrent requests * - Consider WebWorkers for CPU-intensive operations * * 5. Security Considerations: * - Validate transaction parameters * - Check gas limits and prices * - Implement replay protection * - Never log private keys * * 6. Aster Network Specifics: * - Chain ID: 592 (mainnet), 81 (testnet) * - Support for both EVM and WASM contracts * - Gas mechanics similar to Ethereum */