|
1 | | -import { aptosChainId, ASSET_NAMESPACE, toAssetId } from '@shapeshiftoss/caip' |
| 1 | +import { aptosChainId } from '@shapeshiftoss/caip' |
2 | 2 | import type { Asset } from '@shapeshiftoss/types' |
3 | 3 | import { aptos, unfreeze } from '@shapeshiftoss/utils' |
4 | | -import axios from 'axios' |
5 | 4 | import uniqBy from 'lodash/uniqBy' |
6 | 5 |
|
7 | | -const PANORA_API_BASE = 'https://api.panora.exchange' |
| 6 | +import * as coingecko from '../coingecko' |
8 | 7 |
|
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)]) |
36 | 10 |
|
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 [] |
40 | 15 | }) |
41 | 16 |
|
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)) |
77 | 23 |
|
78 | | - const allAssets = uniqBy(panoraAssets, 'assetId') |
| 24 | + const allAssets = uniqBy(tokensOnly, 'assetId') |
79 | 25 |
|
80 | 26 | return [unfreeze(aptos), ...allAssets] |
81 | 27 | } |
0 commit comments