crypull 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ import { ICryptoProvider, PriceData, SearchResult, TokenInfo } from '../types.js';
2
+
3
+ export class CryptoCompareProvider implements ICryptoProvider {
4
+ name = 'CryptoCompare';
5
+ private baseUrl = 'https://min-api.cryptocompare.com/data';
6
+
7
+ async search(query: string): Promise<SearchResult[]> {
8
+ // CryptoCompare search is a bit heavy (fetches all coins), so we mock or skip for basic search
9
+ // Returning empty array and letting other providers handle search is better for perf.
10
+ return [];
11
+ }
12
+
13
+ async getPrice(symbol: string): Promise<PriceData | null> {
14
+ try {
15
+ const sym = symbol.toUpperCase();
16
+ const res = await fetch(`${this.baseUrl}/price?fsym=${sym}&tsyms=USD`);
17
+ if (!res.ok) return null;
18
+ const data = await res.json();
19
+
20
+ if (data.Response === 'Error' || !data.USD) return null;
21
+
22
+ return {
23
+ symbol: sym,
24
+ priceUsd: data.USD,
25
+ source: this.name,
26
+ lastUpdated: Date.now(),
27
+ };
28
+ } catch (e) {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ async getTokenInfo(symbol: string): Promise<TokenInfo | null> {
34
+ try {
35
+ const sym = symbol.toUpperCase();
36
+ const res = await fetch(`${this.baseUrl}/pricemultifull?fsyms=${sym}&tsyms=USD`);
37
+ if (!res.ok) return null;
38
+ const data = await res.json();
39
+
40
+ if (!data.RAW || !data.RAW[sym] || !data.RAW[sym].USD) return null;
41
+ const raw = data.RAW[sym].USD;
42
+
43
+ return {
44
+ name: sym, // CryptoCompare doesn't give full name in this endpoint
45
+ symbol: sym,
46
+ priceUsd: raw.PRICE || 0,
47
+ marketCap: raw.MKTCAP,
48
+ volume24h: raw.TOTALVOLUME24HTO,
49
+ source: this.name,
50
+ lastUpdated: Date.now(),
51
+ };
52
+ } catch (e) {
53
+ return null;
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,82 @@
1
+ import { DexPairInfo, ICryptoProvider, PriceData, SearchResult, TokenInfo } from '../types.js';
2
+
3
+ export class DexScreenerProvider implements ICryptoProvider {
4
+ name = 'DexScreener';
5
+ private baseUrl = 'https://api.dexscreener.com/latest/dex';
6
+
7
+ async search(query: string): Promise<SearchResult[]> {
8
+ try {
9
+ const res = await fetch(`${this.baseUrl}/search?q=${query}`);
10
+ if (!res.ok) return [];
11
+ const data = await res.json();
12
+ return (data.pairs || []).slice(0, 10).map((pair: any) => ({
13
+ id: pair.baseToken.address,
14
+ name: pair.baseToken.name,
15
+ symbol: pair.baseToken.symbol.toUpperCase(),
16
+ network: pair.chainId,
17
+ source: this.name,
18
+ }));
19
+ } catch (e) {
20
+ return [];
21
+ }
22
+ }
23
+
24
+ async getPrice(address: string): Promise<PriceData | null> {
25
+ const info = await this.getTokenInfo(address);
26
+ if (!info) return null;
27
+ return {
28
+ symbol: info.symbol,
29
+ priceUsd: info.priceUsd,
30
+ source: this.name,
31
+ lastUpdated: info.lastUpdated,
32
+ };
33
+ }
34
+
35
+ async getTokenInfo(address: string): Promise<TokenInfo | null> {
36
+ try {
37
+ const res = await fetch(`${this.baseUrl}/tokens/${address}`);
38
+ if (!res.ok) return null;
39
+ const data = await res.json();
40
+
41
+ if (!data.pairs || data.pairs.length === 0) return null;
42
+
43
+ // The first pair is usually the most liquid one
44
+ const pair = data.pairs[0];
45
+
46
+ // Map out all the pairs available on DEXs for this token
47
+ const mappedPairs: DexPairInfo[] = data.pairs.slice(0, 10).map((p: any) => ({
48
+ dexId: p.dexId,
49
+ url: p.url,
50
+ pairAddress: p.pairAddress,
51
+ baseTokenSymbol: p.baseToken.symbol,
52
+ quoteTokenSymbol: p.quoteToken.symbol,
53
+ priceUsd: parseFloat(p.priceUsd) || 0,
54
+ volume24h: p.volume?.h24,
55
+ liquidityUsd: p.liquidity?.usd,
56
+ }));
57
+
58
+ return {
59
+ name: pair.baseToken.name,
60
+ symbol: pair.baseToken.symbol.toUpperCase(),
61
+ address: pair.baseToken.address,
62
+ network: pair.chainId,
63
+ priceUsd: parseFloat(pair.priceUsd) || 0,
64
+ fdv: pair.fdv,
65
+ volume24h: pair.volume?.h24,
66
+ liquidityUsd: pair.liquidity?.usd,
67
+ priceChangePercentage24h: pair.priceChange?.h24,
68
+ pairs: mappedPairs,
69
+ links: {
70
+ website: pair.info?.websites?.[0]?.url,
71
+ twitter: pair.info?.socials?.find((s: any) => s.type === 'twitter')?.url,
72
+ telegram: pair.info?.socials?.find((s: any) => s.type === 'telegram')?.url,
73
+ discord: pair.info?.socials?.find((s: any) => s.type === 'discord')?.url,
74
+ },
75
+ source: this.name,
76
+ lastUpdated: Date.now(),
77
+ };
78
+ } catch (e) {
79
+ return null;
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,70 @@
1
+ import { DexPairInfo, ICryptoProvider, PriceData, SearchResult, TokenInfo } from '../types.js';
2
+
3
+ export class GeckoTerminalProvider implements ICryptoProvider {
4
+ name = 'GeckoTerminal';
5
+ private baseUrl = 'https://api.geckoterminal.com/api/v2';
6
+
7
+ async search(query: string): Promise<SearchResult[]> {
8
+ try {
9
+ const res = await fetch(`${this.baseUrl}/search/pools?query=${query}`);
10
+ if (!res.ok) return [];
11
+ const data = await res.json();
12
+
13
+ const pools = data.data || [];
14
+ return pools.slice(0, 10).map((pool: any) => {
15
+ const attributes = pool.attributes;
16
+ const baseToken = attributes.name.split(' / ')[0]; // Usually Base / Quote
17
+ return {
18
+ id: pool.id, // e.g. "eth_0x..."
19
+ name: attributes.name,
20
+ symbol: baseToken,
21
+ network: pool.relationships.network.data.id,
22
+ source: this.name,
23
+ };
24
+ });
25
+ } catch (e) {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ async getPrice(address: string, network: string = 'eth'): Promise<PriceData | null> {
31
+ const info = await this.getTokenInfo(address, network);
32
+ if (!info) return null;
33
+ return {
34
+ symbol: info.symbol,
35
+ priceUsd: info.priceUsd,
36
+ source: this.name,
37
+ lastUpdated: info.lastUpdated,
38
+ };
39
+ }
40
+
41
+ async getTokenInfo(address: string, network: string = 'eth'): Promise<TokenInfo | null> {
42
+ try {
43
+ const res = await fetch(`${this.baseUrl}/networks/${network}/tokens/${address}`);
44
+ if (!res.ok) return null;
45
+ const json = await res.json();
46
+
47
+ if (!json.data || !json.data.attributes) return null;
48
+
49
+ const attrs = json.data.attributes;
50
+
51
+ // Try to get pairs from pools list if geckoterminal provides it via another endpoint or included relationships
52
+ // For now, GeckoTerminal's token endpoint doesn't return rich pairs list natively without separate call.
53
+ // So we map what we can.
54
+
55
+ return {
56
+ name: attrs.name,
57
+ symbol: attrs.symbol.toUpperCase(),
58
+ address: attrs.address,
59
+ network: network,
60
+ priceUsd: parseFloat(attrs.price_usd) || 0,
61
+ fdv: parseFloat(attrs.fdv_usd),
62
+ volume24h: parseFloat(attrs.volume_usd?.h24 || 0),
63
+ source: this.name,
64
+ lastUpdated: Date.now(),
65
+ };
66
+ } catch (e) {
67
+ return null;
68
+ }
69
+ }
70
+ }
package/src/types.ts ADDED
@@ -0,0 +1,130 @@
1
+ // Base Types for unified response
2
+
3
+ export interface PriceData {
4
+ symbol: string;
5
+ priceUsd: number;
6
+ source: string;
7
+ lastUpdated: number;
8
+ }
9
+
10
+ export interface TokenLinks {
11
+ website?: string;
12
+ twitter?: string;
13
+ telegram?: string;
14
+ discord?: string;
15
+ github?: string;
16
+ [key: string]: string | undefined;
17
+ }
18
+
19
+ export interface DexPairInfo {
20
+ dexId: string;
21
+ url: string;
22
+ pairAddress: string;
23
+ baseTokenSymbol: string;
24
+ quoteTokenSymbol: string;
25
+ priceUsd: number;
26
+ volume24h?: number;
27
+ liquidityUsd?: number;
28
+ }
29
+
30
+ export interface TrendingCoin {
31
+ id: string;
32
+ name: string;
33
+ symbol: string;
34
+ marketCapRank?: number;
35
+ priceUsd?: number;
36
+ priceChange24h?: number;
37
+ volume24h?: number;
38
+ }
39
+
40
+ export interface GlobalMarketData {
41
+ totalMarketCapUsd: number;
42
+ totalVolume24hUsd: number;
43
+ bitcoinDominancePercentage: number;
44
+ ethereumDominancePercentage: number;
45
+ activeCryptocurrencies: number;
46
+ lastUpdated: number;
47
+ }
48
+
49
+ export interface ChartData {
50
+ prices: number[];
51
+ timestamps: number[];
52
+ minPrice: number;
53
+ maxPrice: number;
54
+ }
55
+
56
+ export interface SentimentData {
57
+ value: number; // 0 to 100
58
+ classification: string; // "Extreme Fear", "Fear", "Neutral", "Greed", "Extreme Greed"
59
+ lastUpdated: number;
60
+ }
61
+
62
+ export interface TopCoin {
63
+ id: string;
64
+ name: string;
65
+ symbol: string;
66
+ marketCapRank: number;
67
+ priceUsd: number;
68
+ marketCap: number;
69
+ volume24h: number;
70
+ priceChange24h: number;
71
+ }
72
+
73
+ export interface GasData {
74
+ network: string;
75
+ low: number;
76
+ average: number;
77
+ high: number;
78
+ baseFee?: number;
79
+ }
80
+
81
+ export interface TokenInfo {
82
+ name: string;
83
+ symbol: string;
84
+ address?: string;
85
+ network?: string;
86
+ description?: string;
87
+ priceUsd: number;
88
+ marketCap?: number;
89
+ marketCapRank?: number;
90
+ fdv?: number; // Fully Diluted Valuation
91
+ circulatingSupply?: number;
92
+ totalSupply?: number;
93
+ maxSupply?: number;
94
+ volume24h?: number;
95
+ liquidityUsd?: number;
96
+
97
+ // Price Changes
98
+ priceChange24h?: number;
99
+ priceChangePercentage24h?: number;
100
+ priceChangePercentage7d?: number;
101
+
102
+ // All Time High / Low
103
+ ath?: number;
104
+ athDate?: string;
105
+ atl?: number;
106
+ atlDate?: string;
107
+
108
+ links?: TokenLinks;
109
+
110
+ // Trading Pairs (mainly for DEX data)
111
+ pairs?: DexPairInfo[];
112
+
113
+ source: string;
114
+ lastUpdated: number;
115
+ }
116
+
117
+ export interface SearchResult {
118
+ id?: string; // id or address
119
+ name: string;
120
+ symbol: string;
121
+ network?: string;
122
+ source: string;
123
+ }
124
+
125
+ export interface ICryptoProvider {
126
+ name: string;
127
+ getPrice(symbol: string): Promise<PriceData | null>;
128
+ getTokenInfo(address: string, network?: string): Promise<TokenInfo | null>;
129
+ search(query: string): Promise<SearchResult[]>;
130
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Crypull } from '../src/Crypull.js';
3
+ import { crypull } from '../src/index.js';
4
+
5
+ describe('Crypull Aggregator', () => {
6
+ it('should export a default instance', () => {
7
+ expect(crypull).toBeDefined();
8
+ expect(crypull).toBeInstanceOf(Crypull);
9
+ });
10
+
11
+ it('should initialize successfully', () => {
12
+ const customCrypull = new Crypull();
13
+ expect(customCrypull).toBeDefined();
14
+ });
15
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022", "DOM"],
7
+ "declaration": true,
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "outDir": "./dist"
13
+ },
14
+ "include": ["src/**/*", "examples/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts', 'src/cli.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ splitting: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ treeshake: true,
11
+ });