import { describe, it, expect, beforeEach } from 'vitest' import { EthereumTransactionBuilder } from './ethereum-transaction-builder' import type { EVMNativeTransferRequest, EVMContractCallRequest, EVMDataTransferRequest } from '@meshconnect/uwc-types' describe('WalletConnect EthereumTransactionBuilder', () => { let builder: EthereumTransactionBuilder beforeEach(() => { builder = new EthereumTransactionBuilder() }) describe('getNativeTransaction', () => { it('should build a native transaction request with from field', async () => { const request: EVMNativeTransferRequest = { from: '0x1234567890123456789012345678901234567890', to: '0x0987654321098765432109876543210987654321', amount: BigInt('1000000000000000000'), // 1 ETH gasConfig: { gasLimit: 21000, maxFeePerGas: 50000000000, maxPriorityFeePerGas: 2000000000 } } const result = await builder.getNativeTransaction(request) expect(result).toEqual({ from: request.from, to: request.to, value: request.amount, gasLimit: BigInt(21000), maxFeePerGas: BigInt(50000000000), maxPriorityFeePerGas: BigInt(2000000000) }) }) it('should build a native transaction without gas config', async () => { const request: EVMNativeTransferRequest = { from: '0x1234567890123456789012345678901234567890', to: '0x0987654321098765432109876543210987654321', amount: BigInt('500000000000000000') // 0.5 ETH } const result = await builder.getNativeTransaction(request) expect(result).toEqual({ from: request.from, to: request.to, value: request.amount }) }) }) describe('getContractTransaction', () => { it('should build a contract transaction request with from field', async () => { const erc20ABI = [ { inputs: [ { name: 'recipient', type: 'address' }, { name: 'amount', type: 'uint256' } ], name: 'transfer', outputs: [{ name: '', type: 'bool' }], stateMutability: 'nonpayable', type: 'function' } ] const request: EVMContractCallRequest = { from: '0x1234567890123456789012345678901234567890', contractAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', abi: erc20ABI, functionName: 'transfer', args: ['0x0987654321098765432109876543210987654321', BigInt(1000000)], gasConfig: { gasLimit: 100000 } } const result = await builder.getContractTransaction(request) expect(result.from).toBe(request.from) expect(result.to).toBe(request.contractAddress) expect(result.data).toBeDefined() expect(result.data).toContain('0xa9059cbb') // transfer function selector expect(result.value).toBe(0n) expect(result.gasLimit).toBe(BigInt(100000)) }) it('should build a payable contract transaction with value', async () => { const payableABI = [ { inputs: [], name: 'deposit', outputs: [], stateMutability: 'payable', type: 'function' } ] const request: EVMContractCallRequest = { from: '0x1234567890123456789012345678901234567890', contractAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', abi: payableABI, functionName: 'deposit', args: [], value: BigInt('1000000000000000000') // 1 ETH } const result = await builder.getContractTransaction(request) expect(result.from).toBe(request.from) expect(result.to).toBe(request.contractAddress) expect(result.data).toBeDefined() expect(result.value).toBe(request.value) }) }) describe('getDataTransaction', () => { it('should build a data transaction request with all fields', async () => { const request: EVMDataTransferRequest = { from: '0x1234567890123456789012345678901234567890', to: '0x0987654321098765432109876543210987654321', data: '0xa9059cbb000000000000000000000000123456789012345678901234567890123456789000000000000000000000000000000000000000000000000000000000000f4240', chainId: '1', value: BigInt('500000000000000000'), // 0.5 ETH gasConfig: { gasLimit: 100000, maxFeePerGas: 50000000000, maxPriorityFeePerGas: 2000000000 } } const result = await builder.getDataTransaction(request) expect(result).toEqual({ from: request.from, to: request.to, data: request.data, value: request.value, gasLimit: BigInt(100000), maxFeePerGas: BigInt(50000000000), maxPriorityFeePerGas: BigInt(2000000000) }) }) it('should build a data transaction without value', async () => { const request: EVMDataTransferRequest = { from: '0x1234567890123456789012345678901234567890', to: '0x0987654321098765432109876543210987654321', data: '0xa9059cbb', chainId: '137' // Polygon } const result = await builder.getDataTransaction(request) expect(result).toEqual({ from: request.from, to: request.to, data: request.data, value: 0n }) }) it('should build a data transaction with partial gas config', async () => { const request: EVMDataTransferRequest = { from: '0x1234567890123456789012345678901234567890', to: '0x0987654321098765432109876543210987654321', data: '0x00', chainId: '8453', // Base gasConfig: { gasLimit: 50000 } } const result = await builder.getDataTransaction(request) expect(result.from).toBe(request.from) expect(result.gasLimit).toBe(BigInt(50000)) expect(result.maxFeePerGas).toBeUndefined() expect(result.maxPriorityFeePerGas).toBeUndefined() }) it('should handle empty data field', async () => { const request: EVMDataTransferRequest = { from: '0x1234567890123456789012345678901234567890', to: '0x0987654321098765432109876543210987654321', data: '0x', chainId: '1' } const result = await builder.getDataTransaction(request) expect(result.from).toBe(request.from) expect(result.data).toBe('0x') expect(result.to).toBe(request.to) expect(result.value).toBe(0n) }) it('should handle complex encoded data', async () => { const complexData = '0x' + 'a'.repeat(1000) // Long hex string const request: EVMDataTransferRequest = { from: '0x1234567890123456789012345678901234567890', to: '0x0987654321098765432109876543210987654321', data: complexData, chainId: '42161' // Arbitrum } const result = await builder.getDataTransaction(request) expect(result.from).toBe(request.from) expect(result.data).toBe(complexData) expect(result.value).toBe(0n) }) it('should handle different chain IDs in request', async () => { const chains = [ { id: '1', name: 'Ethereum' }, { id: '137', name: 'Polygon' }, { id: '8453', name: 'Base' }, { id: '42161', name: 'Arbitrum' }, { id: '10', name: 'Optimism' } ] for (const chain of chains) { const request: EVMDataTransferRequest = { from: '0x1234567890123456789012345678901234567890', to: '0x0987654321098765432109876543210987654321', data: '0xa9059cbb', chainId: chain.id } const result = await builder.getDataTransaction(request) // The builder doesn't return chainId, but it should accept it in the request expect(result.from).toBe(request.from) expect(result.data).toBe(request.data) expect(result.value).toBe(0n) } }) }) describe('formatGasConfig', () => { it('should handle null gas config values', async () => { const request: EVMDataTransferRequest = { from: '0x1234567890123456789012345678901234567890', to: '0x0987654321098765432109876543210987654321', data: '0x00', chainId: '1', gasConfig: { gasLimit: null, maxFeePerGas: null, maxPriorityFeePerGas: null } } const result = await builder.getDataTransaction(request) expect(result.gasLimit).toBeUndefined() expect(result.maxFeePerGas).toBeUndefined() expect(result.maxPriorityFeePerGas).toBeUndefined() }) it('should floor decimal gas values', async () => { const request: EVMDataTransferRequest = { from: '0x1234567890123456789012345678901234567890', to: '0x0987654321098765432109876543210987654321', data: '0x00', chainId: '1', gasConfig: { gasLimit: 100000.7, maxFeePerGas: 50000000000.9, maxPriorityFeePerGas: 2000000000.1 } } const result = await builder.getDataTransaction(request) expect(result.gasLimit).toBe(BigInt(100000)) expect(result.maxFeePerGas).toBe(BigInt(50000000000)) expect(result.maxPriorityFeePerGas).toBe(BigInt(2000000000)) }) }) describe('value normalization', () => { const oneEth = 1000000000000000000n const from = '0x1234567890123456789012345678901234567890' const to = '0x0987654321098765432109876543210987654321' const cases: [string, bigint | string][] = [ ['bigint', oneEth], ['decimal string', '1000000000000000000'], ['hex string', '0xde0b6b3a7640000'] ] it.each(cases)( 'contract-call value as %s normalizes to bigint', async (_l, value) => { const result = await builder.getContractTransaction({ from, contractAddress: to, abi: [ { inputs: [], name: 'deposit', outputs: [], stateMutability: 'payable', type: 'function' } ], functionName: 'deposit', args: [], value }) expect(result.value).toBe(oneEth) } ) it.each(cases)( 'data-transfer value as %s normalizes to bigint', async (_l, value) => { const result = await builder.getDataTransaction({ from, to, data: '0x', chainId: '1', value }) expect(result.value).toBe(oneEth) } ) it('defaults a missing value to 0n', async () => { const result = await builder.getDataTransaction({ from, to, data: '0x', chainId: '1' }) expect(result.value).toBe(0n) }) it.each(['', ' ', 'abc', '1.5', '0x'])( 'rejects an invalid data-transfer value %j instead of coercing to 0', async value => { await expect( builder.getDataTransaction({ from, to, data: '0x', chainId: '1', value }) ).rejects.toThrow(/Invalid EVM value/) } ) it.each(['', ' ', 'abc', '1.5', '0x'])( 'rejects an invalid contract-call value %j instead of coercing to 0', async value => { await expect( builder.getContractTransaction({ from, contractAddress: to, abi: [ { inputs: [], name: 'deposit', outputs: [], stateMutability: 'payable', type: 'function' } ], functionName: 'deposit', args: [], value }) ).rejects.toThrow(/Invalid EVM value/) } ) }) })