Skip to content

Commit 73a1d1a

Browse files
committed
feat: fetch-tokenlist | working update export default tokens
1 parent 40f1010 commit 73a1d1a

File tree

3 files changed

+321
-0
lines changed

3 files changed

+321
-0
lines changed

.env.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
INFURA_KEY=xxx
22
ALCHEMY_KEY=xxx # For zkEVM
33
COINGECKO_API_KEY=xxx
4+
GITHUB_TOKEN="github_pat_..."

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"scripts": {
66
"generate": "ts-node src/generator.ts",
77
"tokenlist:create": "node src/lib/scripts/create-tokenlist.js",
8+
"tokenlist:fetch": "node src/lib/scripts/fetch-tokenlist.js",
89
"lint": "eslint . --ext .js,.ts --max-warnings 0",
910
"lint:fix": "npm run lint -- --fix",
1011
"test": "vitest run",

src/lib/scripts/fetch-tokenlist.js

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
#! /usr/bin/env node
2+
/* eslint-disable @typescript-eslint/no-var-requires */
3+
const commander = require('commander')
4+
const chalk = require('chalk')
5+
const fs = require('fs-extra')
6+
const path = require('path')
7+
const { execSync } = require('child_process')
8+
9+
// Configuration
10+
const CONFIG = {
11+
SOURCE_URL:
12+
'https://raw.githubusercontent.com/burrbear-dev/default-lists/main/src/tokens/mainnet/defaultTokenList.json',
13+
TARGET_TOKEN_FILE: 'src/tokenlists/balancer/tokens/berachain.ts',
14+
ASSETS_DIR: 'src/assets/images/tokens',
15+
LOG_FILE: 'fetch-tokenlist.log',
16+
}
17+
18+
// GitHub authentication
19+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN
20+
21+
// Statistics tracking
22+
const stats = {
23+
totalTokens: 0,
24+
successfulDownloads: 0,
25+
failedDownloads: 0,
26+
skippedDownloads: 0,
27+
errors: [],
28+
}
29+
30+
/**
31+
* Execute curl command and return result
32+
*/
33+
function curlGet(url, options = {}) {
34+
try {
35+
const curlOptions = [
36+
'-s', // silent mode
37+
'-L', // follow redirects
38+
'--max-time',
39+
'30', // 30 second timeout
40+
'--retry',
41+
'3', // retry 3 times
42+
'--retry-delay',
43+
'2', // wait 2 seconds between retries
44+
]
45+
46+
// Add GitHub authentication if token is provided
47+
if (
48+
GITHUB_TOKEN &&
49+
(url.includes('github.com') || url.includes('raw.githubusercontent.com'))
50+
) {
51+
curlOptions.push('-H', `Authorization: token ${GITHUB_TOKEN}`)
52+
}
53+
54+
if (options.output) {
55+
curlOptions.push('-o', options.output)
56+
}
57+
58+
// Build command with proper escaping
59+
const command = `curl ${curlOptions.join(' ')} "${url}"`
60+
const result = execSync(command, { encoding: 'utf8', shell: true })
61+
62+
return { success: true, data: result }
63+
} catch (error) {
64+
return {
65+
success: false,
66+
error: error.message,
67+
command: `curl ${options.output ? '-o ' + options.output : ''} "${url}"`,
68+
}
69+
}
70+
}
71+
72+
/**
73+
* Extract filename from URL
74+
*/
75+
function extractFilenameFromUrl(url) {
76+
try {
77+
const urlObj = new URL(url)
78+
const pathname = urlObj.pathname
79+
return path.basename(pathname)
80+
} catch (error) {
81+
console.error(chalk.red(`Failed to parse URL: ${url}`))
82+
return null
83+
}
84+
}
85+
86+
/**
87+
* Validate Ethereum address format
88+
*/
89+
function isValidEthereumAddress(address) {
90+
return /^0x[a-fA-F0-9]{40}$/.test(address)
91+
}
92+
93+
/**
94+
* Log message with timestamp
95+
*/
96+
function log(message, type = 'info') {
97+
const timestamp = new Date().toISOString()
98+
const logMessage = `[${timestamp}] ${message}`
99+
100+
switch (type) {
101+
case 'error':
102+
console.error(chalk.red(logMessage))
103+
stats.errors.push(logMessage)
104+
break
105+
case 'success':
106+
console.log(chalk.green(logMessage))
107+
break
108+
case 'warning':
109+
console.log(chalk.yellow(logMessage))
110+
break
111+
default:
112+
console.log(chalk.blue(logMessage))
113+
}
114+
115+
// Append to log file
116+
fs.appendFileSync(CONFIG.LOG_FILE, logMessage + '\n')
117+
}
118+
119+
/**
120+
* Download and parse the source token list using curl
121+
*/
122+
async function fetchTokenList() {
123+
log('Fetching token list from source using curl...')
124+
125+
try {
126+
const result = curlGet(CONFIG.SOURCE_URL)
127+
128+
if (!result.success) {
129+
throw new Error(`Failed to fetch token list: ${result.error}`)
130+
}
131+
132+
const tokenList = JSON.parse(result.data)
133+
134+
if (!tokenList.tokens || !Array.isArray(tokenList.tokens)) {
135+
throw new Error(
136+
'Invalid token list format: missing or invalid tokens array'
137+
)
138+
}
139+
140+
stats.totalTokens = tokenList.tokens.length
141+
log(`Successfully fetched ${stats.totalTokens} tokens using curl`)
142+
143+
return tokenList.tokens
144+
} catch (error) {
145+
log(`Failed to fetch token list: ${error.message}`, 'error')
146+
throw error
147+
}
148+
}
149+
150+
/**
151+
* Process tokens and extract addresses
152+
*/
153+
function processTokens(tokens) {
154+
log('Processing tokens...')
155+
156+
const tokenAddresses = []
157+
const validTokens = []
158+
159+
for (const token of tokens) {
160+
if (!token.address) {
161+
log(`Token missing address: ${token.symbol || 'unknown'}`, 'warning')
162+
continue
163+
}
164+
165+
if (!isValidEthereumAddress(token.address)) {
166+
log(`Invalid address format: ${token.address}`, 'warning')
167+
continue
168+
}
169+
170+
tokenAddresses.push(token.address)
171+
validTokens.push(token)
172+
}
173+
174+
// Remove duplicates
175+
const uniqueAddresses = [...new Set(tokenAddresses)]
176+
log(`Found ${uniqueAddresses.length} unique valid addresses`)
177+
178+
return { tokenAddresses: uniqueAddresses, validTokens }
179+
}
180+
181+
/**
182+
* Update the berachain token list file
183+
*/
184+
async function updateTokenList(tokenAddresses) {
185+
log('Updating berachain token list...')
186+
187+
try {
188+
const targetFile = path.resolve(CONFIG.TARGET_TOKEN_FILE)
189+
190+
// Read existing content
191+
let existingContent = ''
192+
let existingAddresses = []
193+
194+
if (fs.existsSync(targetFile)) {
195+
existingContent = fs.readFileSync(targetFile, 'utf8')
196+
197+
// Extract existing addresses from array format
198+
const existingMatch = existingContent.match(/export default \[(.*)\]/s)
199+
200+
if (existingMatch) {
201+
existingAddresses = existingMatch[1]
202+
.split(',')
203+
.map((addr) => addr.trim().replace(/['"]/g, ''))
204+
.filter((addr) => addr && addr.length > 0)
205+
}
206+
}
207+
208+
// Merge addresses using Set to ensure uniqueness
209+
const allAddresses = [...new Set([...existingAddresses, ...tokenAddresses])]
210+
211+
// Create new content
212+
const newContent = `export default [\n${allAddresses
213+
.map((addr) => ` '${addr}'`)
214+
.join(',\n')}\n]`
215+
216+
// Backup original file
217+
if (fs.existsSync(targetFile)) {
218+
fs.copyFileSync(targetFile, `${targetFile}.backup`)
219+
}
220+
221+
// Write new content
222+
fs.writeFileSync(targetFile, newContent)
223+
224+
log(
225+
`Successfully updated token list with ${allAddresses.length} addresses (${existingAddresses.length} existing + ${tokenAddresses.length} new)`
226+
)
227+
} catch (error) {
228+
log(`Failed to update token list: ${error.message}`, 'error')
229+
throw error
230+
}
231+
}
232+
233+
/**
234+
* Print summary report
235+
*/
236+
function printSummary() {
237+
console.log('\n' + '='.repeat(50))
238+
console.log(chalk.cyan('FETCH TOKENLIST SUMMARY'))
239+
console.log('='.repeat(50))
240+
console.log(`Total tokens processed: ${stats.totalTokens}`)
241+
console.log(`Token list updated successfully!`)
242+
243+
if (stats.errors.length > 0) {
244+
console.log(`\n${chalk.red('Errors:')}`)
245+
stats.errors.forEach((error) => console.log(` ${error}`))
246+
}
247+
248+
console.log(`\nLog file: ${CONFIG.LOG_FILE}`)
249+
console.log('='.repeat(50))
250+
}
251+
252+
/**
253+
* Main workflow function
254+
*/
255+
async function integrateTokenList() {
256+
log('Starting token list integration workflow...')
257+
258+
// Check for GitHub token
259+
if (!GITHUB_TOKEN) {
260+
log(
261+
'Warning: No GITHUB_TOKEN provided. This may fail if the repository is private.',
262+
'warning'
263+
)
264+
}
265+
266+
try {
267+
// Clear log file
268+
fs.writeFileSync(CONFIG.LOG_FILE, '')
269+
270+
// Step 1: Fetch token list using curl
271+
const tokens = await fetchTokenList()
272+
273+
// Step 2: Process tokens
274+
const { tokenAddresses, validTokens } = processTokens(tokens)
275+
276+
// Step 3: Update token list file
277+
await updateTokenList(tokenAddresses)
278+
279+
// Step 4: Print summary
280+
printSummary()
281+
282+
log('Token list integration completed successfully!', 'success')
283+
} catch (error) {
284+
log(`Workflow failed: ${error.message}`, 'error')
285+
process.exit(1)
286+
}
287+
}
288+
289+
/**
290+
* CLI setup
291+
*/
292+
async function init() {
293+
const program = new commander.Command()
294+
.version('1.0.0')
295+
.name('npm run tokenlist:fetch')
296+
.description(
297+
'Fetch and integrate tokens from default-lists repository using curl'
298+
)
299+
.option('-f, --force', 'Force download even if files exist')
300+
.option('-v, --verbose', 'Enable verbose logging')
301+
.parse(process.argv)
302+
303+
const options = program.opts()
304+
305+
if (options.force) {
306+
log('Force mode enabled - will overwrite existing files')
307+
}
308+
309+
if (options.verbose) {
310+
log('Verbose mode enabled')
311+
}
312+
313+
await integrateTokenList()
314+
}
315+
316+
// Run the script
317+
;(async () => {
318+
await init()
319+
})()

0 commit comments

Comments
 (0)