Skip to content

Commit c197f2d

Browse files
Implement getMany for meta and abi sql store (#235)
* Implement getMany for meta and abi sql store
1 parent 96a802a commit c197f2d

File tree

3 files changed

+222
-71
lines changed

3 files changed

+222
-71
lines changed

.changeset/light-impalas-agree.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@3loop/transaction-decoder': patch
3+
---
4+
5+
Implement getMany for meta and abi sql store

packages/transaction-decoder/src/sql/abi-store.ts

Lines changed: 128 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,92 @@ import * as AbiStore from '../abi-store.js'
22
import { Effect, Layer } from 'effect'
33
import { SqlClient } from '@effect/sql'
44

5+
// Utility function to build query conditions for a single key
6+
const buildQueryForKey = (
7+
sql: SqlClient.SqlClient,
8+
{ address, signature, event, chainID }: { address: string; signature?: string; event?: string; chainID: number },
9+
) => {
10+
const addressQuery = sql.and([
11+
sql`address = ${address.toLowerCase()}`,
12+
sql`chain = ${chainID}`,
13+
sql`type = 'address'`,
14+
])
15+
16+
const signatureQuery = signature ? sql.and([sql`signature = ${signature}`, sql`type = 'func'`]) : undefined
17+
const eventQuery = event ? sql.and([sql`event = ${event}`, sql`type = 'event'`]) : undefined
18+
19+
return signature == null && event == null
20+
? addressQuery
21+
: sql.or([addressQuery, signatureQuery, eventQuery].filter(Boolean))
22+
}
23+
24+
// Convert database items to result format
25+
const createResult = (items: readonly any[], address: string, chainID: number): AbiStore.ContractAbiResult => {
26+
const successItems = items.filter((item) => item.status === 'success')
27+
28+
const item =
29+
successItems.find((item) => {
30+
// Prioritize address over fragments
31+
return item.type === 'address'
32+
}) ?? successItems[0]
33+
34+
if (item != null) {
35+
return {
36+
status: 'success',
37+
result: {
38+
type: item.type,
39+
event: item.event,
40+
signature: item.signature,
41+
address,
42+
chainID,
43+
abi: item.abi,
44+
},
45+
} as AbiStore.ContractAbiResult
46+
} else if (items[0] != null && items[0].status === 'not-found') {
47+
return {
48+
status: 'not-found',
49+
result: null,
50+
}
51+
}
52+
53+
return {
54+
status: 'empty',
55+
result: null,
56+
}
57+
}
58+
59+
// Build single lookup map with prefixed keys
60+
const buildLookupMap = (allItems: readonly any[]) => {
61+
const lookupMap = new Map<string, any[]>()
62+
63+
const addToMap = (key: string, item: any) => {
64+
if (!lookupMap.has(key)) lookupMap.set(key, [])
65+
lookupMap.get(key)?.push(item)
66+
}
67+
68+
for (const item of allItems) {
69+
// Address-based lookup: "addr:address_chain" (with same type check as original)
70+
if (typeof item.address === 'string' && typeof item.chain === 'number') {
71+
const addressKey = `addr:${item.address.toLowerCase()}_${item.chain}`
72+
addToMap(addressKey, item)
73+
}
74+
75+
// Signature-based lookup: "sig:signature"
76+
if (item.signature && item.type === 'func') {
77+
const signatureKey = `sig:${item.signature}`
78+
addToMap(signatureKey, item)
79+
}
80+
81+
// Event-based lookup: "event:event"
82+
if (item.event && item.type === 'event') {
83+
const eventKey = `event:${item.event}`
84+
addToMap(eventKey, item)
85+
}
86+
}
87+
88+
return lookupMap
89+
}
90+
591
export const make = (strategies: AbiStore.AbiStore['strategies']) =>
692
Layer.scoped(
793
AbiStore.AbiStore,
@@ -89,55 +175,56 @@ export const make = (strategies: AbiStore.AbiStore['strategies']) =>
89175

90176
get: ({ address, signature, event, chainID }) =>
91177
Effect.gen(function* () {
92-
const addressQuery = sql.and([
93-
sql`address = ${address.toLowerCase()}`,
94-
sql`chain = ${chainID}`,
95-
sql`type = 'address'`,
96-
])
97-
98-
const signatureQuery = signature ? sql.and([sql`signature = ${signature}`, sql`type = 'func'`]) : undefined
99-
const eventQuery = event ? sql.and([sql`event = ${event}`, sql`type = 'event'`]) : undefined
100-
const query =
101-
signature == null && event == null
102-
? addressQuery
103-
: sql.or([addressQuery, signatureQuery, eventQuery].filter(Boolean))
178+
const query = buildQueryForKey(sql, { address, signature, event, chainID })
104179

105180
const items = yield* sql` SELECT * FROM ${table} WHERE ${query}`.pipe(
106181
Effect.tapError(Effect.logError),
107182
Effect.catchAll(() => Effect.succeed([])),
108183
)
109184

110-
const successItems = items.filter((item) => item.status === 'success')
111-
112-
const item =
113-
successItems.find((item) => {
114-
// Prioritize address over fragments
115-
return item.type === 'address'
116-
}) ?? successItems[0]
117-
118-
if (item != null) {
119-
return {
120-
status: 'success',
121-
result: {
122-
type: item.type,
123-
event: item.event,
124-
signature: item.signature,
125-
address,
126-
chainID,
127-
abi: item.abi,
128-
},
129-
} as AbiStore.ContractAbiResult
130-
} else if (items[0] != null && items[0].status === 'not-found') {
131-
return {
132-
status: 'not-found',
133-
result: null,
185+
return createResult(items, address, chainID)
186+
}),
187+
188+
getMany: (keys) =>
189+
Effect.gen(function* () {
190+
if (keys.length === 0) return []
191+
192+
// Single database query for all keys
193+
const conditions = keys.map((key) => buildQueryForKey(sql, key))
194+
const batchQuery = sql.or(conditions)
195+
196+
const allItems = yield* sql`SELECT * FROM ${table} WHERE ${batchQuery}`.pipe(
197+
Effect.tapError(Effect.logError),
198+
Effect.catchAll(() => Effect.succeed([])),
199+
)
200+
201+
// Build efficient lookup map once
202+
const lookupMap = buildLookupMap(allItems)
203+
204+
// Process results for each key using lookup map
205+
return keys.map(({ address, signature, event, chainID }) => {
206+
const keyItems: any[] = []
207+
208+
// Get address-based matches
209+
const addressKey = `addr:${address.toLowerCase()}_${chainID}`
210+
const addressItems = lookupMap.get(addressKey) || []
211+
keyItems.push(...addressItems)
212+
213+
// Get signature-based matches
214+
if (signature) {
215+
const signatureKey = `sig:${signature}`
216+
const signatureItems = lookupMap.get(signatureKey) || []
217+
keyItems.push(...signatureItems)
134218
}
135-
}
136219

137-
return {
138-
status: 'empty',
139-
result: null,
140-
}
220+
// Get event-based matches
221+
if (event) {
222+
const eventKey = `event:${event}`
223+
const eventItems = lookupMap.get(eventKey) || []
224+
keyItems.push(...eventItems)
225+
}
226+
return createResult(keyItems, address, chainID)
227+
})
141228
}),
142229
})
143230
}),

