File size: 8,984 Bytes
bde2f3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
"""
CoinStats, Mobula, and CryptoNews DataBus Providers
===================================================
Three free/freemium API providers for enhanced crypto intelligence.

1. CoinStats β€” Per-wallet DeFi resolution across 10K+ protocols
2. Mobula β€” Long-tail DEX token pricing (10K free credits/month)
3. CryptoNews β€” Free unlimited news API (no key needed)
"""

import logging

logger = logging.getLogger("databus.api_providers")

# ═══════════════════════════════════════════════════════════════
# 1. COINSTATS β€” Per-Wallet DeFi Resolution
# Free tier: public API, no key needed for basic endpoints
# ═══════════════════════════════════════════════════════════════

COINSTATS_BASE = "https://openapiv1.coinstats.app"


async def fetch_coinstats_wallet(address: str, chain: str = "ethereum") -> dict:
    """Resolve a wallet's complete DeFi position β€” collateral, borrows, LPs, rewards."""
    import aiohttp

    try:
        api_key = __import__("os").environ.get("COINSTATS_API_KEY", "")
        headers = {"X-API-KEY": api_key} if api_key else {}

        async with aiohttp.ClientSession() as session:
            # Wallet balance + DeFi positions
            url = f"{COINSTATS_BASE}/wallet/v1/balance/history/{address}"
            params = {"chain": chain, "limit": 1}

            async with session.get(
                url, params=params, headers=headers, timeout=aiohttp.ClientTimeout(total=15)
            ) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    return {
                        "address": address,
                        "chain": chain,
                        "balance": data,
                        "source": "CoinStats (free tier)",
                        "url": "https://coinstats.app/api-docs",
                    }

            # Fallback: try DeFi-specific endpoint
            url2 = f"{COINSTATS_BASE}/defi/v1/positions/{address}"
            async with session.get(
                url2, headers=headers, timeout=aiohttp.ClientTimeout(total=15)
            ) as resp:
                if resp.status == 200:
                    return {
                        "address": address,
                        "defi_positions": await resp.json(),
                        "source": "CoinStats DeFi (free tier)",
                    }

            return {
                "address": address,
                "error": f"CoinStats returned {resp.status}",
                "source": "CoinStats",
            }

    except Exception as e:
        logger.warning(f"CoinStats fetch failed: {e}")
        return {"error": str(e), "source": "CoinStats"}


# ═══════════════════════════════════════════════════════════════
# 2. MOBULA β€” Long-tail DEX Token Pricing
# Free tier: 10,000 credits/month, no rate limit
# ═══════════════════════════════════════════════════════════════

MOBULA_BASE = "https://api.mobula.io/api/1"


async def fetch_mobula_market(
    asset: str | None = None, blockchain: str | None = None, limit: int = 20
) -> dict:
    """Fetch market data from Mobula β€” covers long-tail tokens missed by CMC/CG."""
    import aiohttp

    mobula_key = __import__("os").environ.get("MOBULA_API_KEY", "")

    try:
        async with aiohttp.ClientSession() as session:
            headers = {"Authorization": mobula_key} if mobula_key else {}

            # Multi-data market endpoint
            url = f"{MOBULA_BASE}/market/multi-data"
            params = {"limit": limit}

            if asset:
                params["assets"] = asset
            if blockchain:
                params["blockchain"] = blockchain

            async with session.get(
                url, params=params, headers=headers, timeout=aiohttp.ClientTimeout(total=15)
            ) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    tokens = data.get("data", [])
                    return {
                        "tokens": tokens[:limit],
                        "count": len(tokens),
                        "query": {"asset": asset, "blockchain": blockchain},
                        "source": "Mobula (free tier β€” 10K credits/month)",
                        "url": "https://docs.mobula.io",
                        "credits_remaining": resp.headers.get("x-credits-remaining", "unknown"),
                    }

            return {"error": f"Mobula returned {resp.status}", "source": "Mobula"}

    except Exception as e:
        logger.warning(f"Mobula fetch failed: {e}")
        return {"error": str(e), "source": "Mobula"}


async def fetch_mobula_wallet(address: str, blockchain: str = "ethereum") -> dict:
    """Fetch wallet portfolio via Mobula β€” balances, tokens, transaction history."""
    import aiohttp

    mobula_key = __import__("os").environ.get("MOBULA_API_KEY", "")

    try:
        async with aiohttp.ClientSession() as session:
            headers = {"Authorization": mobula_key} if mobula_key else {}

            url = f"{MOBULA_BASE}/wallet/portfolio"
            params = {"wallet": address, "blockchain": blockchain}

            async with session.get(
                url, params=params, headers=headers, timeout=aiohttp.ClientTimeout(total=15)
            ) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    return {
                        "address": address,
                        "blockchain": blockchain,
                        "portfolio": data.get("data", data),
                        "source": "Mobula Wallet (free tier)",
                    }

            return {
                "address": address,
                "error": f"Mobula wallet returned {resp.status}",
                "source": "Mobula",
            }

    except Exception as e:
        logger.warning(f"Mobula wallet fetch failed: {e}")
        return {"error": str(e), "source": "Mobula"}


# ═══════════════════════════════════════════════════════════════
# 3. CRYPTONEWS (cryptocurrency.cv) β€” Free unlimited news API
# No API key, no rate limits, REST + RSS
# ═══════════════════════════════════════════════════════════════

CRYPTONEWS_BASE = "https://cryptocurrency.cv/api"


async def fetch_crypto_news(
    category: str | None = None, source: str | None = None, limit: int = 20
) -> dict:
    """Fetch crypto news from cryptocurrency.cv β€” free, no key, unlimited."""
    import aiohttp

    try:
        async with aiohttp.ClientSession() as session:
            # Try REST API
            url = f"{CRYPTONEWS_BASE}/v1/news"
            params = {"limit": limit}
            if category:
                params["category"] = category
            if source:
                params["source"] = source

            async with session.get(
                url, params=params, timeout=aiohttp.ClientTimeout(total=15)
            ) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    articles = data.get("articles", data.get("news", data.get("data", [])))
                    return {
                        "articles": articles[:limit] if isinstance(articles, list) else [],
                        "count": len(articles) if isinstance(articles, list) else 0,
                        "category": category,
                        "source": "cryptocurrency.cv (free, no API key)",
                        "url": "https://github.com/nirholas/cryptocurrency.cv",
                        "features": "RSS/Atom feeds, JSON REST, MCP server, Python SDK",
                    }

            # Fallback: try RSS feed
            url2 = f"{CRYPTONEWS_BASE}/v1/news/rss"
            async with session.get(url2, timeout=aiohttp.ClientTimeout(total=10)) as resp:
                if resp.status == 200:
                    return {
                        "rss": (await resp.text())[:5000],
                        "source": "cryptocurrency.cv RSS (free)",
                    }

            return {"error": f"CryptoNews returned {resp.status}", "source": "cryptocurrency.cv"}

    except Exception as e:
        logger.warning(f"CryptoNews fetch failed: {e}")
        return {"error": str(e), "source": "cryptocurrency.cv"}