uniswap-v2-loader 1.1.0 → 1.2.0

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ ## [1.2.0] - 2026-02-22
2
+ - Added `onupdate` function for subscribing to new pairs.
package/README.md CHANGED
@@ -7,12 +7,26 @@ The package uses Alchemy. Set your key as an environment variable (a default key
7
7
  `export KEY=your_alchemy_key`
8
8
 
9
9
  ## API Reference
10
- ### `all()`
11
- - **Description**: Fetches all token pairs from the Uniswap v2 factory. It utilizes multicall from `viem` and splits the loading process between multiple CPUs for high-speed execution.
12
- - **Returns**: `Promise<Array<Object>>`
10
+ ### `all(params)`
11
+ - **Description**: Fetches token pairs from the Uniswap v2 factory. It utilizes multicall from `viem` and splits the loading process between multiple CPUs for high-speed execution.
12
+ - **Arguments**:
13
+ - `params`: (Object)
14
+ - `from`: (number) Start loading from this index (default 0).
15
+ - `to`: (number) Load up to this index.
16
+ - `filename`: (string) Path to cache CSV file.
17
+ - `chunk_size`: (number) Items per multicall (default 50).
18
+ - **Returns**: `Promise<Array<Object>>` (Array of Pair Objects)
13
19
 
14
- ### Output Format
15
- The function returns a `Promise` resolving to an array of objects with the following fields:
20
+ ### `onupdate(callback, params)`
21
+ - **Description**: Subscribes to new pairs appearing at the factory contract. It initially calls the callback with cached pairs and then polls for updates.
22
+ - **Arguments**:
23
+ - `callback`: (function) Called with an array of Pair Objects.
24
+ - `params`: (Object) Same as `all()` plus:
25
+ - `update_timeout`: (number) Polling interval in ms (default 5000).
26
+ - **Returns**: `Function` An unsubscribe function to stop polling.
27
+
28
+ ### Pair Object
29
+ The pair object contains the following fields:
16
30
  - `id`: (number) The pair index.
17
31
  - `pair`: (string) The pair contract address.
18
32
  - `token0`: (string) The address of the first token in the pair.
@@ -20,11 +34,18 @@ The function returns a `Promise` resolving to an array of objects with the follo
20
34
 
21
35
  ## Usage
22
36
  ```javascript
23
- const { all } = require('uniswap-v2-loader')
37
+ const { all, onupdate } = require('uniswap-v2-loader')
24
38
 
25
- all().then(pairs =>
26
- pairs.forEach(({id, pair, token0, token1}) =>
27
- console.log(`ID: ${id} | Pair: ${pair} | Tokens: ${token0}, ${token1}`)
28
- )
39
+ // Load initial set
40
+ all({to: 10}).then(pairs =>
41
+ pairs.forEach(({id, pair}) => console.log(`ID: ${id} | Pair: ${pair}`))
29
42
  )
43
+
44
+ // Subscribe to new pairs (24/7 monitoring)
45
+ const unsubscribe = onupdate(pairs => {
46
+ console.log(`Pools count: ${pairs.length}`)
47
+ })
48
+
49
+ // Stop monitoring if needed
50
+ // unsubscribe()
30
51
  ```
package/index.js CHANGED
@@ -1,8 +1,26 @@
1
1
  const { spawn } = require('child_process')
2
- const os = require('os')
3
2
  const fs = require('fs')
3
+ const os = require('os')
4
+ const path = require('path')
5
+ const env = process.env
6
+ const home = os.homedir()
7
+ const pkg = require('./package.json')
8
+ const default_filename = path.join(
9
+ ...(process.platform === 'win32'
10
+ ? (env.LOCALAPPDATA || env.APPDATA)
11
+ ? [env.LOCALAPPDATA || env.APPDATA]
12
+ : [home, 'AppData', 'Local']
13
+ : process.platform === 'darwin'
14
+ ? [home, 'Library', 'Caches']
15
+ : (env.XDG_CACHE_HOME && path.isAbsolute(env.XDG_CACHE_HOME))
16
+ ? [env.XDG_CACHE_HOME]
17
+ : [home, '.cache']
18
+ ),
19
+ pkg.name + '_pairs.csv'
20
+ )
4
21
  const { parseAbiItem, createPublicClient, http } = require('viem')
5
22
  const { mainnet } = require('viem/chains')
23
+
6
24
  const workers = os.cpus().length - 1
7
25
  const missed = Array(workers).fill(null).map(() => [])
8
26
  const key = process.env.KEY || 'FZBvlPrOxtgaKBBkry3SH0W1IqH4Y5tu'
@@ -13,12 +31,24 @@ const client = createPublicClient({
13
31
  })
14
32
 
15
33
  const load = params => {
16
- const {filename, to} = params
17
- const pairs_ids = filename && fs.existsSync(filename)
34
+ const {filename = default_filename, to, from = 0, chunk_size = 50} = params
35
+ const pairs = params.pairs || fs.existsSync(filename)
18
36
  ? fs.readFileSync(filename).toString().trim().split('\n')
19
- .map(line => +line.split(',')[0])
37
+ .reduce((pairs, line) => {
38
+ line = line.split(',')
39
+ const id = +line[0]
40
+ if (id >= from && (to == undefined || id <= to)) pairs.push({
41
+ id,
42
+ pair: line[1],
43
+ token0: line[2],
44
+ token1: line[3]
45
+ })
46
+ return pairs
47
+ }, [])
20
48
  : []
21
49
 
50
+ if (pairs.length > to) return Promise.resolve(pairs.slice(0, to))
51
+
22
52
  return (to
23
53
  ? Promise.resolve(to)
24
54
  : client.readContract({
@@ -27,23 +57,24 @@ const load = params => {
27
57
  functionName: 'allPairsLength'
28
58
  }).then(_ => Number(_))
29
59
  ).then(allPairsLength => {
30
- const pairs = Array(allPairsLength)
31
60
  var next_pair_order = 0
32
- var start_from = pairs_ids.length
33
- ? pairs_ids[pairs_ids - 1] + 1
61
+ const start_loading_from = pairs.length
62
+ ? Math.max(from || 0, pairs[pairs.length - 1].id + 1)
34
63
  : 0
35
64
 
36
65
  missed.forEach(_ => _.length = 0)
37
66
 
38
- for (var i = start_from, rr = 0; i < allPairsLength; i++) {
67
+ for (var i = start_loading_from, rr = 0; i < allPairsLength; i++) {
39
68
  missed[rr].push(i)
40
- rr = (rr + 1) % workers
69
+ if (missed[rr].length % chunk_size == 0)
70
+ rr = (rr + 1) % workers
41
71
  }
42
-
72
+
43
73
  const jobs_data_filename = `jobs_data_${Date.now()}.json`
44
74
  fs.writeFileSync(jobs_data_filename, JSON.stringify({
45
75
  missed,
46
76
  factory,
77
+ chunk_size,
47
78
  key
48
79
  }), 'utf8')
49
80
 
@@ -52,16 +83,21 @@ const load = params => {
52
83
  .filter(_ => _.length)
53
84
  .map((_, i) => new Promise(y => {
54
85
  const loader = spawn('node', ['loader.js', jobs_data_filename, i.toString()])
55
- loader.stdout.on('data', line => {
56
- line = line.toString()
57
- const data = line.trim().split(',')
58
- const id = data[0]
59
- pairs[id] = {
60
- id,
61
- pair: data[1],
62
- token0: data[2],
63
- token1: data[3]
64
- }
86
+ loader.stdout.on('data', data => {
87
+ data += data.toString()
88
+ if (!data.includes('\n')) return
89
+ const lines = data.split('\n')
90
+ data = lines.shift()
91
+ lines.forEach(line => {
92
+ const a = line.split(',')
93
+ const id = +a[0]
94
+ pairs[id] = {
95
+ id,
96
+ pair: a[1],
97
+ token0: a[2],
98
+ token1: a[3]
99
+ }
100
+ })
65
101
  if (filename) {
66
102
  var pair
67
103
  while (pair = pairs[next_pair_order]) {
@@ -81,5 +117,36 @@ const load = params => {
81
117
  }
82
118
 
83
119
 
84
- module.exports.all = params =>
120
+ module.exports.all = (params = {}) =>
121
+ load(params)
122
+
123
+ module.exports.onupdate = function onupdate(callback, params = {}) {
124
+ var subscribe = true, timeout
85
125
  load(params)
126
+ .then(pairs => {
127
+ callback(pairs)
128
+
129
+ const update = pairs =>
130
+ timeout = setTimeout(
131
+ () =>
132
+ load({...params, pairs, from: pairs.length})
133
+ .then(pairs => {
134
+ if (!subscribe) return
135
+ callback(pairs)
136
+ if (!subscribe) return
137
+ if (params.to && pairs[pairs.length - 1].id >= params.to) return
138
+ update(pairs)
139
+ }),
140
+ params.update_timeout || 5000
141
+ )
142
+
143
+ if (!subscribe) return
144
+ if (params.to && pairs[pairs.length - 1].id >= params.to) return
145
+ update(pairs)
146
+ })
147
+
148
+ return () => {
149
+ subscribe = false
150
+ if (timeout) clearTimeout(timeout)
151
+ }
152
+ }
package/loader.js CHANGED
@@ -68,14 +68,17 @@ async function load(params) {
68
68
  pool[POOL.ADDRESS] = pools_ok[j].address.toLowerCase()
69
69
  pool[POOL.TOKEN0] = token0_result.result.toLowerCase()
70
70
  pool[POOL.TOKEN1] = token1_result.result.toLowerCase()
71
-
71
+
72
72
  console.log(pool.join(','))
73
73
  } else {
74
74
  retry_missed.push(pools_ok[j].id)
75
75
  }
76
76
  }
77
- } catch (error) {
78
- console.error(error.message)
77
+ } catch (_) {
78
+ chunk.forEach(id => {
79
+ if (retry_missed.includes(id)) return
80
+ retry_missed.push(id)
81
+ })
79
82
  }
80
83
  }
81
84
 
@@ -88,7 +91,7 @@ async function load(params) {
88
91
  const jobs_data_filename = process.argv[2]
89
92
  const job_index = +process.argv[3]
90
93
 
91
- if (isNaN(job_index)) console.error('Required pass an index of job via argument') || process.exit(1)
94
+ if (isNaN(job_index)) process.exit(1)
92
95
 
93
96
  const jobs_data = JSON.parse(fs.readFileSync(jobs_data_filename, 'utf8'))
94
97
  jobs_data.missed = jobs_data.missed[job_index]
@@ -98,4 +101,4 @@ const client = createPublicClient({
98
101
  transport: http(`https://eth-mainnet.g.alchemy.com/v2/${jobs_data.key}`)
99
102
  })
100
103
 
101
- load({client, ...jobs_data, chunk_size: 50})
104
+ load({client, ...jobs_data})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniswap-v2-loader",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Uniswap v2 protocol loader",
5
5
  "keywords": [
6
6
  "uniswap-v2",
package/test.js CHANGED
@@ -28,4 +28,21 @@ test('Load first two pairs to file', () => {
28
28
  .finally(() =>
29
29
  fs.unlinkSync(filename)
30
30
  )
31
- })
31
+ })
32
+
33
+ test('onupdate should call provided callback with 2 pairs for a current moment (from cache)', () => {
34
+ return new Promise(y => {
35
+ const unsubscribe = uniswap_v2_loader.onupdate(pairs => {
36
+ assert.equal(pairs.length, 2)
37
+ unsubscribe()
38
+ y()
39
+ }, {to: 2})
40
+ })
41
+ })
42
+
43
+ test('Heavy test load first 3000 pairs', () =>
44
+ uniswap_v2_loader.all({to: 3000})
45
+ .then(pairs => {
46
+ assert.equal(pairs.length, 3000)
47
+ })
48
+ )