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.
- package/README.md +209 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1005 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +980 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +227 -0
- package/dist/index.d.ts +227 -0
- package/dist/index.js +711 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +701 -0
- package/dist/index.mjs.map +1 -0
- package/examples/basic.ts +28 -0
- package/package.json +52 -0
- package/src/Crypull.ts +288 -0
- package/src/cli.ts +327 -0
- package/src/index.ts +14 -0
- package/src/providers/binance.ts +74 -0
- package/src/providers/coincap.ts +65 -0
- package/src/providers/coingecko.ts +92 -0
- package/src/providers/coinpaprika.ts +82 -0
- package/src/providers/cryptocompare.ts +56 -0
- package/src/providers/dexscreener.ts +82 -0
- package/src/providers/geckoterminal.ts +70 -0
- package/src/types.ts +130 -0
- package/tests/index.test.ts +15 -0
- package/tsconfig.json +16 -0
- package/tsup.config.ts +11 -0
|
@@ -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