Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions src/contract/read.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
}
16 changes: 14 additions & 2 deletions src/escrow/dispute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface RawContractEvent {
value: string;
}

export interface ParsedEvent<T = Record<string, unknown>> {
export interface ParsedEvent<T = unknown> {
type: TrustFlowEventType;
contractId: string;
ledger: number;
Expand Down
71 changes: 71 additions & 0 deletions tests/contract-read.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
10 changes: 8 additions & 2 deletions tests/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
});

Expand Down Expand Up @@ -131,18 +132,23 @@ 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({}),
};
(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();
});
});

Expand Down
8 changes: 4 additions & 4 deletions tests/dispute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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);
Expand Down
1 change: 0 additions & 1 deletion tests/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down