packages/transaction-decoder/src/sql/contract-meta-store.ts

Lines changed: 89 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,60 @@ import { Effect, Layer } from 'effect'
33
import * as ContractMetaStore from '../contract-meta-store.js'
44
import { ContractData } from '../types.js'
55

6+
// Utility function to build query conditions for a single key
7+
const buildQueryForKey = (sql: SqlClient.SqlClient, { address, chainID }: { address: string; chainID: number }) => {
8+
return sql.and([sql`address = ${address.toLowerCase()}`, sql`chain = ${chainID}`])
9+
}
10+
11+
// Convert database items to result format
12+
const createResult = (
13+
items: readonly any[],
14+
address: string,
15+
chainID: number,
16+
): ContractMetaStore.ContractMetaResult => {
17+
const successItems = items.filter((item) => item.status === 'success')
18+
const item = successItems[0]
19+
20+
if (item != null && item.status === 'success') {
21+
return {
22+
status: 'success',
23+
result: {
24+
contractAddress: address,
25+
contractName: item.contract_name,
26+
tokenSymbol: item.token_symbol,
27+
decimals: item.decimals,
28+
type: item.type,
29+
address,
30+
chainID,
31+
} as ContractData,
32+
}
33+
} else if (items[0] != null && items[0].status === 'not-found') {
34+
return {
35+
status: 'not-found',
36+
result: null,
37+
}
38+
}
39+
40+
return {
41+
status: 'empty',
42+
result: null,
43+
}
44+
}
45+
46+
// Build lookup map for efficient batch processing
47+
const buildLookupMap = (allItems: readonly any[]) => {
48+
const lookupMap = new Map<string, any>()
49+
50+
for (const item of allItems) {
51+
if (typeof item.address === 'string' && typeof item.chain === 'number') {
52+
const key = `${item.address.toLowerCase()}-${item.chain}`
53+
lookupMap.set(key, item)
54+
}
55+
}
56+
57+
return lookupMap
58+
}
59+
660
export const make = (strategies: ContractMetaStore.ContractMetaStore['strategies']) =>
761
Layer.effect(
862
ContractMetaStore.ContractMetaStore,
@@ -68,44 +122,49 @@ export const make = (strategies: ContractMetaStore.ContractMetaStore['strategies
68122
Effect.tapError(Effect.logError),
69123
Effect.catchAll(() => Effect.succeed(null)),
70124
),
125+
71126
get: ({ address, chainID }) =>
72127
Effect.gen(function* () {
128+
const query = buildQueryForKey(sql, { address, chainID })
129+
73130
const items = yield* sql`
74-
SELECT * FROM ${table}
75-
WHERE ${sql.and([sql`address = ${address.toLowerCase()}`, sql`chain = ${chainID}`])}
76-
`.pipe(
131+
SELECT * FROM ${table}
132+
WHERE ${query}
133+
`.pipe(
77134
Effect.tapError(Effect.logError),
78135
Effect.catchAll(() => Effect.succeed([])),
79136
)
80137

81-
const successItems = items.filter((item) => item.status === 'success')
82-
83-
const item = successItems[0]
84-
85-
if (item != null && item.status === 'success') {
86-
return {
87-
status: 'success',
88-
result: {
89-
contractAddress: address,
90-
contractName: item.contract_name,
91-
tokenSymbol: item.token_symbol,
92-
decimals: item.decimals,
93-
type: item.type,
94-
address,
95-
chainID,
96-
} as ContractData,
97-
}
98-
} else if (items[0] != null && items[0].status === 'not-found') {
99-
return {
100-
status: 'not-found',
101-
result: null,
102-
}
103-
}
138+
return createResult(items, address, chainID)
139+
}),
104140

105-
return {
106-
status: 'empty',
107-
result: null,
108-
}
141+
getMany: (params) =>
142+
Effect.gen(function* () {
143+
if (params.length === 0) return []
144+
145+
// Single database query for all keys
146+
const conditions = params.map((key) => buildQueryForKey(sql, key))
147+
const batchQuery = sql.or(conditions)
148+
149+
const allItems = yield* sql`
150+
SELECT * FROM ${table}
151+
WHERE ${batchQuery}
152+
`.pipe(
153+
Effect.tapError(Effect.logError),
154+
Effect.catchAll(() => Effect.succeed([])),
155+
)
156+
157+
// Build efficient lookup map once
158+
const lookupMap = buildLookupMap(allItems)
159+
160+
// Process results for each key using lookup map
161+
return params.map(({ address, chainID }) => {
162+
const key = `${address.toLowerCase()}-${chainID}`
163+
const item = lookupMap.get(key)
164+
const items = item ? [item] : []
165+
166+
return createResult(items, address, chainID)
167+
})
109168
}),
110169
})
111170
}),

0 commit comments

Comments
 (0)