Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/api/src/app/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const OptionalAddressSchema = {
* "solana": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
* }
*/
pattern: '^(0x)?[a-fA-F0-9\\.:]{3,80}$',
pattern: '^(0x)?[a-zA-Z0-9\\.:]{3,80}$',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we are already using oneOf, maybe it would be better to break this down into multiple RegExps intead of a single catch-all one, so that this validation is a bit more strict.

},
{
type: 'string',
Expand Down
12 changes: 6 additions & 6 deletions libs/repositories/src/datasources/coingecko.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { SupportedChainId } from '@cowprotocol/cow-sdk';
import { AdditionalTargetChainId, SupportedChainId, TargetChainId } from '@cowprotocol/cow-sdk';

import createClient from 'openapi-fetch';
import type { paths } from '../gen/coingecko/coingecko-pro-types';

import type { components } from '../gen/coingecko/coingecko-pro-types';
import type { components, paths } from '../gen/coingecko/coingecko-pro-types';

export const COINGECKO_PRO_BASE_URL = 'https://pro-api.coingecko.com';

Expand All @@ -19,7 +17,10 @@ export const SUPPORTED_COINGECKO_PLATFORMS = {
[SupportedChainId.LINEA]: 'linea',
[SupportedChainId.PLASMA]: 'plasma',
[SupportedChainId.INK]: 'ink',
} as const satisfies Record<SupportedChainId, string | undefined>;
[AdditionalTargetChainId.OPTIMISM]: 'optimistic-ethereum',
[AdditionalTargetChainId.BITCOIN]: 'bitcoin',
[AdditionalTargetChainId.SOLANA]: 'solana',
} as const satisfies Record<TargetChainId, string | undefined>;

