update token lists (#743)

This commit is contained in:
Felicio
2025-07-10 20:40:26 +09:00
committed by GitHub
parent 264da5076f
commit af9e83f72c
6 changed files with 117409 additions and 1304 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@status-im/wallet': patch
---
update token lists
@@ -23,6 +23,10 @@ const standardTokenLists = [
url: 'https://static.optimism.io/optimism.tokenlist.json',
},
{ name: 'Arbitrum', url: 'https://bridge.arbitrum.io/token-list-42161.json' },
{
name: 'CoinMarketCap',
url: 'https://api.coinmarketcap.com/data-api/v3/uniswap/all.json',
},
// {
// name: 'Base',
// },
@@ -32,10 +36,18 @@ const standardTokenLists = [
// {
// name: 'BSC',
// },
// {
// name: 'Aave',
// url: 'https://raw.githubusercontent.com/bgd-labs/aave-address-book/main/tokenlist.json',
// },
{
name: 'Aave',
url: 'https://raw.githubusercontent.com/bgd-labs/aave-address-book/main/tokenlist.json',
},
{
name: 'CoinGecko',
url: 'https://tokens.coingecko.com/uniswap/all.json',
},
{
name: 'Gemini',
url: 'https://www.gemini.com/uniswap/manifest.json',
},
]
// will assume non-checksummed addresses and always lowercase them
@@ -67,27 +79,42 @@ function toChecksumAddress(address) {
async function fetchStandardTokenList(url) {
try {
const response = await fetch(url)
console.log(`Fetching token list from: ${url}`)
const response = await fetch(url, {
headers: {
'User-Agent': 'Status-Wallet-Token-List-Generator/1.0',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
console.log(
`Successfully fetched ${data.tokens?.length || data.length} tokens from ${url}`,
)
return data.tokens || data
} catch (error) {
console.error(`Error fetching from ${url}:`, error)
console.error(`Error fetching from ${url}:`, error.message)
return []
}
}
async function fetchStatusTokenLists(url) {
try {
const response = await fetch(url)
console.log(`Fetching Status token list from: ${url}`)
const response = await fetch(url, {
headers: {
'User-Agent': 'Status-Wallet-Token-List-Generator/1.0',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.text()
const content = await response.text()
console.log(`Successfully fetched Status token list from ${url}`)
return content
} catch (error) {
console.error(`Error fetching Go file from ${url}:`, error)
console.error(`Error fetching Go file from ${url}:`, error.message)
return ''
}
}
@@ -149,7 +176,13 @@ function generateTemplate(tokens) {
symbol: token.symbol,
decimals: token.decimals,
logoURI: token.logoURI,
...(token.extensions && { extensions: token.extensions }),
...(token.extensions &&
token.extensions.bridgeInfo &&
Object.keys(token.extensions.bridgeInfo).length > 0 && {
extensions: {
bridgeInfo: token.extensions.bridgeInfo,
},
}),
})),
}
@@ -212,31 +245,202 @@ function logDuplicateSymbols(tokensWithSameSymbol) {
console.log()
}
function extendTokensWithBridgeInfo(tokens) {
const tokensByAddress = new Map()
const tokensByChainAndAddress = new Map()
// Index tokens by address and by chain+address
tokens.forEach(token => {
const address = token.address.toLowerCase()
tokensByAddress.set(address, token)
const chainKey = `${token.chainId}-${address}`
tokensByChainAndAddress.set(chainKey, token)
})
// First pass: collect all bridge info from existing tokens
const bridgeInfoMap = new Map()
tokens.forEach(token => {
if (token.extensions?.bridgeInfo) {
const address = token.address.toLowerCase()
if (!bridgeInfoMap.has(address)) {
bridgeInfoMap.set(address, new Map())
}
Object.entries(token.extensions.bridgeInfo).forEach(
([chainId, bridgeData]) => {
if (bridgeData.tokenAddress) {
bridgeInfoMap.get(address).set(chainId, {
tokenAddress: bridgeData.tokenAddress,
// originBridgeAddress: bridgeData.originBridgeAddress,
// destBridgeAddress: bridgeData.destBridgeAddress,
})
}
},
)
}
// Also check for l1Address in extensions
if (token.extensions?.l1Address) {
const l1Address = token.extensions.l1Address.toLowerCase()
if (!bridgeInfoMap.has(l1Address)) {
bridgeInfoMap.set(l1Address, new Map())
}
// Add bridge info from L2 to L1
bridgeInfoMap.get(l1Address).set(token.chainId.toString(), {
tokenAddress: token.address,
// originBridgeAddress: token.extensions.l2GatewayAddress,
// destBridgeAddress: token.extensions.l1GatewayAddress,
})
}
})
// Second pass: extend tokens with bridge info
tokens.forEach(token => {
const address = token.address.toLowerCase()
if (!token.extensions) {
token.extensions = {}
}
if (!token.extensions.bridgeInfo) {
token.extensions.bridgeInfo = {}
}
// Add bridge info for this token
if (bridgeInfoMap.has(address)) {
bridgeInfoMap.get(address).forEach((bridgeData, chainId) => {
if (chainId !== token.chainId.toString()) {
token.extensions.bridgeInfo[chainId] = bridgeData
}
})
}
// // For L1 tokens, also add l1Address extension
// if (token.chainId === 1 && !token.extensions.l1Address) {
// token.extensions.l1Address = token.address
// }
})
return tokens
}
async function main() {
console.log('Starting ERC20 token list generation...')
const statusTokens = new Set()
for (const url of statusTokenLists) {
const content = await fetchStatusTokenLists(url)
const tokens = parseGoFile(content)
// Fetch Status token lists in parallel
console.log('Fetching Status token lists in parallel...')
const statusFetchPromises = statusTokenLists.map(async url => {
try {
const content = await fetchStatusTokenLists(url)
if (content) {
const tokens = parseGoFile(content)
return tokens
}
return []
} catch (error) {
console.error(
`Error fetching/parsing Status tokens from ${url}:`,
error.message,
)
return []
}
})
const statusResults = await Promise.all(statusFetchPromises)
// Combine all Status tokens
statusResults.forEach(tokens => {
tokens.forEach(token => statusTokens.add(token.toLowerCase()))
}
})
let standardTokens = new Map()
for (const list of standardTokenLists) {
const tokens = await fetchStandardTokenList(list.url)
tokens.forEach(token => {
const checksumAddress = toChecksumAddress(token.address)
if (!standardTokens.has(checksumAddress)) {
token.address = checksumAddress
standardTokens.set(checksumAddress, token)
}
})
let successfulFetches = 0
// Fetch all token lists in parallel to minimize rate limits and improve performance
console.log('Fetching standard token lists in parallel...')
const fetchPromises = standardTokenLists.map(async (list, index) => {
// Add small delay between requests to be respectful to rate limits
if (index > 0) {
await new Promise(resolve => setTimeout(resolve, 100 * index))
}
try {
const tokens = await fetchStandardTokenList(list.url)
return { list, tokens, success: tokens.length > 0 }
} catch (error) {
console.error(`Failed to fetch ${list.name}:`, error.message)
return { list, tokens: [], success: false }
}
})
const results = await Promise.all(fetchPromises)
// Process results sequentially to avoid memory issues with large datasets
for (const { list, tokens, success } of results) {
if (success) {
successfulFetches++
console.log(`Processing ${tokens.length} tokens from ${list.name}...`)
tokens.forEach(token => {
const checksumAddress = toChecksumAddress(token.address)
if (!standardTokens.has(checksumAddress)) {
token.address = checksumAddress
standardTokens.set(checksumAddress, token)
}
})
}
}
console.log(
`Successfully fetched ${successfulFetches}/${standardTokenLists.length} token lists`,
)
if (standardTokens.size === 0) {
console.error('No tokens fetched from any source. Exiting.')
process.exit(1)
}
const combinedStandardTokens = Array.from(standardTokens.values())
// Extend tokens with bridge information
const extendedTokens = extendTokensWithBridgeInfo(combinedStandardTokens)
// todo: set extensions.bridgeInfo on each token and per chain based on all token lists and if they list
// note: from arbitrum token list
// {
// "extensions": {
// "bridgeInfo": {
// "1": {
// "tokenAddress": "0x469eda64aed3a3ad6f868c44564291aa415cb1d9",
// "originBridgeAddress": "0x096760F208390250649E3e8763348E783AEF5562",
// "destBridgeAddress": "0xcee284f754e854890e311e3280b767f80797180d"
// }
// },
// "l1Address": "0x469eda64aed3a3ad6f868c44564291aa415cb1d9",
// "l2GatewayAddress": "0x096760F208390250649E3e8763348E783AEF5562",
// "l1GatewayAddress": "0xcee284f754e854890e311e3280b767f80797180d"
// }
// }
// note: from other token lists
// "extensions": {
// "bridgeInfo": {
// "42161": {
// "tokenAddress": "0x63806C056Fa458c548Fb416B15E358A9D685710A"
// }
// }
// }
// todo: set extensions.coingecko.id on each token based on coingecko api response
const {
filteredTokens: standardTokensListedByStatus,
missingTokens: standardTokensNotListedByStatus,
} = compareTokens(combinedStandardTokens, statusTokens)
} = compareTokens(extendedTokens, statusTokens)
const standardTokensUsedByStatus = standardTokensListedByStatus.filter(
token => supportedNetworks.includes(token.chainId),
@@ -256,16 +460,18 @@ async function main() {
const tokensWithSameSymbol = []
const symbolMap = new Map()
standardTokensUsedByStatus.forEach(token => {
// standardTokensUsedByStatus.forEach(token => {
// console.log(combinedStandardTokens)
extendedTokens.forEach(token => {
const tokenWithSameSymbol = symbolMap.get(token.symbol)
if (tokenWithSameSymbol && tokenWithSameSymbol.chainId === token.chainId) {
tokensWithSameSymbol.push(tokenWithSameSymbol)
} else {
symbolMap.set(token.symbol, [token])
symbolMap.set(token.symbol, token)
}
})
const templateContent = generateTemplate(standardTokensUsedByStatus)
const templateContent = generateTemplate(extendedTokens)
await writeToFile(templateContent, outputFilePath)
logDifferences(differences)
@@ -1,6 +1,6 @@
'use client'
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { Avatar, useToast } from '@status-im/components'
import { FeesIcon } from '@status-im/icons/12'
@@ -0,0 +1,196 @@
{
"$schema": "https://raw.githubusercontent.com/Uniswap/token-lists/v1.0.0-beta.32/src/tokenlist.schema.json",
"name": "Status Portfolio ERC20 Token List",
"timestamp": "2025-05-28T08:31:41.152Z",
"version": {
"major": 0,
"minor": 1,
"patch": 0
},
// note: would list l1 first prior reduicing other sources
"tokens": [
{
"chainId": 1, // Ethereum Mainnet
"address": "0xBFDE5ac4f5Adb419A931a5bF64B0f3BB5a623d06",
"name": "Flux",
"symbol": "FLUX",
"decimals": 18,
"logoURI": "https://assets.coingecko.com/coins/images/50870/thumb/FLUXCoinGecko.png?1729398545"
},
{
"chainId": 1,
"address": "0x720CD16b011b987Da3518fbf38c3071d4F0D1495",
"name": "Flux",
"symbol": "FLUX",
"decimals": 18,
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x720CD16b011b987Da3518fbf38c3071d4F0D1495/logo.png",
"extensions": {
"bridgeInfo": {
// note: not confirmed or deleted
// "8453": {
// "tokenAddress": "0xb008BDCF9CdFf9da684a190941dC3dCa8C2Cdd44"
// },
"42161": {
"tokenAddress": "0x63806C056Fa458c548Fb416B15E358A9D685710A"
}
}
}
},
{
"chainId": 1,
"address": "0x469eDA64aEd3A3Ad6f868c44564291aA415cB1d9",
"name": "Datamine FLUX",
"symbol": "FLUX",
"decimals": 18,
"logoURI": "https://s2.coinmarketcap.com/static/img/coins/64x64/5876.png",
// note: added manually
"extensions": {
"bridgeInfo": {
"42161": {
"tokenAddress": "0xF80D589b3Dbe130c270a69F1a69D050f268786Df"
}
}
}
},
{
"chainId": 1,
"address": "0x7645DdfEecedA57e41f92679c4aCd83c56A81D14",
"name": "Flux Protocol",
"symbol": "FLUX",
"decimals": 18,
"logoURI": "https://s2.coinmarketcap.com/static/img/coins/64x64/9837.png",
// note: added manually
"extensions": {
"bridgeInfo": {
"42161": {
"tokenAddress": "0x2338a5d62E9A766289934e8d2e83a443e8065b83"
}
}
}
},
{
"chainId": 8453, // Base
"address": "0xb008BDCF9CdFf9da684a190941dC3dCa8C2Cdd44",
"name": "Flux",
"symbol": "FLUX",
"decimals": 18,
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x720CD16b011b987Da3518fbf38c3071d4F0D1495/logo.png",
"extensions": {
"bridgeInfo": {
// note: not confirmed or deleted
// "1": {
// "tokenAddress": "0x720CD16b011b987Da3518fbf38c3071d4F0D1495"
// }
}
}
},
{
"chainId": 43114, // Avalanche C-Chain
"address": "0xc4B06F17ECcB2215a5DBf042C672101Fc20daF55",
"name": "Flux",
"symbol": "FLUX",
"decimals": 8,
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x720CD16b011b987Da3518fbf38c3071d4F0D1495/logo.png"
},
{
"chainId": 42161, // Arbitrum One
"address": "0x63806C056Fa458c548Fb416B15E358A9D685710A",
"name": "Flux",
"symbol": "FLUX",
"decimals": 18,
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x720CD16b011b987Da3518fbf38c3071d4F0D1495/logo.png",
"extensions": {
"bridgeInfo": {
"1": {
"tokenAddress": "0x720CD16b011b987Da3518fbf38c3071d4F0D1495"
}
}
}
},
{
"chainId": 42161, // Arbitrum One
"address": "0xF80D589b3Dbe130c270a69F1a69D050f268786Df",
"name": "Flux",
"symbol": "FLUX",
"decimals": 18,
"logoURI": "https://s2.coinmarketcap.com/static/img/coins/64x64/5876.png",
"extensions": {
"bridgeInfo": {
"1": {
"tokenAddress": "0x469eda64aed3a3ad6f868c44564291aa415cb1d9"
// "originBridgeAddress": "0x096760F208390250649E3e8763348E783AEF5562",
// "destBridgeAddress": "0xcee284f754e854890e311e3280b767f80797180d"
}
},
"l1Address": "0x469eda64aed3a3ad6f868c44564291aa415cb1d9"
// "l2GatewayAddress": "0x096760F208390250649E3e8763348E783AEF5562",
// "l1GatewayAddress": "0xcee284f754e854890e311e3280b767f80797180d"
}
},
{
"chainId": 42161, // Arbitrum One
"address": "0x2338a5d62E9A766289934e8d2e83a443e8065b83",
"name": "Flux Protocol",
"symbol": "FLUX",
"decimals": 18,
"logoURI": "https://s2.coinmarketcap.com/static/img/coins/64x64/9837.png",
"extensions": {
"bridgeInfo": {
"1": {
"tokenAddress": "0x7645ddfeeceda57e41f92679c4acd83c56a81d14"
// "originBridgeAddress": "0x09e9222E96E7B4AE2a407B98d48e330053351EEe",
// "destBridgeAddress": "0xa3a7b6f88361f48403514059f1f16c8e78d60eec"
}
},
"l1Address": "0x7645ddfeeceda57e41f92679c4acd83c56a81d14"
// "l2GatewayAddress": "0x09e9222E96E7B4AE2a407B98d48e330053351EEe",
// "l1GatewayAddress": "0xa3a7b6f88361f48403514059f1f16c8e78d60eec"
}
},
// note: manually transformed from coingecko api responses
// note: would log as invalid
{
"name": "Flux",
"symbol": "flux",
"extensions": {
"coingecko": {
"id": "zelcash"
},
"bridgeInfo": {}
}
},
{
// note: manually added
"chainId": 1,
// note: manually added
"address": "0x469eDA64aEd3A3Ad6f868c44564291aA415cB1d9",
"symbol": "flux",
"name": "Datamine FLUX",
"extensions": {
"coingecko": {
"id": "flux"
},
"bridgeInfo": {
// note: duplicate
// "1": {
// "tokenAddress": "0x469eda64aed3a3ad6f868c44564291aa415cb1d9"
// },
"42161": {
"tokenAddress": "0xf80d589b3dbe130c270a69f1a69d050f268786df"
}
}
}
},
{
"chainId": 1, // Ethereum Mainnet
"address": "0xBFDE5ac4f5Adb419A931a5bF64B0f3BB5a623d06",
"symbol": "flux",
"name": "Flux",
"extensions": {
"coingecko": {
"id": "flux-2"
}
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -302,7 +302,7 @@ async function all({
acc[network].push(token)
return acc
},
{} as Record<NetworkType, typeof erc20TokenList.tokens>,
{} as Record<NetworkType, ERC20Token[]>,
)
const partialERC20Assets: Map<
@@ -313,7 +313,9 @@ async function all({
>
> = new Map()
for (const [network, tokens] of Object.entries(ERC20TokensByNetwork)) {
for (const [network, tokens] of Object.entries(ERC20TokensByNetwork) as Array<
[string, ERC20Token[]]
>) {
for (let i = 0; i < tokens.length; i += 100) {
const batch = tokens.slice(i, i + 100)
const batchBalances = await getERC20TokensBalance(