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
5 changes: 5 additions & 0 deletions .changeset/light-impalas-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-decoder': patch
---

Implement getMany for meta and abi sql store
169 changes: 128 additions & 41 deletions packages/transaction-decoder/src/sql/abi-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any[]>()

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,
Expand Down Expand Up @@ -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)
})
}),
})
}),
Expand Down
119 changes: 89 additions & 30 deletions packages/transaction-decoder/src/sql/contract-meta-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>()

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,
Expand Down Expand Up @@ -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)
})
}),
})
}),
Expand Down
Loading