diff --git a/.changeset/light-impalas-agree.md b/.changeset/light-impalas-agree.md new file mode 100644 index 00000000..8abe8245 --- /dev/null +++ b/.changeset/light-impalas-agree.md @@ -0,0 +1,5 @@ +--- +'@3loop/transaction-decoder': patch +--- + +Implement getMany for meta and abi sql store diff --git a/packages/transaction-decoder/src/sql/abi-store.ts b/packages/transaction-decoder/src/sql/abi-store.ts index 9676c52d..ffd19279 100644 --- a/packages/transaction-decoder/src/sql/abi-store.ts +++ b/packages/transaction-decoder/src/sql/abi-store.ts @@ -2,6 +2,92 @@ import * as AbiStore from '../abi-store.js' import { Effect, Layer } from 'effect' import { SqlClient } from '@effect/sql' +// Utility function to build query conditions for a single key +const buildQueryForKey = ( + sql: SqlClient.SqlClient, + { address, signature, event, chainID }: { address: string; signature?: string; event?: string; chainID: number }, +) => { + const addressQuery = sql.and([ + sql`address = ${address.toLowerCase()}`, + sql`chain = ${chainID}`, + sql`type = 'address'`, + ]) + + const signatureQuery = signature ? sql.and([sql`signature = ${signature}`, sql`type = 'func'`]) : undefined + const eventQuery = event ? sql.and([sql`event = ${event}`, sql`type = 'event'`]) : undefined + + return signature == null && event == null + ? addressQuery + : sql.or([addressQuery, signatureQuery, eventQuery].filter(Boolean)) +} + +// Convert database items to result format +const createResult = (items: readonly any[], address: string, chainID: number): AbiStore.ContractAbiResult => { + const successItems = items.filter((item) => item.status === 'success') + + const item = + successItems.find((item) => { + // Prioritize address over fragments + return item.type === 'address' + }) ?? successItems[0] + + if (item != null) { + return { + status: 'success', + result: { + type: item.type, + event: item.event, + signature: item.signature, + address, + chainID, + abi: item.abi, + }, + } as AbiStore.ContractAbiResult + } else if (items[0] != null && items[0].status === 'not-found') { + return { + status: 'not-found', + result: null, + } + } + + return { + status: 'empty', + result: null, + } +} + +// Build single lookup map with prefixed keys +const buildLookupMap = (allItems: readonly any[]) => { + const lookupMap = new Map() + + const addToMap = (key: string, item: any) => { + if (!lookupMap.has(key)) lookupMap.set(key, []) + lookupMap.get(key)?.push(item) + } + + for (const item of allItems) { + // Address-based lookup: "addr:address_chain" (with same type check as original) + if (typeof item.address === 'string' && typeof item.chain === 'number') { + const addressKey = `addr:${item.address.toLowerCase()}_${item.chain}` + addToMap(addressKey, item) + } + + // Signature-based lookup: "sig:signature" + if (item.signature && item.type === 'func') { + const signatureKey = `sig:${item.signature}` + addToMap(signatureKey, item) + } + + // Event-based lookup: "event:event" + if (item.event && item.type === 'event') { + const eventKey = `event:${item.event}` + addToMap(eventKey, item) + } + } + + return lookupMap +} + export const make = (strategies: AbiStore.AbiStore['strategies']) => Layer.scoped( AbiStore.AbiStore, @@ -89,55 +175,56 @@ export const make = (strategies: AbiStore.AbiStore['strategies']) => get: ({ address, signature, event, chainID }) => Effect.gen(function* () { - const addressQuery = sql.and([ - sql`address = ${address.toLowerCase()}`, - sql`chain = ${chainID}`, - sql`type = 'address'`, - ]) - - const signatureQuery = signature ? sql.and([sql`signature = ${signature}`, sql`type = 'func'`]) : undefined - const eventQuery = event ? sql.and([sql`event = ${event}`, sql`type = 'event'`]) : undefined - const query = - signature == null && event == null - ? addressQuery - : sql.or([addressQuery, signatureQuery, eventQuery].filter(Boolean)) + const query = buildQueryForKey(sql, { address, signature, event, chainID }) const items = yield* sql` SELECT * FROM ${table} WHERE ${query}`.pipe( Effect.tapError(Effect.logError), Effect.catchAll(() => Effect.succeed([])), ) - const successItems = items.filter((item) => item.status === 'success') - - const item = - successItems.find((item) => { - // Prioritize address over fragments - return item.type === 'address' - }) ?? successItems[0] - - if (item != null) { - return { - status: 'success', - result: { - type: item.type, - event: item.event, - signature: item.signature, - address, - chainID, - abi: item.abi, - }, - } as AbiStore.ContractAbiResult - } else if (items[0] != null && items[0].status === 'not-found') { - return { - status: 'not-found', - result: null, + return createResult(items, address, chainID) + }), + + getMany: (keys) => + Effect.gen(function* () { + if (keys.length === 0) return [] + + // Single database query for all keys + const conditions = keys.map((key) => buildQueryForKey(sql, key)) + const batchQuery = sql.or(conditions) + + const allItems = yield* sql`SELECT * FROM ${table} WHERE ${batchQuery}`.pipe( + Effect.tapError(Effect.logError), + Effect.catchAll(() => Effect.succeed([])), + ) + + // Build efficient lookup map once + const lookupMap = buildLookupMap(allItems) + + // Process results for each key using lookup map + return keys.map(({ address, signature, event, chainID }) => { + const keyItems: any[] = [] + + // Get address-based matches + const addressKey = `addr:${address.toLowerCase()}_${chainID}` + const addressItems = lookupMap.get(addressKey) || [] + keyItems.push(...addressItems) + + // Get signature-based matches + if (signature) { + const signatureKey = `sig:${signature}` + const signatureItems = lookupMap.get(signatureKey) || [] + keyItems.push(...signatureItems) } - } - return { - status: 'empty', - result: null, - } + // Get event-based matches + if (event) { + const eventKey = `event:${event}` + const eventItems = lookupMap.get(eventKey) || [] + keyItems.push(...eventItems) + } + return createResult(keyItems, address, chainID) + }) }), }) }), diff --git a/packages/transaction-decoder/src/sql/contract-meta-store.ts b/packages/transaction-decoder/src/sql/contract-meta-store.ts index 824d048c..504625e9 100644 --- a/packages/transaction-decoder/src/sql/contract-meta-store.ts +++ b/packages/transaction-decoder/src/sql/contract-meta-store.ts @@ -3,6 +3,60 @@ import { Effect, Layer } from 'effect' import * as ContractMetaStore from '../contract-meta-store.js' import { ContractData } from '../types.js' +// Utility function to build query conditions for a single key +const buildQueryForKey = (sql: SqlClient.SqlClient, { address, chainID }: { address: string; chainID: number }) => { + return sql.and([sql`address = ${address.toLowerCase()}`, sql`chain = ${chainID}`]) +} + +// Convert database items to result format +const createResult = ( + items: readonly any[], + address: string, + chainID: number, +): ContractMetaStore.ContractMetaResult => { + const successItems = items.filter((item) => item.status === 'success') + const item = successItems[0] + + if (item != null && item.status === 'success') { + return { + status: 'success', + result: { + contractAddress: address, + contractName: item.contract_name, + tokenSymbol: item.token_symbol, + decimals: item.decimals, + type: item.type, + address, + chainID, + } as ContractData, + } + } else if (items[0] != null && items[0].status === 'not-found') { + return { + status: 'not-found', + result: null, + } + } + + return { + status: 'empty', + result: null, + } +} + +// Build lookup map for efficient batch processing +const buildLookupMap = (allItems: readonly any[]) => { + const lookupMap = new Map() + + for (const item of allItems) { + if (typeof item.address === 'string' && typeof item.chain === 'number') { + const key = `${item.address.toLowerCase()}-${item.chain}` + lookupMap.set(key, item) + } + } + + return lookupMap +} + export const make = (strategies: ContractMetaStore.ContractMetaStore['strategies']) => Layer.effect( ContractMetaStore.ContractMetaStore, @@ -68,44 +122,49 @@ export const make = (strategies: ContractMetaStore.ContractMetaStore['strategies Effect.tapError(Effect.logError), Effect.catchAll(() => Effect.succeed(null)), ), + get: ({ address, chainID }) => Effect.gen(function* () { + const query = buildQueryForKey(sql, { address, chainID }) + const items = yield* sql` - SELECT * FROM ${table} - WHERE ${sql.and([sql`address = ${address.toLowerCase()}`, sql`chain = ${chainID}`])} - `.pipe( + SELECT * FROM ${table} + WHERE ${query} + `.pipe( Effect.tapError(Effect.logError), Effect.catchAll(() => Effect.succeed([])), ) - const successItems = items.filter((item) => item.status === 'success') - - const item = successItems[0] - - if (item != null && item.status === 'success') { - return { - status: 'success', - result: { - contractAddress: address, - contractName: item.contract_name, - tokenSymbol: item.token_symbol, - decimals: item.decimals, - type: item.type, - address, - chainID, - } as ContractData, - } - } else if (items[0] != null && items[0].status === 'not-found') { - return { - status: 'not-found', - result: null, - } - } + return createResult(items, address, chainID) + }), - return { - status: 'empty', - result: null, - } + getMany: (params) => + Effect.gen(function* () { + if (params.length === 0) return [] + + // Single database query for all keys + const conditions = params.map((key) => buildQueryForKey(sql, key)) + const batchQuery = sql.or(conditions) + + const allItems = yield* sql` + SELECT * FROM ${table} + WHERE ${batchQuery} + `.pipe( + Effect.tapError(Effect.logError), + Effect.catchAll(() => Effect.succeed([])), + ) + + // Build efficient lookup map once + const lookupMap = buildLookupMap(allItems) + + // Process results for each key using lookup map + return params.map(({ address, chainID }) => { + const key = `${address.toLowerCase()}-${chainID}` + const item = lookupMap.get(key) + const items = item ? [item] : [] + + return createResult(items, address, chainID) + }) }), }) }),