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
4 changes: 2 additions & 2 deletions src/contract/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export async function readContractState(
const rpcUrl = SOROBAN_RPC_URLS[client.network];
const server = new rpc.Server(rpcUrl);
const contract = new Contract(client.contractId);
const _operation = contract.call(method, ...(args as any[]));
contract.call(method, ...(args as any[]));
const result = await server.simulateTransaction({
toEnvelope: () => ({ toXDR: () => '' }) as FakeEnvelope,
} as rpc.Api.Transaction);
} as any);
return result;
}
2 changes: 1 addition & 1 deletion src/contract/simulate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function simulateContractCall(
try {
const result = await server.simulateTransaction({
toEnvelope: () => ({ toXDR: () => xdr }) as FakeEnvelope,
} as rpc.Api.Transaction);
} as any);
if (rpc.Api.isSimulationError(result)) {
return { success: false, cost: { cpuInsns: '0', memBytes: '0' }, error: result.error };
}
Expand Down
3 changes: 3 additions & 0 deletions src/escrow/dispute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DisputeParams, SDKResult } from '../types/index';
import { TrustFlowError } from '../errors';
import { buildDisputeArgs } from '../contract/build';
import { createApiHttpClient, toApiErrorMessage } from '../utils/http';
import { logger } from '../utils/logger';

