11import * as AbiStore from '../abi-store.js'
22import { Effect , Layer } from 'effect'
33import { SqlClient } from '@effect/sql'
4+ import { runMigrations , migration } from './migrations.js'
45
56// Utility function to build query conditions for a single key
67const buildQueryForKey = (
@@ -21,39 +22,20 @@ const buildQueryForKey = (
2122 : sql . or ( [ addressQuery , signatureQuery , eventQuery ] . filter ( Boolean ) )
2223}
2324
24- // Convert database items to result format
25+ // Convert database items to result format - returns all ABIs with their individual status
2526const 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- }
27+ return items . map ( ( item ) => ( {
28+ type : item . type ,
29+ event : item . event ,
30+ signature : item . signature ,
31+ address,
32+ chainID,
33+ abi : item . abi ,
34+ id : item . id ,
35+ source : item . source || 'unknown' ,
36+ status : item . status as 'success' | 'invalid' | 'not-found' ,
37+ timestamp : item . timestamp ,
38+ } ) )
5739}
5840
5941// Build single lookup map with prefixed keys
@@ -94,77 +76,113 @@ export const make = (strategies: AbiStore.AbiStore['strategies']) =>
9476 Effect . gen ( function * ( ) {
9577 const sql = yield * SqlClient . SqlClient
9678
97- const table = sql ( '_loop_decoder_contract_abi_v2' )
79+ const tableV3 = sql ( '_loop_decoder_contract_abi_v3' )
80+ const tableV2 = sql ( '_loop_decoder_contract_abi_v2' )
9881 const id = sql . onDialectOrElse ( {
9982 sqlite : ( ) => sql `id INTEGER PRIMARY KEY AUTOINCREMENT,` ,
10083 pg : ( ) => sql `id SERIAL PRIMARY KEY,` ,
10184 mysql : ( ) => sql `id INT NOT NULL AUTO_INCREMENT, PRIMARY KEY (id),` ,
10285 orElse : ( ) => sql `` ,
10386 } )
10487
105- // TODO; add timestamp to the table
106- yield * sql `
107- CREATE TABLE IF NOT EXISTS ${ table } (
108- ${ id }
109- type TEXT NOT NULL,
110- address TEXT,
111- event TEXT,
112- signature TEXT,
113- chain INTEGER,
114- abi TEXT,
115- status TEXT NOT NULL,
116- timestamp TEXT DEFAULT CURRENT_TIMESTAMP
117- )
118- ` . pipe (
119- Effect . tapError ( Effect . logError ) ,
120- Effect . catchAll ( ( ) => Effect . dieMessage ( 'Failed to create contractAbi table' ) ) ,
121- )
88+ // TODO: Allow skipping migrations if users want to apply it manually
89+ // Run structured migrations (idempotent, transactional)
90+ yield * runMigrations ( [
91+ migration ( '001_create_contract_abi_v3' , ( q ) =>
92+ Effect . gen ( function * ( ) {
93+ yield * q `CREATE TABLE IF NOT EXISTS ${ tableV3 } (
94+ ${ id }
95+ type TEXT NOT NULL,
96+ address TEXT,
97+ event TEXT,
98+ signature TEXT,
99+ chain INTEGER,
100+ abi TEXT,
101+ status TEXT NOT NULL,
102+ timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
103+ source TEXT DEFAULT 'unknown'
104+ )`
105+
106+ const tsCoalesce = q . onDialectOrElse ( {
107+ sqlite : ( ) => q `COALESCE(timestamp, CURRENT_TIMESTAMP)` ,
108+ pg : ( ) => q `COALESCE(timestamp, CURRENT_TIMESTAMP)` ,
109+ mysql : ( ) => q `IFNULL(timestamp, CURRENT_TIMESTAMP)` ,
110+ orElse : ( ) => q `CURRENT_TIMESTAMP` ,
111+ } )
112+
113+ yield * q `
114+ INSERT INTO ${ tableV3 } (type, address, chain, abi, status, timestamp, source)
115+ SELECT 'address' as type, v.address, v.chain, v.abi, v.status, ${ tsCoalesce } as timestamp, 'unknown' as source
116+ FROM ${ tableV2 } as v
117+ WHERE v.type = 'address'
118+ AND v.address IS NOT NULL AND v.chain IS NOT NULL
119+ AND NOT EXISTS (
120+ SELECT 1 FROM ${ tableV3 } t
121+ WHERE t.type = 'address' AND t.address = v.address AND t.chain = v.chain
122+ )
123+ ` . pipe ( Effect . catchAll ( Effect . logError ) )
124+
125+ yield * q `
126+ INSERT INTO ${ tableV3 } (type, signature, abi, status, timestamp, source)
127+ SELECT 'func' as type, v.signature, v.abi, v.status, ${ tsCoalesce } as timestamp, 'unknown' as source
128+ FROM ${ tableV2 } as v
129+ WHERE v.type = 'func' AND v.signature IS NOT NULL
130+ AND NOT EXISTS (
131+ SELECT 1 FROM ${ tableV3 } t
132+ WHERE t.type = 'func' AND t.signature = v.signature
133+ )
134+ ` . pipe ( Effect . catchAll ( Effect . logError ) )
135+
136+ yield * q `
137+ INSERT INTO ${ tableV3 } (type, event, abi, status, timestamp, source)
138+ SELECT 'event' as type, v.event, v.abi, v.status, ${ tsCoalesce } as timestamp, 'unknown' as source
139+ FROM ${ tableV2 } as v
140+ WHERE v.type = 'event' AND v.event IS NOT NULL
141+ AND NOT EXISTS (
142+ SELECT 1 FROM ${ tableV3 } t
143+ WHERE t.type = 'event' AND t.event = v.event
144+ )
145+ ` . pipe ( Effect . catchAll ( Effect . logError ) )
146+ } ) ,
147+ ) ,
148+ ] )
149+
150+ const table = tableV3
122151
123152 return yield * AbiStore . make ( {
124153 strategies,
125- set : ( key , value ) =>
154+ set : ( key , abi ) =>
126155 Effect . gen ( function * ( ) {
127156 const normalizedAddress = key . address . toLowerCase ( )
128- if ( value . status === 'success' && value . result . type === 'address' ) {
129- const result = value . result
130- yield * sql `
131- INSERT INTO ${ table }
132- ${ sql . insert ( [
133- {
134- type : result . type ,
135- address : normalizedAddress ,
136- chain : key . chainID ,
137- abi : result . abi ,
138- status : 'success' ,
139- } ,
140- ] ) }
141- `
142- } else if ( value . status === 'success' ) {
143- const result = value . result
157+
158+ if ( abi . type === 'address' ) {
144159 yield * sql `
145- INSERT INTO ${ table }
146- ${ sql . insert ( [
147- {
148- type : result . type ,
149- event : 'event' in result ? result . event : null ,
150- signature : 'signature' in result ? result . signature : null ,
151- abi : result . abi ,
152- status : 'success' ,
153- } ,
154- ] ) }
155- `
160+ INSERT INTO ${ table }
161+ ${ sql . insert ( [
162+ {
163+ type : abi . type ,
164+ address : normalizedAddress ,
165+ chain : key . chainID ,
166+ abi : abi . abi ,
167+ status : abi . status ,
168+ source : abi . source || 'unknown' ,
169+ } ,
170+ ] ) }
171+ `
156172 } else {
157173 yield * sql `
158- INSERT INTO ${ table }
159- ${ sql . insert ( [
160- {
161- type : 'address' ,
162- address : normalizedAddress ,
163- chain : key . chainID ,
164- status : 'not-found' ,
165- } ,
166- ] ) }
167- `
174+ INSERT INTO ${ table }
175+ ${ sql . insert ( [
176+ {
177+ type : abi . type ,
178+ event : 'event' in abi ? abi . event : null ,
179+ signature : 'signature' in abi ? abi . signature : null ,
180+ abi : abi . abi ,
181+ status : abi . status ,
182+ source : abi . source || 'unknown' ,
183+ } ,
184+ ] ) }
185+ `
168186 }
169187 } ) . pipe (
170188 Effect . tapError ( Effect . logError ) ,
@@ -226,6 +244,18 @@ export const make = (strategies: AbiStore.AbiStore['strategies']) =>
226244 return createResult ( keyItems , address , chainID )
227245 } )
228246 } ) ,
247+
248+ updateStatus : ( id , status ) =>
249+ Effect . gen ( function * ( ) {
250+ yield * sql `
251+ UPDATE ${ table }
252+ SET status = ${ status }
253+ WHERE id = ${ id }
254+ ` . pipe (
255+ Effect . tapError ( Effect . logError ) ,
256+ Effect . catchAll ( ( ) => Effect . succeed ( null ) ) ,
257+ )
258+ } ) ,
229259 } )
230260 } ) ,
231261 )
0 commit comments