@hugomrdias/foxer 0.1.9 → 0.2.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 (67) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-check.log +2 -0
  3. package/CHANGELOG.md +30 -0
  4. package/README.md +71 -2
  5. package/dist/bin/create.d.ts.map +1 -1
  6. package/dist/bin/create.js +9 -1
  7. package/dist/bin/create.js.map +1 -1
  8. package/dist/bin/index.js +0 -0
  9. package/dist/config/env.d.ts +1 -1
  10. package/dist/config/env.d.ts.map +1 -1
  11. package/dist/config/env.js +2 -2
  12. package/dist/config/env.js.map +1 -1
  13. package/dist/db/actions/blocks.d.ts +9 -6
  14. package/dist/db/actions/blocks.d.ts.map +1 -1
  15. package/dist/db/actions/blocks.js +39 -43
  16. package/dist/db/actions/blocks.js.map +1 -1
  17. package/dist/db/client.d.ts +1 -1
  18. package/dist/db/client.d.ts.map +1 -1
  19. package/dist/db/client.js +5 -1
  20. package/dist/db/client.js.map +1 -1
  21. package/dist/db/column-types.d.ts +2 -2
  22. package/dist/db/column-types.d.ts.map +1 -1
  23. package/dist/db/column-types.js +11 -3
  24. package/dist/db/column-types.js.map +1 -1
  25. package/dist/db/schema/blocks.d.ts +16 -16
  26. package/dist/db/schema/index.d.ts +44 -44
  27. package/dist/db/schema/transactions.d.ts +6 -6
  28. package/dist/db/schema/transactions.d.ts.map +1 -1
  29. package/dist/db/schema/transactions.js +3 -1
  30. package/dist/db/schema/transactions.js.map +1 -1
  31. package/dist/hooks/registry.d.ts +3 -3
  32. package/dist/hooks/registry.d.ts.map +1 -1
  33. package/dist/indexer/backfill.d.ts.map +1 -1
  34. package/dist/indexer/backfill.js +19 -12
  35. package/dist/indexer/backfill.js.map +1 -1
  36. package/dist/indexer/process-block.d.ts +4 -4
  37. package/dist/indexer/process-block.d.ts.map +1 -1
  38. package/dist/indexer/process-block.js +4 -29
  39. package/dist/indexer/process-block.js.map +1 -1
  40. package/dist/indexer/queue-block.d.ts.map +1 -1
  41. package/dist/indexer/queue-block.js +19 -1
  42. package/dist/indexer/queue-block.js.map +1 -1
  43. package/dist/indexer/reorg.d.ts +2 -2
  44. package/dist/indexer/reorg.d.ts.map +1 -1
  45. package/dist/rpc/get-logs.js +1 -1
  46. package/dist/rpc/get-logs.js.map +1 -1
  47. package/dist/types.d.ts +2 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/package.json +16 -15
  50. package/src/bin/create.ts +16 -2
  51. package/src/config/env.ts +2 -2
  52. package/src/db/actions/blocks.ts +51 -51
  53. package/src/db/client.ts +5 -1
  54. package/src/db/column-types.ts +17 -6
  55. package/src/db/schema/transactions.ts +3 -1
  56. package/src/hooks/registry.ts +3 -3
  57. package/src/indexer/backfill.ts +28 -10
  58. package/src/indexer/process-block.ts +10 -39
  59. package/src/indexer/queue-block.ts +24 -1
  60. package/src/indexer/reorg.ts +2 -2
  61. package/src/rpc/get-logs.ts +1 -1
  62. package/src/types.ts +2 -0
  63. package/template/.gitignore.tpl +22 -0
  64. package/template/biome.json.tpl +87 -0
  65. package/template/package.json.tpl +5 -5
  66. package/template/tsconfig.json.tpl +25 -17
  67. package/template/turbo.json.tpl +1 -1
@@ -1,4 +1,5 @@
1
1
  import { customType } from 'drizzle-orm/pg-core'
