import { describe, it, expect, vi, beforeEach } from 'vitest' const { mockSendTransaction, mockSignData, MockUserRejectsError } = vi.hoisted( () => { const mockSendTransaction = vi.fn() const mockSignData = vi.fn() class MockUserRejectsError extends Error { constructor(message?: string) { super(message) Object.setPrototypeOf(this, MockUserRejectsError.prototype) } } return { mockSendTransaction, mockSignData, MockUserRejectsError } } ) vi.mock('@meshconnect/uwc-ton-connector', async () => { const actual = await vi.importActual< typeof import('@meshconnect/uwc-ton-connector') >('@meshconnect/uwc-ton-connector') return { ...actual, bocToHash: async (boc: string) => `hash_of_${boc}` } }) import { TonTransactionService } from './ton-transaction-service' function createMockSdk(overrides?: { account?: { address: string } | null signData?: typeof mockSignData | undefined }) { return { account: overrides?.account ?? { address: '0:abcdef1234567890' }, sendTransaction: mockSendTransaction, signData: overrides && 'signData' in overrides ? overrides.signData : mockSignData } as any } describe('TonTransactionService', () => { let service: TonTransactionService beforeEach(() => { vi.clearAllMocks() service = new TonTransactionService() }) describe('sendTransaction', () => { it('builds tx, sends via sdk, and returns hash from BOC', async () => { const sdk = createMockSdk() mockSendTransaction.mockResolvedValue('te6cckEBAQEA...') const result = await service.sendTransaction( { to: 'UQBxyz', amount: '1000000000', from: 'UQBabc' }, sdk ) expect(mockSendTransaction).toHaveBeenCalledWith( expect.objectContaining({ network: '-239', from: '0:abcdef1234567890', messages: [{ address: 'UQBxyz', amount: '1000000000' }] }) ) expect(result).toBe('hash_of_te6cckEBAQEA...') }) it('uses sdk.account.address as sender raw address', async () => { const sdk = createMockSdk({ account: { address: '0:sender_raw_address' } }) mockSendTransaction.mockResolvedValue('te6cckEBAQEA...') await service.sendTransaction( { to: 'UQBxyz', amount: '500000000', from: 'UQBfriendly' }, sdk ) expect(mockSendTransaction).toHaveBeenCalledWith( expect.objectContaining({ from: '0:sender_raw_address' }) ) }) it('throws rejected WalletConnectorError for UserRejectsError', async () => { const sdk = createMockSdk() mockSendTransaction.mockRejectedValue( new MockUserRejectsError( '[TON_CONNECT_SDK_ERROR] UserRejectsError\nUser rejected the transaction' ) ) try { await service.sendTransaction( { to: 'UQBxyz', amount: '1000000000', from: 'UQBabc' }, sdk, MockUserRejectsError ) expect.unreachable('should have thrown') } catch (error) { expect(error).toHaveProperty('type', 'rejected') expect(error).toHaveProperty('message', 'User rejected the transaction') } }) it('extracts clean message from SDK error', async () => { const sdk = createMockSdk() mockSendTransaction.mockRejectedValue( new Error( '[TON_CONNECT_SDK_ERROR] BadRequestError\nInvalid address format' ) ) try { await service.sendTransaction( { to: 'UQBxyz', amount: '1000000000', from: 'UQBabc' }, sdk ) expect.unreachable('should have thrown') } catch (error) { expect(error).toHaveProperty('type', 'unknown') expect(error).toHaveProperty('message', 'Invalid address format') } }) it('does not produce [object Object] when error has no message', async () => { const sdk = createMockSdk() mockSendTransaction.mockRejectedValue({ code: 999 }) try { await service.sendTransaction( { to: 'UQBxyz', amount: '1000000000', from: 'UQBabc' }, sdk ) expect.unreachable('should have thrown') } catch (error) { expect(error).toHaveProperty('type', 'unknown') expect((error as Error).message).not.toContain('[object Object]') expect((error as Error).message.length).toBeGreaterThan(0) } }) it('uses fallback message when error is completely empty', async () => { const sdk = createMockSdk() mockSendTransaction.mockRejectedValue(null) try { await service.sendTransaction( { to: 'UQBxyz', amount: '1000000000', from: 'UQBabc' }, sdk ) expect.unreachable('should have thrown') } catch (error) { expect(error).toHaveProperty('message', 'TON wallet operation failed') } }) }) describe('signMessage', () => { it('throws when sdk.signData is not a function', async () => { const sdk = createMockSdk({ signData: undefined }) await expect(service.signMessage('hello', sdk)).rejects.toThrow( 'does not support message signing' ) }) it('calls signData and returns typed signature', async () => { const sdk = createMockSdk() mockSignData.mockResolvedValue({ signature: 'sig123', timestamp: 1700000000, domain: 'example.com' }) const result = await service.signMessage('test message', sdk) expect(mockSignData).toHaveBeenCalledWith({ type: 'text', text: 'test message' }) expect(result).toEqual({ type: 'tvm', signature: 'sig123', timestamp: 1700000000, domain: 'example.com', payload: { type: 'text', text: 'test message' } }) }) it('accepts a payload override', async () => { const sdk = createMockSdk() mockSignData.mockResolvedValue({ signature: 'sig456', timestamp: 1700000000, domain: 'example.com' }) const customPayload = { type: 'binary' as const, bytes: 'AQID' } const result = await service.signMessage( 'ignored', sdk, null, customPayload ) expect(mockSignData).toHaveBeenCalledWith(customPayload) expect(result.payload).toEqual(customPayload) }) it('throws rejected WalletConnectorError for UserRejectsError', async () => { const sdk = createMockSdk() mockSignData.mockRejectedValue( new MockUserRejectsError('User rejected signing') ) try { await service.signMessage('hello', sdk, MockUserRejectsError) expect.unreachable('should have thrown') } catch (error) { expect(error).toHaveProperty('type', 'rejected') expect(error).toHaveProperty('message', 'User rejected signing') } }) }) })