Skip to content

Commit

Permalink
Simplify recording client implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
LogvinovLeon committed Mar 4, 2024
1 parent f43d75d commit d98c235
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 45 deletions.
2 changes: 1 addition & 1 deletion ethereum_history_api/oracles/src/ethereum/mockClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function createMockClient(
): Promise<PublicClient> {
const savedCalls = await readObject<Call[]>(filePath);

return mock<PublicClient>(isEthereumApiMethod, (method: string, args: object): object => {
return mock<PublicClient>(isEthereumApiMethod, (method: string, args: unknown): unknown => {
const call: Call | undefined = savedCalls.find((it) => it.method === method && isEqual(it.arguments, args));
assert(!!call, `call not found for: ${method}(${args})`);
return resultModifier(call).result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ describe('recordingClient', async () => {
it('record JSON-RPC API calls', async () => {
const client = createRecordingClient(publicClientMock);

client.getBlock(GET_BLOCK_PARAMETERS);
client.getProof(GET_PROOF_PARAMETERS);
const calls: Call[] = await client.getCalls();
await client.getBlock(GET_BLOCK_PARAMETERS);
await client.getProof(GET_PROOF_PARAMETERS);
const calls: Call[] = client.getCalls();

expect(calls).toMatchObject(EXPECTED_CALLS);
});
Expand All @@ -59,9 +59,9 @@ describe('recordingClient', async () => {
await withTempFile(async (tempFilePath) => {
const client = createRecordingClient(publicClientMock);

client.getBlock(GET_BLOCK_PARAMETERS);
client.getProof(GET_PROOF_PARAMETERS);
await writeObject(await client.getCalls(), tempFilePath);
await client.getBlock(GET_BLOCK_PARAMETERS);
await client.getProof(GET_PROOF_PARAMETERS);
await writeObject(client.getCalls(), tempFilePath);

const mockingClient: PublicClient = await createMockClient(tempFilePath);

Expand Down
56 changes: 23 additions & 33 deletions ethereum_history_api/oracles/src/ethereum/recordingClient.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,46 @@
import { PublicClient } from 'viem';

type CallWithResultPromise = {
method: string;
arguments: object[];
result: Promise<object>;
};

export type Call = {
method: string;
arguments: object[];
result: object;
arguments: unknown[];
result: unknown;
};

export type GetCalls = { getCalls: () => Promise<Call[]> };
export interface EthClient {
[key: string]: unknown;
}

export type GetCalls = { getCalls: () => Call[] };
export type RecordingClient = PublicClient & GetCalls;

export const isEthereumApiMethod = (methodName: string) => methodName.startsWith('get');

export const createRecordingClient = (client: PublicClient): RecordingClient =>
createLoggingProxy(client, isEthereumApiMethod);
export const createRecordingClient = (client: PublicClient): RecordingClient => createLoggingProxy(client);

function createLoggingProxy<Method, Target extends { [key: string]: Method }>(
target: Target,
propCondition: (prop: string) => boolean
): Target & GetCalls {
const handler: ProxyHandler<Target> & { _calls: CallWithResultPromise[] } = {
_calls: [],
function createLoggingProxy<Target extends EthClient>(target: Target): RecordingClient {
const calls: Call[] = [];

get(target: Target, prop: string, receiver) {
if (prop === 'getCalls') {
return async () => awaitResults(this._calls);
const handler: ProxyHandler<Target> = {
get(target: Target, method: string, receiver) {
if (method === 'getCalls') {
return () => calls;
}

const origMethod = target[prop];
if (typeof origMethod === 'function' && propCondition(prop)) {
return async (...args: object[]): Promise<object> => {
const result = origMethod.apply(target, args);
this._calls.push({
method: prop,
const originalMethod = target[method];
if (typeof originalMethod === 'function' && isEthereumApiMethod(method)) {
return async (...args: unknown[]): Promise<unknown> => {
const result = await originalMethod.apply(target, args);
calls.push({
method,
arguments: args,
result: result as Promise<object>
result
});
return result;
};
}
return Reflect.get(target, prop, receiver);
return Reflect.get(target, method, receiver);
}
};

return new Proxy(target, handler) as Target & GetCalls;
}

async function awaitResults(entries: CallWithResultPromise[]): Promise<Call[]> {
return Promise.all(entries.map(async (entry) => ({ ...entry, result: await entry.result })));
return new Proxy(target, handler) as unknown as RecordingClient;
}
10 changes: 5 additions & 5 deletions ethereum_history_api/oracles/src/util/mock.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
export function mock<T extends object>(
methodFilter: (method: string) => boolean,
methodHandler: (method: string, args: object) => object
methodHandler: (method: string, args: unknown) => unknown
): T {
const handler: ProxyHandler<T> = {
get(target: T, prop: string, receiver) {
if (methodFilter(prop)) {
return (...args: object[]) => methodHandler(prop, args);
get(target: T, method: string, receiver) {
if (methodFilter(method)) {
return (...args: object[]) => methodHandler(method, args);
} else {
return Reflect.get(target, prop, receiver);
return Reflect.get(target, method, receiver);
}
}
};
Expand Down

0 comments on commit d98c235

Please sign in to comment.