/**
* Raises a dispute directly against the TrustFlow contract.
Expand Down Expand Up @@ -67,6 +68,7 @@ export class DisputeClient {
const data = response.data;
return { ok: true, data: { disputeId: data.id } };
} catch (e) {
logger.error('Failed to raise dispute', e);
return { ok: false, error: toApiErrorMessage(e) };
}
}
Expand All @@ -81,6 +83,7 @@ export class DisputeClient {
const response = await this.http.get<unknown>(`/disputes/${escrowId}`);
return { ok: true, data: response.data };
} catch (e) {
logger.error(`Failed to get dispute for escrow ${escrowId}`, e);
return { ok: false, error: toApiErrorMessage(e) };
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/escrow/monitor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TrustFlowEvent, EventHandler } from '../types/events';
import { logger } from '../utils/logger';

export class EscrowMonitor {
private handlers = new Map<string, Set<EventHandler>>();
Expand All @@ -24,7 +25,7 @@ export class EscrowMonitor {
const handlers = this.handlers.get(event.type) ?? new Set();
const wildcards = this.handlers.get('*') ?? new Set();
[...handlers, ...wildcards].forEach((h) => {
Promise.resolve(h(event)).catch(console.error);
Promise.resolve(h(event)).catch((err) => logger.error(String(err)));
});
}
}, intervalMs);
Expand Down
182 changes: 182 additions & 0 deletions tests/contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { rpc, Contract } from '@stellar/stellar-sdk';
import { buildCreateEscrowArgs, buildReleaseArgs, buildDisputeArgs } from '../src/contract/build';
import { invokeContract } from '../src/contract/invoke';
import { readContractState } from '../src/contract/read';
import { simulateContractCall } from '../src/contract/simulate';
import type { 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(),
Api: {
...original.rpc.Api,
isSimulationError: jest.fn(),
},
assembleTransaction: jest.fn(),
},
Contract: jest.fn(),
Address: jest.fn().mockImplementation(() => ({
toScVal: jest.fn().mockReturnValue('mock_scval')
})),
TransactionBuilder: jest.fn().mockImplementation(() => ({
addOperation: jest.fn().mockReturnThis(),
setTimeout: jest.fn().mockReturnThis(),
build: jest.fn().mockReturnValue('mock_tx'),
})),
BASE_FEE: '100',
};
});

describe('contract module', () => {
const mockClient = {
network: 'testnet',
contractId: 'C...',
getNetworkPassphrase: jest.fn().mockReturnValue('Test SDF Network ; September 2015'),
} as unknown as TrustFlowClient;

beforeEach(() => {
jest.clearAllMocks();
});

describe('build.ts', () => {
it('buildCreateEscrowArgs returns valid arguments', () => {
const args = buildCreateEscrowArgs({
sender: 'GBM...',
recipient: 'GBA...',
amountStroops: 1000n,
durationBlocks: 100,
});
expect(args.length).toBe(4);
});

it('buildReleaseArgs returns valid arguments', () => {
const args = buildReleaseArgs('escrow1', 'GBM...');
expect(args.length).toBe(2);
});

it('buildDisputeArgs returns valid arguments', () => {
const args = buildDisputeArgs('escrow1', 'fraud');
expect(args.length).toBe(2);
});
});

describe('invoke.ts', () => {
it('returns error if simulation fails', async () => {
(rpc.Api.isSimulationError as unknown as jest.Mock).mockReturnValue(true);
const mockServer = {
getAccount: jest.fn().mockResolvedValue({ accountId: () => 'GBM...', sequenceNumber: () => '1' }),
simulateTransaction: jest.fn().mockResolvedValue({ error: 'sim error' }),
};
(rpc.Server as jest.Mock).mockImplementation(() => mockServer);

const mockContract = {
call: jest.fn().mockReturnValue({}),
};
(Contract as jest.Mock).mockImplementation(() => mockContract);

const result = await invokeContract(mockClient, 'release', [], 'GBM...');
expect(result.success).toBe(false);
});

it('returns success if simulation succeeds and no signAndSubmit provided', async () => {
(rpc.Api.isSimulationError as unknown as jest.Mock).mockReturnValue(false);
const mockServer = {
getAccount: jest.fn().mockResolvedValue({ accountId: () => 'GBM...', sequenceNumber: () => '1' }),
simulateTransaction: jest.fn().mockResolvedValue({ result: { retval: 'value' } }),
};
(rpc.Server as jest.Mock).mockImplementation(() => mockServer);

const mockContract = {
call: jest.fn().mockReturnValue({}),
};
(Contract as jest.Mock).mockImplementation(() => mockContract);

const result = await invokeContract(mockClient, 'release', [], 'GBM...');
expect(result.success).toBe(true);
// @ts-ignore
expect(result.returnValue).toBe('value');
});

it('signs and submits transaction if signAndSubmit is provided', async () => {
(rpc.Api.isSimulationError as unknown as jest.Mock).mockReturnValue(false);
const mockServer = {
getAccount: jest.fn().mockResolvedValue({ accountId: () => 'GBM...', sequenceNumber: () => '1' }),
simulateTransaction: jest.fn().mockResolvedValue({ result: { retval: 'value' } }),
};
(rpc.Server as jest.Mock).mockImplementation(() => mockServer);

const mockContract = {
call: jest.fn().mockReturnValue({}),
};
(Contract as jest.Mock).mockImplementation(() => mockContract);

(rpc.assembleTransaction as jest.Mock).mockReturnValue({
build: jest.fn().mockReturnValue({ toXDR: () => 'xdr_string' }),
});

const signAndSubmit = jest.fn().mockResolvedValue('tx_hash');

const result = await invokeContract(mockClient, 'release', [], 'GBM...', signAndSubmit);
expect(result.success).toBe(true);
// @ts-ignore
expect(result.txHash).toBe('tx_hash');
expect(signAndSubmit).toHaveBeenCalledWith('xdr_string');
});
});

describe('read.ts', () => {
it('readContractState calls simulateTransaction', async () => {
const mockServer = {
simulateTransaction: jest.fn().mockResolvedValue('read_result'),
};
(rpc.Server as jest.Mock).mockImplementation(() => mockServer);

const mockContract = {
call: jest.fn().mockReturnValue({}),
};
(Contract as jest.Mock).mockImplementation(() => mockContract);

const result = await readContractState(mockClient, 'get_state');
expect(result).toBe('read_result');
});
});

describe('simulate.ts', () => {
it('returns failure object on simulation error', async () => {
(rpc.Api.isSimulationError as unknown as jest.Mock).mockReturnValue(true);
const mockServer = {
simulateTransaction: jest.fn().mockResolvedValue({ error: 'failed' }),
};
(rpc.Server as jest.Mock).mockImplementation(() => mockServer);

const result = await simulateContractCall(mockClient, 'xdr_string');
expect(result.success).toBe(false);
expect(result.error).toBe('failed');
});

it('returns success on valid simulation', async () => {
(rpc.Api.isSimulationError as unknown as jest.Mock).mockReturnValue(false);
const mockServer = {
simulateTransaction: jest.fn().mockResolvedValue({}),
};
(rpc.Server as jest.Mock).mockImplementation(() => mockServer);

const result = await simulateContractCall(mockClient, 'xdr_string');
expect(result.success).toBe(true);
});

it('throws TrustFlowError on internal exception', async () => {
const mockServer = {
simulateTransaction: jest.fn().mockRejectedValue(new Error('Network error')),
};
(rpc.Server as jest.Mock).mockImplementation(() => mockServer);

await expect(simulateContractCall(mockClient, 'xdr_string')).rejects.toThrow(TrustFlowError);
});
});
});
145 changes: 0 additions & 145 deletions tests/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,151 +9,6 @@ describe('format', () => {
it('truncates long address', () => { expect(truncateAddress('GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ')).toContain('...'); });
});

describe('SimpleCache', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});

it('stores and retrieves values', () => {
const cache = new SimpleCache<string, number>(1000);
cache.set('key', 42);
expect(cache.get('key')).toBe(42);
});

it('uses default TTL when no custom TTL is provided', () => {
const cache = new SimpleCache<string, number>(1000);
cache.set('key', 42);

jest.advanceTimersByTime(500);
expect(cache.get('key')).toBe(42);

jest.advanceTimersByTime(600);
expect(cache.get('key')).toBeUndefined();
});

it('uses custom TTL when provided', () => {
const cache = new SimpleCache<string, number>(1000);
cache.set('key', 42, 500);

jest.advanceTimersByTime(400);
expect(cache.get('key')).toBe(42);

jest.advanceTimersByTime(150);
expect(cache.get('key')).toBeUndefined();
});

it('lazily evicts expired entries on get', () => {
const cache = new SimpleCache<string, number>(1000);
cache.set('key', 42);
expect(cache.size()).toBe(1);

jest.advanceTimersByTime(1100);
expect(cache.get('key')).toBeUndefined();
expect(cache.size()).toBe(0);
});

it('deletes keys explicitly', () => {
const cache = new SimpleCache<string, number>(1000);
cache.set('key1', 1);
cache.set('key2', 2);
expect(cache.size()).toBe(2);

cache.delete('key1');
expect(cache.get('key1')).toBeUndefined();
expect(cache.get('key2')).toBe(2);
expect(cache.size()).toBe(1);
});

it('clears all entries', () => {
const cache = new SimpleCache<string, number>(1000);
cache.set('key1', 1);
cache.set('key2', 2);
cache.set('key3', 3);
expect(cache.size()).toBe(3);

cache.clear();
expect(cache.size()).toBe(0);
expect(cache.get('key1')).toBeUndefined();
expect(cache.get('key2')).toBeUndefined();
expect(cache.get('key3')).toBeUndefined();
});

it('returns size correctly', () => {
const cache = new SimpleCache<string, number>(1000);
expect(cache.size()).toBe(0);

cache.set('a', 1);
expect(cache.size()).toBe(1);

cache.set('b', 2);
cache.set('c', 3);
expect(cache.size()).toBe(3);

cache.delete('b');
expect(cache.size()).toBe(2);
});

it('handles multiple keys with different expiry times', () => {
const cache = new SimpleCache<string, number>(1000);
cache.set('fast', 1, 100);
cache.set('medium', 2, 500);
cache.set('slow', 3, 1000);

jest.advanceTimersByTime(150);
expect(cache.get('fast')).toBeUndefined();
expect(cache.get('medium')).toBe(2);
expect(cache.get('slow')).toBe(3);

jest.advanceTimersByTime(400);
expect(cache.get('medium')).toBeUndefined();
expect(cache.get('slow')).toBe(3);

jest.advanceTimersByTime(550);
expect(cache.get('slow')).toBeUndefined();
});

it('allows re-setting expired keys', () => {
const cache = new SimpleCache<string, number>(1000);
cache.set('key', 1);

jest.advanceTimersByTime(1100);
expect(cache.get('key')).toBeUndefined();

cache.set('key', 2, 500);
expect(cache.get('key')).toBe(2);

jest.advanceTimersByTime(300);
expect(cache.get('key')).toBe(2);

jest.advanceTimersByTime(250);
expect(cache.get('key')).toBeUndefined();
});

it('handles generic types correctly', () => {
const stringCache = new SimpleCache<string, string>(1000);
stringCache.set('greeting', 'hello');
expect(stringCache.get('greeting')).toBe('hello');

const objectCache = new SimpleCache<string, { id: number; name: string }>(1000);
const obj = { id: 42, name: 'test' };
objectCache.set('data', obj);
expect(objectCache.get('data')).toEqual(obj);
});

it('expires at exact boundary', () => {
const cache = new SimpleCache<string, number>(1000);
cache.set('key', 42);

// At exactly the expiry time (1000ms), should be expired since Date.now() > entry.expiresAt
jest.advanceTimersByTime(1000);
expect(cache.get('key')).toBeUndefined();
});
});

describe('retry', () => {
it('resolves on first success', async () => {
Expand Down
Loading