import { describe, it, expect, beforeEach, vi } from 'vitest' import { SolanaTransactionService } from './solana-transaction-service' import type { SolanaNativeTransferRequest } from '@meshconnect/uwc-types' import type { ExtendedStandardWalletAdapter } from '../../wallet-standard-discovery' vi.mock('@solana/web3.js', () => { class PublicKey { private key: string constructor(key: string) { this.key = key } toBase58() { return this.key } toBuffer() { return new Uint8Array(32) } toString() { return this.key } } class Transaction { instructions: unknown[] = [] recentBlockhash: string | null = null feePayer: unknown = null serialize() { return new Uint8Array([1, 2, 3]) } } class Connection { constructor(_rpcUrl: string) {} } return { PublicKey, Transaction, Connection } }) const mockGetVersionedTransaction = vi.fn() vi.mock('./solana-transaction-builder', () => { return { SolanaTransactionBuilder: class { buildTransferInstructions = vi.fn().mockResolvedValue([]) getVersionedTransaction = mockGetVersionedTransaction } } }) describe('SolanaTransactionService', () => { let service: SolanaTransactionService beforeEach(() => { vi.clearAllMocks() service = new SolanaTransactionService({}) }) describe('executeTransaction — bridge adapter (has sendSerializedTransaction)', () => { const nativeRequest: SolanaNativeTransferRequest = { from: '5SDmgvJWAeu8Pcp1WRV9uyeiRRrFC654n3vGPSuLEdhm', to: '2cXWFBHqJamsAsgZjZgLrDDCvszrRVdMMaaCp9gQTsWL', amount: BigInt(100000), blockhash: '3PQ5dzi2ZCTfnu5aoncPBW1LBkqGGA54xpakg4JlNF1X' } it('should use sendSerializedTransaction when available', async () => { const mockAdapter = { publicKey: { toBase58: vi.fn().mockResolvedValue(nativeRequest.from) }, sendSerializedTransaction: vi .fn() .mockResolvedValue('tx-signature-123'), sendTransaction: vi.fn() } as unknown as ExtendedStandardWalletAdapter const result = await service.sendTransaction( nativeRequest, mockAdapter, 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' ) expect(result).toBe('tx-signature-123') expect(mockAdapter.sendSerializedTransaction).toHaveBeenCalled() expect(mockAdapter.sendTransaction).not.toHaveBeenCalled() }) it('should propagate sendSerializedTransaction errors instead of falling back to sendTransaction', async () => { const walletError = new Error('Snap wallet_invokeMethod failed') const mockAdapter = { publicKey: { toBase58: vi.fn().mockResolvedValue(nativeRequest.from) }, sendSerializedTransaction: vi.fn().mockRejectedValue(walletError), sendTransaction: vi.fn() } as unknown as ExtendedStandardWalletAdapter await expect( service.sendTransaction( nativeRequest, mockAdapter, 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' ) ).rejects.toThrow() // The critical assertion: sendTransaction (non-bridge-safe path) must // NOT be called. Through a Comlink bridge proxy, passing Transaction // and Connection objects via postMessage causes DataCloneError because // they contain functions that can't be structured-cloned. expect(mockAdapter.sendTransaction).not.toHaveBeenCalled() }) it('should preserve the original wallet error message, not mask it with DataCloneError', async () => { const walletError = new Error('Unserializable return value') const mockAdapter = { publicKey: { toBase58: vi.fn().mockResolvedValue(nativeRequest.from) }, sendSerializedTransaction: vi.fn().mockRejectedValue(walletError), sendTransaction: vi.fn() } as unknown as ExtendedStandardWalletAdapter try { await service.sendTransaction( nativeRequest, mockAdapter, 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' ) expect.unreachable('should have thrown') } catch (error: unknown) { const err = error as { message: string } expect(err.message).toContain('Unserializable return value') } }) }) describe('executeTransaction — direct adapter (no sendSerializedTransaction)', () => { const nativeRequest: SolanaNativeTransferRequest = { from: '5SDmgvJWAeu8Pcp1WRV9uyeiRRrFC654n3vGPSuLEdhm', to: '2cXWFBHqJamsAsgZjZgLrDDCvszrRVdMMaaCp9gQTsWL', amount: BigInt(100000), blockhash: '3PQ5dzi2ZCTfnu5aoncPBW1LBkqGGA54xpakg4JlNF1X' } it('should use sendTransaction directly when sendSerializedTransaction is not available', async () => { const mockAdapter = { publicKey: { toBase58: vi.fn().mockResolvedValue(nativeRequest.from) }, sendTransaction: vi.fn().mockResolvedValue('tx-direct-456') } as any const result = await service.sendTransaction( nativeRequest, mockAdapter, 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' ) expect(result).toBe('tx-direct-456') expect(mockAdapter.sendTransaction).toHaveBeenCalled() }) }) describe('sendGenericTransferWithInstructions — versioned transactions', () => { const genericRequest = { feePayer: '5SDmgvJWAeu8Pcp1WRV9uyeiRRrFC654n3vGPSuLEdhm', blockhash: '3PQ5dzi2ZCTfnu5aoncPBW1LBkqGGA54xpakg4JlNF1X', instructions: [{ programId: 'abc', keys: [], data: new Uint8Array() }] } it('should use sendSerializedTransaction for versioned transactions through bridge', async () => { const mockVersionedTx = { serialize: vi.fn().mockReturnValue(new Uint8Array([4, 5, 6])) } mockGetVersionedTransaction.mockResolvedValue(mockVersionedTx) const mockAdapter = { publicKey: { toBase58: vi.fn().mockResolvedValue(genericRequest.feePayer) }, sendSerializedTransaction: vi .fn() .mockResolvedValue('versioned-tx-sig'), sendTransaction: vi.fn() } as unknown as ExtendedStandardWalletAdapter const result = await service.sendTransaction( genericRequest as any, mockAdapter, 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' ) expect(result).toBe('versioned-tx-sig') expect(mockAdapter.sendSerializedTransaction).toHaveBeenCalled() expect(mockAdapter.sendTransaction).not.toHaveBeenCalled() }) it('should propagate errors from versioned transaction path without fallback', async () => { const mockVersionedTx = { serialize: vi.fn().mockReturnValue(new Uint8Array([4, 5, 6])) } mockGetVersionedTransaction.mockResolvedValue(mockVersionedTx) const mockAdapter = { publicKey: { toBase58: vi.fn().mockResolvedValue(genericRequest.feePayer) }, sendSerializedTransaction: vi .fn() .mockRejectedValue(new Error('Snap failed')), sendTransaction: vi.fn() } as unknown as ExtendedStandardWalletAdapter await expect( service.sendTransaction( genericRequest as any, mockAdapter, 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' ) ).rejects.toThrow() expect(mockAdapter.sendTransaction).not.toHaveBeenCalled() }) }) })