diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..48a321a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +.env +__pycache__/ +*.log +.DS_Store +coverage/ +.vscode/ +.idea/ \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..d68fba7c --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "coin-gekko-api", + "version": "1.0.0", + "description": "Coin Gekko API implementation", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "test": "vitest", + "start": "node dist/index.js" + }, + "dependencies": { + "axios": "^0.21.1" + }, + "devDependencies": { + "@types/node": "^16.11.6", + "typescript": "^4.4.4", + "vitest": "^0.24.0" + } +} \ No newline at end of file diff --git a/src/coinGekkoApi.spec.ts b/src/coinGekkoApi.spec.ts new file mode 100644 index 00000000..d947972c --- /dev/null +++ b/src/coinGekkoApi.spec.ts @@ -0,0 +1,98 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import axios from 'axios'; +import { CoinGekkoApi } from './coinGekkoApi'; + +// Mock axios +vi.mock('axios'); + +describe('CoinGekkoApi', () => { + let coinGekkoApi: CoinGekkoApi; + + beforeEach(() => { + coinGekkoApi = new CoinGekkoApi(); + }); + + describe('getCoinById', () => { + it('should fetch coin data successfully', async () => { + const mockCoinData = { + id: 'bitcoin', + symbol: 'btc', + name: 'Bitcoin', + market_data: { + current_price: { usd: 50000 }, + market_cap: { usd: 1000000000000 } + } + }; + + vi.mocked(axios.get).mockResolvedValue({ data: mockCoinData }); + + const result = await coinGekkoApi.getCoinById('bitcoin'); + expect(result).toEqual({ + id: 'bitcoin', + symbol: 'btc', + name: 'Bitcoin', + price: 50000, + marketCap: 1000000000000 + }); + }); + + it('should throw an error if coin ID is not provided', async () => { + await expect(coinGekkoApi.getCoinById('')).rejects.toThrow('Coin ID is required'); + }); + + it('should throw an error if coin is not found', async () => { + vi.mocked(axios.get).mockRejectedValue({ + response: { status: 404 }, + isAxiosError: true + }); + + await expect(coinGekkoApi.getCoinById('nonexistent')).rejects.toThrow('Coin with ID nonexistent not found'); + }); + }); + + describe('getTopCryptocurrencies', () => { + it('should fetch top cryptocurrencies successfully', async () => { + const mockTopCoins = [ + { + id: 'bitcoin', + symbol: 'btc', + name: 'Bitcoin', + current_price: 50000, + market_cap: 1000000000000 + }, + { + id: 'ethereum', + symbol: 'eth', + name: 'Ethereum', + current_price: 4000, + market_cap: 500000000000 + } + ]; + + vi.mocked(axios.get).mockResolvedValue({ data: mockTopCoins }); + + const result = await coinGekkoApi.getTopCryptocurrencies(2); + expect(result).toEqual([ + { + id: 'bitcoin', + symbol: 'btc', + name: 'Bitcoin', + price: 50000, + marketCap: 1000000000000 + }, + { + id: 'ethereum', + symbol: 'eth', + name: 'Ethereum', + price: 4000, + marketCap: 500000000000 + } + ]); + }); + + it('should throw an error if limit is not positive', async () => { + await expect(coinGekkoApi.getTopCryptocurrencies(0)).rejects.toThrow('Limit must be a positive number'); + await expect(coinGekkoApi.getTopCryptocurrencies(-1)).rejects.toThrow('Limit must be a positive number'); + }); + }); +}); \ No newline at end of file diff --git a/src/coinGekkoApi.ts b/src/coinGekkoApi.ts new file mode 100644 index 00000000..52f1df2f --- /dev/null +++ b/src/coinGekkoApi.ts @@ -0,0 +1,79 @@ +import axios from 'axios'; + +export interface CoinData { + id: string; + symbol: string; + name: string; + price: number; + marketCap: number; +} + +export class CoinGekkoApi { + private baseUrl: string; + + constructor(baseUrl: string = 'https://api.coingecko.com/api/v3') { + this.baseUrl = baseUrl; + } + + /** + * Fetch coin data by ID + * @param coinId - The ID of the cryptocurrency + * @returns Promise with coin data + */ + async getCoinById(coinId: string): Promise { + if (!coinId) { + throw new Error('Coin ID is required'); + } + + try { + const response = await axios.get(`${this.baseUrl}/coins/${coinId}`); + return { + id: response.data.id, + symbol: response.data.symbol, + name: response.data.name, + price: response.data.market_data.current_price.usd, + marketCap: response.data.market_data.market_cap.usd + }; + } catch (error) { + if (axios.isAxiosError(error)) { + if (error.response?.status === 404) { + throw new Error(`Coin with ID ${coinId} not found`); + } + throw new Error(`Error fetching coin data: ${error.message}`); + } + throw error; + } + } + + /** + * List top cryptocurrencies by market cap + * @param limit - Number of cryptocurrencies to return (default: 10) + * @returns Promise with list of top cryptocurrencies + */ + async getTopCryptocurrencies(limit: number = 10): Promise { + if (limit <= 0) { + throw new Error('Limit must be a positive number'); + } + + try { + const response = await axios.get(`${this.baseUrl}/coins/markets`, { + params: { + vs_currency: 'usd', + order: 'market_cap_desc', + per_page: limit, + page: 1 + } + }); + + return response.data.map((coin: any) => ({ + id: coin.id, + symbol: coin.symbol, + name: coin.name, + price: coin.current_price, + marketCap: coin.market_cap + })); + } catch (error) { + throw new Error(`Error fetching top cryptocurrencies: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..fc0e0bf2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export { CoinGekkoApi, CoinData } from './coinGekkoApi'; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..aa8d2992 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"] +} \ No newline at end of file