import { describe, it, expect, vi, beforeEach } from 'vitest' import { verifyMessage, type Hex } from 'viem' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { SignatureService } from './signature-service' import type { WalletConnectClient } from './walletconnect-client' import type { ConnectionService } from './connection-service' import type { Network } from '@meshconnect/uwc-types' // Partial Network mocks — the signing path only reads `network.id`, so the // other required Network fields are intentionally omitted via a cast. const EVM_NETWORK = { id: 'eip155:1', name: 'Ethereum Mainnet' } as unknown as Network const SOLANA_NETWORK = { id: 'solana:mainnet', name: 'Solana' } as unknown as Network const ACTIVE_ADDRESS = '0xUserAddress' const MOCK_TOPIC = 'mock-session-topic' function makeMockConnectionService(overrides?: { session?: object | null network?: Network | null address?: string | null }): ConnectionService { return { getSession: vi .fn() .mockReturnValue( overrides?.session !== undefined ? overrides.session : { topic: MOCK_TOPIC, namespaces: {} } ), getActiveNetwork: vi .fn() .mockReturnValue( overrides?.network !== undefined ? overrides.network : EVM_NETWORK ), getActiveAddress: vi .fn() .mockReturnValue( overrides?.address !== undefined ? overrides.address : ACTIVE_ADDRESS ) } as unknown as ConnectionService } const USDC_TYPED_DATA = { domain: { name: 'USD Coin', version: '2', chainId: 1, verifyingContract: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' }, types: { TransferWithAuthorization: [ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'validAfter', type: 'uint256' }, { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' } ] }, primaryType: 'TransferWithAuthorization', message: { from: '0xabc', to: '0xdef', value: '1000000', validAfter: 0, validBefore: 9999999999, nonce: '0xdeadbeef' } } describe('SignatureService (wallet-connect-connector) — signMessage', () => { const EVM_ACCOUNT = `eip155:1:${ACTIVE_ADDRESS}` let mockWCClient: WalletConnectClient let service: SignatureService let mockSignClient: { request: ReturnType } beforeEach(() => { mockSignClient = { request: vi.fn().mockResolvedValue('0xpersonalsig') } mockWCClient = { getClient: vi.fn().mockResolvedValue(mockSignClient) } as unknown as WalletConnectClient const connectionService = makeMockConnectionService({ session: { topic: MOCK_TOPIC, namespaces: { eip155: { accounts: [EVM_ACCOUNT] } } } }) service = new SignatureService(mockWCClient, connectionService) }) it('returns { signature, address } with the exact signer address (eip155)', async () => { const result = await service.signMessage('hello') expect(result).toEqual({ signature: { type: 'standard', signature: '0xpersonalsig' }, address: ACTIVE_ADDRESS }) }) it('hex-encodes the message for personal_sign', async () => { await service.signMessage('hello') expect(mockSignClient.request).toHaveBeenCalledWith({ topic: MOCK_TOPIC, chainId: 'eip155:1', request: { method: 'personal_sign', params: ['0x68656c6c6f', ACTIVE_ADDRESS] } }) }) it('passes through a message that is already even-length 0x-hex', async () => { await service.signMessage('0xdeadbeef') expect(mockSignClient.request.mock.calls[0][0].request.params).toEqual([ '0xdeadbeef', ACTIVE_ADDRESS ]) }) it('yields a signature that verifies against the original text from a wallet that hex-decodes the param leniently', async () => { const account = privateKeyToAccount(generatePrivateKey()) // Crypto.com Onchain over WalletConnect keeps only the leading hex pairs, // so raw text decodes to zero bytes and it signs an empty message. const lenientHexWallet = { request: vi.fn( async ({ request }: { request: { params: [string, string] } }) => { const [data] = request.params const hex = data.replace(/^0x/, '').match(/^(?:[0-9a-fA-F]{2})*/) return account.signMessage({ message: { raw: `0x${hex?.[0] ?? ''}` as Hex } }) } ) } const message = 'Sign to verify you own this wallet for Revolut ✨' service = new SignatureService( { getClient: vi.fn().mockResolvedValue(lenientHexWallet) } as unknown as WalletConnectClient, makeMockConnectionService({ address: account.address, session: { topic: MOCK_TOPIC, namespaces: { eip155: { accounts: [`eip155:1:${account.address}`] } } } }) ) const { signature } = await service.signMessage(message) if (signature.type !== 'standard') throw new Error('expected EIP-191') expect( await verifyMessage({ address: account.address, message, signature: signature.signature as Hex }) ).toBe(true) }) }) describe('SignatureService (wallet-connect-connector) — signTypedData', () => { let mockWCClient: WalletConnectClient let mockConnectionService: ConnectionService let service: SignatureService let mockSignClient: { request: ReturnType } beforeEach(() => { mockSignClient = { request: vi.fn().mockResolvedValue('0xwcsig') } mockWCClient = { getClient: vi.fn().mockResolvedValue(mockSignClient) } as unknown as WalletConnectClient mockConnectionService = makeMockConnectionService() service = new SignatureService(mockWCClient, mockConnectionService) }) it('sends eth_signTypedData_v4 via WalletConnect', async () => { await service.signTypedData(USDC_TYPED_DATA) expect(mockSignClient.request).toHaveBeenCalledWith({ topic: MOCK_TOPIC, chainId: 'eip155:1', request: { method: 'eth_signTypedData_v4', params: [ACTIVE_ADDRESS, expect.any(String)] } }) }) it('injects EIP712Domain into wire payload for mobile wallet compat', async () => { await service.signTypedData(USDC_TYPED_DATA) const [, jsonArg] = mockSignClient.request.mock.calls[0][0].request.params const sent = JSON.parse(jsonArg) expect(sent.types.EIP712Domain).toEqual([ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' } ]) }) it('does not duplicate EIP712Domain when caller already includes it', async () => { const dataWithDomain = { ...USDC_TYPED_DATA, types: { EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'chainId', type: 'uint256' } ], ...USDC_TYPED_DATA.types } } await service.signTypedData(dataWithDomain) const [, jsonArg] = mockSignClient.request.mock.calls[0][0].request.params const sent = JSON.parse(jsonArg) expect(sent.types.EIP712Domain).toEqual([ { name: 'name', type: 'string' }, { name: 'chainId', type: 'uint256' } ]) }) it('omits version from EIP712Domain when domain has no version (UNI-style)', async () => { const uniData = { ...USDC_TYPED_DATA, domain: { name: 'Uniswap V2', chainId: 1, verifyingContract: '0x...' } } await service.signTypedData(uniData) const [, jsonArg] = mockSignClient.request.mock.calls[0][0].request.params const sent = JSON.parse(jsonArg) expect(sent.types.EIP712Domain).toEqual([ { name: 'name', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' } ]) }) it('does not mutate the original typedData object', async () => { const original = JSON.parse(JSON.stringify(USDC_TYPED_DATA)) await service.signTypedData(USDC_TYPED_DATA) expect(USDC_TYPED_DATA.types).toEqual(original.types) expect( (USDC_TYPED_DATA.types as Record).EIP712Domain ).toBeUndefined() }) it('returns the signature from WalletConnect', async () => { mockSignClient.request.mockResolvedValue('0xexpectedsig') const result = await service.signTypedData(USDC_TYPED_DATA) expect(result).toBe('0xexpectedsig') }) it('throws when there is no active session', async () => { service = new SignatureService( mockWCClient, makeMockConnectionService({ session: null }) ) await expect(service.signTypedData(USDC_TYPED_DATA)).rejects.toThrow( 'No active session' ) }) it('throws when the active network is not eip155', async () => { service = new SignatureService( mockWCClient, makeMockConnectionService({ network: SOLANA_NETWORK }) ) await expect(service.signTypedData(USDC_TYPED_DATA)).rejects.toThrow( 'signTypedData is only supported for EVM wallets' ) }) })