Skip to content

Commit f37f911

Browse files
swdiscordiaclaude
andcommitted
refactor(aptos): use CoinGecko tokenlist for asset generation
Replaces the bespoke Panora-based fetcher with the same CoinGecko tokenlist pattern used by every other non-EVM chain (Solana, Sui, Near, Ton, Tron). Removes the VITE_PANORA_API_KEY requirement from the asset generation pipeline. CoinGecko exposes two different platform slugs for Aptos: their /api/v3 endpoints (and our CoinGecko adapter) use 'aptos-network', but the static tokenlist at tokens.coingecko.com only responds to 'aptos' ('aptos-network' returns 403 there). Hardcoded the right slug in scripts/generateAssetData/coingecko.ts to keep the existing 'aptos-network' adapter value untouched for API consumers. Verified `pnpm run generate:chain aptos` returns 70 assets from CoinGecko without any API key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 59b339d commit f37f911

2 files changed

Lines changed: 20 additions & 70 deletions

File tree

Lines changed: 15 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,27 @@
1-
import { aptosChainId, ASSET_NAMESPACE, toAssetId } from '@shapeshiftoss/caip'
1+
import { aptosChainId } from '@shapeshiftoss/caip'
22
import type { Asset } from '@shapeshiftoss/types'
33
import { aptos, unfreeze } from '@shapeshiftoss/utils'
4-
import axios from 'axios'
54
import uniqBy from 'lodash/uniqBy'
65

7-
const PANORA_API_BASE = 'https://api.panora.exchange'
6+
import * as coingecko from '../coingecko'
87

9-
// Aptos has no public CoinGecko token list (their tokens endpoint returns 403),
10-
// so we use Panora's tokenlist as the source of truth for tradeable assets.
11-
// Panora is the DEX aggregator we integrate with, so this matches what users can swap.
12-
13-
type PanoraToken = {
14-
chainId: number
15-
tokenAddress: string | null
16-
faAddress: string | null
17-
name: string
18-
symbol: string
19-
decimals: number
20-
logoUrl: string | null
21-
panoraUI: boolean
22-
panoraTags: string[] | null
23-
isBanned: boolean
24-
}
25-
26-
type PanoraResponse = { status: string; data: PanoraToken[] } | PanoraToken[]
27-
28-
// Trusted tag set — excludes "Emojicoin" / "Meme"-only spam.
29-
// Per Panora's taxonomy: Native/Verified are official, Bridged is canonical cross-chain
30-
// (e.g. LayerZero USDC), Recognized/RWA cover BUIDL etc.
31-
const TRUSTED_TAGS = new Set(['Verified', 'Recognized', 'RWA', 'Bridged'])
32-
33-
const fetchPanoraTokens = async (): Promise<Asset[]> => {
34-
const apiKey = process.env.VITE_PANORA_API_KEY
35-
if (!apiKey) throw new Error('Missing VITE_PANORA_API_KEY - source ~/.zshrc or set it')
8+
export const getAssets = async (): Promise<Asset[]> => {
9+
const results = await Promise.allSettled([coingecko.getAssets(aptosChainId)])
3610

37-
const { data } = await axios.get<PanoraResponse>(`${PANORA_API_BASE}/tokenlist`, {
38-
headers: { 'x-api-key': apiKey },
39-
timeout: 30000,
11+
const [assets] = results.map(result => {
12+
if (result.status === 'fulfilled') return result.value
13+
console.error(result.reason)
14+
return []
4015
})
4116

42-
const tokens = Array.isArray(data) ? data : data.data
43-
44-
return tokens
45-
.filter(t => t.panoraUI && !t.isBanned)
46-
.filter(t => (t.panoraTags ?? []).some(tag => TRUSTED_TAGS.has(tag)))
47-
.filter(t => Boolean(t.faAddress || t.tokenAddress))
48-
.filter(t => t.tokenAddress !== '0x1::aptos_coin::AptosCoin' && t.faAddress !== '0xa')
49-
.map(token => {
50-
// Prefer faAddress (modern Aptos Fungible Asset standard, universally supported by Panora).
51-
// Falls back to tokenAddress for legacy coin-standard-only tokens.
52-
const assetReference = (token.faAddress ?? token.tokenAddress) as string
53-
const assetId = toAssetId({
54-
chainId: aptosChainId,
55-
assetNamespace: ASSET_NAMESPACE.aptosCoin,
56-
assetReference,
57-
})
58-
const asset: Asset = {
59-
assetId,
60-
chainId: aptosChainId,
61-
name: token.name,
62-
symbol: token.symbol,
63-
precision: token.decimals,
64-
color: aptos.color,
65-
icon: token.logoUrl ?? '',
66-
explorer: aptos.explorer,
67-
explorerAddressLink: aptos.explorerAddressLink,
68-
explorerTxLink: aptos.explorerTxLink,
69-
relatedAssetKey: null,
70-
}
71-
return asset
72-
})
73-
}
74-
75-
export const getAssets = async (): Promise<Asset[]> => {
76-
const panoraAssets = await fetchPanoraTokens()
17+
// Filter out the native APT token from CoinGecko to avoid duplicates
18+
// CoinGecko includes native APT both as the slip44 base asset (added manually) and
19+
// as the coin-standard token (0x1::aptos_coin::AptosCoin) at the same metadata
20+
// address 0xa under the aptosCoin namespace.
21+
const nativeAptCoinPattern = /^aptos:[^/]+\/aptosCoin:(0x0*a|0x1::aptos_coin::AptosCoin)$/i
22+
const tokensOnly = assets.filter(asset => !nativeAptCoinPattern.test(asset.assetId))
7723

78-
const allAssets = uniqBy(panoraAssets, 'assetId')
24+
const allAssets = uniqBy(tokensOnly, 'assetId')
7925

8026
return [unfreeze(aptos), ...allAssets]
8127
}

scripts/generateAssetData/coingecko.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,11 @@ export async function getAssets(chainId: ChainId): Promise<Asset[]> {
448448
case aptosChainId:
449449
return {
450450
assetNamespace: ASSET_NAMESPACE.aptosCoin,
451-
category: adapters.chainIdToCoingeckoAssetPlatform(chainId),
451+
// CoinGecko uses two different platform slugs for Aptos: their /api/v3
452+
// endpoints (and our adapter) use 'aptos-network', but the static tokenlist
453+
// at tokens.coingecko.com only responds to 'aptos' (the 'aptos-network'
454+
// slug returns 403 there). Hardcoded to keep the token fetch working.
455+
category: 'aptos',
452456
explorer: aptos.explorer,
453457
explorerAddressLink: aptos.explorerAddressLink,
454458
explorerTxLink: aptos.explorerTxLink,

0 commit comments

Comments
 (0)