Skip to content

Commit

Permalink
feat: get logs improvements (#85)
Browse files Browse the repository at this point in the history
* feat: make get logs type more generic
* fix: slightly longer delay to make sure announcements are watched
  • Loading branch information
marcomariscal authored Nov 12, 2024
1 parent b55a867 commit fff75fa
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 131 deletions.
148 changes: 29 additions & 119 deletions src/lib/actions/getAnnouncements/getAnnouncements.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { BlockType } from '../types';

import { type PublicClient, parseAbiItem } from 'viem';
import { getBlock, getBlockNumber, getLogs } from 'viem/actions';
import type { GetEventArgs } from 'viem';
import { ERC5564AnnouncerAbi } from '../../abi';
import { fetchLogsInChunks } from '../../helpers/logs';
import { handleViemPublicClient } from '../../stealthClient/createStealthClient';
import type {
AnnouncementLog,
AnnouncementArgs,
GetAnnouncementsParams,
GetAnnouncementsReturnType
} from './types';

type AnnouncementFilter = GetEventArgs<
typeof ERC5564AnnouncerAbi,
'Announcement'
>;

/**
* This function queries logs for the `Announcement` event emitted by the ERC5564 contract.
*
Expand All @@ -29,127 +33,33 @@ async function getAnnouncements({
}: GetAnnouncementsParams): Promise<GetAnnouncementsReturnType> {
const publicClient = handleViemPublicClient(clientParams);

const fetchParams = {
address: ERC5564Address,
args
};

const logs = await fetchLogsInChunks({
publicClient,
fetchParams,
publicClient: publicClient,
abi: ERC5564AnnouncerAbi,
eventName: 'Announcement',
address: ERC5564Address,
args: convertAnnouncementArgs(args),
fromBlock,
toBlock
});

// Extract the relevant data from the logs
const announcements: AnnouncementLog[] = logs.map(log => {
const { args } = log;

return {
schemeId: args.schemeId,
stealthAddress: args.stealthAddress,
caller: args.caller,
ephemeralPubKey: args.ephemeralPubKey,
metadata: args.metadata,
...log
};
});

return announcements;
return logs.map(log => ({
schemeId: log.args.schemeId,
stealthAddress: log.args.stealthAddress,
caller: log.args.caller,
ephemeralPubKey: log.args.ephemeralPubKey,
metadata: log.args.metadata,
...log
}));
}

/**
* Fetches logs in chunks to handle potential large range queries efficiently.
*
* @param {Object} params - The parameters for fetching logs in chunks.
* - `publicClient`: An instance of the viem `PublicClient`.
* - `fetchParams`: Parameters for the log fetch query.
* - `fromBlock`: The starting block number for the fetch.
* - `toBlock`: The ending block number for the fetch.
* - `chunkSize`: The number of blocks to query in each chunk.
* @returns {Promise<GetLogsReturnType>} A flattened array of all logs fetched in chunks.
*/
const fetchLogsInChunks = async ({
publicClient,
fetchParams,
fromBlock,
toBlock,
chunkSize = 5000 // Default chunk size, can be adjusted
}: {
publicClient: PublicClient;
fetchParams: {
address: `0x${string}`;
// biome-ignore lint/suspicious/noExplicitAny: TODO handle better
args: any;
fromBlock?: BlockType;
toBlock?: BlockType;
};
fromBlock?: BlockType;
toBlock?: BlockType;
chunkSize?: number;
}) => {
const resolvedFromBlock =
(await resolveBlockNumber({
publicClient,
block: fromBlock ?? 'earliest'
})) || BigInt(0);

const resolvedToBlock = await resolveBlockNumber({
publicClient,
block: toBlock ?? 'latest'
});

let currentBlock = resolvedFromBlock;
const allLogs = [];

while (currentBlock <= resolvedToBlock) {
// Calculate the end block for the current chunk
const endBlock =
currentBlock + BigInt(chunkSize) < resolvedToBlock
? currentBlock + BigInt(chunkSize)
: resolvedToBlock;

const logs = await getLogs(publicClient, {
...fetchParams,
event: parseAbiItem(
'event Announcement(uint256 indexed schemeId,address indexed stealthAddress,address indexed caller,bytes ephemeralPubKey,bytes metadata)'
),
fromBlock: currentBlock,
toBlock: endBlock,
strict: true
});
allLogs.push(...logs);
currentBlock = endBlock + BigInt(1);
}

return allLogs;
};

/**
* Resolves a block number from a given block type (number, tag, or bigint).
*
* @param {Object} params - Parameters for resolving the block number.
* - `publicClient`: An instance of the viem `PublicClient`.
* - `block`: The block number or tag to resolve.
* @returns {Promise<bigint>} The resolved block number as a bigint or null.
*/
export async function resolveBlockNumber({
publicClient,
block
}: {
publicClient: PublicClient;
block?: BlockType;
}): Promise<bigint> {
if (typeof block === 'bigint') {
return block;
}

const { number } = await getBlock(publicClient, { blockTag: block });
// Get the latest block number if null, since it is the pending block
if (!number) {
return getBlockNumber(publicClient);
}
return number;
// Helper function to convert AnnouncementArgs to the array format Viem expects
function convertAnnouncementArgs(args: AnnouncementArgs) {
return [
args.schemeId === undefined ? undefined : args.schemeId,
args.stealthAddress === undefined ? undefined : args.stealthAddress,
args.caller === undefined ? undefined : args.caller
] as AnnouncementFilter;
}

export default getAnnouncements;
5 changes: 2 additions & 3 deletions src/lib/actions/getAnnouncements/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Log } from 'viem';
import type { EthAddress } from '../../../utils/crypto/types';
import type { ClientParams } from '../../stealthClient/types';
import type { BlockType } from '../types';

export type AnnouncementArgs = {
schemeId?: bigint | bigint[] | null | undefined;
Expand All @@ -21,7 +20,7 @@ export type GetAnnouncementsParams = {
clientParams?: ClientParams;
ERC5564Address: EthAddress;
args: AnnouncementArgs;
fromBlock?: BlockType;
toBlock?: BlockType;
fromBlock?: bigint | 'earliest';
toBlock?: bigint | 'latest';
};
export type GetAnnouncementsReturnType = AnnouncementLog[];
8 changes: 0 additions & 8 deletions src/lib/actions/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
export type BlockType =
| bigint
| 'latest'
| 'earliest'
| 'pending'
| 'safe'
| 'finalized';

export type PreparePayload = {
to: `0x${string}`;
account: `0x${string}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const announce = async ({

// Delay to wait for the announcements to be watched in accordance with the polling interval
const delay = async () =>
await new Promise(resolve => setTimeout(resolve, WATCH_POLLING_INTERVAL));
await new Promise(resolve => setTimeout(resolve, WATCH_POLLING_INTERVAL * 2));

describe('watchAnnouncementsForUser', () => {
let stealthClient: StealthActions;
Expand Down
115 changes: 115 additions & 0 deletions src/lib/helpers/logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {
type Abi,
type AbiEvent,
type ContractEventName,
type DecodeEventLogReturnType,
type GetEventArgs,
type Log,
type PublicClient,
decodeEventLog
} from 'viem';
import { getBlockNumber, getLogs } from 'viem/actions';

/**
* Parameters for fetching and decoding logs in chunks.
* @template TAbi - The ABI type.
*/
type FetchLogsParams<TAbi extends Abi> = {
/** An instance of the viem PublicClient. */
publicClient: PublicClient;
/** The ABI of the contract. */
abi: TAbi;
/** The name of the event to fetch logs for. */
eventName: ContractEventName<TAbi>;
/** The address of the contract. */
address: `0x${string}`;
/** Optional arguments to filter the logs. */
args?: GetEventArgs<TAbi, ContractEventName<TAbi>>;
/** The starting block number for the fetch. Defaults to 'earliest'. */
fromBlock?: bigint | 'earliest';
/** The ending block number for the fetch. Defaults to 'latest'. */
toBlock?: bigint | 'latest';
/** The number of blocks to query in each chunk. Defaults to 5000. */
chunkSize?: number;
};

type FetchLogsReturnType<TAbi extends Abi> = Array<
DecodeEventLogReturnType<TAbi, ContractEventName<TAbi>> & Log
>;

/**
* Fetches and decodes logs in chunks to handle potentially large range queries efficiently.
*
* @template TAbi - The ABI type.
* @param {FetchLogsParams<TAbi>} params - The parameters for fetching logs in chunks.
* @returns {Promise<FetchLogsReturnType>} - A flattened array of all logs fetched in chunks, including decoded event data.
*
* @example
* const logs = await fetchLogsInChunks({
* publicClient,
* abi: myContractABI,
* eventName: 'Transfer',
* address: '0x...',
* fromBlock: 1000000n,
* toBlock: 2000000n,
* chunkSize: 10000
* });
*/
export const fetchLogsInChunks = async <TAbi extends Abi>({
publicClient,
abi,
eventName,
address,
args,
fromBlock = 'earliest',
toBlock = 'latest',
chunkSize = 5000
}: FetchLogsParams<TAbi>): Promise<FetchLogsReturnType<TAbi>> => {
const [start, end] = await Promise.all([
fromBlock === 'earliest'
? 0n
: typeof fromBlock === 'bigint'
? fromBlock
: getBlockNumber(publicClient),
toBlock === 'latest' ? getBlockNumber(publicClient) : toBlock
]);

const eventAbi = abi.find(
(item): item is AbiEvent => item.type === 'event' && item.name === eventName
);

if (!eventAbi) throw new Error(`Event ${eventName} not found in ABI`);

const allLogs = [];

for (
let currentBlock = start;
currentBlock <= end;
currentBlock += BigInt(chunkSize)
) {
const logs = await getLogs(publicClient, {
address,
event: eventAbi,
args,
fromBlock: currentBlock,
toBlock: BigInt(
Math.min(Number(currentBlock) + chunkSize - 1, Number(end))
),
strict: true
});

allLogs.push(
...logs.map(log => ({
...log,
...decodeEventLog({
abi,
eventName,
topics: log.topics,
data: log.data
})
}))
);
}

return allLogs;
};

0 comments on commit fff75fa

Please sign in to comment.