Skip to content

Commit 045e826

Browse files
authored
feat: generate separate testnet token list (#40)
* feat: generate separate testnet token list * style: format generated testnet tokenlist changes
1 parent f0929f3 commit 045e826

6 files changed

Lines changed: 116 additions & 31 deletions

File tree

README.md

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
# MegaETH Token List
22

3-
The official token registry for the MegaETH ecosystem. This repository maintains a curated list of tokens deployed on MegaETH and their corresponding addresses on other chains for bridging and cross-chain tracking.
3+
The official token registry for the MegaETH ecosystem. This repository maintains curated token lists for MegaETH and their corresponding addresses on other chains for bridging and cross-chain tracking.
44

5-
The generated tokenlist follows the [Uniswap Token List](https://github.com/Uniswap/token-lists) standard with MegaETH-specific extensions for tracking bridge mechanics and token origins.
5+
The generated tokenlists follow the [Uniswap Token List](https://github.com/Uniswap/token-lists) standard with MegaETH-specific extensions for tracking bridge mechanics and token origins.
66

77
## Supported Chains
88

9-
| Chain | Chain ID | Type | Description |
10-
| -------- | -------- | ------ | --------------------------------------- |
11-
| Ethereum | 1 | L1 | Ethereum mainnet |
12-
| MegaETH | 4326 | L2 | MegaETH mainnet |
13-
| Solana | - | Source | Non-EVM source chain for bridged assets |
9+
| Chain | Chain ID | Type | Description |
10+
| --------------- | -------- | ------ | --------------------------------------- |
11+
| Ethereum | 1 | L1 | Ethereum mainnet |
12+
| MegaETH | 4326 | L2 | MegaETH mainnet |
13+
| MegaETH Testnet | 6343 | L2 | MegaETH testnet |
14+
| Solana | - | Source | Non-EVM source chain for bridged assets |
1415

15-
> **Note:** Only EVM chains (Ethereum, MegaETH) appear in the generated tokenlist. Non-EVM chains like Solana are tracked as source chains — their addresses appear in the `extensions` field.
16+
> **Note:** This repo produces separate outputs for mainnet and testnet. The mainnet tokenlist includes only Ethereum + MegaETH mainnet entries. Testnet tokens are emitted only in the separate testnet list to avoid contaminating production consumers.
1617
1718
---
1819

@@ -25,6 +26,11 @@ The generated tokenlist follows the [Uniswap Token List](https://github.com/Unis
2526
3. Add `logo.svg` or `logo.png` (256×256 recommended)
2627
4. Submit a PR
2728

29+
### Generated Outputs
30+
31+
- `megaeth.tokenlist.json` — mainnet list (Ethereum + MegaETH mainnet)
32+
- `megaeth.testnet.tokenlist.json` — testnet list (MegaETH testnet only)
33+
2834
---
2935

3036
## Token Data Schema
@@ -41,6 +47,15 @@ The generated tokenlist follows the [Uniswap Token List](https://github.com/Unis
4147

4248
### Per-Chain Fields
4349

50+
Each chain entry in `tokens` supports.
51+
52+
Supported keys today:
53+
54+
- `ethereum`
55+
- `megaeth`
56+
- `megaeth_testnet`
57+
- `solana` (source tracking only, not emitted directly into tokenlists)
58+
4459
Each chain entry in `tokens` supports:
4560

4661
| Field | Type | Description |

megaeth.testnet.tokenlist.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "MegaETH Testnet Token List",
3+
"timestamp": "2026-04-22T00:58:21.755Z",
4+
"version": {
5+
"major": 1,
6+
"minor": 0,
7+
"patch": 0
8+
},
9+
"tokens": []
10+
}

megaeth.tokenlist.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "MegaETH Token List",
3-
"timestamp": "2026-02-24T01:41:27.206Z",
3+
"timestamp": "2026-04-22T00:58:21.752Z",
44
"version": {
55
"major": 1,
66
"minor": 0,

src/__tests__/generate.test.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CHAINS, CHAIN_IDS, L2_TO_L1 } from '../chains';
1+
import { CHAINS, CHAIN_IDS, L2_TO_L1, TOKENLIST_TARGET_CHAINS } from '../chains';
22
import { generate } from '../generate';
33

44
describe('Chain Configuration', () => {
@@ -15,10 +15,21 @@ describe('Chain Configuration', () => {
1515
test('MegaETH L2 maps to Ethereum L1', () => {
1616
expect(L2_TO_L1.megaeth).toBe('ethereum');
1717
});
18+
19+
test('MegaETH testnet chain ID is 6343', () => {
20+
expect(CHAIN_IDS.megaeth_testnet).toBe(6343);
21+
expect(CHAINS.megaeth_testnet.id).toBe(6343);
22+
});
23+
24+
test('tokenlist targets separate mainnet and testnet chains', () => {
25+
expect(TOKENLIST_TARGET_CHAINS.mainnet).toEqual(['ethereum', 'megaeth']);
26+
expect(TOKENLIST_TARGET_CHAINS.testnet).toEqual(['megaeth_testnet']);
27+
});
1828
});
1929

2030
describe('Token List Generation', () => {
2131
const tokenList = generate();
32+
const testnetTokenList = generate('testnet');
2233

2334
test('generates valid token list structure', () => {
2435
expect(tokenList).toHaveProperty('name', 'MegaETH Token List');
@@ -28,6 +39,14 @@ describe('Token List Generation', () => {
2839
expect(Array.isArray(tokenList.tokens)).toBe(true);
2940
});
3041

42+
test('generates separate testnet token list structure', () => {
43+
expect(testnetTokenList).toHaveProperty('name', 'MegaETH Testnet Token List');
44+
expect(testnetTokenList).toHaveProperty('timestamp');
45+
expect(testnetTokenList).toHaveProperty('version');
46+
expect(testnetTokenList).toHaveProperty('tokens');
47+
expect(Array.isArray(testnetTokenList.tokens)).toBe(true);
48+
});
49+
3150
test('version has correct structure', () => {
3251
expect(tokenList.version).toHaveProperty('major');
3352
expect(tokenList.version).toHaveProperty('minor');
@@ -58,14 +77,12 @@ describe('Token List Generation', () => {
5877
}
5978
});
6079

61-
test('native tokens have isOrigin: true', () => {
62-
const nativeTokens = tokenList.tokens.filter((t) => t.extensions.isOrigin);
63-
expect(nativeTokens.length).toBeGreaterThan(0);
80+
test('origin tokens have isOrigin: true', () => {
81+
const originTokens = tokenList.tokens.filter((t) => t.extensions.isOrigin);
82+
expect(originTokens.length).toBeGreaterThan(0);
6483

65-
for (const token of nativeTokens) {
84+
for (const token of originTokens) {
6685
expect(token.extensions.isOrigin).toBe(true);
67-
expect(token.extensions.bridgeAddress).toBeUndefined();
68-
expect(token.extensions.bridgeType).toBeUndefined();
6986
}
7087
});
7188

@@ -74,7 +91,7 @@ describe('Token List Generation', () => {
7491

7592
for (const token of tokensWithLogos) {
7693
expect(token.logoURI).toMatch(
77-
/^https:\/\/raw\.githubusercontent\.com\/megaeth-labs\/mega-tokenlist\/main\/data\/\w+\/logo\.(svg|png)$/
94+
/^https:\/\/raw\.githubusercontent\.com\/megaeth-labs\/mega-tokenlist\/main\/data\/.+\/logo\.(svg|png)$/
7895
);
7996
}
8097
});
@@ -87,10 +104,16 @@ describe('Token List Generation', () => {
87104
expect(chainIds).toEqual([1, 4326]);
88105
});
89106

90-
test('WETH token exists only on MegaETH', () => {
107+
test('WETH token includes a MegaETH mainnet entry', () => {
91108
const wethTokens = tokenList.tokens.filter((t) => t.symbol === 'WETH');
92-
expect(wethTokens.length).toBe(1);
93-
expect(wethTokens[0].chainId).toBe(4326);
94-
expect(wethTokens[0].extensions.isOrigin).toBe(true);
109+
expect(wethTokens.some((t) => t.chainId === 4326)).toBe(true);
110+
});
111+
112+
test('mainnet token list excludes testnet chain ids', () => {
113+
expect(tokenList.tokens.every((t) => t.chainId !== 6343)).toBe(true);
114+
});
115+
116+
test('testnet token list only contains testnet chain ids', () => {
117+
expect(testnetTokenList.tokens.every((t) => t.chainId === 6343)).toBe(true);
95118
});
96119
});

src/chains.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,42 @@ export const CHAINS = {
66
name: 'Ethereum',
77
layer: 1,
88
evmCompatible: true,
9+
environment: 'mainnet',
910
},
1011
megaeth: {
1112
id: 4326,
1213
name: 'MegaETH',
1314
layer: 2,
1415
evmCompatible: true,
16+
environment: 'mainnet',
17+
},
18+
megaeth_testnet: {
19+
id: 6343,
20+
name: 'MegaETH Testnet',
21+
layer: 2,
22+
evmCompatible: true,
23+
environment: 'testnet',
1524
},
1625
solana: {
1726
id: null, // Non-EVM chain, uses string identifier
1827
name: 'Solana',
1928
layer: 1,
2029
evmCompatible: false,
30+
environment: 'mainnet',
2131
},
2232
} as const
2333

2434
export type Chain = keyof typeof CHAINS
25-
export type EvmChain = 'ethereum' | 'megaeth'
35+
export type EvmChain = 'ethereum' | 'megaeth' | 'megaeth_testnet'
2636
export type SourceChain = 'solana' // Non-EVM chains used only for source tracking
2737
export type L1Chain = 'ethereum'
28-
export type L2Chain = 'megaeth'
38+
export type L2Chain = 'megaeth' | 'megaeth_testnet'
2939

3040
// Chain ID lookup (EVM chains only - used for tokenlist generation)
3141
export const CHAIN_IDS: Record<EvmChain, number> = {
3242
ethereum: 1,
3343
megaeth: 4326,
44+
megaeth_testnet: 6343,
3445
}
3546

3647
// Source chains (non-EVM) - used for tracking bridged asset origins
@@ -39,4 +50,15 @@ export const SOURCE_CHAINS: readonly SourceChain[] = ['solana'] as const
3950
// L2 to L1 mapping (for bridge relationships)
4051
export const L2_TO_L1: Record<L2Chain, L1Chain> = {
4152
megaeth: 'ethereum',
53+
megaeth_testnet: 'ethereum',
54+
}
55+
56+
export type TokenListTarget = 'mainnet' | 'testnet'
57+
58+
export const TOKENLIST_TARGET_CHAINS: Record<
59+
TokenListTarget,
60+
readonly EvmChain[]
61+
> = {
62+
mainnet: ['ethereum', 'megaeth'],
63+
testnet: ['megaeth_testnet'],
4264
}

src/generate.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import * as path from 'path'
33
import {
44
CHAIN_IDS,
55
SOURCE_CHAINS,
6+
TOKENLIST_TARGET_CHAINS,
67
type EvmChain,
78
type SourceChain,
9+
type TokenListTarget,
810
} from './chains'
911
import type {
1012
Mechanism,
@@ -15,7 +17,10 @@ import type {
1517
} from './types'
1618

1719
const DATA_DIR = path.join(__dirname, '..', 'data')
18-
const OUTPUT_FILE = path.join(__dirname, '..', 'megaeth.tokenlist.json')
20+
const OUTPUT_FILES: Record<TokenListTarget, string> = {
21+
mainnet: path.join(__dirname, '..', 'megaeth.tokenlist.json'),
22+
testnet: path.join(__dirname, '..', 'megaeth.testnet.tokenlist.json'),
23+
}
1924
const LOGO_BASE_URL =
2025
'https://raw.githubusercontent.com/megaeth-labs/mega-tokenlist/main/data'
2126

@@ -103,7 +108,7 @@ function inferMechanism(
103108
return 'unknown'
104109
}
105110

106-
export function generate(): TokenList {
111+
export function generate(target: TokenListTarget = 'mainnet'): TokenList {
107112
// Read all token directories
108113
const tokenDirs = fs
109114
.readdirSync(DATA_DIR)
@@ -114,6 +119,7 @@ export function generate(): TokenList {
114119
.sort()
115120

116121
const tokens: TokenListToken[] = []
122+
const includedChains = new Set<EvmChain>(TOKENLIST_TARGET_CHAINS[target])
117123

118124
for (const symbol of tokenDirs) {
119125
const tokenDir = path.join(DATA_DIR, symbol)
@@ -126,8 +132,9 @@ export function generate(): TokenList {
126132
for (const [chain, chainToken] of Object.entries(tokenData.tokens)) {
127133
if (!chainToken?.address) continue
128134

129-
const chainId = CHAIN_IDS[chain as EvmChain]
130-
if (!chainId) continue
135+
const evmChain = chain as EvmChain
136+
const chainId = CHAIN_IDS[evmChain]
137+
if (!chainId || !includedChains.has(evmChain)) continue
131138

132139
const isOrigin = chainToken.isOrigin === true
133140
const mechanism = inferMechanism(chainToken, isOrigin)
@@ -189,7 +196,10 @@ export function generate(): TokenList {
189196
})
190197

191198
const tokenList: TokenList = {
192-
name: 'MegaETH Token List',
199+
name:
200+
target === 'mainnet'
201+
? 'MegaETH Token List'
202+
: 'MegaETH Testnet Token List',
193203
timestamp: new Date().toISOString(),
194204
version: {
195205
major: 1,
@@ -204,7 +214,12 @@ export function generate(): TokenList {
204214

205215
// Main execution
206216
if (require.main === module) {
207-
const tokenList = generate()
208-
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(tokenList, null, 2))
209-
console.log(`Generated ${OUTPUT_FILE} with ${tokenList.tokens.length} tokens`)
217+
for (const target of ['mainnet', 'testnet'] as const) {
218+
const tokenList = generate(target)
219+
const outputFile = OUTPUT_FILES[target]
220+
fs.writeFileSync(outputFile, JSON.stringify(tokenList, null, 2))
221+
console.log(
222+
`Generated ${outputFile} with ${tokenList.tokens.length} tokens`
223+
)
224+
}
210225
}

0 commit comments

Comments
 (0)