generated from defi-wonderland/ts-turborepo-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: coingecko pricing provider (#3)
# 🤖 Linear Closes GIT-67 ## Description - add `pricing` package with `CoingeckoProvider` - generic interface `IPricingProvider` for future extensibility ## Checklist before requesting a review - [x] I have conducted a self-review of my code. - [x] I have conducted a QA. - [x] If it is a core feature, I have included comprehensive tests.
- Loading branch information
Showing
28 changed files
with
699 additions
and
156 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# Grants Stack Indexer v2: Pricing package | ||
|
||
This package provides different pricing providers that can be used to get | ||
the price of a token at a specific timestamp, using chainId and token address. | ||
|
||
## Setup | ||
|
||
1. Install dependencies running `pnpm install` | ||
|
||
## Available Scripts | ||
|
||
Available scripts that can be run using `pnpm`: | ||
|
||
| Script | Description | | ||
| ------------- | ------------------------------------------------------- | | ||
| `build` | Build library using tsc | | ||
| `check-types` | Check types issues using tsc | | ||
| `clean` | Remove `dist` folder | | ||
| `lint` | Run ESLint to check for coding standards | | ||
| `lint:fix` | Run linter and automatically fix code formatting issues | | ||
| `format` | Check code formatting and style using Prettier | | ||
| `format:fix` | Run formatter and automatically fix issues | | ||
| `test` | Run tests using vitest | | ||
| `test:cov` | Run tests with coverage report | | ||
|
||
## Usage | ||
|
||
### Importing the Package | ||
|
||
You can import the package in your TypeScript or JavaScript files as follows: | ||
|
||
```typescript | ||
import { CoingeckoProvider } from "@grants-stack-indexer/pricing"; | ||
``` | ||
|
||
### Example | ||
|
||
```typescript | ||
const coingecko = new CoingeckoProvider({ | ||
apiKey: "your-api-key", | ||
apiType: "demo", | ||
}); | ||
|
||
const price = await coingecko.getTokenPrice( | ||
1, | ||
"0x0d8775f5d29498461708d85e233a7b3331e6f5a0", | ||
1609459200000, | ||
1640908800000, | ||
); | ||
``` | ||
|
||
## API | ||
|
||
### [IPricingProvider](./src/interfaces/pricing.interface.ts) | ||
|
||
Available methods | ||
|
||
- `getTokenPrice(chainId: number, tokenAddress: Address, startTimestampMs: number, endTimestampMs: number): Promise<TokenPrice | undefined>` | ||
|
||
## References | ||
|
||
- [Coingecko API Historical Data](https://docs.coingecko.com/reference/coins-id-market-chart-range) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{ | ||
"name": "@grants-stack-indexer/pricing", | ||
"version": "0.0.1", | ||
"private": true, | ||
"description": "", | ||
"license": "MIT", | ||
"author": "Wonderland", | ||
"type": "module", | ||
"main": "./dist/src/index.js", | ||
"types": "./dist/src/index.d.ts", | ||
"directories": { | ||
"src": "src" | ||
}, | ||
"files": [ | ||
"dist/*", | ||
"package.json", | ||
"!**/*.tsbuildinfo" | ||
], | ||
"scripts": { | ||
"build": "tsc -p tsconfig.build.json", | ||
"check-types": "tsc --noEmit -p ./tsconfig.json", | ||
"clean": "rm -rf dist/", | ||
"format": "prettier --check \"{src,test}/**/*.{js,ts,json}\"", | ||
"format:fix": "prettier --write \"{src,test}/**/*.{js,ts,json}\"", | ||
"lint": "eslint \"{src,test}/**/*.{js,ts,json}\"", | ||
"lint:fix": "pnpm lint --fix", | ||
"test": "vitest run --config vitest.config.ts --passWithNoTests", | ||
"test:cov": "vitest run --config vitest.config.ts --coverage" | ||
}, | ||
"dependencies": { | ||
"@grants-stack-indexer/shared": "workspace:0.0.1", | ||
"axios": "1.7.7" | ||
}, | ||
"devDependencies": { | ||
"axios-mock-adapter": "2.0.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from "./network.exception.js"; | ||
export * from "./unsupportedChain.exception.js"; | ||
export * from "./unknownPricing.exception.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export class NetworkException extends Error { | ||
constructor( | ||
message: string, | ||
public readonly status: number, | ||
) { | ||
super(message); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export class UnknownPricingException extends Error { | ||
constructor(message: string, stack?: string) { | ||
super(message); | ||
this.stack = stack; | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
packages/pricing/src/exceptions/unsupportedChain.exception.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export class UnsupportedChainException extends Error { | ||
constructor(chainId: number) { | ||
super(`Unsupported chain ID: ${chainId}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export type { TokenPrice, IPricingProvider } from "./internal.js"; | ||
|
||
export { CoingeckoProvider } from "./internal.js"; | ||
export { | ||
UnsupportedChainException, | ||
NetworkException, | ||
UnknownPricingException, | ||
} from "./internal.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./external.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./pricing.interface.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { Address } from "@grants-stack-indexer/shared"; | ||
|
||
import { TokenPrice } from "../internal.js"; | ||
|
||
/** | ||
* Represents a pricing service that retrieves token prices. | ||
* @dev is service responsibility to map address to their internal ID | ||
* @dev for native token (eg. ETH), use the one address | ||
*/ | ||
export interface IPricingProvider { | ||
/** | ||
* Retrieves the price of a token at a timestamp range. | ||
* @param chainId - The ID of the blockchain network. | ||
* @param tokenAddress - The address of the token. | ||
* @param startTimestampMs - The start timestamp for which to retrieve the price. | ||
* @param endTimestampMs - The end timestamp for which to retrieve the price. | ||
* @returns A promise that resolves to the price of the token at the specified timestamp or undefined if no price is found. | ||
* @throws {UnsupportedChainException} if the chain ID is not supported by the pricing provider. | ||
* @throws {NetworkException} if the network is not reachable. | ||
* @throws {UnknownFetchException} if the pricing provider returns an unknown error. | ||
*/ | ||
getTokenPrice( | ||
chainId: number, | ||
tokenAddress: Address, | ||
startTimestampMs: number, | ||
endTimestampMs: number, | ||
): Promise<TokenPrice | undefined>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from "./types/index.js"; | ||
export * from "./interfaces/index.js"; | ||
export * from "./providers/index.js"; | ||
export * from "./exceptions/index.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import { isNativeError } from "util/types"; | ||
import axios, { AxiosInstance, isAxiosError } from "axios"; | ||
|
||
import { Address, isNativeToken } from "@grants-stack-indexer/shared"; | ||
|
||
import { IPricingProvider } from "../interfaces/index.js"; | ||
import { | ||
CoingeckoPlatformId, | ||
CoingeckoPriceChartData, | ||
CoingeckoSupportedChainId, | ||
CoingeckoTokenId, | ||
NetworkException, | ||
TokenPrice, | ||
UnknownPricingException, | ||
UnsupportedChainException, | ||
} from "../internal.js"; | ||
|
||
type CoingeckoOptions = { | ||
apiKey: string; | ||
apiType: "demo" | "pro"; | ||
}; | ||
|
||
const getApiTypeConfig = (apiType: "demo" | "pro"): { baseURL: string; authHeader: string } => | ||
apiType === "demo" | ||
? { baseURL: "https://api.coingecko.com/api/v3", authHeader: "x-cg-demo-api-key" } | ||
: { baseURL: "https://pro-api.coingecko.com/api/v3/", authHeader: "x-cg-pro-api-key" }; | ||
|
||
const platforms: { [key in CoingeckoSupportedChainId]: CoingeckoPlatformId } = { | ||
1: "ethereum" as CoingeckoPlatformId, | ||
10: "optimistic-ethereum" as CoingeckoPlatformId, | ||
100: "xdai" as CoingeckoPlatformId, | ||
250: "fantom" as CoingeckoPlatformId, | ||
42161: "arbitrum-one" as CoingeckoPlatformId, | ||
43114: "avalanche" as CoingeckoPlatformId, | ||
713715: "sei-network" as CoingeckoPlatformId, | ||
1329: "sei-network" as CoingeckoPlatformId, | ||
42: "lukso" as CoingeckoPlatformId, | ||
42220: "celo" as CoingeckoPlatformId, | ||
1088: "metis" as CoingeckoPlatformId, | ||
}; | ||
|
||
const nativeTokens: { [key in CoingeckoSupportedChainId]: CoingeckoTokenId } = { | ||
1: "ethereum" as CoingeckoTokenId, | ||
10: "ethereum" as CoingeckoTokenId, | ||
100: "xdai" as CoingeckoTokenId, | ||
250: "fantom" as CoingeckoTokenId, | ||
42161: "ethereum" as CoingeckoTokenId, | ||
43114: "avalanche-2" as CoingeckoTokenId, | ||
713715: "sei-network" as CoingeckoTokenId, | ||
1329: "sei-network" as CoingeckoTokenId, | ||
42: "lukso-token" as CoingeckoTokenId, | ||
42220: "celo" as CoingeckoTokenId, | ||
1088: "metis-token" as CoingeckoTokenId, | ||
}; | ||
|
||
/** | ||
* The Coingecko provider is a pricing provider that uses the Coingecko API to get the price of a token. | ||
*/ | ||
export class CoingeckoProvider implements IPricingProvider { | ||
private readonly axios: AxiosInstance; | ||
|
||
/** | ||
* @param options.apiKey - Coingecko API key. | ||
* @param options.apiType - Coingecko API type (demo or pro). | ||
*/ | ||
constructor(options: CoingeckoOptions) { | ||
const { apiKey, apiType } = options; | ||
const { baseURL, authHeader } = getApiTypeConfig(apiType); | ||
|
||
this.axios = axios.create({ | ||
baseURL, | ||
headers: { | ||
common: { | ||
[authHeader]: apiKey, | ||
Accept: "application/json", | ||
}, | ||
}, | ||
}); | ||
} | ||
|
||
/* @inheritdoc */ | ||
async getTokenPrice( | ||
chainId: number, | ||
tokenAddress: Address, | ||
startTimestampMs: number, | ||
endTimestampMs: number, | ||
): Promise<TokenPrice | undefined> { | ||
if (!this.isSupportedChainId(chainId)) { | ||
throw new UnsupportedChainException(chainId); | ||
} | ||
|
||
if (startTimestampMs > endTimestampMs) { | ||
return undefined; | ||
} | ||
|
||
const startTimestampSecs = Math.floor(startTimestampMs / 1000); | ||
const endTimestampSecs = Math.floor(endTimestampMs / 1000); | ||
|
||
const path = this.getApiPath(chainId, tokenAddress, startTimestampSecs, endTimestampSecs); | ||
|
||
//TODO: handle retries | ||
try { | ||
const { data } = await this.axios.get<CoingeckoPriceChartData>(path); | ||
|
||
const closestEntry = data.prices.at(0); | ||
if (!closestEntry) { | ||
return undefined; | ||
} | ||
|
||
return { | ||
timestampMs: closestEntry[0], | ||
priceUsd: closestEntry[1], | ||
}; | ||
} catch (error: unknown) { | ||
//TODO: notify | ||
if (isAxiosError(error)) { | ||
if (error.status! >= 400 && error.status! < 500) { | ||
console.error(`Coingecko API error: ${error.message}. Stack: ${error.stack}`); | ||
return undefined; | ||
} | ||
|
||
if (error.status! >= 500 || error.message === "Network Error") { | ||
throw new NetworkException(error.message, error.status!); | ||
} | ||
} | ||
console.error(error); | ||
throw new UnknownPricingException( | ||
isNativeError(error) ? error.message : JSON.stringify(error), | ||
isNativeError(error) ? error.stack : undefined, | ||
); | ||
} | ||
} | ||
|
||
/* | ||
* @returns Whether the given chain ID is supported by the Coingecko API. | ||
*/ | ||
private isSupportedChainId(chainId: number): chainId is CoingeckoSupportedChainId { | ||
return chainId in platforms; | ||
} | ||
|
||
/* | ||
* @returns The API endpoint path for the given parameters. | ||
*/ | ||
private getApiPath( | ||
chainId: CoingeckoSupportedChainId, | ||
tokenAddress: Address, | ||
startTimestampSecs: number, | ||
endTimestampSecs: number, | ||
): string { | ||
const platform = platforms[chainId]; | ||
const nativeTokenId = nativeTokens[chainId]; | ||
|
||
return isNativeToken(tokenAddress) | ||
? `/coins/${nativeTokenId}/market_chart/range?vs_currency=usd&from=${startTimestampSecs}&to=${endTimestampSecs}&precision=full` | ||
: `/coins/${platform}/contract/${tokenAddress.toLowerCase()}/market_chart/range?vs_currency=usd&from=${startTimestampSecs}&to=${endTimestampSecs}&precision=full`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./coingecko.provider.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Branded } from "@grants-stack-indexer/shared"; | ||
|
||
export type CoingeckoSupportedChainId = | ||
| 1 | ||
| 10 | ||
| 100 | ||
| 250 | ||
| 42161 | ||
| 43114 | ||
| 713715 | ||
| 1329 | ||
| 42 | ||
| 42220 | ||
| 1088; | ||
|
||
export type CoingeckoTokenId = Branded<string, "CoingeckoTokenId">; | ||
export type CoingeckoPlatformId = Branded<string, "CoingeckoPlatformId">; | ||
|
||
export type CoingeckoPriceChartData = { | ||
prices: [number, number][]; | ||
market_caps: [number, number][]; | ||
total_volumes: [number, number][]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./coingecko.types.js"; | ||
export * from "./pricing.types.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/** | ||
* @timestampMs - The timestamp in milliseconds | ||
* @priceUsd - The price in USD | ||
*/ | ||
export type TokenPrice = { | ||
timestampMs: number; | ||
priceUsd: number; | ||
}; |
Oops, something went wrong.