2
+ import { hex as hexCodec } from 'iso-base/rfc4648'
2
3
  import { type Address, type Hash, type Hex, stringify } from 'viem'
3
4
 
4
5
  export const numeric78 = customType<{ data: bigint; driverData: string }>({
@@ -90,16 +91,26 @@ export const jsonb = customType<{ data: unknown; driverData: string }>({
90
91
  },
91
92
  })
92
93
 
93
- export const bytea = customType<{ data: Hex; driverData: Buffer }>({
94
+ export const bytea = customType<{ data: Hex; driverData: Uint8Array }>({
94
95
  dataType() {
95
96
  return 'bytea'
96
97
  },
97
- toDriver(value: string): Buffer {
98
+ toDriver(value: string): Uint8Array {
98
99
  return Buffer.from(value.slice(2), 'hex')
99
100
  },
100
- fromDriver(value: Buffer): Hex {
101
- const hex = value.toString('hex')
102
- const _value = hex.startsWith('\\x') ? hex.slice(2) : hex
103
- return `0x${_value}` as Hex
101
+ fromDriver(value: unknown): Hex {
102
+ if (typeof value === 'string') {
103
+ return `0x${value.slice(2)}` as Hex
104
+ }
105
+
106
+ if (value instanceof Buffer) {
107
+ return `0x${value.toString('hex')}` as Hex
108
+ }
109
+
110
+ if (value instanceof Uint8Array) {
111
+ return `0x${hexCodec.encode(value)}` as Hex
112
+ }
113
+
114
+ throw new Error('Invalid value')
104
115
  },
105
116
  })
@@ -35,6 +35,8 @@ export const transactions = pgTable(
35
35
  },
36
36
  (table) => [
37
37
  index('transactions_block_number_index').on(table.blockNumber),
38
- index('transactions_to_index').on(table.to),
38
+ index('transactions_to_block_number_index')
39
+ .on(table.to, table.blockNumber)
40
+ .concurrently(),
39
41
  ]
40
42
  )
@@ -3,7 +3,7 @@ import type { GetEventArgs, Log } from 'viem'
3
3
 
4
4
  import type { InternalConfig } from '../config/config'
5
5
  import type { Database } from '../db/client'
6
- import type { EncodedBlockWithTransactions, EncodedTransaction } from '../types'
6
+ import type { EncodedBlock, EncodedTransaction } from '../types'
7
7
  import type { Logger } from '../utils/logger'
8
8
  import type {
9
9
  ContractAbiByEventKey,
@@ -34,7 +34,7 @@ export type DecodedEvent<
34
34
  { EnableUnion: false; IndexedOnly: false; Required: true }
35
35
  >
36
36
  log: Log<bigint, number, false, ContractAbiEventByEventKey<C, Event>>
37
- block: EncodedBlockWithTransactions
37
+ block: EncodedBlock
38
38
  transaction: EncodedTransaction
39
39
  }
40
40
 
@@ -81,7 +81,7 @@ export class HookRegistry<
81
81
  { EnableUnion: false; IndexedOnly: false; Required: true }
82
82
  >
83
83
  log: Log<bigint, number, false, ContractAbiEventByEventKey<C, K>>
84
- block: EncodedBlockWithTransactions
84
+ block: EncodedBlock
85
85
  transaction: EncodedTransaction
86
86
  context: HookContext<TSchema, TRelations>
87
87
  }): Promise<void> {
@@ -1,3 +1,4 @@
1
+ import type { Hash } from 'viem'
1
2
  import { filterContracts, type InternalConfig } from '../config/config.ts'
2
3
  import { getBlocksInRange } from '../db/actions/blocks.ts'
3
4
  import type { Database } from '../db/client.ts'
@@ -61,17 +62,30 @@ export async function runBackfill(args: {
61
62
  blockNumber += 1n
62
63
  }
63
64
 
64
- const [blocksByNumber, logsByBlock] = await Promise.all([
65
- getBlocksInRange(logger, db, batchBlockNumbers, client, windowContracts),
66
- getLogsInRange({
65
+ const logsByBlock = await getLogsInRange({
66
+ logger,
67
+ client,
68
+ addresses: windowContracts.addresses,
69
+ events: windowContracts.eventAbis,
70
+ fromBlock: cursor,
71
+ toBlock,
72
+ })
73
+
74
+ const logsTxsSet = new Set<Hash>()
75
+ for (const logs of logsByBlock.values()) {
76
+ for (const log of logs) {
77
+ logsTxsSet.add(log.transactionHash)
78
+ }
79
+ }
80
+
81
+ const { blocks: blocksByNumber, transactions: transactionsMap } =
82
+ await getBlocksInRange(
67
83
  logger,
84
+ db,
85
+ batchBlockNumbers,
68
86
  client,
69
- addresses: windowContracts.addresses,
70
- events: windowContracts.eventAbis,
71
- fromBlock: cursor,
72
- toBlock,
73
- }),
74
- ])
87
+ Array.from(logsTxsSet)
88
+ )
75
89
 
76
90
  let blockIndex = 0
77
91
 
@@ -81,15 +95,19 @@ export async function runBackfill(args: {
81
95
  const blockNumber = batchBlockNumbers[blockIndex]
82
96
  const prefetchedBlock = blocksByNumber.get(blockNumber)
83
97
 
98
+ if (!prefetchedBlock) {
99
+ throw new Error(`Block ${blockNumber} not found`)
100
+ }
101
+
84
102
  await processBlock({
85
103
  logger,
86
104
  config,
87
105
  db: tx,
88
106
  client,
89
107
  registry,
90
- blockNumber,
91
108
  logs: logsByBlock.get(blockNumber) ?? [],
92
109
  block: prefetchedBlock,
110
+ transactionsMap,
93
111
  type: 'backfill',
94
112
  contracts: windowContracts,
95
113
  })
@@ -6,8 +6,7 @@ import type { Database } from '../db/client.ts'
6
6
  import type { relations, schema } from '../db/schema/index.ts'
7
7
  import { withTransaction } from '../db/transaction.ts'
8
8
  import type { HookRegistry } from '../hooks/registry.ts'
9
- import { safeGetBlock } from '../rpc/get-block.ts'
10
- import type { EncodedBlockWithTransactions, EncodedTransaction } from '../types'
9
+ import type { EncodedBlock, TransactionsMap } from '../types'
11
10
  import type { Logger } from '../utils/logger.ts'
12
11
  import { ensureParentContinuity } from './reorg.ts'
13
12
 
@@ -24,9 +23,9 @@ export async function processBlock(args: {
24
23
  db: Database<typeof schema, typeof relations>
25
24
  client: PublicClient
26
25
  registry: HookRegistry<NonNullable<unknown>>
27
- blockNumber: bigint
28
- logs?: Log<bigint, number, false, AbiEvent>[]
29
- block?: EncodedBlockWithTransactions
26
+ logs: Log<bigint, number, false, AbiEvent>[]
27
+ block: EncodedBlock
28
+ transactionsMap: TransactionsMap
30
29
  type: 'backfill' | 'live'
31
30
  contracts: FilteredContracts
32
31
  }): Promise<ProcessBlockResult> {
@@ -36,41 +35,12 @@ export async function processBlock(args: {
36
35
  db,
37
36
  client,
38
37
  registry,
39
- blockNumber,
40
- block: prefetchedBlock,
41
- logs: prefetchedLogs,
38
+ block,
39
+ transactionsMap,
40
+ logs,
42
41
  type,
43
42
  contracts,
44
43
  } = args
45
- const transactionByHash = new Map<`0x${string}`, EncodedTransaction>()
46
-
47
- let block: EncodedBlockWithTransactions | undefined
48
- let logs: Log<bigint, number, false, AbiEvent>[] | undefined
49
-
50
- if (prefetchedBlock) {
51
- block = prefetchedBlock
52
- }
53
- if (prefetchedLogs) {
54
- logs = prefetchedLogs
55
- }
56
- if (!block || !logs) {
57
- const [blockResult, logsResult] = await Promise.all([
58
- safeGetBlock({ client, blockNumber, db }),
59
- client.getLogs({
60
- address: contracts.addresses,
61
- events: contracts.eventAbis,
62
- fromBlock: blockNumber,
63
- toBlock: blockNumber,
64
- }),
65
- ])
66
-
67
- block = blockResult
68
- logs = logsResult
69
- }
70
-
71
- for (const tx of block.transactions) {
72
- transactionByHash.set(tx.hash, tx)
73
- }
74
44
 
75
45
  if (type === 'live') {
76
46
  const rewindTo = await ensureParentContinuity({
@@ -88,7 +58,8 @@ export async function processBlock(args: {
88
58
  if (type === 'live') {
89
59
  await cacheBlockAndTransactions({
90
60
  db: tx,
91
- block,
61
+ blocks: [block],
62
+ transactions: Array.from(transactionsMap.values()),
92
63
  logger,
93
64
  })
94
65
  }
@@ -108,7 +79,7 @@ export async function processBlock(args: {
108
79
  if (!contracts.eventNames.has(eventName)) {
109
80
  continue
110
81
  }
111
- const transaction = transactionByHash.get(log.transactionHash)
82
+ const transaction = transactionsMap.get(log.transactionHash)
112
83
 
113
84
  if (!transaction) {
114
85
  logger.debug(
@@ -5,6 +5,8 @@ import { filterContracts, type InternalConfig } from '../config/config.ts'
5
5
  import type { Database } from '../db/client.ts'
6
6
  import type { relations, schema } from '../db/schema/index.ts'
7
7
  import type { HookRegistry } from '../hooks/registry.ts'
8
+ import { safeGetBlock } from '../rpc/get-block.ts'
9
+ import type { EncodedBlock, EncodedTransaction } from '../types.ts'
8
10
  import { startClock } from '../utils/timer.ts'
9
11
  import { processBlock } from './process-block.ts'
10
12
 
@@ -34,15 +36,36 @@ export async function queueBlock(args: QueueBlockArgs): Promise<void> {
34
36
  const endClock = startClock()
35
37
  try {
36
38
  const contracts = filterContracts(config, blockNumber, blockNumber)
39
+
40
+ const [blockResult, logsResult] = await Promise.all([
41
+ safeGetBlock({ client, blockNumber, db }),
42
+ client.getLogs({
43
+ address: contracts.addresses,
44
+ events: contracts.eventAbis,
45
+ fromBlock: blockNumber,
46
+ toBlock: blockNumber,
47
+ }),
48
+ ])
49
+
50
+ const { transactions, ..._block } = blockResult
51
+ const block: EncodedBlock = _block
52
+ const transactionsMap = new Map<`0x${string}`, EncodedTransaction>()
53
+
54
+ for (const tx of transactions) {
55
+ transactionsMap.set(tx.hash, tx)
56
+ }
57
+
37
58
  const result = await processBlock({
38
59
  logger,
39
60
  config,
40
61
  db,
41
62
  client,
42
63
  registry,
43
- blockNumber,
44
64
  type: 'live',
45
65
  contracts,
66
+ block,
67
+ transactionsMap,
68
+ logs: logsResult,
46
69
  })
47
70
 
48
71
  if (result.status === 'reorg') {
@@ -3,7 +3,7 @@ import type { PublicClient } from 'viem'
3
3
  import { deleteBlocksFrom } from '../db/actions/blocks.ts'
4
4
  import type { Database } from '../db/client.ts'
5
5
  import { safeGetBlock } from '../rpc/get-block.ts'
6
- import type { EncodedBlockWithTransactions } from '../types'
6
+ import type { EncodedBlock } from '../types'
7
7
  import { hashEquals } from '../utils/hash.ts'
8
8
  import type { Logger } from '../utils/logger.ts'
9
9
  import { startClock } from '../utils/timer.ts'
@@ -16,7 +16,7 @@ export async function ensureParentContinuity(args: {
16
16
  logger: Logger
17
17
  db: Database
18
18
  client: PublicClient
19
- block: EncodedBlockWithTransactions
19
+ block: EncodedBlock
20
20
  }): Promise<bigint | null> {
21
21
  const { logger, db, client, block } = args
22
22
  if (block.number === 0n) return null
@@ -30,7 +30,7 @@ export async function getLogsInRange(args: {
30
30
  }
31
31
  logger.trace(
32
32
  {
33
- logs: logsByBlock.size,
33
+ logs: logs.length,
34
34
  duration: endClock(),
35
35
  },
36
36
  'get logs'
package/src/types.ts CHANGED
@@ -31,3 +31,5 @@ export type EncodedBlockWithTransactions = Simplify<
31
31
  transactions: EncodedTransaction[]
32
32
  }
33
33
  >
34
+ export type TransactionsMap = Map<`0x${string}`, EncodedTransaction>
35
+ export type BlocksMap = Map<bigint, EncodedBlock>
@@ -0,0 +1,22 @@
1
+ # npm
2
+ node_modules/
3
+
4
+ # Logs
5
+ logs
6
+ *.log
7
+ npm-debug.log*
8
+ yarn-debug.log*
9
+ yarn-error.log*
10
+ pnpm-debug.log*
11
+ lerna-debug.log*
12
+
13
+
14
+ # Build
15
+ dist/
16
+ .turbo/
17
+
18
+ # Env files
19
+ .zed
20
+ .env
21
+ .env.local
22
+ .pglite
@@ -0,0 +1,87 @@
1
+ {
2
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3
+ "root": false,
4
+ "files": {
5
+ "ignoreUnknown": true,
6
+ "includes": [
7
+ "**",
8
+ "!**/node_modules",
9
+ "!**/dist",
10
+ "!**/package-lock.json",
11
+ "!**/yarn.lock",
12
+ "!**/pnpm-lock.yaml",
13
+ "!**/bun.lockb",
14
+ "!**/output",
15
+ "!**/coverage",
16
+ "!**/temp",
17
+ "!**/.temp",
18
+ "!**/tmp",
19
+ "!**/.tmp",
20
+ "!**/.history",
21
+ "!**/.vitepress/cache",
22
+ "!**/.nuxt",
23
+ "!**/.next",
24
+ "!**/.vercel",
25
+ "!**/.changeset",
26
+ "!**/.idea",
27
+ "!**/.cache",
28
+ "!**/.output",
29
+ "!**/.vite-inspect",
30
+ "!**/.yarn",
31
+ "!**/CHANGELOG*.md",
32
+ "!**/*.min.*",
33
+ "!**/LICENSE*",
34
+ "!**/__snapshots__",
35
+ "!**/components.d.ts"
36
+ ]
37
+ },
38
+ "vcs": {
39
+ "clientKind": "git",
40
+ "enabled": true,
41
+ "useIgnoreFile": true,
42
+ "defaultBranch": "main"
43
+ },
44
+ "formatter": {
45
+ "enabled": true,
46
+ "formatWithErrors": true,
47
+ "indentStyle": "space",
48
+ "indentWidth": 2,
49
+ "lineEnding": "lf",
50
+ "lineWidth": 80,
51
+ "attributePosition": "auto",
52
+ "includes": ["**"]
53
+ },
54
+ "assist": {
55
+ "enabled": true,
56
+ "actions": {
57
+ "source": {
58
+ "organizeImports": "on",
59
+ "useSortedAttributes": "on",
60
+ "useSortedKeys": "off"
61
+ }
62
+ }
63
+ },
64
+ "linter": {
65
+ "enabled": true,
66
+ "domains": {
67
+ "test": "recommended"
68
+ },
69
+ "rules": {
70
+ "recommended": true
71
+ }
72
+ },
73
+ "javascript": {
74
+ "formatter": {
75
+ "jsxQuoteStyle": "double",
76
+ "quoteProperties": "asNeeded",
77
+ "trailingCommas": "es5",
78
+ "semicolons": "asNeeded",
79
+ "arrowParentheses": "always",
80
+ "bracketSpacing": true,
81
+ "bracketSameLine": false,
82
+ "quoteStyle": "single",
83
+ "attributePosition": "auto"
84
+ },
85
+ "globals": ["document", "navigator", "window"]
86
+ }
87
+ }
@@ -12,13 +12,13 @@
12
12
  "main": "index.js",
13
13
  "scripts": {
14
14
  "test": "echo \"Error: no test specified\" && exit 1",
15
- "check": "turbo run check",
15
+ "check": "turbo run check",
16
16
  "build": "turbo run build",
17
- "lint": "biome check ."
17
+ "lint:fix": "biome check --write ."
18
18
  },
19
19
  "devDependencies": {
20
- "@biomejs/biome": "^2.4.7",
21
- "@hugomrdias/configs": "^1.1.3",
22
- "turbo": "^2.8.17"
20
+ "@biomejs/biome": "^2.4.8",
21
+ "turbo": "^2.8.20",
22
+ "typescript": "^5.9.3"
23
23
  }
24
24
  }
@@ -3,34 +3,42 @@
3
3
  /* Projects */
4
4
  "incremental": true,
5
5
  "composite": true,
6
- // Emit
7
6
 
8
- "declaration": true,
9
- "declarationMap": true,
10
- "sourceMap": true,
11
- "removeComments": true,
12
-
13
- // Language and Environment
14
- "lib": ["ESNext", "DOM"],
15
- "target": "ESNext",
7
+ /* Language and Environment */
8
+ "target": "esnext",
9
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
16
10
  "moduleDetection": "force",
17
11
 
18
- // Modules
19
- "module": "preserve",
12
+ /* Modules */
13
+ "module": "preserve", // "NodeNext"
20
14
  "resolveJsonModule": true,
21
15
  "rewriteRelativeImportExtensions": true,
22
16
 
23
- // Interop Constraints
17
+ /* JavaScript Support */
18
+ "allowJs": true,
19
+ "checkJs": true,
20
+
21
+ /* Emit */
22
+ "outDir": "dist",
23
+ "declaration": true,
24
+ "declarationMap": true,
25
+ "emitDeclarationOnly": false,
26
+ "sourceMap": true,
27
+ "removeComments": false,
28
+ "noEmit": false,
29
+ "importsNotUsedAsValues": "remove",
30
+ "stripInternal": true,
31
+ "noEmitOnError": true,
32
+
33
+ /* Interop Constraints */
24
34
  "isolatedModules": true,
25
35
  "verbatimModuleSyntax": true,
26
- "erasableSyntaxOnly": true,
27
36
  "esModuleInterop": true,
37
+ "forceConsistentCasingInFileNames": true,
38
+ "erasableSyntaxOnly": true,
28
39
 
29
- // Type Checking
40
+ /* Type Checking */
30
41
  "strict": true,
31
- "noImplicitOverride": true,
32
-
33
- // Completeness
34
42
  "skipLibCheck": true
35
43
  }
36
44
  }
@@ -10,7 +10,7 @@
10
10
  "outputs": ["dist/**"]
11
11
  },
12
12
  "check": {
13
- "dependsOn": ["^check"]
13
+ "dependsOn": ["build","^check"]
14
14
  },
15
15
  "dev": {
16
16
  "dependsOn": ["^build"],