@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,142 @@
1
+ import type { AbiEvent, Log, PublicClient } from 'viem'
2
+ import type { FilteredContracts, InternalConfig } from '../config/config.ts'
3
+ import { cacheBlockAndTransactions } from '../db/actions/blocks.ts'
4
+ import type { Database } from '../db/client.ts'
5
+ import type { relations, schema } from '../db/schema/index.ts'
6
+ import { withTransaction } from '../db/transaction.ts'
7
+ import type { HookRegistry } from '../hooks/registry.ts'
8
+ import { safeGetBlock } from '../rpc/get-block.ts'
9
+ import type { EncodedBlockWithTransactions, EncodedTransaction } from '../types'
10
+ import type { Logger } from '../utils/logger.ts'
11
+ import { ensureParentContinuity } from './reorg.ts'
12
+
13
+ export type ProcessBlockResult =
14
+ | { status: 'processed' }
15
+ | { status: 'reorg'; rewindTo: bigint }
16
+
17
+ /**
18
+ * Processes one block: continuity check, event writes, and optional cursor update.
19
+ */
20
+ export async function processBlock(args: {
21
+ logger: Logger
22
+ config: InternalConfig
23
+ db: Database<typeof schema, typeof relations>
24
+ client: PublicClient
25
+ registry: HookRegistry<NonNullable<unknown>>
26
+ blockNumber: bigint
27
+ logs?: Log<bigint, number, false, AbiEvent>[]
28
+ block?: EncodedBlockWithTransactions
29
+ type: 'backfill' | 'live'
30
+ contracts: FilteredContracts
31
+ }): Promise<ProcessBlockResult> {
32
+ const {
33
+ logger,
34
+ config,
35
+ db,
36
+ client,
37
+ registry,
38
+ blockNumber,
39
+ block: prefetchedBlock,
40
+ logs: prefetchedLogs,
41
+ type,
42
+ contracts,
43
+ } = args
44
+ const transactionByHash = new Map<`0x${string}`, EncodedTransaction>()
45
+
46
+ let block: EncodedBlockWithTransactions | undefined
47
+ let logs: Log<bigint, number, false, AbiEvent>[] | undefined
48
+
49
+ if (prefetchedBlock) {
50
+ block = prefetchedBlock
51
+ }
52
+ if (prefetchedLogs) {
53
+ logs = prefetchedLogs
54
+ }
55
+ if (!block || !logs) {
56
+ const [blockResult, logsResult] = await Promise.all([
57
+ safeGetBlock({ client, blockNumber, db }),
58
+ client.getLogs({
59
+ address: contracts.addresses,
60
+ events: contracts.eventAbis,
61
+ fromBlock: blockNumber,
62
+ toBlock: blockNumber,
63
+ }),
64
+ ])
65
+
66
+ block = blockResult
67
+ logs = logsResult
68
+ }
69
+
70
+ for (const tx of block.transactions) {
71
+ transactionByHash.set(tx.hash, tx)
72
+ }
73
+
74
+ if (type === 'live') {
75
+ const rewindTo = await ensureParentContinuity({
76
+ logger,
77
+ db,
78
+ client,
79
+ block,
80
+ })
81
+ if (rewindTo != null) {
82
+ return { status: 'reorg', rewindTo }
83
+ }
84
+ }
85
+
86
+ const write = async (tx: Database<typeof schema, typeof relations>) => {
87
+ if (type === 'live') {
88
+ await cacheBlockAndTransactions({
89
+ db: tx,
90
+ block,
91
+ logger,
92
+ })
93
+ }
94
+
95
+ for (const log of logs) {
96
+ const contractName = contracts.contractNameByAddress[log.address]
97
+
98
+ if (!contractName) {
99
+ logger.trace(
100
+ { address: log.address },
101
+ 'contract not found in contract name by address'
102
+ )
103
+ continue
104
+ }
105
+ const eventName = log.eventName
106
+
107
+ if (!contracts.eventNames.has(eventName)) {
108
+ continue
109
+ }
110
+ const transaction = transactionByHash.get(log.transactionHash)
111
+
112
+ if (!transaction) {
113
+ logger.trace(
114
+ { transactionHash: log.transactionHash },
115
+
116
+ 'transaction not found in block transaction list'
117
+ )
118
+ continue
119
+ }
120
+ await registry.dispatch({
121
+ key: `${contractName}:${eventName}` as never,
122
+ args: log.args as never,
123
+ log: log as never,
124
+ block,
125
+ transaction,
126
+ context: {
127
+ db: tx,
128
+ chainId: config.client.chain.id,
129
+ logger,
130
+ },
131
+ })
132
+ }
133
+ }
134
+
135
+ if (type === 'backfill') {
136
+ await write(db)
137
+ } else {
138
+ await withTransaction(db, write)
139
+ }
140
+
141
+ return { status: 'processed' }
142
+ }
@@ -0,0 +1,74 @@
1
+ import type { Logger } from 'pino'
2
+ import type { PublicClient } from 'viem'
3
+ import { filterContracts, 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 { startClock } from '../utils/timer.ts'
8
+ import { processBlock } from './process-block.ts'
9
+
10
+ export type QueueBlockArgs = {
11
+ logger: Logger
12
+ blockNumber: bigint
13
+ onRewind: (rewindTo: bigint) => void
14
+ config: InternalConfig
15
+ db: Database<typeof schema, typeof relations>
16
+ client: PublicClient
17
+ registry: HookRegistry
18
+ queueSize: number
19
+ }
20
+
21
+ export async function queueBlock(args: QueueBlockArgs): Promise<void> {
22
+ const {
23
+ config,
24
+ db,
25
+ client,
26
+ registry,
27
+ blockNumber,
28
+ logger,
29
+ onRewind,
30
+ queueSize,
31
+ } = args
32
+
33
+ const endClock = startClock()
34
+ try {
35
+ const contracts = filterContracts(config, blockNumber, blockNumber)
36
+ const result = await processBlock({
37
+ logger,
38
+ config,
39
+ db,
40
+ client,
41
+ registry,
42
+ blockNumber,
43
+ type: 'live',
44
+ contracts,
45
+ })
46
+
47
+ if (result.status === 'reorg') {
48
+ logger.warn(
49
+ {
50
+ blockNumber: blockNumber.toString(),
51
+ rewindTo: result.rewindTo.toString(),
52
+ },
53
+ 'reorg detected during live processing; rewinding'
54
+ )
55
+ onRewind(result.rewindTo)
56
+ return
57
+ }
58
+
59
+ logger.info(
60
+ {
61
+ duration: endClock(),
62
+ blockNumber: blockNumber.toString(),
63
+ queueSize,
64
+ },
65
+ 'processed live block'
66
+ )
67
+ } catch (error) {
68
+ logger.error(
69
+ { error, blockNumber: blockNumber.toString() },
70
+ 'block processing failed; rewinding'
71
+ )
72
+ onRewind(blockNumber - 1n)
73
+ }
74
+ }
@@ -0,0 +1,120 @@
1
+ import type { PublicClient } from 'viem'
2
+ import { deleteBlocksFrom } from '../db/actions/blocks.ts'
3
+ import type { Database } from '../db/client.ts'
4
+ import { safeGetBlock } from '../rpc/get-block.ts'
5
+ import type { EncodedBlockWithTransactions } from '../types'
6
+ import { hashEquals } from '../utils/hash.ts'
7
+ import type { Logger } from '../utils/logger.ts'
8
+ import { startClock } from '../utils/timer.ts'
9
+
10
+ /**
11
+ * Verifies parent-hash continuity and rolls back divergent canonical rows if needed.
12
+ * Returns the rewind start block when a reorg is detected.
13
+ */
14
+ export async function ensureParentContinuity(args: {
15
+ logger: Logger
16
+ db: Database
17
+ client: PublicClient
18
+ block: EncodedBlockWithTransactions
19
+ }): Promise<bigint | null> {
20
+ const { logger, db, client, block } = args
21
+ if (block.number === 0n) return null
22
+
23
+ // get the previous block
24
+ const previous = (
25
+ await db.$prepared.getBlockById.execute({
26
+ db,
27
+ blockNumber: block.number - 1n,
28
+ })
29
+ )[0]
30
+ if (!previous) return null
31
+
32
+ // check if the previous block's hash is the same as the block's parent hash
33
+ if (hashEquals(previous.hash, block.parentHash)) {
34
+ return null
35
+ }
36
+
37
+ logger.warn(
38
+ { blockNumber: block.number.toString() },
39
+ 'parent mismatch detected; rolling back'
40
+ )
41
+
42
+ // Walk backward from the immediate parent of the failing block until we find
43
+ // a block number where DB and chain hashes agree again.
44
+ let cursor = block.number - 1n
45
+
46
+ while (true) {
47
+ // 1) Read the DB's canonical block at this height.
48
+ const dbBlock = (
49
+ await db.$prepared.getBlockById.execute({ blockNumber: cursor })
50
+ )[0]
51
+ if (!dbBlock) {
52
+ cursor -= 1n
53
+ continue
54
+ }
55
+
56
+ // 2) Read chain block at the same height.
57
+ const chainBlock = await safeGetBlock({ client, blockNumber: cursor, db })
58
+ // if (blockResult.status === 'null_round') {
59
+ // cursor -= 1n
60
+ // continue
61
+ // }
62
+
63
+ // 3) Found the last common ancestor. Rewind to the first divergent height.
64
+ if (hashEquals(chainBlock.hash, dbBlock.hash)) {
65
+ const rewindTo = cursor
66
+ await deleteBlocksFrom(db, rewindTo)
67
+ return rewindTo
68
+ }
69
+
70
+ // 4) Still divergent at this height; keep scanning backward.
71
+ // TODO This should finality depth and it should delete and throw critical error
72
+ if (cursor === 0n) {
73
+ await deleteBlocksFrom(db, 0n)
74
+ return 0n
75
+ }
76
+ cursor -= 1n
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Validates recent indexed blocks against chain state on startup.
82
+ */
83
+ export async function verifyRecentBlocks(args: {
84
+ logger: Logger
85
+ db: Database
86
+ client: PublicClient
87
+ depth: bigint
88
+ }): Promise<void> {
89
+ const { logger, db, client, depth } = args
90
+ const endClock = startClock()
91
+ const latest =
92
+ (await db.$prepared.getLatestBlock.execute())[0]?.number ?? null
93
+
94
+ if (latest == null) return
95
+
96
+ const start = latest - depth >= 0n ? latest - depth : 0n
97
+ let blockNumber = start
98
+ while (blockNumber <= latest) {
99
+ const dbBlock = (
100
+ await db.$prepared.getBlockById.execute({ blockNumber })
101
+ )[0]
102
+
103
+ if (!dbBlock) {
104
+ blockNumber += 1n
105
+ continue
106
+ }
107
+ const chainBlock = await safeGetBlock({ client, blockNumber, db })
108
+
109
+ if (!hashEquals(chainBlock.hash, dbBlock.hash)) {
110
+ logger.warn(
111
+ { blockNumber: blockNumber.toString() },
112
+ 'startup sanity check mismatch detected'
113
+ )
114
+ await deleteBlocksFrom(db, blockNumber)
115
+ return
116
+ }
117
+ blockNumber += 1n
118
+ }
119
+ logger.info({ duration: endClock() }, 'startup sanity check completed')
120
+ }
@@ -0,0 +1,35 @@
1
+ import type { InternalConfig } from '../config/config.ts'
2
+ import type { Database } from '../db/client.ts'
3
+ import type { relations, schema } from '../db/schema/index.ts'
4
+ import type { HookRegistry } from '../hooks/registry.ts'
5
+ import type { Logger } from '../utils/logger.ts'
6
+ import { runBackfill } from './backfill.ts'
7
+ import { startLiveSync } from './live.ts'
8
+ import { verifyRecentBlocks } from './reorg.ts'
9
+
10
+ export async function bootstrapIndexer(options: {
11
+ logger: Logger
12
+ db: Database<typeof schema, typeof relations>
13
+ registry: HookRegistry
14
+ config: InternalConfig
15
+ }): Promise<{ stop: () => void }> {
16
+ await verifyRecentBlocks({
17
+ logger: options.logger,
18
+ db: options.db,
19
+ client: options.config.clients.backfill,
20
+ depth: options.config.finality,
21
+ })
22
+
23
+ const nextCursor = await runBackfill(options)
24
+
25
+ const live = startLiveSync({
26
+ logger: options.logger,
27
+ config: options.config,
28
+ db: options.db,
29
+ client: options.config.clients.live,
30
+ registry: options.registry,
31
+ initialCursor: nextCursor,
32
+ })
33
+
34
+ return live
35
+ }
@@ -0,0 +1,27 @@
1
+ import { createPublicClient, type PublicClient } from 'viem'
2
+ import type { ClientConfig } from '../config/config.ts'
3
+
4
+ export type RpcClients = {
5
+ backfill: PublicClient
6
+ live: PublicClient
7
+ }
8
+
9
+ /**
10
+ * Creates a viem public client configured for the target FEVM chain.
11
+ */
12
+ export function createRpcClients(options: ClientConfig): RpcClients {
13
+ const backfill = createPublicClient(options)
14
+
15
+ const liveTransport = options.realtimeTransport ?? options.transport
16
+
17
+ const live = createPublicClient({
18
+ chain: options.chain,
19
+ transport: liveTransport,
20
+ pollingInterval: 1000,
21
+ })
22
+
23
+ return {
24
+ backfill,
25
+ live,
26
+ }
27
+ }
@@ -0,0 +1,100 @@
1
+ import type { Hash, PublicClient } from 'viem'
2
+ import type { Database } from '../db/client.ts'
3
+ import {
4
+ encodeBlockWithTransactions,
5
+ encodeNullRoundBlock,
6
+ } from '../db/encode.ts'
7
+ import type { EncodedBlockWithTransactions } from '../types.ts'
8
+
9
+ /**
10
+ * Fetches a block while normalizing null-round behavior into an explicit result.
11
+ */
12
+ export async function safeGetBlock(options: {
13
+ client: PublicClient
14
+ blockNumber: bigint
15
+ db: Database
16
+ }): Promise<EncodedBlockWithTransactions> {
17
+ const { client, blockNumber, db } = options
18
+ try {
19
+ const block = await client.getBlock({
20
+ blockNumber,
21
+ includeTransactions: true,
22
+ })
23
+
24
+ return encodeBlockWithTransactions(block)
25
+ } catch (error) {
26
+ if (isNullRoundRpcError(error)) {
27
+ let previousBlock:
28
+ | { number: bigint; hash: Hash; parentHash: Hash }
29
+ | undefined
30
+
31
+ previousBlock = (
32
+ await db.$prepared.getBlockById.execute({
33
+ blockNumber: blockNumber - 1n,
34
+ })
35
+ )[0]
36
+
37
+ let previousBlockNumber = blockNumber - 1n
38
+ // go to the chain and loop back until a full block is found
39
+ if (!previousBlock) {
40
+ while (!previousBlock) {
41
+ try {
42
+ const block = await client.getBlock({
43
+ blockNumber: previousBlockNumber,
44
+ })
45
+ previousBlock = {
46
+ number: block.number,
47
+ hash: block.hash,
48
+ parentHash: block.parentHash,
49
+ }
50
+ } catch (error) {
51
+ // catched another null round, keep going
52
+ if (isNullRoundRpcError(error)) {
53
+ previousBlockNumber -= 1n
54
+ continue
55
+ }
56
+ throw error
57
+ }
58
+ }
59
+ }
60
+ return encodeNullRoundBlock({
61
+ number: blockNumber,
62
+ hash: previousBlock.hash,
63
+ })
64
+ }
65
+ throw error
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Detects Filecoin null-round RPC errors so callers can skip non-existent rounds.
71
+ */
72
+ export function isNullRoundRpcError(error: unknown): boolean {
73
+ if (!(error instanceof Error)) return false
74
+
75
+ const message = error.message.toLowerCase()
76
+ if (message.includes('null round')) {
77
+ return true
78
+ }
79
+
80
+ const details = (error as { details?: unknown }).details
81
+ if (
82
+ typeof details === 'string' &&
83
+ details.toLowerCase().includes('null round')
84
+ ) {
85
+ return true
86
+ }
87
+
88
+ const cause = (error as { cause?: unknown }).cause
89
+ if (cause && typeof cause === 'object') {
90
+ const causeMessage = (cause as { message?: unknown }).message
91
+ if (
92
+ typeof causeMessage === 'string' &&
93
+ causeMessage.toLowerCase().includes('null round')
94
+ ) {
95
+ return true
96
+ }
97
+ }
98
+
99
+ return false
100
+ }
@@ -0,0 +1,38 @@
1
+ import type { AbiEvent, Address, Log, PublicClient } from 'viem'
2
+ import type { Logger } from '../utils/logger.ts'
3
+ import { startClock } from '../utils/timer.ts'
4
+
5
+ export async function getLogsInRange(args: {
6
+ logger: Logger
7
+ client: PublicClient
8
+ addresses: Address[]
9
+ events: readonly AbiEvent[]
10
+ fromBlock: bigint
11
+ toBlock: bigint
12
+ }): Promise<Map<bigint, Log<bigint, number, false, AbiEvent>[]>> {
13
+ const { logger, client, addresses, events, fromBlock, toBlock } = args
14
+
15
+ const endClock = startClock()
16
+
17
+ const logsByBlock = new Map<bigint, Log<bigint, number, false, AbiEvent>[]>()
18
+ const logs = await client.getLogs({
19
+ address: addresses,
20
+ events: events,
21
+ fromBlock: fromBlock,
22
+ toBlock: toBlock,
23
+ })
24
+
25
+ for (const log of logs) {
26
+ const byBlock = logsByBlock.get(log.blockNumber) ?? []
27
+ byBlock.push(log)
28
+ logsByBlock.set(log.blockNumber, byBlock)
29
+ }
30
+ logger.info(
31
+ {
32
+ logs: logsByBlock.size,
33
+ duration: endClock(),
34
+ },
35
+ 'get logs'
36
+ )
37
+ return logsByBlock
38
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { dirname, resolve } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ const __dirname = dirname(fileURLToPath(import.meta.url))
5
+
6
+ export const schemaFiles = [
7
+ resolve(__dirname, './db/schema/blocks.js'),
8
+ resolve(__dirname, './db/schema/transactions.js'),
9
+ ]
10
+ export { schema } from './db/schema/index.ts'
package/src/types.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { Simplify } from 'type-fest'
2
+ import type { Block, Transaction } from 'viem'
3
+ import type { Schema } from './db/schema/index'
4
+ /**
5
+ * Generic result with error
6
+ */
7
+ export type MaybeResult<ResultType = unknown, ErrorType = Error> =
8
+ | {
9
+ error: ErrorType
10
+ result?: undefined
11
+ }
12
+ | {
13
+ result: ResultType
14
+ error?: undefined
15
+ }
16
+
17
+ export type UnknownObject = NonNullable<unknown>
18
+
19
+ export type ChainTransaction = Transaction<bigint, number, false>
20
+ export type ChainBlock = Block<
21
+ bigint,
22
+ true,
23
+ 'latest' | 'safe' | 'finalized',
24
+ ChainTransaction
25
+ >
26
+ export type EncodedBlock = Schema['blocks']['$inferInsert']
27
+ export type EncodedTransaction = Schema['transactions']['$inferInsert']
28
+ export type EncodedBlockWithTransactions = Simplify<
29
+ EncodedBlock & {
30
+ transactions: EncodedTransaction[]
31
+ }
32
+ >
@@ -0,0 +1,41 @@
1
+ /** biome-ignore-all lint/style/noNonNullAssertion: no need to check for null */
2
+ import { type Hex, hexToBytes, keccak256 } from 'viem'
3
+
4
+ export const zeroLogsBloom =
5
+ '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
6
+
7
+ // const BLOOM_SIZE_BYTES = 256;
8
+
9
+ export const isInBloom = (bloomHex: Hex, value: Hex): boolean => {
10
+ const bloom = hexToBytes(bloomHex)
11
+ const hash = hexToBytes(keccak256(value))
12
+
13
+ // Ethereum uses 3 pairs of bytes from the hash to determine 3 bits
14
+ for (let i = 0; i < 6; i += 2) {
15
+ // Calculate the bit index (0 to 2047)
16
+ const bitIndex = ((hash[i]! << 8) | hash[i + 1]!) & 0x7ff
17
+
18
+ // Check if that bit is set in the 256-byte bloom array
19
+ const byteIndex = 255 - Math.floor(bitIndex / 8)
20
+ const bitMask = 1 << (bitIndex % 8)
21
+
22
+ if ((bloom[byteIndex]! & bitMask) === 0) {
23
+ return false // Definitely NOT in this block
24
+ }
25
+ }
26
+ return true // PROBABLY in this block (could be a false positive)
27
+ }
28
+
29
+ export function isBlockInteresting(
30
+ bloomHex: Hex,
31
+ addresses: Hex[],
32
+ topics: Hex[]
33
+ ): boolean {
34
+ // Check if ANY of our target contracts might be in this block
35
+ const hasContract = addresses.some((addr) => isInBloom(bloomHex, addr))
36
+ if (!hasContract) return false
37
+
38
+ // Check if ANY of our target event signatures might be in this block
39
+ const hasEvent = topics.some((topic) => isInBloom(bloomHex, topic))
40
+ return hasEvent
41
+ }
@@ -0,0 +1,26 @@
1
+ import { type SQL, sql } from 'drizzle-orm'
2
+ import type { PgTable } from 'drizzle-orm/pg-core'
3
+ import { getColumns } from 'drizzle-orm/utils'
4
+ import { snakeCase } from 'scule'
5
+
6
+ export const buildConflictUpdateColumns = <
7
+ T extends PgTable,
8
+ Q extends keyof T['_']['columns'],
9
+ >(
10
+ table: T,
11
+ columns?: Q[]
12
+ ) => {
13
+ const cls = getColumns(table)
14
+ const cols = columns ?? (Object.keys(cls) as Q[])
15
+ const r = cols.reduce(
16
+ (acc, column) => {
17
+ const colName = snakeCase(cls[column].name)
18
+
19
+ acc[column] = sql.raw(`excluded.${colName}`)
20
+ return acc
21
+ },
22
+ {} as Record<Q, SQL>
23
+ )
24
+
25
+ return r
26
+ }
@@ -0,0 +1,3 @@
1
+ export const noop = (): void => {
2
+ /** no-op */
3
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Returns an inclusive window end, capped by a max block.
3
+ */
4
+ export function windowEnd(start: bigint, size: bigint, max: bigint): bigint {
5
+ const end = start + size - 1n
6
+ return end <= max ? end : max
7
+ }
@@ -0,0 +1,18 @@
1
+ export const formatLogDuration = (ms: number) => {
2
+ // If less than 1 second, return ms.
3
+ if (ms < 1000) return `${Math.round(ms)}ms`
4
+ const seconds = Math.floor(ms / 1000)
5
+
6
+ const h = Math.floor(seconds / 3600)
7
+ const m = Math.floor((seconds - h * 3600) / 60)
8
+ const s = seconds - h * 3600 - m * 60
9
+ const secWithMs = ((ms % 60000) / 1000).toFixed(3).replace(/\.?0+$/, '') // seconds including ms fraction, max 59.999
10
+
11
+ const hstr = h > 0 ? `${h}h ` : ''
12
+ const mstr = m > 0 || h > 0 ? `${m < 10 && h > 0 ? '0' : ''}${m}m ` : ''
13
+ // Add milliseconds fraction to the seconds string, e.g., 2.322s
14
+ const sstr =
15
+ s > 0 || m > 0 ? `${s < 10 && m > 0 ? '0' : ''}${secWithMs}s` : ''
16
+
17
+ return `${hstr}${mstr}${sstr}`
18
+ }