@hugomrdias/foxer 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/README.md +56 -0
  2. package/dist/src/api/index.d.ts +2 -0
  3. package/dist/src/api/index.d.ts.map +1 -0
  4. package/dist/src/api/index.js +2 -0
  5. package/dist/src/api/index.js.map +1 -0
  6. package/dist/src/api/runner.d.ts +12 -0
  7. package/dist/src/api/runner.d.ts.map +1 -0
  8. package/dist/src/api/runner.js +29 -0
  9. package/dist/src/api/runner.js.map +1 -0
  10. package/dist/src/api/server.d.ts +18 -0
  11. package/dist/src/api/server.d.ts.map +1 -0
  12. package/dist/src/api/server.js +21 -0
  13. package/dist/src/api/server.js.map +1 -0
  14. package/dist/src/api/sql-middleware.d.ts +7 -0
  15. package/dist/src/api/sql-middleware.d.ts.map +1 -0
  16. package/dist/src/api/sql-middleware.js +95 -0
  17. package/dist/src/api/sql-middleware.js.map +1 -0
  18. package/dist/src/api/sql.d.ts +14 -0
  19. package/dist/src/api/sql.d.ts.map +1 -0
  20. package/dist/src/api/sql.js +108 -0
  21. package/dist/src/api/sql.js.map +1 -0
  22. package/dist/src/api/sse.d.ts +3 -0
  23. package/dist/src/api/sse.d.ts.map +1 -0
  24. package/dist/src/api/sse.js +11 -0
  25. package/dist/src/api/sse.js.map +1 -0
  26. package/dist/src/bin/dev.d.ts +3 -0
  27. package/dist/src/bin/dev.d.ts.map +1 -0
  28. package/dist/src/bin/dev.js +76 -0
  29. package/dist/src/bin/dev.js.map +1 -0
  30. package/dist/src/bin/flags.d.ts +32 -0
  31. package/dist/src/bin/flags.d.ts.map +1 -0
  32. package/dist/src/bin/flags.js +55 -0
  33. package/dist/src/bin/flags.js.map +1 -0
  34. package/dist/src/bin/index.d.ts +3 -0
  35. package/dist/src/bin/index.d.ts.map +1 -0
  36. package/dist/src/bin/index.js +22 -0
  37. package/dist/src/bin/index.js.map +1 -0
  38. package/dist/src/bin/utils.d.ts +4 -0
  39. package/dist/src/bin/utils.d.ts.map +1 -0
  40. package/dist/src/bin/utils.js +52 -0
  41. package/dist/src/bin/utils.js.map +1 -0
  42. package/dist/src/client/index.d.ts +18 -0
  43. package/dist/src/client/index.d.ts.map +1 -0
  44. package/dist/src/client/index.js +150 -0
  45. package/dist/src/client/index.js.map +1 -0
  46. package/dist/src/config/config.d.ts +157 -0
  47. package/dist/src/config/config.d.ts.map +1 -0
  48. package/dist/src/config/config.js +65 -0
  49. package/dist/src/config/config.js.map +1 -0
  50. package/dist/src/config/env.d.ts +25 -0
  51. package/dist/src/config/env.d.ts.map +1 -0
  52. package/dist/src/config/env.js +21 -0
  53. package/dist/src/config/env.js.map +1 -0
  54. package/dist/src/contants.d.ts +4 -0
  55. package/dist/src/contants.d.ts.map +1 -0
  56. package/dist/src/contants.js +4 -0
  57. package/dist/src/contants.js.map +1 -0
  58. package/dist/src/db/actions/blocks.d.ts +44 -0
  59. package/dist/src/db/actions/blocks.d.ts.map +1 -0
  60. package/dist/src/db/actions/blocks.js +152 -0
  61. package/dist/src/db/actions/blocks.js.map +1 -0
  62. package/dist/src/db/actions/index.d.ts +2 -0
  63. package/dist/src/db/actions/index.d.ts.map +1 -0
  64. package/dist/src/db/actions/index.js +2 -0
  65. package/dist/src/db/actions/index.js.map +1 -0
  66. package/dist/src/db/actions/transactions.d.ts +11 -0
  67. package/dist/src/db/actions/transactions.d.ts.map +1 -0
  68. package/dist/src/db/actions/transactions.js +22 -0
  69. package/dist/src/db/actions/transactions.js.map +1 -0
  70. package/dist/src/db/client.d.ts +329 -0
  71. package/dist/src/db/client.d.ts.map +1 -0
  72. package/dist/src/db/client.js +108 -0
  73. package/dist/src/db/client.js.map +1 -0
  74. package/dist/src/db/column-types.d.ts +132 -0
  75. package/dist/src/db/column-types.d.ts.map +1 -0
  76. package/dist/src/db/column-types.js +86 -0
  77. package/dist/src/db/column-types.js.map +1 -0
  78. package/dist/src/db/encode.d.ts +10 -0
  79. package/dist/src/db/encode.d.ts.map +1 -0
  80. package/dist/src/db/encode.js +79 -0
  81. package/dist/src/db/encode.js.map +1 -0
  82. package/dist/src/db/migrate.d.ts +31 -0
  83. package/dist/src/db/migrate.d.ts.map +1 -0
  84. package/dist/src/db/migrate.js +147 -0
  85. package/dist/src/db/migrate.js.map +1 -0
  86. package/dist/src/db/schema/blocks.d.ts +369 -0
  87. package/dist/src/db/schema/blocks.d.ts.map +1 -0
  88. package/dist/src/db/schema/blocks.js +24 -0
  89. package/dist/src/db/schema/blocks.js.map +1 -0
  90. package/dist/src/db/schema/index.d.ts +1415 -0
  91. package/dist/src/db/schema/index.d.ts.map +1 -0
  92. package/dist/src/db/schema/index.js +18 -0
  93. package/dist/src/db/schema/index.js.map +1 -0
  94. package/dist/src/db/schema/transactions.d.ts +336 -0
  95. package/dist/src/db/schema/transactions.d.ts.map +1 -0
  96. package/dist/src/db/schema/transactions.js +33 -0
  97. package/dist/src/db/schema/transactions.js.map +1 -0
  98. package/dist/src/db/transaction.d.ts +7 -0
  99. package/dist/src/db/transaction.d.ts.map +1 -0
  100. package/dist/src/db/transaction.js +8 -0
  101. package/dist/src/db/transaction.js.map +1 -0
  102. package/dist/src/hooks/default-hooks.d.ts +2 -0
  103. package/dist/src/hooks/default-hooks.d.ts.map +1 -0
  104. package/dist/src/hooks/default-hooks.js +107 -0
  105. package/dist/src/hooks/default-hooks.js.map +1 -0
  106. package/dist/src/hooks/registry.d.ts +51 -0
  107. package/dist/src/hooks/registry.d.ts.map +1 -0
  108. package/dist/src/hooks/registry.js +32 -0
  109. package/dist/src/hooks/registry.js.map +1 -0
  110. package/dist/src/index.d.ts +10 -0
  111. package/dist/src/index.d.ts.map +1 -0
  112. package/dist/src/index.js +4 -0
  113. package/dist/src/index.js.map +1 -0
  114. package/dist/src/indexer/backfill.d.ts +15 -0
  115. package/dist/src/indexer/backfill.d.ts.map +1 -0
  116. package/dist/src/indexer/backfill.js +95 -0
  117. package/dist/src/indexer/backfill.js.map +1 -0
  118. package/dist/src/indexer/live.d.ts +20 -0
  119. package/dist/src/indexer/live.d.ts.map +1 -0
  120. package/dist/src/indexer/live.js +51 -0
  121. package/dist/src/indexer/live.js.map +1 -0
  122. package/dist/src/indexer/process-block.d.ts +29 -0
  123. package/dist/src/indexer/process-block.d.ts.map +1 -0
  124. package/dist/src/indexer/process-block.js +91 -0
  125. package/dist/src/indexer/process-block.js.map +1 -0
  126. package/dist/src/indexer/queue-block.d.ts +18 -0
  127. package/dist/src/indexer/queue-block.d.ts.map +1 -0
  128. package/dist/src/indexer/queue-block.js +38 -0
  129. package/dist/src/indexer/queue-block.js.map +1 -0
  130. package/dist/src/indexer/reorg.d.ts +24 -0
  131. package/dist/src/indexer/reorg.d.ts.map +1 -0
  132. package/dist/src/indexer/reorg.js +83 -0
  133. package/dist/src/indexer/reorg.js.map +1 -0
  134. package/dist/src/indexer/runner.d.ts +14 -0
  135. package/dist/src/indexer/runner.d.ts.map +1 -0
  136. package/dist/src/indexer/runner.js +22 -0
  137. package/dist/src/indexer/runner.js.map +1 -0
  138. package/dist/src/rpc/client.d.ts +11 -0
  139. package/dist/src/rpc/client.d.ts.map +1 -0
  140. package/dist/src/rpc/client.js +18 -0
  141. package/dist/src/rpc/client.js.map +1 -0
  142. package/dist/src/rpc/get-block.d.ts +16 -0
  143. package/dist/src/rpc/get-block.d.ts.map +1 -0
  144. package/dist/src/rpc/get-block.js +77 -0
  145. package/dist/src/rpc/get-block.js.map +1 -0
  146. package/dist/src/rpc/get-logs.d.ts +11 -0
  147. package/dist/src/rpc/get-logs.d.ts.map +1 -0
  148. package/dist/src/rpc/get-logs.js +23 -0
  149. package/dist/src/rpc/get-logs.js.map +1 -0
  150. package/dist/src/schema.d.ts +3 -0
  151. package/dist/src/schema.d.ts.map +1 -0
  152. package/dist/src/schema.js +9 -0
  153. package/dist/src/schema.js.map +1 -0
  154. package/dist/src/types.d.ts +22 -0
  155. package/dist/src/types.d.ts.map +1 -0
  156. package/dist/src/types.js +1 -0
  157. package/dist/src/types.js.map +1 -0
  158. package/dist/src/utils/bloom.d.ts +6 -0
  159. package/dist/src/utils/bloom.d.ts.map +1 -0
  160. package/dist/src/utils/bloom.js +30 -0
  161. package/dist/src/utils/bloom.js.map +1 -0
  162. package/dist/src/utils/build-conflict-columns.d.ts +4 -0
  163. package/dist/src/utils/build-conflict-columns.d.ts.map +1 -0
  164. package/dist/src/utils/build-conflict-columns.js +14 -0
  165. package/dist/src/utils/build-conflict-columns.js.map +1 -0
  166. package/dist/src/utils/common.d.ts +2 -0
  167. package/dist/src/utils/common.d.ts.map +1 -0
  168. package/dist/src/utils/common.js +4 -0
  169. package/dist/src/utils/common.js.map +1 -0
  170. package/dist/src/utils/cursor.d.ts +5 -0
  171. package/dist/src/utils/cursor.d.ts.map +1 -0
  172. package/dist/src/utils/cursor.js +8 -0
  173. package/dist/src/utils/cursor.js.map +1 -0
  174. package/dist/src/utils/format.d.ts +2 -0
  175. package/dist/src/utils/format.d.ts.map +1 -0
  176. package/dist/src/utils/format.js +16 -0
  177. package/dist/src/utils/format.js.map +1 -0
  178. package/dist/src/utils/hash.d.ts +9 -0
  179. package/dist/src/utils/hash.d.ts.map +1 -0
  180. package/dist/src/utils/hash.js +15 -0
  181. package/dist/src/utils/hash.js.map +1 -0
  182. package/dist/src/utils/json.d.ts +5 -0
  183. package/dist/src/utils/json.d.ts.map +1 -0
  184. package/dist/src/utils/json.js +11 -0
  185. package/dist/src/utils/json.js.map +1 -0
  186. package/dist/src/utils/logger.d.ts +11 -0
  187. package/dist/src/utils/logger.d.ts.map +1 -0
  188. package/dist/src/utils/logger.js +111 -0
  189. package/dist/src/utils/logger.js.map +1 -0
  190. package/dist/src/utils/shutdown.d.ts +9 -0
  191. package/dist/src/utils/shutdown.d.ts.map +1 -0
  192. package/dist/src/utils/shutdown.js +24 -0
  193. package/dist/src/utils/shutdown.js.map +1 -0
  194. package/dist/src/utils/timer.d.ts +6 -0
  195. package/dist/src/utils/timer.d.ts.map +1 -0
  196. package/dist/src/utils/timer.js +9 -0
  197. package/dist/src/utils/timer.js.map +1 -0
  198. package/dist/src/utils/types.d.ts +39 -0
  199. package/dist/src/utils/types.d.ts.map +1 -0
  200. package/dist/src/utils/types.js +1 -0
  201. package/dist/src/utils/types.js.map +1 -0
  202. package/dist/tsconfig.tsbuildinfo +1 -0
  203. package/hello/apps/foc-api/README.md +69 -0
  204. package/hello/apps/foc-api/biome.json +8 -0
  205. package/hello/apps/foc-api/index.html +13 -0
  206. package/hello/apps/foc-api/package.json +39 -0
  207. package/hello/apps/foc-api/public/vite.svg +1 -0
  208. package/hello/apps/foc-api/src/app.css +45 -0
  209. package/hello/apps/foc-api/src/app.tsx +43 -0
  210. package/hello/apps/foc-api/src/assets/Cloudflare_Logo.svg +51 -0
  211. package/hello/apps/foc-api/src/assets/react.svg +1 -0
  212. package/hello/apps/foc-api/src/client.ts +41 -0
  213. package/hello/apps/foc-api/src/components/account.tsx +100 -0
  214. package/hello/apps/foc-api/src/components/wallet-options.tsx +43 -0
  215. package/hello/apps/foc-api/src/index.css +68 -0
  216. package/hello/apps/foc-api/src/main.tsx +38 -0
  217. package/hello/apps/foc-api/src/vite-env.d.ts +1 -0
  218. package/hello/apps/foc-api/tsconfig.app.json +44 -0
  219. package/hello/apps/foc-api/tsconfig.json +17 -0
  220. package/hello/apps/foc-api/tsconfig.node.json +25 -0
  221. package/hello/apps/foc-api/tsconfig.worker.json +8 -0
  222. package/hello/apps/foc-api/vite.config.ts +8 -0
  223. package/hello/apps/foc-api/worker/capabilities.ts +25 -0
  224. package/hello/apps/foc-api/worker/index.ts +64 -0
  225. package/hello/apps/foc-api/worker/router.ts +35 -0
  226. package/hello/apps/foc-api/worker-configuration.d.ts +7357 -0
  227. package/hello/apps/foc-api/wrangler.jsonc +50 -0
  228. package/hello/apps/foc-app/README.md +69 -0
  229. package/hello/apps/foc-app/biome.json +8 -0
  230. package/hello/apps/foc-app/index.html +13 -0
  231. package/hello/apps/foc-app/package.json +39 -0
  232. package/hello/apps/foc-app/public/vite.svg +1 -0
  233. package/hello/apps/foc-app/src/app.css +45 -0
  234. package/hello/apps/foc-app/src/app.tsx +43 -0
  235. package/hello/apps/foc-app/src/assets/Cloudflare_Logo.svg +51 -0
  236. package/hello/apps/foc-app/src/assets/react.svg +1 -0
  237. package/hello/apps/foc-app/src/client.ts +41 -0
  238. package/hello/apps/foc-app/src/components/account.tsx +100 -0
  239. package/hello/apps/foc-app/src/components/wallet-options.tsx +43 -0
  240. package/hello/apps/foc-app/src/index.css +68 -0
  241. package/hello/apps/foc-app/src/main.tsx +38 -0
  242. package/hello/apps/foc-app/src/vite-env.d.ts +1 -0
  243. package/hello/apps/foc-app/tsconfig.app.json +44 -0
  244. package/hello/apps/foc-app/tsconfig.json +17 -0
  245. package/hello/apps/foc-app/tsconfig.node.json +25 -0
  246. package/hello/apps/foc-app/tsconfig.worker.json +8 -0
  247. package/hello/apps/foc-app/vite.config.ts +8 -0
  248. package/hello/apps/foc-app/worker/capabilities.ts +25 -0
  249. package/hello/apps/foc-app/worker/index.ts +64 -0
  250. package/hello/apps/foc-app/worker/router.ts +35 -0
  251. package/hello/apps/foc-app/worker-configuration.d.ts +7357 -0
  252. package/hello/apps/foc-app/wrangler.jsonc +50 -0
  253. package/hello/biome.json +50 -0
  254. package/hello/package.json +22 -0
  255. package/hello/pnpm-workspace.yaml +3 -0
  256. package/hello/tsconfig.json +37 -0
  257. package/package.json +78 -0
  258. package/src/api/index.ts +1 -0
  259. package/src/api/runner.ts +43 -0
  260. package/src/api/server.ts +38 -0
  261. package/src/api/sql-middleware.ts +131 -0
  262. package/src/api/sql.ts +149 -0
  263. package/src/api/sse.ts +12 -0
  264. package/src/bin/create.ts +199 -0
  265. package/src/bin/dev.ts +91 -0
  266. package/src/bin/flags.ts +65 -0
  267. package/src/bin/index.ts +28 -0
  268. package/src/bin/utils.ts +55 -0
  269. package/src/config/config.ts +221 -0
  270. package/src/config/env.ts +28 -0
  271. package/src/contants.ts +3 -0
  272. package/src/db/actions/blocks.ts +209 -0
  273. package/src/db/actions/index.ts +1 -0
  274. package/src/db/actions/transactions.ts +32 -0
  275. package/src/db/client.ts +186 -0
  276. package/src/db/column-types.ts +105 -0
  277. package/src/db/encode.ts +99 -0
  278. package/src/db/migrate.ts +222 -0
  279. package/src/db/schema/blocks.ts +24 -0
  280. package/src/db/schema/index.ts +21 -0
  281. package/src/db/schema/transactions.ts +39 -0
  282. package/src/db/transaction.ts +20 -0
  283. package/src/hooks/registry.ts +107 -0
  284. package/src/index.ts +9 -0
  285. package/src/indexer/backfill.ts +133 -0
  286. package/src/indexer/live.ts +76 -0
  287. package/src/indexer/process-block.ts +142 -0
  288. package/src/indexer/queue-block.ts +74 -0
  289. package/src/indexer/reorg.ts +120 -0
  290. package/src/indexer/runner.ts +35 -0
  291. package/src/rpc/client.ts +27 -0
  292. package/src/rpc/get-block.ts +100 -0
  293. package/src/rpc/get-logs.ts +38 -0
  294. package/src/schema.ts +10 -0
  295. package/src/types.ts +32 -0
  296. package/src/utils/bloom.ts +41 -0
  297. package/src/utils/build-conflict-columns.ts +26 -0
  298. package/src/utils/common.ts +3 -0
  299. package/src/utils/cursor.ts +7 -0
  300. package/src/utils/format.ts +18 -0
  301. package/src/utils/hash.ts +17 -0
  302. package/src/utils/json.ts +11 -0
  303. package/src/utils/logger.ts +149 -0
  304. package/src/utils/shutdown.ts +36 -0
  305. package/src/utils/timer.ts +8 -0
  306. package/src/utils/types.ts +87 -0
  307. package/template/biome.json +50 -0
  308. package/template/package.json +22 -0
  309. package/template/pnpm-workspace.yaml +3 -0
  310. package/template/tsconfig.json +37 -0
  311. package/tsconfig.json +8 -0
