Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 107 additions & 5 deletions src/scripts/chains-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,45 @@ interface ChainMetadata {
slip44?: number
}

/**
* Normalizes a chain name for comparison by lowercasing and removing spaces,
* hyphens, and other non-alphanumeric characters.
* e.g. "zkSync Sepolia Testnet" and "zksync-sepolia-testnet" both become "zksyncsepoliatestnet"
*/
function normalizeChainName(name: string): string {
return name.toLowerCase().replace(/[\s\-_]/g, "")
}

type ChainMatchResult =
| { matched: true; reason: "name" | "nativeCurrency" }
| { matched: false; existingName: string | undefined }

/**
* When chainid.network flags a chain as reusedChainId, determines whether the incoming
* entry is the same chain we already support, using two signals:
* 1. Name match (normalized) — primary check
* 2. nativeCurrency.symbol match — secondary check for renamed chains
* Returns the match reason so callers can log appropriately.
*/
function matchAgainstExistingChain(incomingChain: ChainMetadata): ChainMatchResult {
const existing = (currentChainsMetadata as ChainMetadata[]).find((c) => c.chainId === incomingChain.chainId)
if (!existing) return { matched: false, existingName: undefined }

if (normalizeChainName(existing.name) === normalizeChainName(incomingChain.name)) {
return { matched: true, reason: "name" }
}

if (
existing.nativeCurrency?.symbol &&
incomingChain.nativeCurrency?.symbol &&
existing.nativeCurrency.symbol === incomingChain.nativeCurrency.symbol
) {
return { matched: true, reason: "nativeCurrency" }
}

return { matched: false, existingName: existing.name }
}

// Type guard functions
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
Expand Down Expand Up @@ -391,15 +430,41 @@ async function handleSpecificChains(chainIds: number[]): Promise<void> {
throw new ValidationError("Expected an array of chain metadata", data)
}

// Pre-filter the chains we're interested in
const requestedChains = data.filter(
(item) => isRecord(item) && typeof item.chainId === "number" && chainIds.includes(item.chainId)
)
// Pre-filter the chains we're interested in, skipping any that chainid.network
// has flagged as a reused chain ID to prevent overwriting our known-correct entries.
const skippedReusedIds = new Set<number>()
const requestedChains = data.filter((item) => {
if (!isRecord(item) || typeof item.chainId !== "number") return false
if (!chainIds.includes(item.chainId)) return false
const isReusedId = Array.isArray(item.redFlags) && item.redFlags.includes("reusedChainId")
if (isReusedId) {
const match = matchAgainstExistingChain(item as unknown as ChainMetadata)
if (match.matched) {
const detail =
match.reason === "nativeCurrency"
? `name changed ("${match.reason}" matched on nativeCurrency symbol "${(item as unknown as ChainMetadata).nativeCurrency?.symbol}")`
: "name matched"
console.log(
`Chain ${item.chainId} ("${item.name}") is flagged as reusedChainId but ${detail} — allowing update`
)
return true
}
console.warn(
`Skipping chain ${item.chainId} ("${item.name}"): flagged as reusedChainId and does not match our existing entry ("${match.existingName ?? "none"}") by name or nativeCurrency`
)
skippedReusedIds.add(item.chainId)
}
return !isReusedId
})

// Now validate only the chains we care about
const validatedChains = validateChainMetadataArray(requestedChains)

if (validatedChains.length === 0) {
if (skippedReusedIds.size > 0) {
console.log(`\nAll requested chain IDs were skipped due to reusedChainId conflicts. No updates made.`)
return
}
throw new ValidationError("No valid chains found to update", chainIds)
}

Expand Down Expand Up @@ -470,11 +535,48 @@ async function handleFullComparison(): Promise<void> {
const getSupportedChainsMetadata = async (): Promise<ChainMetadata[]> => {
try {
const chainsMetadata = await getChainsMetadata()
// Track chain IDs that are skipped due to reusedChainId conflicts so we can
// preserve the existing entry rather than letting it disappear from the output.
const preservedChainIds = new Set<number>()

const supportedChainsMetadata = chainsMetadata.filter((chainMetadata) => {
if (!chainMetadata.chainId) {
throw new ValidationError("Chain metadata missing chainId", chainMetadata)
}
return chainMetadata.chainId.toString() in linkNameSymbol
if (!(chainMetadata.chainId.toString() in linkNameSymbol)) return false
// For chains flagged as reusedChainId by chainid.network, check whether the incoming
// entry is the same chain we already support. If names match, allow the update through
// (e.g. explorer URL changed). If names differ, it's a different chain squatting on
// the same ID — preserve our existing entry instead of deleting or overwriting it.
const isReusedId = chainMetadata.redFlags?.includes("reusedChainId") ?? false
if (isReusedId) {
const match = matchAgainstExistingChain(chainMetadata)
if (match.matched) {
const detail =
match.reason === "nativeCurrency"
? `name changed ("${match.reason}" matched on nativeCurrency symbol "${chainMetadata.nativeCurrency?.symbol}")`
: "name matched"
console.log(
`Chain ${chainMetadata.chainId} ("${chainMetadata.name}") is flagged as reusedChainId but ${detail} — allowing update`
)
return true
}
console.warn(
`Skipping chain ${chainMetadata.chainId} ("${chainMetadata.name}"): flagged as reusedChainId and does not match our existing entry ("${match.existingName ?? "none"}") by name or nativeCurrency — preserving existing entry`
)
preservedChainIds.add(chainMetadata.chainId)
return false
}
return true
})

// Re-inject preserved entries so they are not lost in the full replacement.
preservedChainIds.forEach((chainId) => {
const existing = (currentChainsMetadata as ChainMetadata[]).find((c) => c.chainId === chainId)
if (existing) {
supportedChainsMetadata.push(existing)
console.log(`Preserved existing entry for chain ${chainId} ("${existing.name}")`)
}
})

return supportedChainsMetadata.sort((a, b) => a.chainId - b.chainId)
Expand Down
Loading