diff --git a/src/contract/read.ts b/src/contract/read.ts index 329ba84..51ba01b 100644 --- a/src/contract/read.ts +++ b/src/contract/read.ts @@ -1,10 +1,7 @@ -import { rpc, Contract } from '@stellar/stellar-sdk'; +import { rpc, Contract, Account, TransactionBuilder, BASE_FEE, scValToNative } from '@stellar/stellar-sdk'; import { SOROBAN_RPC_URLS } from '../constants'; import type { TrustFlowClient } from '../client'; - -interface FakeEnvelope { - toXDR(): string; -} +import { TrustFlowError } from '../errors'; export async function readContractState( client: TrustFlowClient, @@ -14,9 +11,28 @@ export async function readContractState( const rpcUrl = SOROBAN_RPC_URLS[client.network]; const server = new rpc.Server(rpcUrl); const contract = new Contract(client.contractId); - contract.call(method, ...(args as any[])); - const result = await server.simulateTransaction({ - toEnvelope: () => ({ toXDR: () => '' }) as FakeEnvelope, - } as any); - return result; + const operation = contract.call(method, ...(args as any[])); + + // Use a dummy account for simulation + const dummyAccount = new Account( + 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF', + '0' + ); + + const tx = new TransactionBuilder(dummyAccount, { + fee: BASE_FEE, + networkPassphrase: client.getNetworkPassphrase(), + }) + .addOperation(operation) + .setTimeout(30) + .build(); + + const result = await server.simulateTransaction(tx); + + if (rpc.Api.isSimulationError(result as any)) { + throw new TrustFlowError('Read simulation failed', 'SIMULATION_ERROR'); + } + + const retval = (result as any).result?.retval; + return retval ? scValToNative(retval) : undefined; } diff --git a/src/escrow/dispute.ts b/src/escrow/dispute.ts index a70644a..f1526e8 100644 --- a/src/escrow/dispute.ts +++ b/src/escrow/dispute.ts @@ -36,18 +36,30 @@ export async function disputeEscrow( return `tx_dispute_${params.escrowId}_${Date.now()}`; } +import type { ContractConfig } from '../types/contract'; + export interface DisputeClientOptions { timeoutMs?: number; } export class DisputeClient { private readonly http; + private readonly apiUrl: string; + private readonly token: string; constructor( - private apiUrl: string, - private token: string, + config: ContractConfig, options: DisputeClientOptions = {}, ) { + if (!config.apiBaseUrl) { + throw new Error('apiBaseUrl is required for DisputeClient'); + } + if (!config.apiKey) { + throw new Error('apiKey is required for DisputeClient'); + } + this.apiUrl = config.apiBaseUrl; + this.token = config.apiKey; + this.http = createApiHttpClient({ baseURL: this.apiUrl, timeoutMs: options.timeoutMs, diff --git a/src/events.ts b/src/events.ts index f7a2710..c3fd6ab 100644 --- a/src/events.ts +++ b/src/events.ts @@ -22,7 +22,7 @@ export interface RawContractEvent { value: string; } -export interface ParsedEvent> { +export interface ParsedEvent { type: TrustFlowEventType; contractId: string; ledger: number; diff --git a/tests/contract-read.test.ts b/tests/contract-read.test.ts new file mode 100644 index 0000000..244176f --- /dev/null +++ b/tests/contract-read.test.ts @@ -0,0 +1,71 @@ +import { readContractState } from '../src/contract/read'; +import { rpc, Contract, TransactionBuilder } from '@stellar/stellar-sdk'; +import { TrustFlowClient } from '../src/client'; +import { TrustFlowError } from '../src/errors'; + +jest.mock('@stellar/stellar-sdk', () => { + const original = jest.requireActual('@stellar/stellar-sdk'); + return { + ...original, + rpc: { + ...original.rpc, + Server: jest.fn().mockImplementation(() => ({ + simulateTransaction: jest.fn(), + })), + Api: { + ...original.rpc.Api, + isSimulationError: jest.fn(), + }, + }, + Contract: jest.fn().mockImplementation(() => ({ + call: jest.fn().mockReturnValue('mock_operation'), + })), + Account: jest.fn().mockImplementation(() => ({})), + TransactionBuilder: jest.fn().mockImplementation(() => ({ + addOperation: jest.fn().mockReturnThis(), + setTimeout: jest.fn().mockReturnThis(), + build: jest.fn().mockReturnValue('mock_tx'), + })), + BASE_FEE: '100', + scValToNative: jest.fn().mockReturnValue('decoded_value'), + }; +}); + +describe('readContractState', () => { + let mockClient: TrustFlowClient; + + beforeEach(() => { + jest.clearAllMocks(); + mockClient = new TrustFlowClient({ + network: 'TESTNET', + contractId: 'C123', + }); + }); + + it('builds a real transaction and simulates it', async () => { + const mockServer = { + simulateTransaction: jest.fn().mockResolvedValue({ + result: { retval: 'mock_retval' } + }), + }; + (rpc.Server as jest.Mock).mockImplementation(() => mockServer); + (rpc.Api.isSimulationError as unknown as jest.Mock).mockReturnValue(false); + + const result = await readContractState(mockClient, 'get_escrow', ['esc-123']); + + expect(Contract).toHaveBeenCalledWith('C123'); + expect(TransactionBuilder).toHaveBeenCalled(); + expect(mockServer.simulateTransaction).toHaveBeenCalledWith('mock_tx'); + expect(result).toBe('decoded_value'); + }); + + it('throws an error if simulation fails', async () => { + const mockServer = { + simulateTransaction: jest.fn().mockResolvedValue({}), + }; + (rpc.Server as jest.Mock).mockImplementation(() => mockServer); + (rpc.Api.isSimulationError as unknown as jest.Mock).mockReturnValue(true); + + await expect(readContractState(mockClient, 'get_escrow')).rejects.toThrow(TrustFlowError); + }); +}); diff --git a/tests/contract.test.ts b/tests/contract.test.ts index 415ae71..e19430b 100644 --- a/tests/contract.test.ts +++ b/tests/contract.test.ts @@ -29,6 +29,7 @@ jest.mock('@stellar/stellar-sdk', () => { build: jest.fn().mockReturnValue('mock_tx'), })), BASE_FEE: '100', + scValToNative: jest.fn().mockImplementation((v: unknown) => v), }; }); @@ -131,10 +132,14 @@ describe('contract module', () => { describe('read.ts', () => { it('readContractState calls simulateTransaction', async () => { + const mockRetval = { type: 'mock' }; const mockServer = { - simulateTransaction: jest.fn().mockResolvedValue('read_result'), + simulateTransaction: jest.fn().mockResolvedValue({ + result: { retval: mockRetval }, + }), }; (rpc.Server as jest.Mock).mockImplementation(() => mockServer); + (rpc.Api.isSimulationError as unknown as jest.Mock).mockReturnValue(false); const mockContract = { call: jest.fn().mockReturnValue({}), @@ -142,7 +147,8 @@ describe('contract module', () => { (Contract as jest.Mock).mockImplementation(() => mockContract); const result = await readContractState(mockClient, 'get_state'); - expect(result).toBe('read_result'); + expect(mockServer.simulateTransaction).toHaveBeenCalled(); + expect(result).toBeDefined(); }); }); diff --git a/tests/dispute.test.ts b/tests/dispute.test.ts index d723014..d7cfb17 100644 --- a/tests/dispute.test.ts +++ b/tests/dispute.test.ts @@ -19,14 +19,14 @@ describe('DisputeClient', () => { }); it('initialises with api url and token', () => { - const client = new DisputeClient('http://api', 'tok'); + const client = new DisputeClient({ apiBaseUrl: 'http://api', apiKey: 'tok' } as any); expect(client).toBeDefined(); }); it('returns success for raiseDispute when API responds with ID', async () => { mockHttpPost.mockResolvedValueOnce({ data: { id: 'dsp-1' } }); - const client = new DisputeClient('http://api', 'tok'); + const client = new DisputeClient({ apiBaseUrl: 'http://api', apiKey: 'tok' } as any); const result = await client.raiseDispute({ escrowId: 'esc-1', reason: 'test' }); expect(result.ok).toBe(true); @@ -38,7 +38,7 @@ describe('DisputeClient', () => { it('returns error result on network failure', async () => { mockHttpPost.mockRejectedValueOnce(new Error('connection reset')); - const client = new DisputeClient('http://api', 'tok'); + const client = new DisputeClient({ apiBaseUrl: 'http://api', apiKey: 'tok' } as any); const result = await client.raiseDispute({ escrowId: 'esc-1', reason: 'test' }); expect(result.ok).toBe(false); if (!result.ok) { @@ -49,7 +49,7 @@ describe('DisputeClient', () => { it('returns dispute payload for getDispute', async () => { mockHttpGet.mockResolvedValueOnce({ data: { id: 'dsp-1', status: 'open' } }); - const client = new DisputeClient('http://api', 'tok'); + const client = new DisputeClient({ apiBaseUrl: 'http://api', apiKey: 'tok' } as any); const result = await client.getDispute('esc-1'); expect(result.ok).toBe(true); diff --git a/tests/utils.test.ts b/tests/utils.test.ts index 9719114..28efca0 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -9,7 +9,6 @@ describe('format', () => { it('truncates long address', () => { expect(truncateAddress('GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ')).toContain('...'); }); }); - describe('retry', () => { it('resolves on first success', async () => { const result = await retry(async () => 'ok', 3, 100);