import { describe, it, expect, beforeEach, vi } from 'vitest' import { InjectedConnector } from './injected-connector' import type { Network, ExtensionInjectedProvider, TransactionRequest, EIP712TypedData } from '@meshconnect/uwc-types' // Mock all service dependencies vi.mock('./services/storage-service') vi.mock('./services/ethereum/ethereum-wallet-service') vi.mock('./services/solana/solana-wallet-service') vi.mock('./services/tron/tron-wallet-service') vi.mock('./services/connection-manager') vi.mock('./services/signature-service') vi.mock('./services/transaction-service') vi.mock('@meshconnect/uwc-tron-connector', () => ({ TronConnector: vi.fn() })) vi.mock('@meshconnect/uwc-bridge-child', async importOriginal => ({ ...(await importOriginal>()), BridgeChild: vi.fn() })) describe('InjectedConnector', () => { let connector: InjectedConnector let mockEthProvider: ExtensionInjectedProvider let mockNetwork: Network beforeEach(async () => { // Clear all mocks vi.clearAllMocks() // Create connector connector = new InjectedConnector({ networkRpcMap: { 'eip155:1': 'https://eth-rpc.example.com' } }) // Setup mock provider mockEthProvider = { supportedNetworkIds: ['eip155:1', 'eip155:137'], namespaceMetaData: { eip155: { detectedWallet: { uuid: 'wallet-uuid-123' } } } } as any // Setup mock network mockNetwork = { id: 'eip155:1', internalId: 1, name: 'Ethereum Mainnet', namespace: 'eip155', networkType: 'evm', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 } } as Network }) describe('constructor', () => { it('should initialize with default empty network RPC map', () => { const newConnector = new InjectedConnector() expect(newConnector).toBeDefined() }) it('should initialize with provided network RPC map', () => { const networkRpcMap = { 'eip155:1': 'https://mainnet.infura.io/v3/test', 'eip155:137': 'https://polygon-rpc.com' } const newConnector = new InjectedConnector({ networkRpcMap }) expect(newConnector).toBeDefined() }) it('should initialize all services', async () => { const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const { SolanaWalletService } = await import('./services/solana/solana-wallet-service') const { StorageService } = await import('./services/storage-service') const { ConnectionManager } = await import('./services/connection-manager') const { SignatureService } = await import('./services/signature-service') const { TransactionService } = await import('./services/transaction-service') new InjectedConnector() expect(StorageService).toHaveBeenCalled() expect(EthereumWalletService).toHaveBeenCalled() expect(SolanaWalletService).toHaveBeenCalled() expect(ConnectionManager).toHaveBeenCalled() expect(SignatureService).toHaveBeenCalled() expect(TransactionService).toHaveBeenCalled() }) }) describe('constructor options wiring', () => { it('defaults every option when constructed with no arguments', async () => { const { TransactionService } = await import('./services/transaction-service') const { TronConnector } = await import('@meshconnect/uwc-tron-connector') const { BridgeChild } = await import('@meshconnect/uwc-bridge-child') new InjectedConnector() // Empty rpc map, no Mesh Tron connector, and a bridge with no error sink. expect(TransactionService).toHaveBeenCalledWith({}) expect(TronConnector).not.toHaveBeenCalled() const bridgeOpts = vi.mocked(BridgeChild).mock.calls.at(-1)![0] as { onError?: unknown } expect(bridgeOpts.onError).toBeUndefined() }) it('routes networkRpcMap to the TransactionService', async () => { const { TransactionService } = await import('./services/transaction-service') const networkRpcMap = { 'eip155:1': 'https://rpc.example.com' } new InjectedConnector({ networkRpcMap }) expect(TransactionService).toHaveBeenCalledWith(networkRpcMap) }) it('constructs the Mesh Tron connector only when tronConnectorConfig is set', async () => { const { TronConnector } = await import('@meshconnect/uwc-tron-connector') const tronConnectorConfig = { enabledWallets: ['trust'] } as never new InjectedConnector({ tronConnectorConfig }) expect(TronConnector).toHaveBeenCalledWith(tronConnectorConfig) }) it('wires onBridgeError into BridgeChild.onError', async () => { const { BridgeChild } = await import('@meshconnect/uwc-bridge-child') const onBridgeError = vi.fn() new InjectedConnector({ onBridgeError }) const bridgeOpts = vi.mocked(BridgeChild).mock.calls.at(-1)![0] as { onError?: unknown } expect(bridgeOpts.onError).toBe(onBridgeError) }) it('threads usingIntegratedBrowser + a deduping failure sink into Solana discovery', async () => { const { SolanaWalletService } = await import('./services/solana/solana-wallet-service') const onLegacyDiscoveryFailure = vi.fn() const expectedWallets = [{ id: 'coinbase' }] as never const c = new InjectedConnector({ usingIntegratedBrowser: true, onLegacyDiscoveryFailure }) const solService = vi .mocked(SolanaWalletService) .mock.results.at(-1)!.value solService.getDetectedWallets.mockReturnValue([]) await c.getAvailableWallets('solana', expectedWallets) const call = solService.initializeDiscovery.mock.calls.at(-1)! expect(call[0]).toBe(expectedWallets) expect(call[1]).toBe(true) const sink = call[2] as (info: { injectedId: string walletName: string }) => void expect(typeof sink).toBe('function') // Deduped: the same injectedId reported across repeated detection cycles // forwards once; a new injectedId forwards again. sink({ injectedId: 'coinbaseSolana', walletName: 'Base Wallet' }) sink({ injectedId: 'coinbaseSolana', walletName: 'Base Wallet' }) expect(onLegacyDiscoveryFailure).toHaveBeenCalledTimes(1) sink({ injectedId: 'phantomSolana', walletName: 'Phantom' }) expect(onLegacyDiscoveryFailure).toHaveBeenCalledTimes(2) }) it('defaults Solana discovery to the extension flow (usingIntegratedBrowser false, no failure sink)', async () => { const { SolanaWalletService } = await import('./services/solana/solana-wallet-service') const c = new InjectedConnector() const solService = vi .mocked(SolanaWalletService) .mock.results.at(-1)!.value solService.getDetectedWallets.mockReturnValue([]) await c.getAvailableWallets('solana', []) expect(solService.initializeDiscovery).toHaveBeenCalledWith( [], false, undefined ) }) }) describe('getAvailableWallets', () => { it('should return Ethereum wallets for eip155 namespace', async () => { const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value const mockWallets = [ { uuid: 'wallet-1', name: 'MetaMask', icon: 'icon-url', rdns: 'io.metamask' } ] mockEthService.getDetectedWallets.mockReturnValue(mockWallets as any) const result = await connector.getAvailableWallets('eip155') expect(result).toEqual([ { uuid: 'wallet-1', name: 'MetaMask', icon: 'icon-url', rdns: 'io.metamask' } ]) }) it('should re-fetch Ethereum wallets if none detected', async () => { const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value // Clear the call from constructor initialization vi.clearAllMocks() mockEthService.getDetectedWallets .mockReturnValueOnce([]) // First call returns empty .mockReturnValueOnce([ // Second call after re-fetch { uuid: 'wallet-1', name: 'MetaMask', icon: 'icon-url', rdns: 'io.metamask' } ] as any) await connector.getAvailableWallets('eip155') expect(mockEthService.initializeDiscovery).toHaveBeenCalledTimes(1) }) it('should return Solana wallets for solana namespace', async () => { const { SolanaWalletService } = await import('./services/solana/solana-wallet-service') const mockSolService = vi.mocked(SolanaWalletService).mock.results[0].value const mockWallets = [ { uuid: 'wallet-1', name: 'Phantom', features: ['signMessage', 'signTransaction'] } ] mockSolService.getDetectedWallets.mockReturnValue(mockWallets as any) const result = await connector.getAvailableWallets('solana') expect(mockSolService.initializeDiscovery).toHaveBeenCalled() expect(result).toEqual([ { uuid: 'wallet-1', name: 'Phantom', features: ['signMessage', 'signTransaction'] } ]) }) it('should return empty array for unsupported namespace', async () => { const result = await connector.getAvailableWallets('cosmos' as any) expect(result).toEqual([]) }) }) describe('connect', () => { it('should return existing connection if already connected', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value mockConnManager.checkExistingConnections.mockResolvedValue({ address: '0xexisting', availableAddresses: [ { address: '0xexisting', networkId: 'eip155:1' as any } ] }) const result = await connector.connect(mockNetwork, mockEthProvider) expect(result).toEqual({ networkId: 'eip155:1', address: '0xexisting', availableAddresses: [{ address: '0xexisting', networkId: 'eip155:1' }] }) expect(mockConnManager.setCurrentNetworkId).toHaveBeenCalledWith( 'eip155:1' ) expect(mockConnManager.connectEthereum).not.toHaveBeenCalled() }) it('should connect to Ethereum wallet if not already connected', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value mockConnManager.checkExistingConnections.mockResolvedValue({ address: null, availableAddresses: [] }) mockConnManager.connectEthereum.mockResolvedValue({ networkId: 'eip155:1' as any, address: '0xnewaddress', availableAddresses: [ { address: '0xnewaddress', networkId: 'eip155:1' as any } ] }) const result = await connector.connect(mockNetwork, mockEthProvider) expect(mockConnManager.connectEthereum).toHaveBeenCalledWith( mockNetwork, mockEthProvider ) expect(result.address).toBe('0xnewaddress') }) describe('cross-namespace Tron gather (tronConnectorConfig set)', () => { const tronNetwork = { id: 'tron:0x2b6653dc', internalId: 2, name: 'Tron', namespace: 'tron', networkType: 'tron', nativeCurrency: { name: 'Tron', symbol: 'TRX', decimals: 6 } } as Network // A multi-namespace wallet (e.g. Trust): EVM target + a detected Tron provider. const multiNsProvider = { supportedNetworkIds: ['eip155:1', 'tron:0x2b6653dc'], namespaceMetaData: { eip155: { detectedWallet: { uuid: 'eth-uuid' } }, // Metadata-driven: the connector reads the tron injectedId from here. tron: { injectedId: 'trustWallet.tronLink' } } } as unknown as ExtensionInjectedProvider // Build an InjectedConnector wired with the Tron connector + networks, and // return the stubbed TronConnector instance so the test can assert on it. const buildWithTron = async (tronConnect: ReturnType) => { const { TronConnector } = await import('@meshconnect/uwc-tron-connector') // Must be constructable (`new TronConnector(...)`), so use a function, // not an arrow. vi.mocked(TronConnector).mockImplementation(function () { return { connect: tronConnect } as never } as never) const c = new InjectedConnector({ tronConnectorConfig: { enabledWallets: ['trust'] } as never, networks: [mockNetwork, tronNetwork] }) const { ConnectionManager } = await import('./services/connection-manager') const connManager = vi .mocked(ConnectionManager) .mock.results.at(-1)!.value connManager.checkExistingConnections.mockResolvedValue({ address: null, availableAddresses: [] }) connManager.connectEthereum.mockResolvedValue({ networkId: 'eip155:1', address: '0xeth', availableAddresses: [{ address: '0xeth', networkId: 'eip155:1' }] }) return c } it('auto-connects Tron alongside an EVM target and merges the address', async () => { const tronConnect = vi.fn().mockResolvedValue({ networkId: 'tron:0x2b6653dc', address: 'TTronAddr', availableAddresses: [ { address: 'TTronAddr', networkId: 'tron:0x2b6653dc' } ] }) const c = await buildWithTron(tronConnect) const result = await c.connect(mockNetwork, multiNsProvider) // Routed through the Tron connector with the wallet provider (the // connector reads the tron injectedId from it). expect(tronConnect).toHaveBeenCalledWith(tronNetwork, multiNsProvider) // EVM address preserved AND the Tron address merged in. expect(result.address).toBe('0xeth') expect(result.availableAddresses).toContainEqual({ address: 'TTronAddr', networkId: 'tron:0x2b6653dc' }) }) it('swallows a declined/failed Tron connect and keeps the EVM connection', async () => { const tronConnect = vi .fn() .mockRejectedValue(new Error('user declined')) const c = await buildWithTron(tronConnect) const result = await c.connect(mockNetwork, multiNsProvider) expect(tronConnect).toHaveBeenCalled() expect(result.address).toBe('0xeth') expect( result.availableAddresses.some(a => a.networkId.startsWith('tron:')) ).toBe(false) }) it('still runs the eager Tron gather when framed (bridge-aware connector, ONC-3383 follow-up)', async () => { const tronConnect = vi.fn().mockResolvedValue({ networkId: 'tron:0x2b6653dc', address: 'TTronAddr', availableAddresses: [ { address: 'TTronAddr', networkId: 'tron:0x2b6653dc' } ] }) const c = await buildWithTron(tronConnect) // Simulate an iframe embed: window.top !== window.self. The Tron connector is // bridge-aware (#176) — when a BridgeParent is present it routes the connect // and its verification signature to the TOP frame, satisfying Trust Wallet's // frame-position gate. So the eager Tron gather must NOT be skipped when // framed (mirrors the framed Solana gather); a multi-namespace wallet // connected on EVM must still surface its Tron address. Object.defineProperty(window, 'top', { value: {}, configurable: true }) try { const result = await c.connect(mockNetwork, multiNsProvider) expect(tronConnect).toHaveBeenCalledWith(tronNetwork, multiNsProvider) expect(result.address).toBe('0xeth') expect(result.availableAddresses).toContainEqual({ address: 'TTronAddr', networkId: 'tron:0x2b6653dc' }) } finally { Object.defineProperty(window, 'top', { value: window, configurable: true }) } }) }) it('should connect to Solana wallet for solana namespace', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const solanaNetwork: Network = { id: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', internalId: 1, name: 'Solana Mainnet', namespace: 'solana', logoUrl: '', networkType: 'solana', nativeCurrency: { name: 'SOL', symbol: 'SOL', decimals: 9 } } mockConnManager.checkExistingConnections.mockResolvedValue({ address: null, availableAddresses: [] }) mockConnManager.connectSolana.mockResolvedValue({ networkId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', address: 'SolanaAddress123', availableAddresses: [ { address: 'SolanaAddress123', networkId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' } ] }) const result = await connector.connect(solanaNetwork, mockEthProvider) expect(mockConnManager.connectSolana).toHaveBeenCalledWith( solanaNetwork, mockEthProvider ) expect(result.address).toBe('SolanaAddress123') }) it('should connect to Tron wallet for tron namespace', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const tronNetwork: Network = { id: 'tron:0x2b6653dc', internalId: 1, name: 'Tron', namespace: 'tron', logoUrl: '', networkType: 'tron', nativeCurrency: { name: 'TRX', symbol: 'TRX', decimals: 6 } } mockConnManager.checkExistingConnections.mockResolvedValue({ address: null, availableAddresses: [] }) mockConnManager.connectTron.mockResolvedValue({ networkId: 'tron:0x2b6653dc', address: 'TRonAddress123', availableAddresses: [ { address: 'TRonAddress123', networkId: 'tron:0x2b6653dc' } ] }) const result = await connector.connect(tronNetwork, mockEthProvider) expect(mockConnManager.connectTron).toHaveBeenCalledWith( tronNetwork, mockEthProvider ) expect(result.address).toBe('TRonAddress123') }) it('should throw error for unsupported namespace', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const unsupportedNetwork: Network = { id: 'cosmos:hub' as any, internalId: 1, name: 'Cosmos Hub', namespace: 'cosmos' as any, logoUrl: '', networkType: 'cosmos' as any, nativeCurrency: { name: 'ATOM', symbol: 'ATOM', decimals: 6 } } mockConnManager.checkExistingConnections.mockResolvedValue({ address: null, availableAddresses: [] }) await expect( connector.connect(unsupportedNetwork, mockEthProvider) ).rejects.toThrow( "Namespace 'cosmos' is not implemented yet. Only 'eip155', 'solana', 'tron', and 'tvm' are currently supported." ) }) }) describe('switchNetwork', () => { it('should delegate to connection manager', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const expectedResult = { networkId: 'eip155:137' as any, address: '0xaddress', availableAddresses: [] } mockConnManager.switchNetwork.mockResolvedValue(expectedResult) const targetNetwork = { ...mockNetwork, id: 'eip155:137' as any } const result = await connector.switchNetwork( targetNetwork, mockEthProvider ) expect(mockConnManager.switchNetwork).toHaveBeenCalledWith( targetNetwork, mockEthProvider ) expect(result).toEqual(expectedResult) }) }) describe('disconnect', () => { it('should delegate to connection manager', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value await connector.disconnect() expect(mockConnManager.disconnect).toHaveBeenCalled() }) }) describe('signMessage', () => { it('should throw error if provider is not provided', async () => { await expect( connector.signMessage('test message', null as any) ).rejects.toThrow('Provider is required for message signing') }) it('should throw error if message is empty', async () => { await expect(connector.signMessage('', mockEthProvider)).rejects.toThrow( 'Message is required for signing' ) }) it('should throw error if no active network connection', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value mockConnManager.getCurrentNetworkId.mockReturnValue(null) await expect( connector.signMessage('test message', mockEthProvider) ).rejects.toThrow('No active network connection') }) it('should sign Ethereum message successfully', async () => { const { ConnectionManager } = await import('./services/connection-manager') const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const { SignatureService } = await import('./services/signature-service') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value const mockSigService = vi.mocked(SignatureService).mock.results[0].value const mockEthProvider = { request: vi.fn() } as any mockConnManager.getCurrentNetworkId.mockReturnValue('eip155:1') mockConnManager.getCurrentAccount.mockReturnValue('0xaddress') mockEthService.getConnectedProvider.mockReturnValue(mockEthProvider) mockSigService.signEthereumMessage.mockResolvedValue('0xsignature') const result = await connector.signMessage( 'test message', mockEthProvider as any ) expect(mockSigService.signEthereumMessage).toHaveBeenCalledWith( 'test message', '0xaddress', mockEthProvider ) expect(result).toEqual({ type: 'standard', signature: '0xsignature' }) }) it('should throw error if no Ethereum address for signing', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value mockConnManager.getCurrentNetworkId.mockReturnValue('eip155:1') mockConnManager.getCurrentAccount.mockReturnValue(null) await expect( connector.signMessage('test message', mockEthProvider) ).rejects.toThrow('No active wallet address for signing') }) it('should throw error if no connected Ethereum provider', async () => { const { ConnectionManager } = await import('./services/connection-manager') const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value mockConnManager.getCurrentNetworkId.mockReturnValue('eip155:1') mockConnManager.getCurrentAccount.mockReturnValue('0xaddress') mockEthService.getConnectedProvider.mockReturnValue(null) await expect( connector.signMessage('test message', mockEthProvider) ).rejects.toThrow('No connected Ethereum provider') }) it('should sign Solana message successfully', async () => { const { ConnectionManager } = await import('./services/connection-manager') const { SolanaWalletService } = await import('./services/solana/solana-wallet-service') const { SignatureService } = await import('./services/signature-service') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const mockSolService = vi.mocked(SolanaWalletService).mock.results[0].value const mockSigService = vi.mocked(SignatureService).mock.results[0].value const mockSolanaAdapter = { signMessage: vi.fn() } as any mockConnManager.getCurrentNetworkId.mockReturnValue('solana:mainnet') mockSolService.getConnectedAdapter.mockReturnValue(mockSolanaAdapter) mockSigService.signSolanaMessage.mockResolvedValue('solana-signature') const result = await connector.signMessage( 'test message', mockEthProvider ) expect(mockSigService.signSolanaMessage).toHaveBeenCalledWith( 'test message', mockSolanaAdapter ) expect(result).toEqual({ type: 'standard', signature: 'solana-signature' }) }) it('should throw error if no connected Solana adapter', async () => { const { ConnectionManager } = await import('./services/connection-manager') const { SolanaWalletService } = await import('./services/solana/solana-wallet-service') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const mockSolService = vi.mocked(SolanaWalletService).mock.results[0].value mockConnManager.getCurrentNetworkId.mockReturnValue('solana:mainnet') mockSolService.getConnectedAdapter.mockReturnValue(null) await expect( connector.signMessage('test message', mockEthProvider) ).rejects.toThrow('No connected Solana adapter') }) it('should throw error for unsupported namespace', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value mockConnManager.getCurrentNetworkId.mockReturnValue('cosmos:hub') await expect( connector.signMessage('test message', mockEthProvider) ).rejects.toThrow('Signing not supported for namespace: cosmos') }) }) describe('signTypedData', () => { const typedData: EIP712TypedData = { 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: '0', validAfter: '0', validBefore: '9999999999', nonce: '0xdeadbeef' } } it('should throw error if no active network connection', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value mockConnManager.getCurrentNetworkId.mockReturnValue(null) await expect(connector.signTypedData(typedData)).rejects.toThrow( 'No active network connection' ) }) it('should throw error for non-EVM (non-eip155) namespace', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value mockConnManager.getCurrentNetworkId.mockReturnValue('solana:mainnet') await expect(connector.signTypedData(typedData)).rejects.toThrow( 'signTypedData is only supported for EVM wallets, got: solana' ) }) it('should throw error if no active wallet address', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value mockConnManager.getCurrentNetworkId.mockReturnValue('eip155:1') mockConnManager.getCurrentAccount.mockReturnValue(null) await expect(connector.signTypedData(typedData)).rejects.toThrow( 'No active wallet address for signing' ) }) it('should throw error if no connected Ethereum provider', async () => { const { ConnectionManager } = await import('./services/connection-manager') const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value mockConnManager.getCurrentNetworkId.mockReturnValue('eip155:1') mockConnManager.getCurrentAccount.mockReturnValue('0xaddress') mockEthService.getConnectedProvider.mockReturnValue(null) await expect(connector.signTypedData(typedData)).rejects.toThrow( 'No connected Ethereum provider' ) }) it('should sign typed data and return the raw signature', async () => { const { ConnectionManager } = await import('./services/connection-manager') const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const { SignatureService } = await import('./services/signature-service') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value const mockSigService = vi.mocked(SignatureService).mock.results[0].value const mockProvider = { request: vi.fn() } as any mockConnManager.getCurrentNetworkId.mockReturnValue('eip155:1') mockConnManager.getCurrentAccount.mockReturnValue('0xaddress') mockEthService.getConnectedProvider.mockReturnValue(mockProvider) mockSigService.signEthereumTypedData.mockResolvedValue('0xtypedsig') const result = await connector.signTypedData(typedData) // Connector returns the raw hex string, not a wrapped SignatureType. expect(mockSigService.signEthereumTypedData).toHaveBeenCalledWith( typedData, '0xaddress', mockProvider ) expect(result).toBe('0xtypedsig') }) }) describe('sendTransaction', () => { const mockTxRequest: TransactionRequest = { to: '0xrecipient', amount: 1000n, from: '0xsender' } it('should throw error if provider is not provided', async () => { await expect( connector.sendTransaction(mockTxRequest, null as any) ).rejects.toThrow('Provider is required for sending transactions') }) it('should throw error if no active network connection', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value mockConnManager.getCurrentNetworkId.mockReturnValue(null) await expect( connector.sendTransaction(mockTxRequest, mockEthProvider) ).rejects.toThrow('No active network connection') }) it('should send Ethereum transaction successfully', async () => { const { ConnectionManager } = await import('./services/connection-manager') const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const { TransactionService } = await import('./services/transaction-service') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value const mockTxService = vi.mocked(TransactionService).mock.results[0].value const mockEthProvider = { request: vi.fn() } as any const expectedResult = { hash: '0xtxhash' } mockConnManager.getCurrentNetworkId.mockReturnValue('eip155:1') mockEthService.getConnectedProvider.mockReturnValue(mockEthProvider) mockTxService.sendTransaction.mockResolvedValue(expectedResult as any) const result = await connector.sendTransaction( mockTxRequest, mockEthProvider as any ) expect(mockTxService.sendTransaction).toHaveBeenCalledWith( mockTxRequest, 'eip155', mockEthProvider ) expect(result).toEqual(expectedResult) }) it('should throw error if no connected Ethereum provider', async () => { const { ConnectionManager } = await import('./services/connection-manager') const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value mockConnManager.getCurrentNetworkId.mockReturnValue('eip155:1') mockEthService.getConnectedProvider.mockReturnValue(null) await expect( connector.sendTransaction(mockTxRequest, mockEthProvider) ).rejects.toThrow('No connected Ethereum provider') }) it('should send Solana transaction successfully', async () => { const { ConnectionManager } = await import('./services/connection-manager') const { SolanaWalletService } = await import('./services/solana/solana-wallet-service') const { TransactionService } = await import('./services/transaction-service') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const mockSolService = vi.mocked(SolanaWalletService).mock.results[0].value const mockTxService = vi.mocked(TransactionService).mock.results[0].value const mockSolanaAdapter = { sendTransaction: vi.fn() } as any const expectedResult = { hash: 'solana-tx-hash' } mockConnManager.getCurrentNetworkId.mockReturnValue('solana:mainnet') mockSolService.getConnectedAdapter.mockReturnValue(mockSolanaAdapter) mockTxService.sendTransaction.mockResolvedValue(expectedResult as any) const result = await connector.sendTransaction( mockTxRequest, mockEthProvider ) expect(mockTxService.sendTransaction).toHaveBeenCalledWith( mockTxRequest, 'solana', mockSolanaAdapter, 'solana:mainnet' ) expect(result).toEqual(expectedResult) }) it('should throw error if no connected Solana adapter', async () => { const { ConnectionManager } = await import('./services/connection-manager') const { SolanaWalletService } = await import('./services/solana/solana-wallet-service') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const mockSolService = vi.mocked(SolanaWalletService).mock.results[0].value mockConnManager.getCurrentNetworkId.mockReturnValue('solana:mainnet') mockSolService.getConnectedAdapter.mockReturnValue(null) await expect( connector.sendTransaction(mockTxRequest, mockEthProvider) ).rejects.toThrow('No connected Solana adapter') }) it('should throw error for unsupported namespace', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value mockConnManager.getCurrentNetworkId.mockReturnValue('cosmos:hub') await expect( connector.sendTransaction(mockTxRequest, mockEthProvider) ).rejects.toThrow('Transactions not supported for namespace: cosmos') }) }) describe('getWalletCapabilities', () => { it('should delegate to ethereum service for eip155 namespace', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value const mockCapabilities = { 'eip155:1': { paymasterService: { status: 'supported' as const } } } mockEthService.getCapabilities.mockResolvedValue(mockCapabilities) mockConnManager.getCurrentNetworkId.mockReturnValue('eip155:1') const networks = [mockNetwork] const result = await connector.getWalletCapabilities( '0xaddress', networks ) expect(mockEthService.getCapabilities).toHaveBeenCalledWith( '0xaddress', networks ) expect(result).toEqual(mockCapabilities) }) it('should NOT call ethereum service for solana namespace', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value mockConnManager.getCurrentNetworkId.mockReturnValue( 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' ) const result = await connector.getWalletCapabilities( '5SDmgvJWAeu8Pcp1WRV9uyeiRRrFC654n3vGPSuLEdhm', [mockNetwork] ) expect(mockEthService.getCapabilities).not.toHaveBeenCalled() expect(result).toEqual({}) }) it('should NOT call ethereum service for tron namespace', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value mockConnManager.getCurrentNetworkId.mockReturnValue('tron:0x2b6653dc') const result = await connector.getWalletCapabilities('TRonAddress123', [ mockNetwork ]) expect(mockEthService.getCapabilities).not.toHaveBeenCalled() expect(result).toEqual({}) }) it('should return empty when no active network', async () => { const { ConnectionManager } = await import('./services/connection-manager') const mockConnManager = vi.mocked(ConnectionManager).mock.results[0].value const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const mockEthService = vi.mocked(EthereumWalletService).mock.results[0] .value mockConnManager.getCurrentNetworkId.mockReturnValue(null) const result = await connector.getWalletCapabilities('0xaddress', [ mockNetwork ]) expect(mockEthService.getCapabilities).not.toHaveBeenCalled() expect(result).toEqual({}) }) }) describe('onLifecycleEvent routing', () => { // The core passes an explicit context naming the session it is attaching // for — the connector must never infer it from ambient state. The // secondary-namespace gather can leave MORE than one service holding a live // surface (e.g. a Solana-primary Phantom session with an authorized EVM side // sets the Ethereum service's connectedProvider too), so lifecycle follows // context.namespace, never a fixed service preference. const getMocks = async () => { const { EthereumWalletService } = await import('./services/ethereum/ethereum-wallet-service') const { SolanaWalletService } = await import('./services/solana/solana-wallet-service') return { ethService: vi.mocked(EthereumWalletService).mock.results[0].value, solService: vi.mocked(SolanaWalletService).mock.results[0].value } } it('routes a solana session to the Solana service even when the EVM side is live', async () => { const { ethService, solService } = await getMocks() const ethUnsub = vi.fn() const solUnsub = vi.fn() ethService.onLifecycleEvent.mockReturnValue(ethUnsub) solService.onLifecycleEvent.mockReturnValue(solUnsub) const listener = vi.fn() const teardown = connector.onLifecycleEvent(listener, { connectionMode: 'injected', namespace: 'solana', walletId: 'phantom' }) expect(solService.onLifecycleEvent).toHaveBeenCalledWith(listener) expect(ethService.onLifecycleEvent).not.toHaveBeenCalled() expect(teardown).toBe(solUnsub) }) it('routes an eip155 session to the Ethereum service', async () => { const { ethService, solService } = await getMocks() const ethUnsub = vi.fn() ethService.onLifecycleEvent.mockReturnValue(ethUnsub) const listener = vi.fn() const teardown = connector.onLifecycleEvent(listener, { connectionMode: 'injected', namespace: 'eip155' }) expect(ethService.onLifecycleEvent).toHaveBeenCalledWith(listener) expect(solService.onLifecycleEvent).not.toHaveBeenCalled() expect(teardown).toBe(ethUnsub) }) it('returns undefined for a tron session even when the EVM side is live', async () => { // TRON injected lifecycle isn't wired yet; attaching the live EVM provider // instead would mislabel EVM events with the TRON session's routing. const { ethService, solService } = await getMocks() ethService.onLifecycleEvent.mockReturnValue(vi.fn()) expect( connector.onLifecycleEvent(vi.fn(), { connectionMode: 'injected', namespace: 'tron' }) ).toBeUndefined() expect(ethService.onLifecycleEvent).not.toHaveBeenCalled() expect(solService.onLifecycleEvent).not.toHaveBeenCalled() }) it('returns undefined for a tvm session (TON injected lifecycle not wired)', async () => { const { ethService } = await getMocks() ethService.onLifecycleEvent.mockReturnValue(vi.fn()) expect( connector.onLifecycleEvent(vi.fn(), { connectionMode: 'injected', namespace: 'tvm' }) ).toBeUndefined() expect(ethService.onLifecycleEvent).not.toHaveBeenCalled() }) it('returns undefined when the context names no namespace', async () => { const { ethService, solService } = await getMocks() expect( connector.onLifecycleEvent(vi.fn(), { connectionMode: 'injected' }) ).toBeUndefined() expect(ethService.onLifecycleEvent).not.toHaveBeenCalled() expect(solService.onLifecycleEvent).not.toHaveBeenCalled() }) }) })