From 03873a5d0d6339e7f5ef452a6fcad4e572c74e48 Mon Sep 17 00:00:00 2001 From: Vasilis Xouris Date: Mon, 17 Jun 2024 11:43:59 -0700 Subject: [PATCH] feat: Introduce GraphQLTokenFeeFetcher (no traffic - 4kb limit) --- .github/workflows/test.yml | 3 + README.md | 4 +- bin/app.ts | 12 +++ bin/stacks/routing-api-stack.ts | 6 ++ bin/stacks/routing-lambda-stack.ts | 6 ++ lib/graphql/graphql-client.ts | 38 ++++++++ lib/graphql/graphql-provider.ts | 61 ++++++++++++ lib/graphql/graphql-queries.ts | 35 +++++++ lib/graphql/graphql-schemas.ts | 25 +++++ lib/graphql/graphql-token-fee-fetcher.ts | 75 +++++++++++++++ .../integ/graphql/graphql-provider.test.ts | 58 ++++++++++++ .../graphql/graphql-token-fee-fetcher.test.ts | 65 +++++++++++++ .../unit/graphql/graphql-provider.test.ts | 92 +++++++++++++++++++ 13 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 lib/graphql/graphql-client.ts create mode 100644 lib/graphql/graphql-provider.ts create mode 100644 lib/graphql/graphql-queries.ts create mode 100644 lib/graphql/graphql-schemas.ts create mode 100644 lib/graphql/graphql-token-fee-fetcher.ts create mode 100644 test/mocha/integ/graphql/graphql-provider.test.ts create mode 100644 test/mocha/integ/graphql/graphql-token-fee-fetcher.test.ts create mode 100644 test/mocha/unit/graphql/graphql-provider.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c63d35e61d..89209cf878 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,9 @@ jobs: test: name: Run tests runs-on: ubuntu-latest + env: + GQL_URL: ${{ secrets.UNI_GRAPHQL_ENDPOINT }} + GQL_H_ORGN: ${{ secrets.UNI_GRAPHQL_HEADER_ORIGIN }} steps: - name: Checkout Repo diff --git a/README.md b/README.md index 28555d79c6..f2eea74815 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,9 @@ The best way to develop and test the API is to deploy your own instance to AWS. TENDERLY_PROJECT = '' # For enabling Tenderly simulations TENDERLY_ACCESS_KEY = '' # For enabling Tenderly simulations TENDERLY_NODE_API_KEY = '' # For enabling Tenderly node-level RPC access - ALCHEMY_QUERY_KEY = '' For Alchemy subgraph query access + ALCHEMY_QUERY_KEY = '' # For Alchemy subgraph query access + GQL_URL = '' # The GraphQL endpoint url, for Uniswap graphql query access + GQL_H_ORGN = '' # The GraphQL header origin, for Uniswap graphql query access ``` 3. Install and build the package ``` diff --git a/bin/app.ts b/bin/app.ts index e36dfbb264..9877358b2f 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -39,6 +39,8 @@ export class RoutingAPIStage extends Stage { unicornSecret: string alchemyQueryKey?: string decentralizedNetworkApiKey?: string + uniGraphQLEndpoint: string + uniGraphQLHeaderOrigin: string } ) { super(scope, id, props) @@ -60,6 +62,8 @@ export class RoutingAPIStage extends Stage { unicornSecret, alchemyQueryKey, decentralizedNetworkApiKey, + uniGraphQLEndpoint, + uniGraphQLHeaderOrigin, } = props const { url } = new RoutingAPIStack(this, 'RoutingAPI', { @@ -80,6 +84,8 @@ export class RoutingAPIStage extends Stage { unicornSecret, alchemyQueryKey, decentralizedNetworkApiKey, + uniGraphQLEndpoint, + uniGraphQLHeaderOrigin, }) this.url = url } @@ -255,6 +261,8 @@ export class RoutingAPIPipeline extends Stack { unicornSecret: unicornSecrets.secretValueFromJson('debug-config-unicorn-key').toString(), alchemyQueryKey: routingApiNewSecrets.secretValueFromJson('alchemy-query-key').toString(), decentralizedNetworkApiKey: routingApiNewSecrets.secretValueFromJson('decentralized-network-api-key').toString(), + uniGraphQLEndpoint: routingApiNewSecrets.secretValueFromJson('uni-graphql-endpoint').toString(), + uniGraphQLHeaderOrigin: routingApiNewSecrets.secretValueFromJson('uni-graphql-header-origin').toString(), }) const betaUsEast2AppStage = pipeline.addStage(betaUsEast2Stage) @@ -281,6 +289,8 @@ export class RoutingAPIPipeline extends Stack { unicornSecret: unicornSecrets.secretValueFromJson('debug-config-unicorn-key').toString(), alchemyQueryKey: routingApiNewSecrets.secretValueFromJson('alchemy-query-key').toString(), decentralizedNetworkApiKey: routingApiNewSecrets.secretValueFromJson('decentralized-network-api-key').toString(), + uniGraphQLEndpoint: routingApiNewSecrets.secretValueFromJson('uni-graphql-endpoint').toString(), + uniGraphQLHeaderOrigin: routingApiNewSecrets.secretValueFromJson('uni-graphql-header-origin').toString(), }) const prodUsEast2AppStage = pipeline.addStage(prodUsEast2Stage) @@ -417,6 +427,8 @@ new RoutingAPIStack(app, 'RoutingAPIStack', { tenderlyAccessKey: process.env.TENDERLY_ACCESS_KEY!, tenderlyNodeApiKey: process.env.TENDERLY_NODE_API_KEY!, unicornSecret: process.env.UNICORN_SECRET!, + uniGraphQLEndpoint: process.env.GQL_URL!, + uniGraphQLHeaderOrigin: process.env.GQL_H_ORGN!, }) new RoutingAPIPipeline(app, 'RoutingAPIPipelineStack', { diff --git a/bin/stacks/routing-api-stack.ts b/bin/stacks/routing-api-stack.ts index bee79f6756..aa3bf45c60 100644 --- a/bin/stacks/routing-api-stack.ts +++ b/bin/stacks/routing-api-stack.ts @@ -49,6 +49,8 @@ export class RoutingAPIStack extends cdk.Stack { unicornSecret: string alchemyQueryKey?: string decentralizedNetworkApiKey?: string + uniGraphQLEndpoint: string + uniGraphQLHeaderOrigin: string } ) { super(parent, name, props) @@ -72,6 +74,8 @@ export class RoutingAPIStack extends cdk.Stack { unicornSecret, alchemyQueryKey, decentralizedNetworkApiKey, + uniGraphQLEndpoint, + uniGraphQLHeaderOrigin, } = props const { @@ -129,6 +133,8 @@ export class RoutingAPIStack extends cdk.Stack { tokenPropertiesCachingDynamoDb, rpcProviderHealthStateDynamoDb, unicornSecret, + uniGraphQLEndpoint, + uniGraphQLHeaderOrigin, }) const accessLogGroup = new aws_logs.LogGroup(this, 'RoutingAPIGAccessLogs') diff --git a/bin/stacks/routing-lambda-stack.ts b/bin/stacks/routing-lambda-stack.ts index b6bee0d54a..9e4f3db829 100644 --- a/bin/stacks/routing-lambda-stack.ts +++ b/bin/stacks/routing-lambda-stack.ts @@ -38,6 +38,8 @@ export interface RoutingLambdaStackProps extends cdk.NestedStackProps { tokenPropertiesCachingDynamoDb: aws_dynamodb.Table rpcProviderHealthStateDynamoDb: aws_dynamodb.Table unicornSecret: string + uniGraphQLEndpoint: string + uniGraphQLHeaderOrigin: string } export class RoutingLambdaStack extends cdk.NestedStack { public readonly routingLambda: aws_lambda_nodejs.NodejsFunction @@ -69,6 +71,8 @@ export class RoutingLambdaStack extends cdk.NestedStack { tokenPropertiesCachingDynamoDb, rpcProviderHealthStateDynamoDb, unicornSecret, + uniGraphQLEndpoint, + uniGraphQLHeaderOrigin, } = props new CfnOutput(this, 'jsonRpcProviders', { @@ -150,6 +154,8 @@ export class RoutingLambdaStack extends cdk.NestedStack { // we will start using the correct ones going forward TOKEN_PROPERTIES_CACHING_TABLE_NAME: tokenPropertiesCachingDynamoDb.tableName, UNICORN_SECRET: unicornSecret, + GQL_URL: uniGraphQLEndpoint, + GQL_H_ORGN: uniGraphQLHeaderOrigin, ...jsonRpcProviders, }, layers: [ diff --git a/lib/graphql/graphql-client.ts b/lib/graphql/graphql-client.ts new file mode 100644 index 0000000000..3286aae32e --- /dev/null +++ b/lib/graphql/graphql-client.ts @@ -0,0 +1,38 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' + +import { GraphQLResponse } from './graphql-schemas' + +/* Interface for accessing any GraphQL API */ +export interface IGraphQLClient { + fetchData(query: string, variables?: { [key: string]: any }): Promise +} + +/* Implementation of the IGraphQLClient interface to give access to any GraphQL API */ +export class GraphQLClient implements IGraphQLClient { + constructor(private readonly endpoint: string, private readonly headers: Record) {} + + async fetchData(query: string, variables: { [key: string]: any } = {}): Promise { + const requestConfig: AxiosRequestConfig = { + method: 'POST', + url: this.endpoint, + headers: this.headers, + data: { query, variables }, + } + + try { + const response: AxiosResponse> = await axios.request(requestConfig) + const responseBody = response.data + if (responseBody.errors) { + throw new Error(`GraphQL error! ${JSON.stringify(responseBody.errors)}`) + } + + return responseBody.data + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(`HTTP error! status: ${error.response?.status}`) + } else { + throw new Error(`Unexpected error: ${error}`) + } + } + } +} diff --git a/lib/graphql/graphql-provider.ts b/lib/graphql/graphql-provider.ts new file mode 100644 index 0000000000..e58643dbf9 --- /dev/null +++ b/lib/graphql/graphql-provider.ts @@ -0,0 +1,61 @@ +import { ChainId } from '@uniswap/sdk-core' + +import { GraphQLClient, IGraphQLClient } from './graphql-client' +import { + GRAPHQL_QUERY_MULTIPLE_TOKEN_INFO_BY_CONTRACTS, + GRAPHQL_QUERY_TOKEN_INFO_BY_ADDRESS_CHAIN, +} from './graphql-queries' +import { TokenInfoResponse, TokensInfoResponse } from './graphql-schemas' + +/* Interface for accessing Uniswap GraphQL API */ +export interface IUniGraphQLProvider { + /* Fetch token info for a given chain and address */ + getTokenInfo(chainId: ChainId, address: string): Promise + /* Fetch token info for multiple tokens given a chain and addresses */ + getTokensInfo(chainId: ChainId, addresses: string[]): Promise + // Add more methods here as needed. + // - more details: https://github.com/Uniswap/data-api-graphql/blob/main/graphql/schema.graphql +} + +/* Implementation of the UniGraphQLProvider interface to give access to Uniswap GraphQL API */ +export class UniGraphQLProvider implements IUniGraphQLProvider { + private readonly endpoint = process.env.GQL_URL! + private readonly headers = { + Origin: process.env.GQL_H_ORGN!, + 'Content-Type': 'application/json', + } + private client: IGraphQLClient + + constructor() { + this.client = new GraphQLClient(this.endpoint, this.headers) + } + + /* Convert ChainId to a string recognized by data-graph-api graphql endpoint. + * GraphQL Chain Enum located here: https://github.com/Uniswap/data-api-graphql/blob/main/graphql/schema.graphql#L155 + * */ + private _chainIdToGraphQLChainName(chainId: ChainId): string | undefined { + // TODO: add complete list / use data-graphql-api to populate. Only MAINNET for now. + switch (chainId) { + case ChainId.MAINNET: + return 'ETHEREUM' + default: + throw new Error(`UniGraphQLProvider._chainIdToGraphQLChainName unsupported ChainId: ${chainId}`) + } + } + + async getTokenInfo(chainId: ChainId, address: string): Promise { + const query = GRAPHQL_QUERY_TOKEN_INFO_BY_ADDRESS_CHAIN + const variables = { chain: this._chainIdToGraphQLChainName(chainId), address: address } + return this.client.fetchData(query, variables) + } + + async getTokensInfo(chainId: ChainId, addresses: string[]): Promise { + const query = GRAPHQL_QUERY_MULTIPLE_TOKEN_INFO_BY_CONTRACTS + const contracts = addresses.map((address) => ({ + chain: this._chainIdToGraphQLChainName(chainId), + address: address, + })) + const variables = { contracts: contracts } + return this.client.fetchData(query, variables) + } +} diff --git a/lib/graphql/graphql-queries.ts b/lib/graphql/graphql-queries.ts new file mode 100644 index 0000000000..8166aa0417 --- /dev/null +++ b/lib/graphql/graphql-queries.ts @@ -0,0 +1,35 @@ +/* Query to get the token info by address and chain */ +export const GRAPHQL_QUERY_TOKEN_INFO_BY_ADDRESS_CHAIN = ` +query Token($chain: Chain!, $address: String!) { + token(chain: $chain, address: $address) { + name + chain + address + decimals + symbol + standard + feeData { + buyFeeBps + sellFeeBps + } + } + } +` + +/* Query to get the token info by multiple addresses and chain */ +export const GRAPHQL_QUERY_MULTIPLE_TOKEN_INFO_BY_CONTRACTS = ` +query Tokens($contracts: [ContractInput!]!) { + tokens(contracts: $contracts) { + name + chain + address + decimals + symbol + standard + feeData { + buyFeeBps + sellFeeBps + } + } + } +` diff --git a/lib/graphql/graphql-schemas.ts b/lib/graphql/graphql-schemas.ts new file mode 100644 index 0000000000..99fe9ee3c9 --- /dev/null +++ b/lib/graphql/graphql-schemas.ts @@ -0,0 +1,25 @@ +export interface GraphQLResponse { + data: T + errors?: Array<{ message: string }> +} + +export interface TokenInfoResponse { + token: TokenInfo +} + +export interface TokensInfoResponse { + tokens: TokenInfo[] +} + +export interface TokenInfo { + name: string + chain: string + address: string + decimals: number + symbol: string + standard: string + feeData: { + buyFeeBps: string + sellFeeBps: string + } +} diff --git a/lib/graphql/graphql-token-fee-fetcher.ts b/lib/graphql/graphql-token-fee-fetcher.ts new file mode 100644 index 0000000000..af73597ed3 --- /dev/null +++ b/lib/graphql/graphql-token-fee-fetcher.ts @@ -0,0 +1,75 @@ +import { ITokenFeeFetcher } from '@uniswap/smart-order-router/build/main/providers/token-fee-fetcher' +import { IUniGraphQLProvider } from './graphql-provider' +import { TokenFeeMap } from '@uniswap/smart-order-router/build/main/providers/token-fee-fetcher' +import { ProviderConfig } from '@uniswap/smart-order-router/build/main/providers/provider' +import { TokensInfoResponse } from './graphql-schemas' +import { BigNumber } from 'ethers' +import { ChainId } from '@uniswap/sdk-core' +import { metric } from '@uniswap/smart-order-router/build/main/util/metric' +import { log, MetricLoggerUnit } from '@uniswap/smart-order-router' + +/* Implementation of the ITokenFeeFetcher interface to give access to Uniswap GraphQL API token fee data. + * This fetcher is used to get token fees from GraphQL API and fallback to OnChainTokenFeeFetcher if GraphQL API fails + * or not all addresses could be fetched. + * Note: OnChainTokenFeeFetcher takes into account the provided blocknumber when retrieving token fees (through providerConfig), + * but GraphQLTokenFeeFetcher always returns the latest token fee (GraphQl doesn't keep historical data). + * FOT tax doesn't change often, hence ok to not use blocknumber here. + * */ +export class GraphQLTokenFeeFetcher implements ITokenFeeFetcher { + private readonly graphQLProvider: IUniGraphQLProvider + private readonly onChainFeeFetcherFallback: ITokenFeeFetcher + private readonly chainId: ChainId + + constructor( + graphQLProvider: IUniGraphQLProvider, + onChainTokenFeeFetcherFallback: ITokenFeeFetcher, + chainId: ChainId + ) { + this.graphQLProvider = graphQLProvider + this.onChainFeeFetcherFallback = onChainTokenFeeFetcherFallback + this.chainId = chainId + } + + async fetchFees(addresses: string[], providerConfig?: ProviderConfig): Promise { + let tokenFeeMap: TokenFeeMap = {} + + try { + const tokenFeeResponse: TokensInfoResponse = await this.graphQLProvider.getTokensInfo(this.chainId, addresses) + tokenFeeResponse.tokens.forEach((token) => { + if (token.feeData.buyFeeBps || token.feeData.sellFeeBps) { + const buyFeeBps = token.feeData.buyFeeBps ? BigNumber.from(token.feeData.buyFeeBps) : undefined + const sellFeeBps = token.feeData.sellFeeBps ? BigNumber.from(token.feeData.sellFeeBps) : undefined + tokenFeeMap[token.address] = { buyFeeBps, sellFeeBps } + } + }) + + metric.putMetric('GraphQLTokenFeeFetcherFetchFeesSuccess', 1, MetricLoggerUnit.Count) + } catch (err) { + log.error({ err }, `Error calling GraphQLTokenFeeFetcher for tokens: ${addresses}`) + + metric.putMetric('GraphQLTokenFeeFetcherFetchFeesFailure', 1, MetricLoggerUnit.Count) + } + + // If we couldn't fetch all addresses from GraphQL then use fallback on chain fetcher for the rest. + const addressesToFetchFeesWithFallbackFetcher = addresses.filter((address) => !tokenFeeMap[address]) + if (addressesToFetchFeesWithFallbackFetcher.length > 0) { + try { + const tokenFeeMapFromFallback = await this.onChainFeeFetcherFallback.fetchFees( + addressesToFetchFeesWithFallbackFetcher, + providerConfig + ) + tokenFeeMap = { + ...tokenFeeMap, + ...tokenFeeMapFromFallback, + } + } catch (err) { + log.error( + { err }, + `Error fetching fees for tokens ${addressesToFetchFeesWithFallbackFetcher} using onChain fallback` + ) + } + } + + return tokenFeeMap + } +} diff --git a/test/mocha/integ/graphql/graphql-provider.test.ts b/test/mocha/integ/graphql/graphql-provider.test.ts new file mode 100644 index 0000000000..ce2b62684d --- /dev/null +++ b/test/mocha/integ/graphql/graphql-provider.test.ts @@ -0,0 +1,58 @@ +import { ChainId } from '@uniswap/sdk-core' +import { expect } from 'chai' +import { UniGraphQLProvider } from '../../../../lib/graphql/graphql-provider' +import dotenv from 'dotenv' + +dotenv.config() + +describe('integration test for UniGraphQLProvider', () => { + let provider: UniGraphQLProvider + + beforeEach(() => { + provider = new UniGraphQLProvider() + }) + + it('should fetch Ethereum token info', async () => { + const address = '0xBbE460dC4ac73f7C13A2A2feEcF9aCF6D5083F9b' + const chainId = ChainId.MAINNET + const tokenInfoResponse = await provider.getTokenInfo(chainId, address) + + expect(tokenInfoResponse?.token).to.not.be.undefined + expect(tokenInfoResponse.token.address).equals(address) + expect(tokenInfoResponse.token.name).equals('Wick Finance') + expect(tokenInfoResponse.token.symbol).equals('WICK') + expect(tokenInfoResponse.token.feeData?.buyFeeBps).to.not.be.undefined + }) + + it('should fetch Ethereum low traffic token info', async () => { + const address = '0x4a500ed6add5994569e66426588168705fcc9767' + const chainId = ChainId.MAINNET + const tokenInfoResponse = await provider.getTokenInfo(chainId, address) + + expect(tokenInfoResponse?.token).to.not.be.undefined + expect(tokenInfoResponse.token.address).equals(address) + expect(tokenInfoResponse.token.symbol).equals('BITBOY') + expect(tokenInfoResponse.token.feeData?.buyFeeBps).to.not.be.undefined + expect(tokenInfoResponse.token.feeData?.sellFeeBps).to.not.be.undefined + }) + + it('should fetch multiple Ethereum token info', async () => { + const chainId = ChainId.MAINNET + const addresses = ['0xBbE460dC4ac73f7C13A2A2feEcF9aCF6D5083F9b', '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'] + const tokensInfoResponse = await provider.getTokensInfo(chainId, addresses) + + expect(tokensInfoResponse?.tokens).to.not.be.undefined + expect(tokensInfoResponse.tokens.length == 2) + const token1 = tokensInfoResponse.tokens.find((tokenInfo) => tokenInfo.address === addresses[0]) + const token2 = tokensInfoResponse.tokens.find((tokenInfo) => tokenInfo.address === addresses[1]) + expect(token1).to.not.be.undefined + expect(token2).to.not.be.undefined + + expect(token1?.symbol).equals('WICK') + expect(token1?.feeData?.buyFeeBps).to.not.be.undefined + expect(token1?.feeData?.sellFeeBps).to.not.be.undefined + expect(token2?.symbol).equals('UNI') + expect(token2?.feeData?.buyFeeBps).to.be.null + expect(token2?.feeData?.sellFeeBps).to.be.null + }) +}) diff --git a/test/mocha/integ/graphql/graphql-token-fee-fetcher.test.ts b/test/mocha/integ/graphql/graphql-token-fee-fetcher.test.ts new file mode 100644 index 0000000000..a7c62a4921 --- /dev/null +++ b/test/mocha/integ/graphql/graphql-token-fee-fetcher.test.ts @@ -0,0 +1,65 @@ +import { ChainId, Token, WETH9 } from '@uniswap/sdk-core' +import { expect } from 'chai' +import dotenv from 'dotenv' +import { GraphQLTokenFeeFetcher } from '../../../../lib/graphql/graphql-token-fee-fetcher' +import { JsonRpcProvider } from '@ethersproject/providers' +import { ID_TO_PROVIDER } from '@uniswap/smart-order-router' +import { BigNumber } from 'ethers' +import { OnChainTokenFeeFetcher } from '@uniswap/smart-order-router/build/main/providers/token-fee-fetcher' +import { UniGraphQLProvider } from '../../../../lib/graphql/graphql-provider' + +dotenv.config() + +const BULLET = new Token( + ChainId.MAINNET, + '0x8ef32a03784c8Fd63bBf027251b9620865bD54B6', + 8, + 'BULLET', + 'Bullet Game Betting Token', + false, + BigNumber.from(500), + BigNumber.from(500) +) + +const BITBOY = new Token( + ChainId.MAINNET, + '0x4a500ed6add5994569e66426588168705fcc9767', + 8, + 'BITBOY', + 'BitBoy Fund', + false, + BigNumber.from(300), + BigNumber.from(300) +) + +describe('integration test for GraphQLTokenFeeFetcher', () => { + let tokenFeeFetcher: GraphQLTokenFeeFetcher + let onChainTokenFeeFetcher: OnChainTokenFeeFetcher + + beforeEach(() => { + const chain = ChainId.MAINNET + const chainProvider = ID_TO_PROVIDER(chain) + const provider = new JsonRpcProvider(chainProvider, chain) + + onChainTokenFeeFetcher = new OnChainTokenFeeFetcher(chain, provider) + tokenFeeFetcher = new GraphQLTokenFeeFetcher(new UniGraphQLProvider(), onChainTokenFeeFetcher, chain) + }) + + it('Fetch WETH and BITBOY, should only return BITBOY', async () => { + const tokenFeeMap = await tokenFeeFetcher.fetchFees([WETH9[ChainId.MAINNET]!.address, BITBOY.address]) + expect(tokenFeeMap[WETH9[ChainId.MAINNET]!.address]).to.be.undefined + expect(tokenFeeMap[BITBOY.address]).to.not.be.undefined + expect(tokenFeeMap[BITBOY.address]?.buyFeeBps?._hex).equals(BITBOY.buyFeeBps?._hex) + expect(tokenFeeMap[BITBOY.address]?.sellFeeBps?._hex).equals(BITBOY.sellFeeBps?._hex) + }) + + it('Fetch BULLET and BITBOY, should return BOTH', async () => { + const tokenFeeMap = await tokenFeeFetcher.fetchFees([BULLET.address, BITBOY.address]) + expect(tokenFeeMap[BULLET.address]).to.not.be.undefined + expect(tokenFeeMap[BULLET.address]?.buyFeeBps?._hex).equals(BULLET.buyFeeBps?._hex) + expect(tokenFeeMap[BULLET.address]?.sellFeeBps?._hex).equals(BULLET.sellFeeBps?._hex) + expect(tokenFeeMap[BITBOY.address]).to.not.be.undefined + expect(tokenFeeMap[BITBOY.address]?.buyFeeBps?._hex).equals(BITBOY.buyFeeBps?._hex) + expect(tokenFeeMap[BITBOY.address]?.sellFeeBps?._hex).equals(BITBOY.sellFeeBps?._hex) + }) +}) diff --git a/test/mocha/unit/graphql/graphql-provider.test.ts b/test/mocha/unit/graphql/graphql-provider.test.ts new file mode 100644 index 0000000000..efcac62f90 --- /dev/null +++ b/test/mocha/unit/graphql/graphql-provider.test.ts @@ -0,0 +1,92 @@ +import { ChainId } from '@uniswap/sdk-core' +import { IUniGraphQLProvider, UniGraphQLProvider } from '../../../../lib/graphql/graphql-provider' +import sinon from 'sinon' +import { TokensInfoResponse } from '../../../../lib/graphql/graphql-schemas' +import { expect } from 'chai' + +describe('UniGraphQLProvider', () => { + let mockUniGraphQLProvider: sinon.SinonStubbedInstance + + beforeEach(() => { + mockUniGraphQLProvider = sinon.createStubInstance(UniGraphQLProvider) + + mockUniGraphQLProvider.getTokenInfo.callsFake(async (_: ChainId, address: string) => { + return { + token: { + address: address, + decimals: 18, + name: `Wick Finance`, + symbol: 'WICK', + standard: 'ERC20', + chain: 'ETHEREUM', + feeData: { + buyFeeBps: '213', + sellFeeBps: '800', + }, + }, + } + }) + + mockUniGraphQLProvider.getTokensInfo.callsFake(async (_: ChainId, addresses: string[]) => { + const tokensInfoResponse: TokensInfoResponse = { + tokens: [ + { + address: addresses[0], + decimals: 18, + name: 'Wick Finance', + symbol: 'WICK', + standard: 'ERC20', + chain: 'ETHEREUM', + feeData: { + buyFeeBps: '213', + sellFeeBps: '800', + }, + }, + { + address: addresses[1], + decimals: 18, + name: 'Uniswap', + symbol: 'UNI', + standard: 'ERC20', + chain: 'ETHEREUM', + feeData: { + buyFeeBps: '213', + sellFeeBps: '800', + }, + }, + ], + } + return tokensInfoResponse + }) + }) + + it('should fetch Ethereum token info', async () => { + const address = '0xBbE460dC4ac73f7C13A2A2feEcF9aCF6D5083F9b' + const chainId = ChainId.MAINNET + + const tokenInfoResponse = await mockUniGraphQLProvider.getTokenInfo(chainId, address) + + expect(tokenInfoResponse?.token).to.not.be.undefined + expect(tokenInfoResponse.token.address).equals(address) + expect(tokenInfoResponse.token.name).equals('Wick Finance') + expect(tokenInfoResponse.token.symbol).equals('WICK') + }) + + it('should fetch multiple Ethereum token info', async () => { + const chainId = ChainId.MAINNET + const addresses = ['0xBbE460dC4ac73f7C13A2A2feEcF9aCF6D5083F9b', '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'] + + const tokensInfoResponse = await mockUniGraphQLProvider.getTokensInfo(chainId, addresses) + + expect(tokensInfoResponse?.tokens).to.not.be.undefined + expect(tokensInfoResponse.tokens.length == 2) + const token1 = tokensInfoResponse.tokens.find((tokenInfo) => tokenInfo.address === addresses[0]) + const token2 = tokensInfoResponse.tokens.find((tokenInfo) => tokenInfo.address === addresses[1]) + expect(token1).to.not.be.undefined + expect(token2).to.not.be.undefined + + expect(token1?.symbol).equals('WICK') + expect(token1?.feeData?.buyFeeBps).to.not.be.undefined + expect(token2?.symbol).equals('UNI') + }) +})