From 276e2d5e14e96203de7ecb61ce20bcb613d9da2a Mon Sep 17 00:00:00 2001 From: Sam Bugs <101145325+0xsambugs@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:02:10 -0300 Subject: [PATCH 01/12] chore: rename klaytn to kaia [breaking change] (#623) --- src/chains.ts | 17 +++++++++++------ .../price-sources/coingecko-price-source.ts | 2 +- .../providers/provider-sources/ankr-provider.ts | 2 +- .../providers/provider-sources/drpc-provider.ts | 2 +- .../provider-sources/on-finality-provider.ts | 2 +- .../provider-sources/one-rpc-provider.ts | 2 +- .../quotes/quote-sources/1inch-quote-source.ts | 2 +- src/shared/defi-llama.ts | 2 +- test/integration/utils/erc20.ts | 2 +- 9 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/chains.ts b/src/chains.ts index 93eca2f9..05ec75f5 100644 --- a/src/chains.ts +++ b/src/chains.ts @@ -322,14 +322,19 @@ export const Chains = { ], explorer: 'https://explorer.ont.io/', }, - KLAYTN: { + KAIA: { chainId: 8217, - name: 'Klaytn', - ids: ['klaytn'], - nativeCurrency: { symbol: 'KLAY', name: 'Klaytn' }, + name: 'Kaia', + ids: ['klaytn', 'kaia'], + nativeCurrency: { symbol: 'KAIA', name: 'Kaia' }, wToken: '0xe4f05a66ec68b54a58b17c22107b02e0232cc817', - publicRPCs: ['https://public-en-cypress.klaytn.net', 'https://public-node-api.klaytnapi.com/v1/cypress', 'https://klaytn-pokt.nodies.app'], - explorer: 'https://scope.klaytn.com/', + publicRPCs: [ + 'https://public-en-cypress.klaytn.net', + 'https://public-node-api.klaytnapi.com/v1/cypress', + 'https://klaytn-pokt.nodies.app', + 'https://public-en.node.kaia.io', + ], + explorer: 'https://kaiascan.io/', }, AURORA: { chainId: 1313161554, diff --git a/src/services/prices/price-sources/coingecko-price-source.ts b/src/services/prices/price-sources/coingecko-price-source.ts index 07f84f87..e5321637 100644 --- a/src/services/prices/price-sources/coingecko-price-source.ts +++ b/src/services/prices/price-sources/coingecko-price-source.ts @@ -11,7 +11,7 @@ const COINGECKO_CHAIN_KEYS: Record = { [Chains.MOONBEAM.chainId]: 'https://rpc.ankr.com/moonbeam', [Chains.BIT_TORRENT.chainId]: 'https://rpc.ankr.com/bttc', [Chains.BASE.chainId]: 'https://rpc.ankr.com/base', - [Chains.KLAYTN.chainId]: 'https://rpc.ankr.com/klaytn', + [Chains.KAIA.chainId]: 'https://rpc.ankr.com/klaytn', [Chains.SCROLL.chainId]: 'https://rpc.ankr.com/scroll', }; diff --git a/src/services/providers/provider-sources/drpc-provider.ts b/src/services/providers/provider-sources/drpc-provider.ts index 47bab7d4..d4ca2f19 100644 --- a/src/services/providers/provider-sources/drpc-provider.ts +++ b/src/services/providers/provider-sources/drpc-provider.ts @@ -17,7 +17,7 @@ const SUPPORTED_CHAINS: Record = { [Chains.GNOSIS.chainId]: 'gnosis', [Chains.AURORA.chainId]: 'aurora', [Chains.POLYGON_ZKEVM.chainId]: 'polygon-zkevm', - [Chains.KLAYTN.chainId]: 'klaytn', + [Chains.KAIA.chainId]: 'klaytn', [Chains.BOBA.chainId]: 'boba-eth', [Chains.CELO.chainId]: 'celo', [Chains.CRONOS.chainId]: 'cronos', diff --git a/src/services/providers/provider-sources/on-finality-provider.ts b/src/services/providers/provider-sources/on-finality-provider.ts index 86fcaa6a..845e6fc8 100644 --- a/src/services/providers/provider-sources/on-finality-provider.ts +++ b/src/services/providers/provider-sources/on-finality-provider.ts @@ -14,7 +14,7 @@ const SUPPORTED_CHAINS: Record = { [Chains.FANTOM.chainId]: 'https://fantom.api.onfinality.io/public', [Chains.AVALANCHE.chainId]: 'https://avalanche.api.onfinality.io/public/ext/bc/C', [Chains.GNOSIS.chainId]: 'https://gnosis.api.onfinality.io/public', - [Chains.KLAYTN.chainId]: 'https://klaytn.api.onfinality.io/public', + [Chains.KAIA.chainId]: 'https://klaytn.api.onfinality.io/public', [Chains.CELO.chainId]: 'https://celo.api.onfinality.io/public', [Chains.FUSE.chainId]: 'https://fuse.api.onfinality.io/public', [Chains.KAVA.chainId]: 'https://kava.api.onfinality.io/public', diff --git a/src/services/providers/provider-sources/one-rpc-provider.ts b/src/services/providers/provider-sources/one-rpc-provider.ts index bc30692b..37b2aa32 100644 --- a/src/services/providers/provider-sources/one-rpc-provider.ts +++ b/src/services/providers/provider-sources/one-rpc-provider.ts @@ -15,7 +15,7 @@ const SUPPORTED_CHAINS: Record = { [Chains.OPTIMISM.chainId]: 'op', [Chains.FANTOM.chainId]: 'ftm', [Chains.CELO.chainId]: 'celo', - [Chains.KLAYTN.chainId]: 'klay', + [Chains.KAIA.chainId]: 'klay', [Chains.AURORA.chainId]: 'aurora', [Chains.BASE.chainId]: 'base', [Chains.GNOSIS.chainId]: 'gnosis', diff --git a/src/services/quotes/quote-sources/1inch-quote-source.ts b/src/services/quotes/quote-sources/1inch-quote-source.ts index 98958301..7813bde8 100644 --- a/src/services/quotes/quote-sources/1inch-quote-source.ts +++ b/src/services/quotes/quote-sources/1inch-quote-source.ts @@ -18,7 +18,7 @@ export const ONE_INCH_METADATA: QuoteSourceMetadata = { Chains.GNOSIS.chainId, Chains.AVALANCHE.chainId, Chains.FANTOM.chainId, - Chains.KLAYTN.chainId, + Chains.KAIA.chainId, Chains.AURORA.chainId, Chains.BASE.chainId, ], diff --git a/src/shared/defi-llama.ts b/src/shared/defi-llama.ts index 77b9d5a7..05f92642 100644 --- a/src/shared/defi-llama.ts +++ b/src/shared/defi-llama.ts @@ -21,7 +21,7 @@ const CHAIN_ID_TO_KEY: Record> = { [Chains.MOONRIVER.chainId]: 'moonriver', [Chains.OKC.chainId]: 'okexchain', [Chains.ONTOLOGY.chainId]: 'ontology', - [Chains.KLAYTN.chainId]: 'klaytn', + [Chains.KAIA.chainId]: 'klaytn', [Chains.AURORA.chainId]: 'aurora', [Chains.HARMONY_SHARD_0.chainId]: 'harmony', [Chains.MOONBEAM.chainId]: 'moonbeam', diff --git a/test/integration/utils/erc20.ts b/test/integration/utils/erc20.ts index 07bdf176..e5c635a4 100644 --- a/test/integration/utils/erc20.ts +++ b/test/integration/utils/erc20.ts @@ -147,7 +147,7 @@ export const TOKENS: Record> = { whale: '0xc73eed4494382093c6a7c284426a9a00f6c79939', }, }, - [Chains.KLAYTN.chainId]: { + [Chains.KAIA.chainId]: { STABLE_ERC20: { address: '0x6270b58be569a7c0b8f47594f191631ae5b2c86c', whale: '0x7d274dce8e2467fc4cdb6e8e1755db5686daebbb', From 33040147e971911a72318d8a270ac288e6d17f9f Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:03:19 +0000 Subject: [PATCH 02/12] chore: release v0.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e3bb5a50..a4fe76a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@balmy/sdk", - "version": "0.4.8", + "version": "0.5.0", "contributors": [ { "name": "Nicolás Chamo", From ff2e083464e11ea6722b186cf73809b92ede8431 Mon Sep 17 00:00:00 2001 From: Sam Bugs Date: Wed, 4 Dec 2024 10:40:06 -0300 Subject: [PATCH 03/12] feat: add linea chain to alchemy provider --- src/services/providers/provider-sources/alchemy-provider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/providers/provider-sources/alchemy-provider.ts b/src/services/providers/provider-sources/alchemy-provider.ts index 326f7356..4b68fbab 100644 --- a/src/services/providers/provider-sources/alchemy-provider.ts +++ b/src/services/providers/provider-sources/alchemy-provider.ts @@ -28,6 +28,7 @@ const ALCHEMY_NETWORKS: Record = { // [Chains.ZKSYNC_SEPOLIA.chainId]: { key: 'zksync-sepolia' }, [Chains.MANTLE.chainId]: { key: 'mantle-mainnet' }, [Chains.ROOTSTOCK.chainId]: { key: 'rootstock-mainnet' }, + [Chains.LINEA.chainId]: { key: 'linea-mainnet' }, }; export class AlchemyProviderSource extends BaseHttpProvider { From 488b12bcacfa9ca8df1b06f64ff7706578b11eea Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:42:40 +0000 Subject: [PATCH 04/12] chore: release v0.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a4fe76a9..53ec3d7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@balmy/sdk", - "version": "0.5.0", + "version": "0.5.1", "contributors": [ { "name": "Nicolás Chamo", From 9481b6b2bf52d48099892a5c60c44fec52af1a33 Mon Sep 17 00:00:00 2001 From: CryptoFede <43999360+fede2442@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:02:13 -0300 Subject: [PATCH 05/12] Update README.md (#626) --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6b0ed835..479bb84a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Balmy SDK +### [Docs](https://docs.balmy.xyz) | [X](https://x.com/balmy_xyz) | [Discord](http://discord.balmy.xyz/) -This repository contains the code for the Balmy sdk. +Balmy is the state-of-the-art DCA open protocol that enables users (or dapps) to Dollar Cost Average (DCA) any ERC20 into any ERC20 with their preferred period frequency, without sacrificing decentralization or giving up personal information to any centralized parties. + +The Balmy SDK allows you to interact with the Balmy protocol, providing efficient tools to manage token balances, retrieve trade quotes from DEX aggregators, and check token holdings across multiple chains. ## 🧪 Installing @@ -108,7 +111,3 @@ await signer.sendTransaction(bestTradeBySort.tx); ```bash yarn install ``` - -## 📖 Docs - -WIP - Will be at [docs.balmy.xyz](https://docs.balmy.xyz) From 14fb7dd22c4706f3c2e733906da4ce037d2a915f Mon Sep 17 00:00:00 2001 From: Sam Bugs Date: Mon, 16 Dec 2024 15:12:47 -0300 Subject: [PATCH 06/12] fix: lint --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 479bb84a..1dc0b106 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Balmy SDK + ### [Docs](https://docs.balmy.xyz) | [X](https://x.com/balmy_xyz) | [Discord](http://discord.balmy.xyz/) Balmy is the state-of-the-art DCA open protocol that enables users (or dapps) to Dollar Cost Average (DCA) any ERC20 into any ERC20 with their preferred period frequency, without sacrificing decentralization or giving up personal information to any centralized parties. From 6394feff5aef740709367d7478c355d6ee977449 Mon Sep 17 00:00:00 2001 From: 0xKoaj <141654454+0xKoaj@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:19:18 -0300 Subject: [PATCH 07/12] feat: add codex price source (#630) * feat: add codex price source * feat: implement historical and bulk historical data * refactor: throw error without api key --- .github/workflows/tests.yml | 2 +- src/sdk/builders/price-builder.ts | 4 + .../price-sources/codex-price-source.ts | 180 ++++++++++++++++++ .../services/prices/price-sources.spec.ts | 10 +- 4 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 src/services/prices/price-sources/codex-price-source.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6c6ca2fc..43df816c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,7 +53,7 @@ jobs: ALCHEMY_API_KEY: ${{secrets.ALCHEMY_API_KEY}} BARTER_AUTH_HEADER: ${{secrets.BARTER_AUTH_HEADER}} BARTER_CUSTOM_SUBDOMAIN: ${{secrets.BARTER_CUSTOM_SUBDOMAIN}} - + CODEX_API_KEY: ${{secrets.CODEX_API_KEY}} # integration-quotes: # needs: ['integration'] # runs-on: ubuntu-latest diff --git a/src/sdk/builders/price-builder.ts b/src/sdk/builders/price-builder.ts index e2149ace..191f98ae 100644 --- a/src/sdk/builders/price-builder.ts +++ b/src/sdk/builders/price-builder.ts @@ -10,9 +10,11 @@ import { PrioritizedPriceSource } from '@services/prices/price-sources/prioritiz import { FastestPriceSource } from '@services/prices/price-sources/fastest-price-source'; import { AggregatorPriceSource, PriceAggregationMethod } from '@services/prices/price-sources/aggregator-price-source'; import { BalmyPriceSource } from '@services/prices/price-sources/balmy-price-source'; +import { CodexPriceSource } from '@services/prices/price-sources/codex-price-source'; export type PriceSourceInput = | { type: 'defi-llama' } + | { type: 'codex'; apiKey: string } | { type: 'odos' } | { type: 'coingecko' } | { type: 'balmy'; apiKey: string } @@ -35,6 +37,8 @@ function buildSource(source: PriceSourceInput | undefined, { fetchService }: { f case undefined: // Defi Llama is basically Coingecko with some token mappings. Defi Llama has a 5 min cache, so the priority will be Coingecko => DefiLlama return new PrioritizedPriceSource([coingecko, defiLlama]); + case 'codex': + return new CodexPriceSource(fetchService, source.apiKey); case 'defi-llama': return defiLlama; case 'odos': diff --git a/src/services/prices/price-sources/codex-price-source.ts b/src/services/prices/price-sources/codex-price-source.ts new file mode 100644 index 00000000..a3367d08 --- /dev/null +++ b/src/services/prices/price-sources/codex-price-source.ts @@ -0,0 +1,180 @@ +import { ChainId, TimeString, Timestamp, TokenAddress } from '@types'; +import { IFetchService } from '@services/fetch/types'; +import { PriceResult, IPriceSource, PricesQueriesSupport, PriceInput } from '../types'; +import { Chains, getChainByKeyOrFail } from '@chains'; +import { isSameAddress, splitInChunks } from '@shared/utils'; +import { Addresses } from '@shared/constants'; +import ms from 'ms'; + +const SUPPORTED_CHAINS = [ + Chains.ARBITRUM, + Chains.ASTAR, + Chains.AURORA, + Chains.AVALANCHE, + Chains.BASE, + Chains.BLAST, + Chains.BNB_CHAIN, + Chains.BOBA, + Chains.CANTO, + // Chains.CELO, // wrapped native not supported + Chains.CRONOS, + Chains.ETHEREUM, + Chains.EVMOS, + Chains.FANTOM, + Chains.FUSE, + Chains.GNOSIS, + Chains.HARMONY_SHARD_0, + Chains.HECO, + Chains.KAIA, + Chains.LINEA, + Chains.MANTLE, + Chains.METIS_ANDROMEDA, + Chains.MODE, + Chains.MOONBEAM, + Chains.MOONRIVER, + Chains.OKC, + Chains.OPTIMISM, + Chains.POLYGON, + Chains.POLYGON_ZKEVM, + Chains.SCROLL, + Chains.VELAS, +]; + +export class CodexPriceSource implements IPriceSource { + constructor(private readonly fetch: IFetchService, private readonly apiKey: string) {} + + supportedQueries() { + const support: PricesQueriesSupport = { + getCurrentPrices: true, + getHistoricalPrices: true, + getBulkHistoricalPrices: true, + getChart: false, + }; + const entries = SUPPORTED_CHAINS.map(({ chainId }) => chainId).map((chainId) => [chainId, support]); + return Object.fromEntries(entries); + } + + async getCurrentPrices({ + tokens, + config, + }: { + tokens: PriceInput[]; + config: { timeout?: TimeString } | undefined; + }): Promise>> { + const input = tokens.map(({ token, chainId }) => ({ chainId, token })); + const prices = await this.getBulkPrices({ tokens: input, config }); + return Object.fromEntries( + Object.entries(prices).map(([chainId, tokens]) => [ + chainId, + Object.fromEntries(Object.entries(tokens).map(([token, price]) => [token, Object.values(price).at(0)!])), + ]) + ); + } + + async getHistoricalPrices({ + tokens, + timestamp, + searchWidth, + config, + }: { + tokens: PriceInput[]; + timestamp: Timestamp; + searchWidth: TimeString | undefined; + config: { timeout?: TimeString } | undefined; + }): Promise>> { + const input = tokens.map(({ token, chainId }) => ({ chainId, token, timestamp })); + const prices = await this.getBulkHistoricalPrices({ tokens: input, searchWidth, config }); + return Object.fromEntries( + Object.entries(prices).map(([chainId, tokens]) => [ + chainId, + Object.fromEntries(Object.entries(tokens).map(([token, price]) => [token, price[timestamp]])), + ]) + ); + } + + getBulkHistoricalPrices({ + tokens, + searchWidth, + config, + }: { + tokens: { chainId: ChainId; token: TokenAddress; timestamp: Timestamp }[]; + searchWidth: TimeString | undefined; + config: { timeout?: TimeString } | undefined; + }): Promise>>> { + return this.getBulkPrices({ tokens, searchWidth, config }); + } + + async getChart(_: { + tokens: PriceInput[]; + span: number; + period: TimeString; + bound: { from: Timestamp } | { upTo: Timestamp | 'now' }; + searchWidth?: TimeString; + config: { timeout?: TimeString } | undefined; + }): Promise>> { + return Promise.reject(new Error('Operation not supported')); + } + + private async getBulkPrices({ + tokens, + searchWidth, + config, + }: { + tokens: { chainId: ChainId; token: TokenAddress; timestamp?: Timestamp }[]; + searchWidth?: TimeString; + config: { timeout?: TimeString } | undefined; + }): Promise>>> { + if (!this.apiKey) throw new Error('API key is required'); + const result: Record>> = {}; + const chunks = splitInChunks(tokens, 25); + const requests = chunks.map(async (chunk) => { + const query = { + query: `query { + getTokenPrices( + inputs: [ + ${chunk + .map( + ({ token, chainId, timestamp }) => + `{ address: "${ + // Codex doesn't support native tokens, so we use the wrapped native token + isSameAddress(token, Addresses.NATIVE_TOKEN) ? getChainByKeyOrFail(chainId).wToken : token + }", networkId: ${chainId} ${timestamp ? `, timestamp: ${timestamp}` : ''} }` + ) + .join('\n')} + ] + ) { + networkId + address + priceUsd + timestamp + } + }`, + }; + const response = await this.fetch.fetch(`https://graph.defined.fi/graphql`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: this.apiKey }, + body: JSON.stringify(query), + timeout: config?.timeout, + }); + + if (!response.ok) { + return; + } + + const body: Response = await response.json(); + chunk.forEach(({ chainId, token, timestamp }, index) => { + const tokenPrice = body.data.getTokenPrices[index]; + if (!tokenPrice) return; + if (searchWidth && timestamp && Math.abs(tokenPrice.timestamp - timestamp) > ms(searchWidth)) return; + if (!result[chainId]) result[chainId] = {}; + if (!result[chainId][token]) result[chainId][token] = {}; + result[chainId][token][timestamp ?? tokenPrice.timestamp] = { price: tokenPrice.priceUsd, closestTimestamp: tokenPrice.timestamp }; + }); + }); + + await Promise.all(requests); + return result; + } +} + +type Response = { data: { getTokenPrices: { networkId: ChainId; address: TokenAddress; priceUsd: number; timestamp: number }[] } }; diff --git a/test/integration/services/prices/price-sources.spec.ts b/test/integration/services/prices/price-sources.spec.ts index 91b834e4..95191a4b 100644 --- a/test/integration/services/prices/price-sources.spec.ts +++ b/test/integration/services/prices/price-sources.spec.ts @@ -1,6 +1,7 @@ import ms from 'ms'; import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; +import dotenv from 'dotenv'; import { DefiLlamaPriceSource } from '@services/prices/price-sources/defi-llama-price-source'; import { OdosPriceSource } from '@services/prices/price-sources/odos-price-source'; import { CoingeckoPriceSource } from '@services/prices/price-sources/coingecko-price-source'; @@ -13,7 +14,9 @@ import { IPriceSource, PriceInput, PricesQueriesSupport } from '@services/prices import { PrioritizedPriceSource } from '@services/prices/price-sources/prioritized-price-source'; import { FastestPriceSource } from '@services/prices/price-sources/fastest-price-source'; import { AggregatorPriceSource } from '@services/prices/price-sources/aggregator-price-source'; +import { CodexPriceSource } from '@services/prices/price-sources/codex-price-source'; chai.use(chaiAsPromised); +dotenv.config(); const TESTS: Record = { [Chains.OPTIMISM.chainId]: { address: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', symbol: 'DAI' }, @@ -33,6 +36,7 @@ const CACHED_PRICE_SOURCE = new CachedPriceSource(DEFI_LLAMA_PRICE_SOURCE, { }, maxSize: 100, }); +const CODEX_PRICE_SOURCE = new CodexPriceSource(FETCH_SERVICE, process.env.CODEX_API_KEY!); const PRIORITIZED_PRICE_SOURCE = new PrioritizedPriceSource([ODOS_PRICE_SOURCE, DEFI_LLAMA_PRICE_SOURCE]); const FASTEST_PRICE_SOURCE = new FastestPriceSource([ODOS_PRICE_SOURCE, DEFI_LLAMA_PRICE_SOURCE]); const AGGREGATOR_PRICE_SOURCE = new AggregatorPriceSource([ODOS_PRICE_SOURCE, DEFI_LLAMA_PRICE_SOURCE], 'median'); @@ -50,7 +54,7 @@ describe('Token Price Sources', () => { priceSourceTest({ title: 'Aggregator Source', source: AGGREGATOR_PRICE_SOURCE }); // priceSourceTest({ title: 'Balmy', source: BALMY_PRICE_SOURCE }); Needs API key // priceSourceTest({ title: 'Coingecko Source', source: COINGECKO_TOKEN_SOURCE }); Commented out because of rate limiting issues - + priceSourceTest({ title: 'Codex Source', source: CODEX_PRICE_SOURCE }); function priceSourceTest({ title, source }: { title: string; source: IPriceSource }) { describe(title, () => { queryTest({ @@ -72,7 +76,7 @@ describe('Token Price Sources', () => { getResult: (source, tokens) => source.getHistoricalPrices({ tokens, - timestamp: 1711843200, // Friday, 31 March 2024 0:00:00 + timestamp: 1729123200, // Thursday, 17 October 2024 0:00:00 config: { timeout: '10s' }, searchWidth: undefined, }), @@ -81,7 +85,7 @@ describe('Token Price Sources', () => { expect(typeof timestamp).to.equal('number'); }, }); - const from = 1711843200; // Friday, 31 March 2024 0:00:00 + const from = 1729123200; // Thursday, 17 October 2024 0:00:00 const span = 10; const period = '1d'; queryTest({ From 4c885d4d1e90e16463d6e3d65bb3eff172e240bf Mon Sep 17 00:00:00 2001 From: Alex Novikov Date: Wed, 18 Dec 2024 16:51:54 -0300 Subject: [PATCH 08/12] fix(readme): buildSDK name tipo; (#631) --- .gitignore | 3 ++- README.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index e0c3f116..af109754 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ coverage .env newrelic_agent.log yarn-error.log -cache \ No newline at end of file +cache +.idea diff --git a/README.md b/README.md index 1dc0b106..355ea229 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ npm install @balmy/sdk ### 👷🏽‍♀️ Building the SDK ```javascript -import { buildSdk } from "@balmy/sdk"; +import { buildSDK } from "@balmy/sdk"; -const sdk = buildSdk(config); +const sdk = buildSDK(config); ``` ### ⚖️ Getting balance for multiple tokens on several chains From 16f8821e1245918087178b97496a2f7f6268f61c Mon Sep 17 00:00:00 2001 From: 0xKoaj <141654454+0xKoaj@users.noreply.github.com> Date: Thu, 19 Dec 2024 20:04:52 -0300 Subject: [PATCH 09/12] feat: add alchemy price source (#632) * feat: add alchemy price source * refactor: reuse alchemy networks --- src/sdk/builders/price-builder.ts | 5 +- .../price-sources/alchemy-price-source.ts | 108 ++++++++++++++++++ .../provider-sources/alchemy-provider.ts | 31 +---- src/shared/alchemy.ts | 31 +++++ .../services/prices/price-sources.spec.ts | 3 + 5 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 src/services/prices/price-sources/alchemy-price-source.ts create mode 100644 src/shared/alchemy.ts diff --git a/src/sdk/builders/price-builder.ts b/src/sdk/builders/price-builder.ts index 191f98ae..5920cdca 100644 --- a/src/sdk/builders/price-builder.ts +++ b/src/sdk/builders/price-builder.ts @@ -11,11 +11,12 @@ import { FastestPriceSource } from '@services/prices/price-sources/fastest-price import { AggregatorPriceSource, PriceAggregationMethod } from '@services/prices/price-sources/aggregator-price-source'; import { BalmyPriceSource } from '@services/prices/price-sources/balmy-price-source'; import { CodexPriceSource } from '@services/prices/price-sources/codex-price-source'; - +import { AlchemyPriceSource } from '@services/prices/price-sources/alchemy-price-source'; export type PriceSourceInput = | { type: 'defi-llama' } | { type: 'codex'; apiKey: string } | { type: 'odos' } + | { type: 'alchemy'; apiKey: string } | { type: 'coingecko' } | { type: 'balmy'; apiKey: string } | { type: 'prioritized'; sources: PriceSourceInput[] } @@ -39,6 +40,8 @@ function buildSource(source: PriceSourceInput | undefined, { fetchService }: { f return new PrioritizedPriceSource([coingecko, defiLlama]); case 'codex': return new CodexPriceSource(fetchService, source.apiKey); + case 'alchemy': + return new AlchemyPriceSource(fetchService, source.apiKey); case 'defi-llama': return defiLlama; case 'odos': diff --git a/src/services/prices/price-sources/alchemy-price-source.ts b/src/services/prices/price-sources/alchemy-price-source.ts new file mode 100644 index 00000000..29c41c03 --- /dev/null +++ b/src/services/prices/price-sources/alchemy-price-source.ts @@ -0,0 +1,108 @@ +import { ChainId, TimeString, Timestamp, TokenAddress } from '@types'; +import { IFetchService } from '@services/fetch/types'; +import { PriceResult, IPriceSource, PricesQueriesSupport, TokenPrice, PriceInput } from '../types'; +import { Chains, getChainByKeyOrFail } from '@chains'; +import { reduceTimeout, timeoutPromise } from '@shared/timeouts'; +import { filterRejectedResults, groupByChain, isSameAddress, splitInChunks } from '@shared/utils'; +import { Addresses } from '@shared/constants'; +import { ALCHEMY_NETWORKS } from '@shared/alchemy'; +export class AlchemyPriceSource implements IPriceSource { + constructor(private readonly fetch: IFetchService, private readonly apiKey: string) { + if (!this.apiKey) throw new Error('API key is required'); + } + + supportedQueries() { + const support: PricesQueriesSupport = { + getCurrentPrices: true, + getHistoricalPrices: false, + getBulkHistoricalPrices: false, + getChart: false, + }; + const entries = Object.entries(ALCHEMY_NETWORKS).map(([chainId]) => [chainId, support]); + return Object.fromEntries(entries); + } + + async getCurrentPrices({ + tokens, + config, + }: { + tokens: PriceInput[]; + config: { timeout?: TimeString } | undefined; + }): Promise>> { + const groupedByChain = groupByChain(tokens, ({ token }) => token); + const reducedTimeout = reduceTimeout(config?.timeout, '100'); + const promises = Object.entries(groupedByChain).map(async ([chainId, tokens]) => [ + Number(chainId), + await timeoutPromise(this.getCurrentPricesInChain(Number(chainId), tokens, reducedTimeout), reducedTimeout), + ]); + return Object.fromEntries(await filterRejectedResults(promises)); + } + + getHistoricalPrices(_: { + tokens: PriceInput[]; + timestamp: Timestamp; + searchWidth: TimeString | undefined; + config: { timeout?: TimeString } | undefined; + }): Promise>> { + // Only supports historical prices searching by token symbol + return Promise.reject(new Error('Operation not supported')); + } + + getBulkHistoricalPrices(_: { + tokens: { chainId: ChainId; token: TokenAddress; timestamp: Timestamp }[]; + searchWidth: TimeString | undefined; + config: { timeout?: TimeString } | undefined; + }): Promise>>> { + return Promise.reject(new Error('Operation not supported')); + } + + async getChart(_: { + tokens: PriceInput[]; + span: number; + period: TimeString; + bound: { from: Timestamp } | { upTo: Timestamp | 'now' }; + searchWidth?: TimeString; + config: { timeout?: TimeString } | undefined; + }): Promise>> { + return Promise.reject(new Error('Operation not supported')); + } + + private async getCurrentPricesInChain(chainId: ChainId, addresses: TokenAddress[], timeout?: TimeString) { + const url = `https://api.g.alchemy.com/prices/v1/${this.apiKey}/tokens/by-address`; + const result: Record = {}; + const chunks = splitInChunks(addresses, 25); + const promises = chunks.map(async (chunk) => { + const response = await this.fetch.fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + addresses: chunk.map((address) => ({ + network: ALCHEMY_NETWORKS[chainId].key, + // Alchemy doesn't support native tokens (only on Ethereum), so we use the wrapped native token + address: + isSameAddress(address, Addresses.NATIVE_TOKEN) && chainId !== Chains.ETHEREUM.chainId + ? getChainByKeyOrFail(chainId).wToken + : address, + })), + }), + timeout, + }); + + if (!response.ok) { + return; + } + const body: Response = await response.json(); + chunk.forEach((address, index) => { + const tokenPrice = body.data[index].prices[0]; + if (!tokenPrice) return; + const timestamp = Math.floor(new Date(tokenPrice.lastUpdatedAt).getTime() / 1000); + result[address] = { price: Number(tokenPrice.value), closestTimestamp: timestamp }; + }); + }); + + await Promise.all(promises); + return result; + } +} + +type Response = { data: { address: TokenAddress; prices: { currency: string; value: string; lastUpdatedAt: string }[] }[] }; diff --git a/src/services/providers/provider-sources/alchemy-provider.ts b/src/services/providers/provider-sources/alchemy-provider.ts index 4b68fbab..b4939ead 100644 --- a/src/services/providers/provider-sources/alchemy-provider.ts +++ b/src/services/providers/provider-sources/alchemy-provider.ts @@ -1,35 +1,6 @@ import { ChainId } from '@types'; -import { Chains } from '@chains'; import { BaseHttpProvider } from './base/base-http-provider'; - -const ALCHEMY_NETWORKS: Record = { - [Chains.ETHEREUM.chainId]: { key: 'eth-mainnet' }, - [Chains.ETHEREUM_SEPOLIA.chainId]: { key: 'eth-sepolia' }, - [Chains.OPTIMISM.chainId]: { key: 'opt-mainnet' }, - // [Chains.OPTIMISM_SEPOLIA.chainId]: { key: 'opt-sepolia' }, - [Chains.ARBITRUM.chainId]: { key: 'arb-mainnet' }, - // [Chains.ARBITRUM_SEPOLIA.chainId]: { key: 'arb-sepolia' }, - [Chains.POLYGON.chainId]: { key: 'polygon-mainnet' }, - [Chains.POLYGON_MUMBAI.chainId]: { key: 'polygon-mumbai' }, - [Chains.ASTAR.chainId]: { key: 'astar-mainnet' }, - [Chains.BLAST.chainId]: { key: 'blast-mainnet' }, - [Chains.BNB_CHAIN.chainId]: { key: 'bnb-mainnet', onlyPaid: true }, - [Chains.AVALANCHE.chainId]: { key: 'avax-mainnet', onlyPaid: true }, - [Chains.FANTOM.chainId]: { key: 'fantom-mainnet' }, - [Chains.METIS_ANDROMEDA.chainId]: { key: 'metis-mainnet', onlyPaid: true }, - [Chains.POLYGON_ZKEVM.chainId]: { key: 'polygonzkevm-mainnet' }, - // [Chains.POLYGON_ZKEVM_TESTNET.chainId]: { key: 'polygonzkevm-testnet' }, - [Chains.BASE.chainId]: { key: 'base-mainnet' }, - [Chains.GNOSIS.chainId]: { key: 'gnosis-mainnet', onlyPaid: true }, - [Chains.SCROLL.chainId]: { key: 'scroll-mainnet' }, - [Chains.opBNB.chainId]: { key: 'opbnb-mainnet', onlyPaid: true }, - // [Chains.BASE_SEPOLIA.chainId]: { key: 'base-sepolia' }, - // [Chains.ZKSYNC.chainId]: { key: 'zksync-mainnet' }, - // [Chains.ZKSYNC_SEPOLIA.chainId]: { key: 'zksync-sepolia' }, - [Chains.MANTLE.chainId]: { key: 'mantle-mainnet' }, - [Chains.ROOTSTOCK.chainId]: { key: 'rootstock-mainnet' }, - [Chains.LINEA.chainId]: { key: 'linea-mainnet' }, -}; +import { ALCHEMY_NETWORKS } from '@shared/alchemy'; export class AlchemyProviderSource extends BaseHttpProvider { private readonly supported: ChainId[]; diff --git a/src/shared/alchemy.ts b/src/shared/alchemy.ts new file mode 100644 index 00000000..a7776d3f --- /dev/null +++ b/src/shared/alchemy.ts @@ -0,0 +1,31 @@ +import { Chains } from '@chains'; +import { ChainId } from '@types'; + +export const ALCHEMY_NETWORKS: Record = { + [Chains.ETHEREUM.chainId]: { key: 'eth-mainnet' }, + [Chains.ETHEREUM_SEPOLIA.chainId]: { key: 'eth-sepolia' }, + [Chains.OPTIMISM.chainId]: { key: 'opt-mainnet' }, + // [Chains.OPTIMISM_SEPOLIA.chainId]: { key: 'opt-sepolia' }, + [Chains.ARBITRUM.chainId]: { key: 'arb-mainnet' }, + // [Chains.ARBITRUM_SEPOLIA.chainId]: { key: 'arb-sepolia' }, + [Chains.POLYGON.chainId]: { key: 'polygon-mainnet' }, + [Chains.POLYGON_MUMBAI.chainId]: { key: 'polygon-mumbai' }, + [Chains.ASTAR.chainId]: { key: 'astar-mainnet' }, + [Chains.BLAST.chainId]: { key: 'blast-mainnet' }, + [Chains.BNB_CHAIN.chainId]: { key: 'bnb-mainnet', onlyPaid: true }, + [Chains.AVALANCHE.chainId]: { key: 'avax-mainnet', onlyPaid: true }, + [Chains.FANTOM.chainId]: { key: 'fantom-mainnet' }, + [Chains.METIS_ANDROMEDA.chainId]: { key: 'metis-mainnet', onlyPaid: true }, + [Chains.POLYGON_ZKEVM.chainId]: { key: 'polygonzkevm-mainnet' }, + // [Chains.POLYGON_ZKEVM_TESTNET.chainId]: { key: 'polygonzkevm-testnet' }, + [Chains.BASE.chainId]: { key: 'base-mainnet' }, + [Chains.GNOSIS.chainId]: { key: 'gnosis-mainnet', onlyPaid: true }, + [Chains.SCROLL.chainId]: { key: 'scroll-mainnet' }, + [Chains.opBNB.chainId]: { key: 'opbnb-mainnet', onlyPaid: true }, + // [Chains.BASE_SEPOLIA.chainId]: { key: 'base-sepolia' }, + // [Chains.ZKSYNC.chainId]: { key: 'zksync-mainnet' }, + // [Chains.ZKSYNC_SEPOLIA.chainId]: { key: 'zksync-sepolia' }, + [Chains.MANTLE.chainId]: { key: 'mantle-mainnet' }, + [Chains.ROOTSTOCK.chainId]: { key: 'rootstock-mainnet' }, + [Chains.LINEA.chainId]: { key: 'linea-mainnet' }, +}; diff --git a/test/integration/services/prices/price-sources.spec.ts b/test/integration/services/prices/price-sources.spec.ts index 95191a4b..f8ecbf9b 100644 --- a/test/integration/services/prices/price-sources.spec.ts +++ b/test/integration/services/prices/price-sources.spec.ts @@ -15,6 +15,7 @@ import { PrioritizedPriceSource } from '@services/prices/price-sources/prioritiz import { FastestPriceSource } from '@services/prices/price-sources/fastest-price-source'; import { AggregatorPriceSource } from '@services/prices/price-sources/aggregator-price-source'; import { CodexPriceSource } from '@services/prices/price-sources/codex-price-source'; +import { AlchemyPriceSource } from '@services/prices/price-sources/alchemy-price-source'; chai.use(chaiAsPromised); dotenv.config(); @@ -37,6 +38,7 @@ const CACHED_PRICE_SOURCE = new CachedPriceSource(DEFI_LLAMA_PRICE_SOURCE, { maxSize: 100, }); const CODEX_PRICE_SOURCE = new CodexPriceSource(FETCH_SERVICE, process.env.CODEX_API_KEY!); +const ALCHEMY_PRICE_SOURCE = new AlchemyPriceSource(FETCH_SERVICE, process.env.ALCHEMY_API_KEY!); const PRIORITIZED_PRICE_SOURCE = new PrioritizedPriceSource([ODOS_PRICE_SOURCE, DEFI_LLAMA_PRICE_SOURCE]); const FASTEST_PRICE_SOURCE = new FastestPriceSource([ODOS_PRICE_SOURCE, DEFI_LLAMA_PRICE_SOURCE]); const AGGREGATOR_PRICE_SOURCE = new AggregatorPriceSource([ODOS_PRICE_SOURCE, DEFI_LLAMA_PRICE_SOURCE], 'median'); @@ -55,6 +57,7 @@ describe('Token Price Sources', () => { // priceSourceTest({ title: 'Balmy', source: BALMY_PRICE_SOURCE }); Needs API key // priceSourceTest({ title: 'Coingecko Source', source: COINGECKO_TOKEN_SOURCE }); Commented out because of rate limiting issues priceSourceTest({ title: 'Codex Source', source: CODEX_PRICE_SOURCE }); + priceSourceTest({ title: 'Alchemy Source', source: ALCHEMY_PRICE_SOURCE }); function priceSourceTest({ title, source }: { title: string; source: IPriceSource }) { describe(title, () => { queryTest({ From ffe06fc69d5727ac37d658efa81b47eb0b1ee58b Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 13:30:25 +0000 Subject: [PATCH 10/12] chore: release v0.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 53ec3d7f..f8aa12d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@balmy/sdk", - "version": "0.5.1", + "version": "0.5.2", "contributors": [ { "name": "Nicolás Chamo", From 2b6966b97e279429c5bddfeb741819b95c295507 Mon Sep 17 00:00:00 2001 From: Sam Bugs Date: Sat, 28 Dec 2024 09:36:41 -0300 Subject: [PATCH 11/12] fix: alchemy price source chains --- .../price-sources/alchemy-price-source.ts | 11 ++++- .../provider-sources/alchemy-provider.ts | 9 +++- src/shared/alchemy.ts | 48 ++++++++----------- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/services/prices/price-sources/alchemy-price-source.ts b/src/services/prices/price-sources/alchemy-price-source.ts index 29c41c03..7b2cf808 100644 --- a/src/services/prices/price-sources/alchemy-price-source.ts +++ b/src/services/prices/price-sources/alchemy-price-source.ts @@ -18,7 +18,16 @@ export class AlchemyPriceSource implements IPriceSource { getBulkHistoricalPrices: false, getChart: false, }; - const entries = Object.entries(ALCHEMY_NETWORKS).map(([chainId]) => [chainId, support]); + const entries = Object.entries(ALCHEMY_NETWORKS) + .filter( + ([ + _, + { + price: { supported }, + }, + ]) => supported + ) + .map(([chainId]) => [chainId, support]); return Object.fromEntries(entries); } diff --git a/src/services/providers/provider-sources/alchemy-provider.ts b/src/services/providers/provider-sources/alchemy-provider.ts index b4939ead..abc7a5d3 100644 --- a/src/services/providers/provider-sources/alchemy-provider.ts +++ b/src/services/providers/provider-sources/alchemy-provider.ts @@ -25,7 +25,14 @@ export class AlchemyProviderSource extends BaseHttpProvider { export function alchemySupportedChains(args?: { onlyFree?: boolean }): ChainId[] { return Object.entries(ALCHEMY_NETWORKS) - .filter(([_, { onlyPaid }]) => !onlyPaid || !args?.onlyFree) + .filter( + ([ + _, + { + rpc: { tier }, + }, + ]) => tier === 'free' || !args?.onlyFree + ) .map(([chainId]) => Number(chainId)); } diff --git a/src/shared/alchemy.ts b/src/shared/alchemy.ts index a7776d3f..ab88e5a9 100644 --- a/src/shared/alchemy.ts +++ b/src/shared/alchemy.ts @@ -1,31 +1,25 @@ import { Chains } from '@chains'; import { ChainId } from '@types'; -export const ALCHEMY_NETWORKS: Record = { - [Chains.ETHEREUM.chainId]: { key: 'eth-mainnet' }, - [Chains.ETHEREUM_SEPOLIA.chainId]: { key: 'eth-sepolia' }, - [Chains.OPTIMISM.chainId]: { key: 'opt-mainnet' }, - // [Chains.OPTIMISM_SEPOLIA.chainId]: { key: 'opt-sepolia' }, - [Chains.ARBITRUM.chainId]: { key: 'arb-mainnet' }, - // [Chains.ARBITRUM_SEPOLIA.chainId]: { key: 'arb-sepolia' }, - [Chains.POLYGON.chainId]: { key: 'polygon-mainnet' }, - [Chains.POLYGON_MUMBAI.chainId]: { key: 'polygon-mumbai' }, - [Chains.ASTAR.chainId]: { key: 'astar-mainnet' }, - [Chains.BLAST.chainId]: { key: 'blast-mainnet' }, - [Chains.BNB_CHAIN.chainId]: { key: 'bnb-mainnet', onlyPaid: true }, - [Chains.AVALANCHE.chainId]: { key: 'avax-mainnet', onlyPaid: true }, - [Chains.FANTOM.chainId]: { key: 'fantom-mainnet' }, - [Chains.METIS_ANDROMEDA.chainId]: { key: 'metis-mainnet', onlyPaid: true }, - [Chains.POLYGON_ZKEVM.chainId]: { key: 'polygonzkevm-mainnet' }, - // [Chains.POLYGON_ZKEVM_TESTNET.chainId]: { key: 'polygonzkevm-testnet' }, - [Chains.BASE.chainId]: { key: 'base-mainnet' }, - [Chains.GNOSIS.chainId]: { key: 'gnosis-mainnet', onlyPaid: true }, - [Chains.SCROLL.chainId]: { key: 'scroll-mainnet' }, - [Chains.opBNB.chainId]: { key: 'opbnb-mainnet', onlyPaid: true }, - // [Chains.BASE_SEPOLIA.chainId]: { key: 'base-sepolia' }, - // [Chains.ZKSYNC.chainId]: { key: 'zksync-mainnet' }, - // [Chains.ZKSYNC_SEPOLIA.chainId]: { key: 'zksync-sepolia' }, - [Chains.MANTLE.chainId]: { key: 'mantle-mainnet' }, - [Chains.ROOTSTOCK.chainId]: { key: 'rootstock-mainnet' }, - [Chains.LINEA.chainId]: { key: 'linea-mainnet' }, +export const ALCHEMY_NETWORKS: Record = { + [Chains.ETHEREUM.chainId]: { key: 'eth-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, + [Chains.ETHEREUM_SEPOLIA.chainId]: { key: 'eth-sepolia', rpc: { tier: 'free' }, price: { supported: false } }, + [Chains.OPTIMISM.chainId]: { key: 'opt-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, + [Chains.ARBITRUM.chainId]: { key: 'arb-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, + [Chains.POLYGON.chainId]: { key: 'polygon-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, + [Chains.POLYGON_MUMBAI.chainId]: { key: 'polygon-mumbai', rpc: { tier: 'free' }, price: { supported: false } }, + [Chains.ASTAR.chainId]: { key: 'astar-mainnet', rpc: { tier: 'free' }, price: { supported: false } }, + [Chains.BLAST.chainId]: { key: 'blast-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, + [Chains.BNB_CHAIN.chainId]: { key: 'bnb-mainnet', rpc: { tier: 'paid' }, price: { supported: true } }, + [Chains.AVALANCHE.chainId]: { key: 'avax-mainnet', rpc: { tier: 'paid' }, price: { supported: true } }, + [Chains.FANTOM.chainId]: { key: 'fantom-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, + [Chains.METIS_ANDROMEDA.chainId]: { key: 'metis-mainnet', rpc: { tier: 'paid' }, price: { supported: true } }, + [Chains.POLYGON_ZKEVM.chainId]: { key: 'polygonzkevm-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, + [Chains.BASE.chainId]: { key: 'base-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, + [Chains.GNOSIS.chainId]: { key: 'gnosis-mainnet', rpc: { tier: 'paid' }, price: { supported: true } }, + [Chains.SCROLL.chainId]: { key: 'scroll-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, + [Chains.opBNB.chainId]: { key: 'opbnb-mainnet', rpc: { tier: 'paid' }, price: { supported: true } }, + [Chains.MANTLE.chainId]: { key: 'mantle-mainnet', rpc: { tier: 'free' }, price: { supported: false } }, + [Chains.ROOTSTOCK.chainId]: { key: 'rootstock-mainnet', rpc: { tier: 'free' }, price: { supported: false } }, + [Chains.LINEA.chainId]: { key: 'linea-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, }; From 8863e38d22ddec6ef574e826e9baaa375d85b723 Mon Sep 17 00:00:00 2001 From: Sam Bugs Date: Sat, 28 Dec 2024 09:42:01 -0300 Subject: [PATCH 12/12] test: reject promises test --- .../source-lists/local-source-list.spec.ts | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 test/unit/services/quotes/source-lists/local-source-list.spec.ts diff --git a/test/unit/services/quotes/source-lists/local-source-list.spec.ts b/test/unit/services/quotes/source-lists/local-source-list.spec.ts new file mode 100644 index 00000000..22df66b3 --- /dev/null +++ b/test/unit/services/quotes/source-lists/local-source-list.spec.ts @@ -0,0 +1,75 @@ +import chai, { expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { LocalSourceList } from '@services/quotes/source-lists/local-source-list'; +import { SourceListQuoteRequest } from '@services/quotes/source-lists/types'; +import { then, when } from '@test-utils/bdd'; +import { IProviderService } from '@services/providers'; +import { IFetchService } from '@services/fetch'; +chai.use(chaiAsPromised); + +describe('Local Source List', () => { + describe('Rejected Promises', () => { + when('asking for unknown source', () => { + then('promise is rejected', async () => { + const sourceList = new LocalSourceList({ + providerService: PROVIDER_SERVICE, + fetchService: FETCH_SERVICE, + }); + const quotes = sourceList.getQuotes({ + ...REQUEST, + sources: ['unknown'], + order: { type: 'sell', sellAmount: 100 }, + }); + expect(Object.keys(quotes)).to.have.lengthOf(1); + await expect(quotes['unknown']).to.have.rejectedWith(`Could not find a source with id 'unknown'`); + }); + }); + + when('executing a buy order for a source that does not support it', () => { + then('promise is rejected', async () => { + const sourceList = new LocalSourceList({ + providerService: PROVIDER_SERVICE, + fetchService: FETCH_SERVICE, + }); + const quotes = sourceList.getQuotes({ + ...REQUEST, + order: { type: 'buy', buyAmount: 100 }, + sources: ['odos'], + }); + expect(Object.keys(quotes)).to.have.lengthOf(1); + await expect(quotes['odos']).to.have.rejectedWith(`Source with id 'odos' does not support buy orders`); + }); + }); + + when('context/config is invalid for a source', () => { + then('promise is rejected', async () => { + const sourceList = new LocalSourceList({ + providerService: PROVIDER_SERVICE, + fetchService: FETCH_SERVICE, + }); + const quotes = sourceList.getQuotes({ + ...REQUEST, + order: { type: 'sell', sellAmount: 100 }, + sources: ['enso'], + }); + expect(Object.keys(quotes)).to.have.lengthOf(1); + await expect(quotes['enso']).to.have.rejectedWith(`The current context or config is not valid for source with id 'enso'`); + }); + }); + }); +}); + +const REQUEST: Omit = { + chainId: 1, + sellToken: '0x0000000000000000000000000000000000000001', + buyToken: '0x0000000000000000000000000000000000000002', + slippagePercentage: 0.03, + takerAddress: '0x0000000000000000000000000000000000000003', + external: { + tokenData: {} as any, + gasPrice: {} as any, + }, +}; + +const PROVIDER_SERVICE: IProviderService = {} as any; +const FETCH_SERVICE: IFetchService = {} as any;