/**
* Map of chain IDs to Coingecko platform IDs, for every platform that has a network id.
Expand All @@ -28,7 +29,6 @@ export const SUPPORTED_COINGECKO_PLATFORMS = {
*/
export const COINGECKO_PLATFORMS: Record<number, string | undefined> = {
...SUPPORTED_COINGECKO_PLATFORMS,
[10]: 'optimistic-ethereum',
[10000]: 'smartbch',
[100009]: 'vechain',
[10201]: 'maxxchain',
Expand Down
40 changes: 19 additions & 21 deletions libs/repositories/src/repos/UsdRepository/UsdRepositoryCoingecko.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { injectable } from 'inversify';
import {
getCoingeckoProClient,
SimplePriceResponse,
} from '../../datasources/coingecko';
import {
getAddressOrPlatform,
getCoingeckoPlatform,
} from '../../utils/coingeckoUtils';
import { getAddressKey } from '@cowprotocol/cow-sdk';
import { getCoingeckoProClient, SimplePriceResponse } from '../../datasources/coingecko';
import { getAddressOrPlatform, getCoingeckoPlatform } from '../../utils/coingeckoUtils';
import { throwIfUnsuccessful } from '../../utils/throwIfUnsuccessful';
import { PricePoint, PriceStrategy, UsdRepository } from './UsdRepository';

Expand Down Expand Up @@ -40,9 +35,10 @@ export class UsdRepositoryCoingecko implements UsdRepository {

const addressOrPlatform = getAddressOrPlatform(tokenAddress, platform);

const fetchPromise = tokenAddress
? this.getSinglePriceByContractAddress(platform, addressOrPlatform)
: this.getSinglePriceByPlatformId(platform);
const fetchPromise =
tokenAddress && addressOrPlatform !== platform
? this.getSinglePriceByContractAddress(platform, addressOrPlatform)
: this.getSinglePriceByPlatformId(platform);

return this.handleSinglePriceResponse(fetchPromise, addressOrPlatform);
}
Expand All @@ -60,14 +56,17 @@ export class UsdRepositoryCoingecko implements UsdRepository {
const days = DAYS_PER_PRICE_STRATEGY[priceStrategy].toString();
const interval = priceStrategy === 'daily' ? 'daily' : undefined;

const { data, response } = tokenAddress
? await this.getMarketDataByTokenAddress(
platform,
days,
interval,
tokenAddress
)
: await this.getMarketDataByPlatformId(platform, days, interval);
const addressOrPlatform = getAddressOrPlatform(tokenAddress, platform);

const { data, response } =
tokenAddress && addressOrPlatform !== platform
? await this.getMarketDataByTokenAddress(
platform,
days,
interval,
addressOrPlatform
)
: await this.getMarketDataByPlatformId(platform, days, interval);

if (response.status === 404 || !data) {
return null;
Expand Down Expand Up @@ -154,15 +153,14 @@ export class UsdRepositoryCoingecko implements UsdRepository {
interval: 'daily' | undefined,
tokenAddress: string
) {
const address = tokenAddress.toLowerCase();
// Get prices: See https://docs.coingecko.com/reference/contract-address-market-chart
return getCoingeckoProClient().GET(
`/coins/{id}/contract/{contract_address}/market_chart`,
{
params: {
path: {
id: platform,
contract_address: address,
contract_address: getAddressKey(tokenAddress),
},
query: {
vs_currency: 'usd',
Expand Down
4 changes: 2 additions & 2 deletions libs/repositories/src/repos/UsdRepository/UsdRepositoryCow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SupportedChainId } from '@cowprotocol/cow-sdk';
import { isSupportedChain, SupportedChainId } from '@cowprotocol/cow-sdk';
import { logger } from '@cowprotocol/shared';
import { BigNumber } from 'bignumber.js';
import { injectable } from 'inversify';
Expand Down Expand Up @@ -27,7 +27,7 @@ export class UsdRepositoryCow extends UsdRepositoryNoop {
tokenAddress?: string | undefined
): Promise<number | null> {
const chainId = getSupportedCoingeckoChainId(chainIdOrSlug);
if (!chainId) {
if (!chainId || !isSupportedChain(chainId)) {
return null;
}

Expand Down
45 changes: 32 additions & 13 deletions libs/repositories/src/utils/coingeckoUtils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { isAddress } from 'viem';
import {
COINGECKO_PLATFORMS,
SUPPORTED_COINGECKO_PLATFORMS,
} from '../datasources/coingecko';
import { SupportedChainId } from '@cowprotocol/cow-sdk';
import {
AdditionalTargetChainId,
BTC_CURRENCY_ADDRESS,
getAddressKey,
SOL_NATIVE_CURRENCY_ADDRESS,
SupportedChainId,
TargetChainId,
} from '@cowprotocol/cow-sdk';

// for sol/btc we use our internal convention of the native address
// for coingecko we should just replace the address by platform
const NON_EVM_NATIVE_TOKENS = new Set([
getAddressKey(SOL_NATIVE_CURRENCY_ADDRESS),
getAddressKey(BTC_CURRENCY_ADDRESS),
]);

// Invert number→slug map to slug→SupportedChainId
const SUPPORTED_CHAIN_SLUG_TO_ID: Record<string, SupportedChainId> =
const SUPPORTED_CHAIN_SLUG_TO_ID: Record<string, TargetChainId> =
Object.entries(SUPPORTED_COINGECKO_PLATFORMS).reduce((map, [id, slug]) => {
if (slug) {
map[slug as string] = +id as SupportedChainId;
map[slug as string] = +id as TargetChainId;
}
return map;
}, {} as Record<string, SupportedChainId>);
}, {} as Record<string, TargetChainId>);

export function getAddressOrPlatform(
tokenAddress: string | undefined,
Expand All @@ -22,13 +35,17 @@ export function getAddressOrPlatform(
return platform;
}

if (isAddress(tokenAddress)) {
// EVM like address, Coingecko expects it lowercased
return tokenAddress.toLowerCase();
// Native currency addresses are conventions, not real contracts.
// CoinGecko expects platform-level lookup for native tokens.
const addressKey = getAddressKey(tokenAddress);

if (NON_EVM_NATIVE_TOKENS.has(addressKey)) {
return platform;
}

// Non-EVM address, Coingecko expects it as is
return tokenAddress;
// getAddressKey lowercases EVM addresses (as CoinGecko expects)
// and preserves case for non-EVM addresses
return addressKey;
}

export function getCoingeckoPlatform(
Expand All @@ -40,12 +57,14 @@ export function getCoingeckoPlatform(

export function getSupportedCoingeckoChainId(
chainIdOrSlug: string
): SupportedChainId | null {
): TargetChainId | null {
const chainIdAsNumber = +chainIdOrSlug;
// Only SupportedChainIds are supported
const numericId = isNaN(chainIdAsNumber)
? SUPPORTED_CHAIN_SLUG_TO_ID[chainIdOrSlug]
: (chainIdAsNumber as SupportedChainId);
: (chainIdAsNumber as TargetChainId);

return SupportedChainId[numericId] ? numericId : null;
return SupportedChainId[numericId] || AdditionalTargetChainId[numericId]
? numericId
: null;
}
Loading