Skip to content
Merged
Changes from 1 commit
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
124 changes: 96 additions & 28 deletions api/swap/tokens/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@ import axios from "axios";
import { constants } from "ethers";
import { type, assert, Infer, optional, array, union } from "superstruct";

import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../../_constants";
import { getRequestId, setRequestSpanAttributes } from "../../_request_utils";
import { sendResponse } from "../../_response_utils";
import { TypedVercelRequest } from "../../_types";
import {
ENABLED_ROUTES,
getFallbackTokenLogoURI,
getLogger,
handleErrorCondition,
paramToArray,
positiveIntStr,
} from "../../_utils";
import { TypedVercelRequest } from "../../_types";

import { tracer, processor } from "../../../instrumentation";
import mainnetChains from "../../../src/data/chains_1.json";
import indirectChains from "../../../src/data/indirect_chains_1.json";
import { getRequestId, setRequestSpanAttributes } from "../../_request_utils";
import { sendResponse } from "../../_response_utils";
import { tracer, processor } from "../../../instrumentation";
import { CHAIN_IDs } from "../../_constants";

type Token = {
chainId: number;
Expand All @@ -31,21 +32,6 @@ type Token = {
const chains = mainnetChains;
const chainIds = [...chains, ...indirectChains].map((chain) => chain.chainId);

// List of tokens that are statically defined locally. Currently, this list is used for
// indirect chain tokens, e.g. USDT-SPOT on HyperCore.
const staticTokens = indirectChains.flatMap((chain) =>
chain.outputTokens.map((token) => ({
chainId: chain.chainId,
address: token.address,
name: token.name,
symbol: token.symbol,
decimals: token.decimals,
logoUrl: token.logoUrl,
// TODO: Add more generic price resolution logic for static tokens
priceUsd: token.symbol === "USDT-SPOT" ? "1" : null,
}))
);

const SwapTokensQueryParamsSchema = type({
chainId: optional(union([positiveIntStr(), array(positiveIntStr())])),
});
Expand Down Expand Up @@ -111,18 +97,28 @@ export default async function handler(
);
responseJson.push(...jupiterTokens);

// Add static tokens
const staticTokens = getStaticTokens(filteredChainIds);
responseJson.push(...staticTokens);
// Add tokens from indirect chains (e.g., USDT-SPOT on HyperCore)
const indirectChainTokens = getIndirectChainTokens(filteredChainIds);
responseJson.push(...indirectChainTokens);

// Add tokens from Across' enabled routes (fills gaps from external sources)
const tokensFromEnabledRoutes = getTokensFromEnabledRoutes(
filteredChainIds,
pricesForLifiTokens
);
responseJson.push(...tokensFromEnabledRoutes);

// Deduplicate tokens (external sources take precedence)
const deduplicatedTokens = deduplicateTokens(responseJson);

logger.debug({
at: "swap/tokens",
message: "Response data",
responseJson,
responseJson: deduplicatedTokens,
});
sendResponse({
response,
body: responseJson,
body: deduplicatedTokens,
statusCode: 200,
requestId,
cacheSeconds: 60 * 5,
Expand Down Expand Up @@ -232,6 +228,78 @@ function getJupiterTokens(
}, []);
}

function getStaticTokens(chainIds: number[]): Token[] {
return staticTokens.filter((token) => chainIds.includes(token.chainId));
function getIndirectChainTokens(chainIds: number[]): Token[] {
return indirectChains.flatMap((chain) => {
if (!chainIds.includes(chain.chainId)) {
return [];
}

return chain.outputTokens.map((token) => ({
chainId: chain.chainId,
address: token.address,
name: token.name,
symbol: token.symbol,
decimals: token.decimals,
logoUrl: token.logoUrl,
// TODO: Add more generic price resolution logic for indirect chain tokens
priceUsd: token.symbol === "USDT-SPOT" ? "1" : null,
}));
});
}

function getTokensFromEnabledRoutes(
chainIds: number[],
pricesForLifiTokens: Record<number, Record<string, string>>
): Token[] {
const tokens: Token[] = [];
const seenTokens = new Set<string>();

ENABLED_ROUTES.routes.forEach((route) => {
if (chainIds.includes(route.fromChain)) {
// Use AddressZero for native tokens, original address otherwise
const tokenAddress = route.isNative
? constants.AddressZero
: route.fromTokenAddress;

const tokenKey = `${route.fromChain}-${route.fromTokenSymbol}-${tokenAddress.toLowerCase()}`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it okay to only look at route.fromTokenSymbol? Would we miss some tokens which are only defined as destination tokens like USDC.e, USDbC, etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! The only token that matched this condition right now was USDbC on Base, which was already included since it’s part of third party lists. Still, to make this more future-proof, I’ve added those here: 873d726


// Only add each unique token once
if (!seenTokens.has(tokenKey)) {
seenTokens.add(tokenKey);

const tokenInfo =
TOKEN_SYMBOLS_MAP[
route.fromTokenSymbol as keyof typeof TOKEN_SYMBOLS_MAP
];

tokens.push({
chainId: route.fromChain,
address: tokenAddress,
name: tokenInfo.name,
symbol: route.fromTokenSymbol,
decimals: tokenInfo.decimals,
logoUrl: getFallbackTokenLogoURI(route.l1TokenAddress),
priceUsd:
pricesForLifiTokens[route.fromChain]?.[tokenAddress] || null,
});
}
}
});

return tokens;
}

function deduplicateTokens(tokens: Token[]): Token[] {
const seen = new Map<string, Token>();

tokens.forEach((token) => {
const key = `${token.chainId}-${token.symbol}-${token.address.toLowerCase()}`;

// Keep first occurrence
if (!seen.has(key)) {
seen.set(key, token);
}
});

return Array.from(seen.values());
}
Loading