@@ -0,0 +1,222 @@
1
+ import { migrate as migratePostgresJs } from 'drizzle-orm/node-postgres/migrator'
2
+ import { getTableConfig, type PgTable } from 'drizzle-orm/pg-core'
3
+ import { IndexedColumn } from 'drizzle-orm/pg-core/columns/common'
4
+ import { migrate as migratePglite } from 'drizzle-orm/pglite/migrator'
5
+ import { FOXER_TABLES, PUBLICATION_NAME } from '../contants.ts'
6
+ import type { Logger } from '../utils/logger.ts'
7
+ import { startClock } from '../utils/timer.ts'
8
+ import type { Database, DatabaseContext } from './client.ts'
9
+
10
+ /**
11
+ * Applies pending SQL migrations at runtime.
12
+ */
13
+ export async function runMigrations({
14
+ dbContext,
15
+ folder,
16
+ logger,
17
+ }: {
18
+ folder: string
19
+ dbContext: DatabaseContext
20
+ logger: Logger
21
+ }): Promise<void> {
22
+ const endClock = startClock()
23
+ const { db, driver } = dbContext
24
+ // apply migrations
25
+ if (driver === 'postgres') {
26
+ await migratePostgresJs(db, { migrationsFolder: folder })
27
+ } else {
28
+ await migratePglite(db, { migrationsFolder: folder })
29
+ }
30
+
31
+ // get tables to migrate
32
+ const tables = Object.keys(dbContext.db._.fullSchema).filter(
33
+ (table) => !FOXER_TABLES.includes(table)
34
+ )
35
+ // assert tables have blockNumber column and index
36
+ assertTablesHaveBlockNumberIndex(dbContext.db._.fullSchema, tables)
37
+
38
+ // check if wal is enabled
39
+ const wal = await isWalEnabled(db)
40
+ if (!wal) {
41
+ throw new Error(
42
+ 'WAL is not enabled, set wal_level=logical in your postgresql.conf or pass -c wal_level=logical to the postgres client'
43
+ )
44
+ }
45
+
46
+ // create publications
47
+ await createPublications(db, tables)
48
+
49
+ logger.info({ driver, duration: endClock() }, 'migrations applied')
50
+ }
51
+
52
+ /**
53
+ * Creates or updates a publication for the provided tables in the database.
54
+ * The resulting publication contains only the provided tables.
55
+ * This is needed for the live sync to work.
56
+ *
57
+ * @param db - The database to create the publication for
58
+ * @param tables - The list of table names to include
59
+ */
60
+ export async function createPublications(db: Database, tables: string[]) {
61
+ if (tables.length === 0) return
62
+
63
+ const quotedTables = tables.map((table) => `"${table.replaceAll('"', '""')}"`)
64
+ const publication = await db.execute(
65
+ `SELECT puballtables FROM pg_publication WHERE pubname = '${PUBLICATION_NAME}'`
66
+ )
67
+
68
+ if (publication.rows.length === 0) {
69
+ await db.execute(
70
+ `CREATE PUBLICATION ${PUBLICATION_NAME} FOR TABLE ${quotedTables.join(', ')}`
71
+ )
72
+ return
73
+ }
74
+
75
+ const isForAllTables = Boolean(publication.rows[0].puballtables)
76
+ if (isForAllTables) {
77
+ await db.execute(`DROP PUBLICATION ${PUBLICATION_NAME}`)
78
+ await db.execute(
79
+ `CREATE PUBLICATION ${PUBLICATION_NAME} FOR TABLE ${quotedTables.join(', ')}`
80
+ )
81
+ return
82
+ }
83
+
84
+ await db.execute(
85
+ `ALTER PUBLICATION ${PUBLICATION_NAME} SET TABLE ${quotedTables.join(', ')}`
86
+ )
87
+ }
88
+
89
+ /**
90
+ * Ensures every provided table has a blockNumber column and an index on it.
91
+ */
92
+ export function assertTablesHaveBlockNumberIndex(
93
+ fullSchema: Record<string, unknown>,
94
+ tableNames: string[]
95
+ ) {
96
+ const missingBlockNumberColumn: string[] = []
97
+ const missingBlockNumberIndex: string[] = []
98
+ const tableConfigs = new Map<string, ReturnType<typeof getTableConfig>>()
99
+ const tableHasBlockNumberColumn = new Map<string, boolean>()
100
+
101
+ for (const tableName of tableNames) {
102
+ const table = fullSchema[tableName]
103
+ if (!table) continue
104
+
105
+ const config = getTableConfig(table as PgTable)
106
+ tableConfigs.set(tableName, config)
107
+ tableHasBlockNumberColumn.set(
108
+ tableName,
109
+ config.columns.some((column) =>
110
+ ['blockNumber', 'block_number'].includes(column.name)
111
+ )
112
+ )
113
+ }
114
+
115
+ for (const tableName of tableNames) {
116
+ const config = tableConfigs.get(tableName)
117
+ if (!config) continue
118
+ const blockNumberColumns = config.columns.filter((column) =>
119
+ ['blockNumber', 'block_number'].includes(column.name)
120
+ )
121
+
122
+ if (blockNumberColumns.length === 0) {
123
+ if (
124
+ hasCascadeForeignKeyToBlockNumberTable(
125
+ config,
126
+ tableHasBlockNumberColumn
127
+ )
128
+ ) {
129
+ continue
130
+ }
131
+ missingBlockNumberColumn.push(tableName)
132
+ continue
133
+ }
134
+
135
+ const blockNumberColumnNames = new Set(
136
+ blockNumberColumns.map((column) => column.name)
137
+ )
138
+ const hasBlockNumberIndex = config.indexes.some((index) => {
139
+ const indexColumns = index.config?.columns
140
+
141
+ if (!indexColumns || indexColumns.length === 0) return false
142
+ return indexColumns.some((column) => {
143
+ if (!(column instanceof IndexedColumn)) return false
144
+ if (!column.name) return false
145
+ return blockNumberColumnNames.has(column.name)
146
+ })
147
+ })
148
+
149
+ if (!hasBlockNumberIndex) {
150
+ missingBlockNumberIndex.push(tableName)
151
+ }
152
+ }
153
+
154
+ if (
155
+ missingBlockNumberColumn.length > 0 ||
156
+ missingBlockNumberIndex.length > 0
157
+ ) {
158
+ const missingColumnTables = missingBlockNumberColumn.sort()
159
+ const missingIndexTables = missingBlockNumberIndex.sort()
160
+ const lines = [
161
+ 'Invalid schema for Foxer sync.',
162
+ '',
163
+ 'Each published table must have:',
164
+ "1) a 'blockNumber' column (db name can be 'block_number')",
165
+ "2) an index that includes 'blockNumber'",
166
+ "Exception: table can skip both when it has a foreign key with onDelete('cascade') to a table with blockNumber.",
167
+ '',
168
+ ]
169
+
170
+ if (missingColumnTables.length > 0) {
171
+ lines.push(
172
+ `Tables missing blockNumber column: ${missingColumnTables.join(', ')}`
173
+ )
174
+ }
175
+ if (missingIndexTables.length > 0) {
176
+ lines.push(
177
+ `Tables missing blockNumber index: ${missingIndexTables.join(', ')}`
178
+ )
179
+ }
180
+
181
+ lines.push(
182
+ '',
183
+ 'Drizzle example:',
184
+ "const myTable = pgTable('my_table', {",
185
+ ' // 1) Add the blockNumber column',
186
+ ' blockNumber: bigint().notNull(),',
187
+ ' // ...other columns',
188
+ '}, (table) => [',
189
+ ' // 2) Add an index on blockNumber',
190
+ " index('my_table_block_number_index').on(table.blockNumber),",
191
+ '])'
192
+ )
193
+
194
+ throw new Error(lines.join('\n'))
195
+ }
196
+ }
197
+
198
+ function hasCascadeForeignKeyToBlockNumberTable(
199
+ tableConfig: ReturnType<typeof getTableConfig>,
200
+ tableHasBlockNumberColumn: Map<string, boolean>
201
+ ) {
202
+ return tableConfig.foreignKeys.some((foreignKey) => {
203
+ if (foreignKey.onDelete !== 'cascade') return false
204
+
205
+ const referencedTable = foreignKey.reference().foreignTable
206
+ const referencedTableName = getTableConfig(referencedTable).name
207
+ return tableHasBlockNumberColumn.get(referencedTableName) === true
208
+ })
209
+ }
210
+
211
+ /**
212
+ * Checks if WAL is enabled.
213
+ *
214
+ * @param db - The database to check
215
+ * @returns True if WAL is enabled, false otherwise
216
+ */
217
+ export async function isWalEnabled(db: Database) {
218
+ const wal = await db
219
+ .execute('SHOW WAL_LEVEL')
220
+ .then((result) => result.rows[0].wal_level)
221
+ return wal === 'logical'
222
+ }
@@ -0,0 +1,24 @@
1
+ import { pgTable } from 'drizzle-orm/pg-core'
2
+ import { address, bigint, bytea, hash, numeric78 } from '../column-types.ts'
3
+
4
+ export const blocks = pgTable('blocks', {
5
+ number: bigint().notNull().primaryKey(),
6
+ timestamp: bigint().notNull(),
7
+ hash: hash().notNull(),
8
+ parentHash: hash().notNull(),
9
+ logsBloom: bytea().notNull(),
10
+ miner: address().notNull(),
11
+ gasUsed: numeric78().notNull(),
12
+ gasLimit: numeric78().notNull(),
13
+ baseFeePerGas: numeric78(),
14
+ nonce: bytea().notNull(),
15
+ mixHash: bytea().notNull(),
16
+ stateRoot: bytea().notNull(),
17
+ receiptsRoot: bytea().notNull(),
18
+ transactionsRoot: bytea().notNull(),
19
+ sha3Uncles: bytea().notNull(),
20
+ size: numeric78().notNull(),
21
+ difficulty: numeric78().notNull(),
22
+ totalDifficulty: numeric78(),
23
+ extraData: bytea().notNull(),
24
+ })
@@ -0,0 +1,21 @@
1
+ import { defineRelations } from 'drizzle-orm'
2
+
3
+ import { blocks } from './blocks.ts'
4
+ import { transactions } from './transactions.ts'
5
+
6
+ export const relations = defineRelations({ blocks, transactions }, (r) => {
7
+ return {
8
+ blocks: {
9
+ transactions: r.many.transactions(),
10
+ },
11
+ transactions: {
12
+ block: r.one.blocks({
13
+ from: r.transactions.blockNumber,
14
+ to: r.blocks.number,
15
+ }),
16
+ },
17
+ }
18
+ })
19
+
20
+ export const schema = { blocks, transactions }
21
+ export type Schema = typeof schema
@@ -0,0 +1,39 @@
1
+ import { index, integer, jsonb, pgEnum, pgTable } from 'drizzle-orm/pg-core'
2
+ import type { AccessList } from 'viem'
3
+ import { address, bigint, bytea, hash, numeric78 } from '../column-types.ts'
4
+
5
+ export const transactionTypeEnum = pgEnum('transaction_type', [
6
+ 'legacy',
7
+ 'eip1559',
8
+ 'eip2930',
9
+ 'eip4844',
10
+ 'eip7702',
11
+ ])
12
+
13
+ export const transactions = pgTable(
14
+ 'transactions',
15
+ {
16
+ hash: hash().primaryKey(),
17
+ blockNumber: bigint().notNull(),
18
+ transactionIndex: integer().notNull(),
19
+ blockHash: hash().notNull(),
20
+ from: address().notNull(),
21
+ to: address(),
22
+ input: bytea().notNull(),
23
+ value: numeric78().notNull(),
24
+ nonce: integer().notNull(),
25
+ r: bytea().notNull(),
26
+ s: bytea().notNull(),
27
+ v: numeric78().notNull(),
28
+ type: transactionTypeEnum().notNull(),
29
+ gas: numeric78().notNull(),
30
+ gasPrice: numeric78(),
31
+ maxFeePerGas: numeric78(),
32
+ maxPriorityFeePerGas: numeric78(),
33
+ accessList: jsonb().$type<AccessList>(),
34
+ },
35
+ (table) => [
36
+ index('transactions_block_number_index').on(table.blockNumber),
37
+ index('transactions_to_index').on(table.to),
38
+ ]
39
+ )
@@ -0,0 +1,20 @@
1
+ import type { Database } from './client'
2
+ import type { relations, schema } from './schema/index.ts'
3
+
4
+ /**
5
+ * Runs work in a transaction for either postgres or pglite drivers.
6
+ */
7
+ export function withTransaction<T>(
8
+ db: Database<typeof schema, typeof relations>,
9
+ run: (tx: Database<typeof schema, typeof relations>) => Promise<T>
10
+ ): Promise<T> {
11
+ const executor = db as unknown as Database<
12
+ typeof schema,
13
+ typeof relations
14
+ > & {
15
+ transaction: <R>(
16
+ fn: (tx: Database<typeof schema, typeof relations>) => Promise<R>
17
+ ) => Promise<R>
18
+ }
19
+ return executor.transaction(run)
20
+ }
@@ -0,0 +1,107 @@
1
+ import type { AnyRelations, EmptyRelations } from 'drizzle-orm/relations'
2
+ import type { GetEventArgs, Log } from 'viem'
3
+ import type { Database } from '../db/client'
4
+ import type { EncodedBlockWithTransactions, EncodedTransaction } from '../types'
5
+ import type { Logger } from '../utils/logger'
6
+ import type {
7
+ ContractAbiByEventKey,
8
+ ContractAbiEventByEventKey,
9
+ ContractsConfig,
10
+ EventKey,
11
+ EventNameFromEventKey,
12
+ MergedContractEvents,
13
+ } from '../utils/types'
14
+
15
+ export type HookContext<
16
+ TSchema extends Record<string, unknown> = Record<string, unknown>,
17
+ TRelations extends AnyRelations = EmptyRelations,
18
+ > = {
19
+ db: Database<TSchema, TRelations>
20
+ chainId: number
21
+ logger: Logger
22
+ }
23
+
24
+ export type DecodedEvent<
25
+ C extends ContractsConfig<NonNullable<unknown>>,
26
+ Event extends EventKey,
27
+ > = {
28
+ // Resolve the concrete ABI for the `contract:event` key.
29
+ args: GetEventArgs<
30
+ ContractAbiByEventKey<C, Event>,
31
+ EventNameFromEventKey<Event>,
32
+ { EnableUnion: false; IndexedOnly: false; Required: true }
33
+ >
34
+ log: Log<bigint, number, false, ContractAbiEventByEventKey<C, Event>>
35
+ block: EncodedBlockWithTransactions
36
+ transaction: EncodedTransaction
37
+ }
38
+
39
+ export type EventHook<
40
+ C extends ContractsConfig<NonNullable<unknown>>,
41
+ Event extends EventKey = EventKey,
42
+ TSchema extends Record<string, unknown> = Record<string, unknown>,
43
+ TRelations extends AnyRelations = EmptyRelations,
44
+ > = (args: {
45
+ context: HookContext<TSchema, TRelations>
46
+ event: DecodedEvent<C, Event>
47
+ }) => Promise<void> | void
48
+
49
+ /**
50
+ * Registry for strongly typed contract-event hooks.
51
+ */
52
+ export class HookRegistry<
53
+ C extends ContractsConfig<NonNullable<unknown>> = ContractsConfig<
54
+ NonNullable<unknown>
55
+ >,
56
+ TSchema extends Record<string, unknown> = Record<string, unknown>,
57
+ TRelations extends AnyRelations = EmptyRelations,
58
+ > {
59
+ private readonly hooks = new Map<MergedContractEvents<C>, unknown>()
60
+
61
+ /**
62
+ * Registers a hook for a specific `contract:event` key.
63
+ */
64
+ on<K extends MergedContractEvents<C>>(
65
+ streamKey: K,
66
+ hook: EventHook<C, K, TSchema, TRelations>
67
+ ): void {
68
+ this.hooks.set(streamKey, hook)
69
+ }
70
+
71
+ /**
72
+ * Decodes a log using stream ABI metadata and dispatches to the registered hook.
73
+ */
74
+ async dispatch<K extends MergedContractEvents<C>>(options: {
75
+ key: K
76
+ args: GetEventArgs<
77
+ ContractAbiByEventKey<C, K>,
78
+ EventNameFromEventKey<K>,
79
+ { EnableUnion: false; IndexedOnly: false; Required: true }
80
+ >
81
+ log: Log<bigint, number, false, ContractAbiEventByEventKey<C, K>>
82
+ block: EncodedBlockWithTransactions
83
+ transaction: EncodedTransaction
84
+ context: HookContext<TSchema, TRelations>
85
+ }): Promise<void> {
86
+ const { key, args, log, block, transaction, context } = options
87
+ const hook = this.hooks.get(key) as unknown as EventHook<
88
+ C,
89
+ K,
90
+ TSchema,
91
+ TRelations
92
+ >
93
+ if (!hook) return
94
+
95
+ const event = {
96
+ args,
97
+ log,
98
+ block,
99
+ transaction,
100
+ } as DecodedEvent<C, K>
101
+
102
+ await hook({
103
+ context,
104
+ event,
105
+ })
106
+ }
107
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export type * from './config/config.ts'
2
+ export { createConfig } from './config/config.ts'
3
+ export type { Database } from './db/client.ts'
4
+ export * from './db/column-types.ts'
5
+ export type { HookRegistry } from './hooks/registry.ts'
6
+ export type * from './rpc/client.ts'
7
+ export { buildConflictUpdateColumns } from './utils/build-conflict-columns.ts'
8
+ export type { Logger } from './utils/logger.ts'
9
+ export type * from './utils/types.ts'
@@ -0,0 +1,133 @@
1
+ import { filterContracts, type InternalConfig } from '../config/config.ts'
2
+ import { getBlocksInRange } from '../db/actions/blocks.ts'
3
+ import type { Database } from '../db/client.ts'
4
+ import type { relations, schema } from '../db/schema/index.ts'
5
+ import { withTransaction } from '../db/transaction.ts'
6
+ import type { HookRegistry } from '../hooks/registry.ts'
7
+ import { getLogsInRange } from '../rpc/get-logs.ts'
8
+ import { windowEnd } from '../utils/cursor.ts'
9
+ import type { Logger } from '../utils/logger.ts'
10
+ import { startClock } from '../utils/timer.ts'
11
+ import { processBlock } from './process-block.ts'
12
+
13
+ /**
14
+ * Executes historical catch-up from the current cursor to the safe head.
15
+ */
16
+ export async function runBackfill(args: {
17
+ logger: Logger
18
+ config: InternalConfig
19
+ db: Database<typeof schema, typeof relations>
20
+ registry: HookRegistry
21
+ }): Promise<bigint> {
22
+ const endClock = startClock()
23
+ const { db, registry, config, logger } = args
24
+ const client = config.clients.backfill
25
+ const chainHead = await client.getBlockNumber()
26
+ const safeHead =
27
+ chainHead > config.finality ? chainHead - config.finality : 0n
28
+ let cursor = config.startBlockNumber
29
+
30
+ if (cursor > safeHead) {
31
+ logger.debug(
32
+ {
33
+ cursor: cursor.toString(),
34
+ backfillHead: safeHead.toString(),
35
+ head: chainHead.toString(),
36
+ },
37
+ 'no historical catch-up needed'
38
+ )
39
+ return cursor
40
+ }
41
+
42
+ const batchSize = config.batchSize
43
+ logger.debug(
44
+ {
45
+ fromBlock: cursor.toString(),
46
+ toBlock: safeHead.toString(),
47
+ batchSize: batchSize.toString(),
48
+ },
49
+ 'starting backfill'
50
+ )
51
+
52
+ while (cursor <= safeHead) {
53
+ const batchStartMs = Date.now()
54
+ const toBlock = windowEnd(cursor, batchSize, safeHead)
55
+ const windowContracts = filterContracts(config, cursor, toBlock)
56
+
57
+ logger.debug(
58
+ {
59
+ batchFromBlock: cursor.toString(),
60
+ batchToBlock: toBlock.toString(),
61
+ streamCount: windowContracts.addresses.length,
62
+ },
63
+ 'processing backfill batch'
64
+ )
65
+ const batchBlockNumbers: bigint[] = []
66
+ let blockNumber = cursor
67
+ while (blockNumber <= toBlock) {
68
+ batchBlockNumbers.push(blockNumber)
69
+ blockNumber += 1n
70
+ }
71
+
72
+ const [blocksByNumber, logsByBlock] = await Promise.all([
73
+ getBlocksInRange(logger, db, batchBlockNumbers, client, windowContracts),
74
+ getLogsInRange({
75
+ logger,
76
+ client,
77
+ addresses: windowContracts.addresses,
78
+ events: windowContracts.eventAbis,
79
+ fromBlock: cursor,
80
+ toBlock,
81
+ }),
82
+ ])
83
+
84
+ let blockIndex = 0
85
+
86
+ const endClockBatch = startClock()
87
+ await withTransaction(db, async (tx) => {
88
+ while (blockIndex < batchBlockNumbers.length) {
89
+ const blockNumber = batchBlockNumbers[blockIndex]
90
+ const prefetchedBlock = blocksByNumber.get(blockNumber)
91
+
92
+ await processBlock({
93
+ logger,
94
+ config,
95
+ db: tx,
96
+ client,
97
+ registry,
98
+ blockNumber,
99
+ logs: logsByBlock.get(blockNumber) ?? [],
100
+ block: prefetchedBlock,
101
+ type: 'backfill',
102
+ contracts: windowContracts,
103
+ })
104
+ blockIndex += 1
105
+ }
106
+ })
107
+ logger.info(
108
+ { duration: endClockBatch() },
109
+ 'batch block and events processed'
110
+ )
111
+ const batchElapsedMs = Date.now() - batchStartMs
112
+ const blocksInRange = Number(toBlock - cursor + 1n)
113
+ const blocksPerSecond =
114
+ batchElapsedMs > 0
115
+ ? blocksInRange / (batchElapsedMs / 1000)
116
+ : blocksInRange
117
+ logger.info(
118
+ {
119
+ indexedUpTo: toBlock.toString(),
120
+ duration: batchElapsedMs,
121
+ throughput: Number(blocksPerSecond.toFixed(2)),
122
+ },
123
+ 'backfill batch completed'
124
+ )
125
+ cursor = toBlock + 1n
126
+ }
127
+
128
+ logger.info(
129
+ { duration: endClock(), blocks: cursor - config.startBlockNumber },
130
+ 'backfill completed'
131
+ )
132
+ return cursor
133
+ }
@@ -0,0 +1,76 @@
1
+ import PQueue from 'p-queue'
2
+ import type { PublicClient } from 'viem'
3
+ import type { InternalConfig } from '../config/config.ts'
4
+ import type { Database } from '../db/client.ts'
5
+ import type { relations, schema } from '../db/schema/index.ts'
6
+ import type { HookRegistry } from '../hooks/registry.ts'
7
+ import { noop } from '../utils/common.ts'
8
+ import type { Logger } from '../utils/logger.ts'
9
+ import { queueBlock } from './queue-block.ts'
10
+
11
+ /**
12
+ * Starts live head following and sequential block processing.
13
+ */
14
+ export function startLiveSync(args: {
15
+ logger: Logger
16
+ config: InternalConfig
17
+ db: Database<typeof schema, typeof relations>
18
+ client: PublicClient
19
+ registry: HookRegistry
20
+ initialCursor: bigint
21
+ }): { stop: () => void } {
22
+ const { config, db, client, registry, logger } = args
23
+
24
+ // filter out contracts that have endBlock set
25
+ const contracts = config.contractsForLive
26
+
27
+ if (contracts.length === 0) {
28
+ logger.debug(
29
+ 'all configured contracts have endBlock set; live sync disabled'
30
+ )
31
+ return { stop: noop }
32
+ }
33
+
34
+ const pqueue = new PQueue({ concurrency: 1 })
35
+ pqueue.on('error', (error) => {
36
+ logger.error({ error }, 'live queue error')
37
+ })
38
+
39
+ let nextBlockToQueue = args.initialCursor
40
+
41
+ const unwatch = client.watchBlockNumber({
42
+ emitMissed: true,
43
+ emitOnBegin: true,
44
+ onBlockNumber: (head) => {
45
+ while (nextBlockToQueue <= head) {
46
+ const blockNumber = nextBlockToQueue
47
+ pqueue.add(async () => {
48
+ await queueBlock({
49
+ logger,
50
+ blockNumber,
51
+ config,
52
+ db,
53
+ client,
54
+ registry,
55
+ queueSize: pqueue.size,
56
+ onRewind: (rewindTo) => {
57
+ nextBlockToQueue = rewindTo
58
+ pqueue.clear()
59
+ },
60
+ })
61
+ })
62
+ nextBlockToQueue += 1n
63
+ }
64
+ },
65
+ onError: (error) => {
66
+ logger.error({ error }, 'watchBlockNumber stream error')
67
+ },
68
+ })
69
+
70
+ logger.debug(
71
+ { startBlock: nextBlockToQueue.toString() },
72
+ 'watching latest chain head'
73
+ )
74
+
75
+ return { stop: unwatch }
76
+ }