fix(token-aggregation): emit Uniswap-schema token lists for mainnet cWUSDC
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m35s
CI/CD Pipeline / Security Scanning (push) Successful in 2m58s
CI/CD Pipeline / Lint and Format (push) Failing after 40s
CI/CD Pipeline / Terraform Validation (push) Failing after 24s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 25s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 56s
Validation / validate-genesis (push) Successful in 32s
Validation / validate-terraform (push) Failing after 30s
Validation / validate-kubernetes (push) Failing after 10s
Validation / validate-smart-contracts (push) Failing after 10s
Validation / validate-security (push) Failing after 1m23s
Validation / validate-documentation (push) Failing after 17s
Verify Deployment / Verify Deployment (push) Failing after 1m0s
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m35s
CI/CD Pipeline / Security Scanning (push) Successful in 2m58s
CI/CD Pipeline / Lint and Format (push) Failing after 40s
CI/CD Pipeline / Terraform Validation (push) Failing after 24s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 25s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 56s
Validation / validate-genesis (push) Successful in 32s
Validation / validate-terraform (push) Failing after 30s
Validation / validate-kubernetes (push) Failing after 10s
Validation / validate-smart-contracts (push) Failing after 10s
Validation / validate-security (push) Failing after 1m23s
Validation / validate-documentation (push) Failing after 17s
Verify Deployment / Verify Deployment (push) Failing after 1m0s
Normalize /report/token-list to checksummed addresses, version objects, and extensions bag; add pinned static ethereum-mainnet list endpoint. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"name": "DBIS Ethereum Mainnet GRU",
|
||||
"version": {
|
||||
"major": 1,
|
||||
"minor": 1,
|
||||
"patch": 0
|
||||
},
|
||||
"timestamp": "2026-06-06T00:00:00.000Z",
|
||||
"logoURI": "https://d-bis.org/tokens/cwusdc.svg",
|
||||
"keywords": [
|
||||
"ethereum",
|
||||
"mainnet",
|
||||
"stablecoin",
|
||||
"gru",
|
||||
"dbis",
|
||||
"cwusdc"
|
||||
],
|
||||
"tokens": [
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a",
|
||||
"name": "Wrapped cUSDC",
|
||||
"symbol": "cWUSDC",
|
||||
"decimals": 6,
|
||||
"logoURI": "https://d-bis.org/tokens/cwusdc.svg",
|
||||
"tags": [
|
||||
"stablecoin",
|
||||
"defi",
|
||||
"compliant",
|
||||
"fiat",
|
||||
"cash",
|
||||
"gru",
|
||||
"wrapped"
|
||||
],
|
||||
"extensions": {
|
||||
"category": "tokenized-fiat",
|
||||
"instrument": "emoney-wrapped-transport",
|
||||
"currency": "USD",
|
||||
"settlement": "fiat",
|
||||
"cashLike": true,
|
||||
"backing": "cash,cash-equivalents,gru-reserve-policy",
|
||||
"gruVersion": "v1",
|
||||
"gruFamily": "cUSDC",
|
||||
"canonicalSourceChainId": 138,
|
||||
"canonicalSourceAddress": "0xf22258f57794CC8E06237084b353Ab30fFfa640b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
"name": "Tether USD",
|
||||
"symbol": "USDT",
|
||||
"decimals": 6,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png",
|
||||
"tags": [
|
||||
"stablecoin",
|
||||
"defi",
|
||||
"fiat",
|
||||
"cash"
|
||||
],
|
||||
"extensions": {
|
||||
"category": "tokenized-fiat",
|
||||
"instrument": "fiat-backed-stablecoin",
|
||||
"currency": "USD",
|
||||
"settlement": "fiat",
|
||||
"cashLike": true,
|
||||
"backing": "cash,cash-equivalents",
|
||||
"x402Ready": false,
|
||||
"fwdCanon": false,
|
||||
"walletClass": "cash-like-token"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": {
|
||||
"stablecoin": {
|
||||
"name": "Stablecoin",
|
||||
"description": "Stable value tokens pegged to fiat"
|
||||
},
|
||||
"defi": {
|
||||
"name": "DeFi",
|
||||
"description": "Decentralized Finance tokens"
|
||||
},
|
||||
"compliant": {
|
||||
"name": "Compliant",
|
||||
"description": "DBIS GRU compliant eMoney transport assets"
|
||||
},
|
||||
"fiat": {
|
||||
"name": "Fiat",
|
||||
"description": "Fiat referenced tokens"
|
||||
},
|
||||
"cash": {
|
||||
"name": "Cashlike",
|
||||
"description": "Cash reserve or cash rail assets"
|
||||
},
|
||||
"gru": {
|
||||
"name": "GRU",
|
||||
"description": "Global Reserve Unit family assets"
|
||||
},
|
||||
"wrapped": {
|
||||
"name": "Wrapped",
|
||||
"description": "Public network wrapped transport mirrors of hub assets"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,9 +482,11 @@ describe('Report API', () => {
|
||||
expect.objectContaining({
|
||||
symbol: 'cUSDT_V2',
|
||||
chainId: 138,
|
||||
familySymbol: 'cUSDT',
|
||||
deploymentVersion: 'v2',
|
||||
preferredForX402: true,
|
||||
extensions: expect.objectContaining({
|
||||
familySymbol: 'cUSDT',
|
||||
deploymentVersion: 'v2',
|
||||
preferredForX402: true,
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
symbol: 'cUSDC',
|
||||
@@ -493,9 +495,11 @@ describe('Report API', () => {
|
||||
expect.objectContaining({
|
||||
symbol: 'cUSDC_V2',
|
||||
chainId: 138,
|
||||
familySymbol: 'cUSDC',
|
||||
deploymentVersion: 'v2',
|
||||
preferredForX402: true,
|
||||
extensions: expect.objectContaining({
|
||||
familySymbol: 'cUSDC',
|
||||
deploymentVersion: 'v2',
|
||||
preferredForX402: true,
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
@@ -598,7 +602,9 @@ describe('Report API', () => {
|
||||
expect.objectContaining({
|
||||
symbol: 'cBTC',
|
||||
chainId: 138,
|
||||
registryFamily: 'monetary_unit',
|
||||
extensions: expect.objectContaining({
|
||||
registryFamily: 'monetary_unit',
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
@@ -611,7 +617,9 @@ describe('Report API', () => {
|
||||
expect.objectContaining({
|
||||
symbol: 'cWBTC',
|
||||
chainId: 1,
|
||||
registryFamily: 'monetary_unit',
|
||||
extensions: expect.objectContaining({
|
||||
registryFamily: 'monetary_unit',
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
@@ -626,12 +634,16 @@ describe('Report API', () => {
|
||||
expect.objectContaining({
|
||||
symbol: 'cETH',
|
||||
chainId: 138,
|
||||
registryFamily: 'gas_native',
|
||||
extensions: expect.objectContaining({
|
||||
registryFamily: 'gas_native',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
symbol: 'cETHL2',
|
||||
chainId: 138,
|
||||
registryFamily: 'gas_native',
|
||||
extensions: expect.objectContaining({
|
||||
registryFamily: 'gas_native',
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
@@ -644,7 +656,9 @@ describe('Report API', () => {
|
||||
expect.objectContaining({
|
||||
symbol: 'cWETHL2',
|
||||
chainId: 10,
|
||||
registryFamily: 'gas_native',
|
||||
extensions: expect.objectContaining({
|
||||
registryFamily: 'gas_native',
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
@@ -657,7 +671,9 @@ describe('Report API', () => {
|
||||
expect.objectContaining({
|
||||
symbol: 'cWETH',
|
||||
chainId: 1,
|
||||
registryFamily: 'gas_native',
|
||||
extensions: expect.objectContaining({
|
||||
registryFamily: 'gas_native',
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
@@ -669,10 +685,40 @@ describe('Report API', () => {
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
|
||||
expect(cwusdc).toMatchObject({
|
||||
name: 'Wrapped cUSDC',
|
||||
address: '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a',
|
||||
logoURI: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/cUSDC$/),
|
||||
originalLogoURI: expect.stringContaining('/token-lists/logos/gru/cUSDC.svg'),
|
||||
extensions: expect.objectContaining({
|
||||
originalLogoURI: expect.stringContaining('/token-lists/logos/gru/cUSDC.svg'),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns Uniswap token list schema fields for mainnet lists', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
expect(body.name).toBe('DBIS Ethereum Mainnet GRU');
|
||||
expect(body.version).toEqual({ major: 1, minor: 0, patch: 0 });
|
||||
expect(body.tokens[0]).not.toHaveProperty('type');
|
||||
});
|
||||
|
||||
it('serves the pinned static ethereum-mainnet token list', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/token-list/static/ethereum-mainnet`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
expect(body.name).toBe('DBIS Ethereum Mainnet GRU');
|
||||
expect(body.version).toEqual({ major: 1, minor: 1, patch: 0 });
|
||||
expect(body.tokens).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
symbol: 'cWUSDC',
|
||||
name: 'Wrapped cUSDC',
|
||||
address: '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/cw-registry', () => {
|
||||
|
||||
@@ -38,6 +38,11 @@ import {
|
||||
import { getGruV2DeploymentPoolRows } from '../../config/gru-v2-deployment-pools';
|
||||
import { getCanonicalPriceSnapshotGeneratedAt, getCanonicalPriceUsd } from '../../services/canonical-price-oracle';
|
||||
import { pmmVaultReserveFromChain, resolvePmmQuoteRpcUrl } from '../../services/pmm-onchain-quote';
|
||||
import {
|
||||
buildUniswapTokenList,
|
||||
MAINNET_PUBLIC_DISPLAY_NAMES,
|
||||
resolveTokenListName,
|
||||
} from '../utils/uniswap-token-list';
|
||||
|
||||
const router: Router = Router();
|
||||
const tokenRepo = new TokenRepository();
|
||||
@@ -628,6 +633,70 @@ function absoluteReportLogoUri(remoteLogoUri: string, symbol: string): string {
|
||||
return localLogoURI ?? remoteLogoUri;
|
||||
}
|
||||
|
||||
function resolveEthereumMainnetTokenListPath(): string | null {
|
||||
const candidates = [
|
||||
process.env.ETHEREUM_MAINNET_TOKEN_LIST_JSON_PATH?.trim(),
|
||||
path.join(__dirname, '../../../config/token-lists/ethereum-mainnet.tokenlist.json'),
|
||||
path.join(process.cwd(), 'config/token-lists/ethereum-mainnet.tokenlist.json'),
|
||||
].filter((value): value is string => Boolean(value));
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function loadStaticEthereumMainnetTokenList(): Record<string, unknown> | null {
|
||||
const filePath = resolveEthereumMainnetTokenListPath();
|
||||
if (!filePath) return null;
|
||||
try {
|
||||
return JSON.parse(readFileSync(filePath, 'utf8')) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
logger.error('Failed to read static ethereum-mainnet token list:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeUniswapTokenListResponse(
|
||||
req: Request,
|
||||
res: Response,
|
||||
payload: Record<string, unknown>,
|
||||
chainIds: number[]
|
||||
): void {
|
||||
const logoURI = absolutePublicUrl(req, typeof payload.logoURI === 'string' ? payload.logoURI : undefined);
|
||||
const tokens = Array.isArray(payload.tokens)
|
||||
? payload.tokens.map((token) => {
|
||||
const record = token as Record<string, unknown>;
|
||||
return {
|
||||
...record,
|
||||
logoURI:
|
||||
absolutePublicUrl(req, typeof record.logoURI === 'string' ? record.logoURI : undefined) ??
|
||||
record.logoURI,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const displayNames = chainIds.length === 1 && chainIds[0] === 1 ? MAINNET_PUBLIC_DISPLAY_NAMES : undefined;
|
||||
const body = buildUniswapTokenList({
|
||||
name: resolveTokenListName(
|
||||
chainIds,
|
||||
typeof payload.name === 'string' ? payload.name : 'GRU Canonical Token List'
|
||||
),
|
||||
version: payload.version,
|
||||
timestamp: typeof payload.timestamp === 'string' ? payload.timestamp : new Date().toISOString(),
|
||||
logoURI,
|
||||
tokens,
|
||||
keywords: Array.isArray(payload.keywords) ? (payload.keywords as string[]) : undefined,
|
||||
tags:
|
||||
payload.tags && typeof payload.tags === 'object' && !Array.isArray(payload.tags)
|
||||
? (payload.tags as Record<string, unknown>)
|
||||
: undefined,
|
||||
displayNames,
|
||||
});
|
||||
|
||||
res.json(body);
|
||||
}
|
||||
|
||||
/** Build token entries with DB market/pool data for a chain */
|
||||
async function buildTokenReport(chainId: number) {
|
||||
const canonical = getCanonicalTokensByChain(chainId);
|
||||
@@ -1511,7 +1580,25 @@ router.get(
|
||||
}
|
||||
);
|
||||
|
||||
/** GET /report/token-list — flat list of all canonical tokens (Uniswap token list format with logoURI).
|
||||
/** GET /report/token-list/static/ethereum-mainnet — pinned Uniswap-schema list for tokenlists.org submissions. */
|
||||
router.get(
|
||||
'/token-list/static/ethereum-mainnet',
|
||||
cacheMiddleware(5 * 60 * 1000),
|
||||
(req: Request, res: Response) => {
|
||||
try {
|
||||
const payload = loadStaticEthereumMainnetTokenList();
|
||||
if (!payload) {
|
||||
return res.status(404).json({ error: 'Static ethereum-mainnet token list not found' });
|
||||
}
|
||||
return finalizeUniswapTokenListResponse(req, res, payload, [1]);
|
||||
} catch (error) {
|
||||
logger.error('Error serving static ethereum-mainnet token list:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/** GET /report/token-list — Uniswap token list schema (version object, checksummed addresses, extensions bag).
|
||||
* If TOKEN_LIST_JSON_URL is set (e.g. GitHub raw URL), fetches and returns that JSON; optional ?chainId= filters tokens.
|
||||
*/
|
||||
router.get(
|
||||
@@ -1519,58 +1606,37 @@ router.get(
|
||||
cacheMiddleware(5 * 60 * 1000),
|
||||
async (req: Request, res: Response) => {
|
||||
try {
|
||||
const tokenListUrl = process.env.TOKEN_LIST_JSON_URL?.trim();
|
||||
if (tokenListUrl) {
|
||||
try {
|
||||
const data = (await fetchRemoteJson(tokenListUrl)) as {
|
||||
name?: string;
|
||||
version?: string;
|
||||
timestamp?: string;
|
||||
logoURI?: string;
|
||||
tokens?: Array<{ chainId?: number; address?: string; symbol?: string; name?: string; decimals?: number; [key: string]: unknown }>;
|
||||
};
|
||||
const chainIdParam = req.query.chainId as string | undefined;
|
||||
const chainIdFilter = chainIdParam ? parseInt(chainIdParam, 10) : null;
|
||||
let tokens = Array.isArray(data.tokens) ? data.tokens : [];
|
||||
if (!isNaN(chainIdFilter as number)) {
|
||||
tokens = tokens.filter((t) => t.chainId === chainIdFilter);
|
||||
}
|
||||
const normalizedTokens = tokens.map((token) => ({
|
||||
...token,
|
||||
logoURI: absolutePublicUrl(req, typeof token.logoURI === 'string' ? token.logoURI : undefined) ?? token.logoURI,
|
||||
}));
|
||||
return res.json({
|
||||
name: data.name ?? 'Token List',
|
||||
version: data.version ?? '1.0.0',
|
||||
timestamp: data.timestamp ?? new Date().toISOString(),
|
||||
logoURI: absolutePublicUrl(req, data.logoURI) ?? data.logoURI,
|
||||
tokens: normalizedTokens,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('TOKEN_LIST_JSON_URL fetch failed, using built-in token list:', err);
|
||||
}
|
||||
}
|
||||
|
||||
const chainIdParam = req.query.chainId as string | undefined;
|
||||
const chainIds = chainIdParam
|
||||
? [parseInt(chainIdParam, 10)].filter((n) => !isNaN(n))
|
||||
: getSupportedChainIds();
|
||||
|
||||
const list: Array<{
|
||||
chainId: number;
|
||||
address: string;
|
||||
symbol: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
type: string;
|
||||
logoURI: string;
|
||||
originalLogoURI?: string;
|
||||
registryFamily?: string;
|
||||
familySymbol?: string;
|
||||
deploymentVersion?: string;
|
||||
deploymentStatus?: string;
|
||||
preferredForX402?: boolean;
|
||||
}> = [];
|
||||
const tokenListUrl = process.env.TOKEN_LIST_JSON_URL?.trim();
|
||||
if (tokenListUrl) {
|
||||
try {
|
||||
const data = (await fetchRemoteJson(tokenListUrl)) as Record<string, unknown>;
|
||||
let tokens = Array.isArray(data.tokens) ? data.tokens : [];
|
||||
if (chainIds.length === 1) {
|
||||
tokens = tokens.filter((token) => {
|
||||
const record = token as { chainId?: number };
|
||||
return record.chainId === chainIds[0];
|
||||
});
|
||||
}
|
||||
return finalizeUniswapTokenListResponse(
|
||||
req,
|
||||
res,
|
||||
{
|
||||
...data,
|
||||
tokens,
|
||||
},
|
||||
chainIds
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error('TOKEN_LIST_JSON_URL fetch failed, using built-in token list:', err);
|
||||
}
|
||||
}
|
||||
|
||||
const list: Array<Record<string, unknown>> = [];
|
||||
|
||||
for (const chainId of chainIds) {
|
||||
const specs = getCanonicalTokensByChain(chainId);
|
||||
@@ -1580,7 +1646,7 @@ router.get(
|
||||
const originalLogoURI = getLogoUriForSpec(spec);
|
||||
list.push({
|
||||
chainId,
|
||||
address: address.toLowerCase(),
|
||||
address,
|
||||
symbol: spec.symbol,
|
||||
name: spec.name,
|
||||
decimals: spec.decimals,
|
||||
@@ -1597,13 +1663,18 @@ router.get(
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
name: 'GRU Canonical Token List',
|
||||
version: '1.0.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
logoURI: absolutePublicUrl(req, DBIS_CHAIN_138_LOGO_PATH),
|
||||
tokens: list,
|
||||
});
|
||||
return finalizeUniswapTokenListResponse(
|
||||
req,
|
||||
res,
|
||||
{
|
||||
name: 'GRU Canonical Token List',
|
||||
version: { major: 1, minor: 0, patch: 0 },
|
||||
timestamp: new Date().toISOString(),
|
||||
logoURI: absolutePublicUrl(req, DBIS_CHAIN_138_LOGO_PATH),
|
||||
tokens: list,
|
||||
},
|
||||
chainIds
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Error building report/token-list:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
|
||||
@@ -203,6 +203,7 @@ export class ApiServer {
|
||||
tokenMappingPairs: '/api/v1/token-mapping/pairs',
|
||||
tokenMappingResolve: '/api/v1/token-mapping/resolve',
|
||||
reportTokenList: '/api/v1/report/token-list',
|
||||
reportTokenListEthereumMainnet: '/api/v1/report/token-list/static/ethereum-mainnet',
|
||||
routesTree: '/api/v1/routes/tree',
|
||||
plannerProvidersCapabilities: '/api/v2/providers/capabilities',
|
||||
plannerRoutesPlan: '/api/v2/routes/plan',
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
buildUniswapTokenList,
|
||||
checksumTokenAddress,
|
||||
normalizeTokenListVersion,
|
||||
toUniswapTokenListToken,
|
||||
} from './uniswap-token-list';
|
||||
|
||||
describe('uniswap-token-list utils', () => {
|
||||
it('normalizes string versions into major/minor/patch objects', () => {
|
||||
expect(normalizeTokenListVersion('1.2.3')).toEqual({ major: 1, minor: 2, patch: 3 });
|
||||
expect(normalizeTokenListVersion({ major: 4, minor: 5, patch: 6 })).toEqual({
|
||||
major: 4,
|
||||
minor: 5,
|
||||
patch: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it('moves non-schema token fields into extensions and checksums addresses', () => {
|
||||
const token = toUniswapTokenListToken(
|
||||
{
|
||||
chainId: 1,
|
||||
address: '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a',
|
||||
symbol: 'cWUSDC',
|
||||
name: 'USD Coin (Compliant Wrapped ISO-4217 M1)',
|
||||
decimals: 6,
|
||||
logoURI: 'https://d-bis.org/tokens/cwusdc.svg',
|
||||
type: 'w',
|
||||
registryFamily: 'iso4217',
|
||||
originalLogoURI: 'https://example.com/logo.svg',
|
||||
},
|
||||
{ displayNames: { cWUSDC: 'Wrapped cUSDC' } }
|
||||
);
|
||||
|
||||
expect(token).toMatchObject({
|
||||
chainId: 1,
|
||||
address: '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a',
|
||||
symbol: 'cWUSDC',
|
||||
name: 'Wrapped cUSDC',
|
||||
decimals: 6,
|
||||
logoURI: 'https://d-bis.org/tokens/cwusdc.svg',
|
||||
extensions: {
|
||||
type: 'w',
|
||||
registryFamily: 'iso4217',
|
||||
originalLogoURI: 'https://example.com/logo.svg',
|
||||
},
|
||||
});
|
||||
expect(token).not.toHaveProperty('type');
|
||||
expect(token).not.toHaveProperty('registryFamily');
|
||||
expect(checksumTokenAddress('0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a')).toBe(
|
||||
'0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a'
|
||||
);
|
||||
});
|
||||
|
||||
it('builds a schema-compliant list envelope', () => {
|
||||
const list = buildUniswapTokenList({
|
||||
name: 'DBIS Ethereum Mainnet GRU',
|
||||
version: '1.1.0',
|
||||
timestamp: '2026-06-06T00:00:00.000Z',
|
||||
logoURI: 'https://d-bis.org/tokens/cwusdc.svg',
|
||||
tokens: [
|
||||
{
|
||||
chainId: 1,
|
||||
address: '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a',
|
||||
symbol: 'cWUSDC',
|
||||
name: 'Wrapped cUSDC',
|
||||
decimals: 6,
|
||||
logoURI: 'https://d-bis.org/tokens/cwusdc.svg',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(list.version).toEqual({ major: 1, minor: 1, patch: 0 });
|
||||
expect(Array.isArray(list.tokens)).toBe(true);
|
||||
expect((list.tokens as unknown[]).length).toBe(1);
|
||||
});
|
||||
});
|
||||
119
services/token-aggregation/src/api/utils/uniswap-token-list.ts
Normal file
119
services/token-aggregation/src/api/utils/uniswap-token-list.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { getAddress } from 'ethers';
|
||||
|
||||
export type UniswapTokenListVersion = {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
};
|
||||
|
||||
const TOKEN_EXTENSION_TOP_LEVEL_KEYS = [
|
||||
'type',
|
||||
'originalLogoURI',
|
||||
'registryFamily',
|
||||
'familySymbol',
|
||||
'deploymentVersion',
|
||||
'deploymentStatus',
|
||||
'preferredForX402',
|
||||
] as const;
|
||||
|
||||
/** Wallet/registry-facing names for mainnet transport tokens. */
|
||||
export const MAINNET_PUBLIC_DISPLAY_NAMES: Record<string, string> = {
|
||||
cWUSDC: 'Wrapped cUSDC',
|
||||
cWUSDT: 'Wrapped cUSDT',
|
||||
};
|
||||
|
||||
export function normalizeTokenListVersion(version: unknown): UniswapTokenListVersion {
|
||||
if (version && typeof version === 'object' && 'major' in version) {
|
||||
const record = version as { major?: unknown; minor?: unknown; patch?: unknown };
|
||||
return {
|
||||
major: Number(record.major ?? 1),
|
||||
minor: Number(record.minor ?? 0),
|
||||
patch: Number(record.patch ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof version === 'string' && version.trim() !== '') {
|
||||
const [major = '1', minor = '0', patch = '0'] = version.trim().split('.');
|
||||
return {
|
||||
major: Number.parseInt(major, 10) || 1,
|
||||
minor: Number.parseInt(minor, 10) || 0,
|
||||
patch: Number.parseInt(patch, 10) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
return { major: 1, minor: 0, patch: 0 };
|
||||
}
|
||||
|
||||
export function checksumTokenAddress(address: string): string {
|
||||
try {
|
||||
return getAddress(address);
|
||||
} catch {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
export function toUniswapTokenListToken(
|
||||
token: Record<string, unknown>,
|
||||
options?: { displayNames?: Record<string, string> }
|
||||
): Record<string, unknown> {
|
||||
const extensions: Record<string, unknown> = {};
|
||||
|
||||
for (const key of TOKEN_EXTENSION_TOP_LEVEL_KEYS) {
|
||||
if (token[key] !== undefined) {
|
||||
extensions[key] = token[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (token.extensions && typeof token.extensions === 'object' && !Array.isArray(token.extensions)) {
|
||||
Object.assign(extensions, token.extensions as Record<string, unknown>);
|
||||
}
|
||||
|
||||
const symbol = String(token.symbol ?? '');
|
||||
const displayName = options?.displayNames?.[symbol];
|
||||
const out: Record<string, unknown> = {
|
||||
chainId: token.chainId,
|
||||
address: checksumTokenAddress(String(token.address ?? '')),
|
||||
name: displayName ?? token.name,
|
||||
symbol,
|
||||
decimals: token.decimals,
|
||||
};
|
||||
|
||||
if (token.logoURI) out.logoURI = token.logoURI;
|
||||
if (Array.isArray(token.tags) && token.tags.length > 0) out.tags = token.tags;
|
||||
if (Object.keys(extensions).length > 0) out.extensions = extensions;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildUniswapTokenList(params: {
|
||||
name: string;
|
||||
version: unknown;
|
||||
timestamp: string;
|
||||
logoURI?: string;
|
||||
tokens: Array<Record<string, unknown>>;
|
||||
keywords?: string[];
|
||||
tags?: Record<string, unknown>;
|
||||
displayNames?: Record<string, string>;
|
||||
}): Record<string, unknown> {
|
||||
const body: Record<string, unknown> = {
|
||||
name: params.name,
|
||||
timestamp: params.timestamp,
|
||||
version: normalizeTokenListVersion(params.version),
|
||||
tokens: params.tokens.map((token) =>
|
||||
toUniswapTokenListToken(token, { displayNames: params.displayNames })
|
||||
),
|
||||
};
|
||||
|
||||
if (params.logoURI) body.logoURI = params.logoURI;
|
||||
if (params.keywords?.length) body.keywords = params.keywords;
|
||||
if (params.tags && Object.keys(params.tags).length > 0) body.tags = params.tags;
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
export function resolveTokenListName(chainIds: number[], defaultName: string): string {
|
||||
if (chainIds.length === 1 && chainIds[0] === 1) {
|
||||
return 'DBIS Ethereum Mainnet GRU';
|
||||
}
|
||||
return defaultName;
|
||||
}
|
||||
Reference in New Issue
Block a user