├── .github └── workflows │ ├── lint.yml │ ├── semgrep.yml │ ├── test.yml │ └── trufflehog.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── LICENSE ├── README.md ├── bin ├── app.ts └── stacks │ ├── routing-api-stack.ts │ ├── routing-caching-stack.ts │ ├── routing-dashboard-stack.ts │ ├── routing-database-stack.ts │ ├── routing-lambda-stack.ts │ ├── rpc-gateway-dashboard.ts │ └── rpc-gateway-fallback-stack.ts ├── cdk.json ├── hardhat.config.js ├── jest.config.ts ├── lib ├── abis │ ├── Permit2.json │ ├── Router.json │ └── erc20.json ├── config │ ├── rpcProviderProdConfig.json │ └── unsupported.tokenlist.json ├── cron │ ├── cache-config.ts │ ├── cache-pools-ipfs.ts │ ├── cache-pools.ts │ ├── cache-token-lists.ts │ └── clean-pools-ipfs.ts ├── dashboards │ ├── cached-routes-widgets-factory.ts │ ├── core │ │ ├── model │ │ │ └── widget.ts │ │ └── widgets-factory.ts │ ├── quote-amounts-widgets-factory.ts │ └── rpc-providers-widgets-factory.ts ├── graphql │ ├── graphql-client.ts │ ├── graphql-provider.ts │ ├── graphql-queries.ts │ ├── graphql-schemas.ts │ ├── graphql-token-fee-fetcher.ts │ └── token-fee-utils.ts ├── handlers │ ├── CurrencyLookup.ts │ ├── evm │ │ ├── EVMClient.ts │ │ └── provider │ │ │ ├── InstrumentedEVMProvider.ts │ │ │ └── ProviderName.ts │ ├── handler.ts │ ├── index.ts │ ├── injector-sor.ts │ ├── marshalling │ │ ├── cached-route-marshaller.ts │ │ ├── cached-routes-marshaller.ts │ │ ├── currency-amount-marshaller.ts │ │ ├── index.ts │ │ ├── pair-marshaller.ts │ │ ├── route-marshaller.ts │ │ ├── token-marshaller.ts │ │ ├── v3 │ │ │ └── pool-marshaller.ts │ │ └── v4 │ │ │ └── pool-marshaller.ts │ ├── pools │ │ ├── pool-caching │ │ │ ├── cache-dynamo.ts │ │ │ ├── v2 │ │ │ │ └── v2-dynamo-cache.ts │ │ │ └── v3 │ │ │ │ ├── cache-dynamo-pool.ts │ │ │ │ └── dynamo-caching-pool-provider.ts │ │ ├── provider-migration │ │ │ └── v3 │ │ │ │ └── traffic-switch-v3-pool-provider.ts │ │ └── util │ │ │ └── pool-provider-traffic-switch-configuration.ts │ ├── quote │ │ ├── SwapOptionsFactory.ts │ │ ├── injector.ts │ │ ├── provider-migration │ │ │ └── v3 │ │ │ │ └── traffic-switch-on-chain-quote-provider.ts │ │ ├── quote.ts │ │ ├── schema │ │ │ └── quote-schema.ts │ │ └── util │ │ │ ├── pairs-to-track.ts │ │ │ ├── quote-provider-traffic-switch-configuration.ts │ │ │ └── simulation.ts │ ├── router-entities │ │ ├── aws-metrics-logger.ts │ │ ├── aws-subgraph-provider.ts │ │ ├── aws-token-list-provider.ts │ │ ├── route-caching │ │ │ ├── dynamo-route-caching-provider.ts │ │ │ ├── index.ts │ │ │ └── model │ │ │ │ ├── cached-routes-bucket.ts │ │ │ │ ├── cached-routes-strategy.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pair-trade-type-chain-id.ts │ │ │ │ └── protocols-bucket-block-number.ts │ │ ├── static-gas-price-provider.ts │ │ └── v3-aws-subgraph-provider.ts │ ├── schema.ts │ └── shared.ts ├── rpc │ ├── GlobalRpcProviders.ts │ ├── ProdConfig.ts │ ├── ProviderHealthState.ts │ ├── ProviderHealthStateDynamoDbRepository.ts │ ├── ProviderHealthStateRepository.ts │ ├── SingleJsonRpcProvider.ts │ ├── UniJsonRpcProvider.ts │ ├── config.ts │ ├── handler │ │ ├── FallbackHandler.ts │ │ └── index.ts │ └── utils.ts └── util │ ├── alpha-config-measurement.ts │ ├── estimateGasUsed.ts │ ├── estimateGasUsedUSD.ts │ ├── eth_feeHistory.ts │ ├── gasLimit.ts │ ├── isAddress.ts │ ├── onChainQuoteProviderConfigs.ts │ ├── pool-cache-key.ts │ ├── poolCachingFilePrefixes.ts │ ├── requestSources.ts │ ├── stage.ts │ ├── supportedProtocolVersions.ts │ ├── testNets.ts │ └── traffic-switch │ ├── traffic-switcher-i-token-fee-fetcher.ts │ └── traffic-switcher.ts ├── package-lock.json ├── package.json ├── scripts └── get_quote.ts ├── test ├── jest │ └── unit │ │ ├── dashboards │ │ ├── cached-routes-widgets-factory.test.ts │ │ └── quote-amounts-widgets-factory.test.ts │ │ └── handlers │ │ ├── CurrencyLookup.test.ts │ │ ├── SwapOptionsFactory.test.ts │ │ ├── quote.test.ts │ │ ├── router-entities │ │ └── route-caching │ │ │ └── model │ │ │ ├── cached-routes-strategy.test.ts │ │ │ ├── pair-trade-type-chain-id.test.ts │ │ │ ├── protocols-bucket-block-number.test.ts │ │ │ └── token-marshaller.test.ts │ │ ├── shared.test.ts │ │ └── util │ │ ├── estimateGasUsed.test.ts │ │ ├── isAddress.test.ts │ │ └── simulation.test.ts ├── mocha │ ├── dbSetup.ts │ ├── dynamoDBLocalFixture.ts │ ├── e2e │ │ └── quote.test.ts │ ├── integ │ │ ├── graphql │ │ │ ├── graphql-provider.test.ts │ │ │ └── graphql-token-fee-fetcher.test.ts │ │ ├── handlers │ │ │ ├── pools │ │ │ │ ├── pool-caching │ │ │ │ │ └── v3 │ │ │ │ │ │ └── dynamo-caching-pool-provider.test.ts │ │ │ │ └── provider-migration │ │ │ │ │ └── traffic-switch-pool-provider.test.ts │ │ │ ├── quote │ │ │ │ └── provider-migration │ │ │ │ │ └── traffic-switch-quote-provider.test.ts │ │ │ └── router-entities │ │ │ │ └── route-caching │ │ │ │ └── dynamo-route-caching-provider.test.ts │ │ └── rpc │ │ │ └── ProviderHealthStateDynamoDbRepository.test.ts │ └── unit │ │ ├── graphql │ │ └── graphql-provider.test.ts │ │ ├── rpc │ │ ├── FallbackHandler.test.ts │ │ ├── GlobalRpcProviders.test.ts │ │ ├── ProdConfig.test.ts │ │ ├── SingleJsonRpcProvider.test.ts │ │ ├── UniJsonRpcProvider.test.ts │ │ └── rpcProviderTestProdConfig.json │ │ ├── traffic-switch │ │ ├── traffic-switcher-i-token-fee-fetcher.test.ts │ │ └── traffic-switcher.test.ts │ │ └── util │ │ └── supportedProtocolVersions.test.ts ├── test-utils │ ├── mocked-data.ts │ └── mocked-dependencies.ts └── utils │ ├── absoluteValue.ts │ ├── forkAndFund.ts │ ├── getBalanceAndApprove.ts │ ├── minimumAmountOut.ts │ ├── parseEvents.ts │ ├── ticks.ts │ └── tokens.ts ├── tsconfig.cdk.json └── tsconfig.json /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | run-linters: 11 | name: Run linters 2 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out Git repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up node 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 18.x 22 | registry-url: https://registry.npmjs.org 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | - name: Run linters 28 | uses: wearerequired/lint-action@v1 29 | with: 30 | github_token: ${{ secrets.github_token }} 31 | prettier: true 32 | auto_fix: false 33 | prettier_extensions: 'css,html,js,json,jsx,md,sass,scss,ts,tsx,vue,yaml,yml,sol' -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | on: 3 | workflow_dispatch: {} 4 | pull_request: {} 5 | push: 6 | branches: 7 | - main 8 | schedule: 9 | # random HH:MM to avoid a load spike on GitHub Actions at 00:00 10 | - cron: '35 11 * * *' 11 | jobs: 12 | semgrep: 13 | name: semgrep/ci 14 | runs-on: ubuntu-20.04 15 | env: 16 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 17 | container: 18 | image: returntocorp/semgrep 19 | if: (github.actor != 'dependabot[bot]') 20 | steps: 21 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 22 | - run: semgrep ci 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | name: Run tests 12 | runs-on: ubuntu-latest 13 | env: 14 | GQL_URL: ${{ secrets.UNI_GRAPHQL_ENDPOINT }} 15 | GQL_H_ORGN: ${{ secrets.UNI_GRAPHQL_HEADER_ORIGIN }} 16 | 17 | steps: 18 | - name: Checkout Repo 19 | uses: actions/checkout@v3 20 | - name: Set up node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 18.x 24 | registry-url: https://registry.npmjs.org 25 | - name: Install Dependencies 26 | run: npm ci 27 | - name: Build 28 | run: npm run build 29 | - name: Run unit tests 30 | run: npm run test:unit 31 | - name: Run integration tests 32 | run: npm run test:integ -------------------------------------------------------------------------------- /.github/workflows/trufflehog.yml: -------------------------------------------------------------------------------- 1 | name: Trufflehog 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | id-token: write 12 | issues: write 13 | pull-requests: write 14 | 15 | jobs: 16 | TruffleHog: 17 | runs-on: ubuntu-latest 18 | defaults: 19 | run: 20 | shell: bash 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: TruffleHog OSS 28 | id: trufflehog 29 | uses: trufflesecurity/trufflehog@b0fd951652a50ffb1911073f0bfb6a8ade7afc37 30 | continue-on-error: true 31 | with: 32 | path: ./ 33 | base: "${{ github.event.repository.default_branch }}" 34 | head: HEAD 35 | extra_args: --debug 36 | 37 | - name: Scan Results Status 38 | if: steps.trufflehog.outcome == 'failure' 39 | run: exit 1 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .vscode 4 | build 5 | cdk.out 6 | .yalc 7 | yalc.lock 8 | dist 9 | .DS_Store 10 | lib/types 11 | cache 12 | .idea 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.13.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github/workflows/*.yml 2 | dist/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uniswap Routing API 2 | 3 | This repository contains routing API for the Uniswap V3 protocol. 4 | 5 | It deploys an API to AWS that uses @uniswap/smart-order-router to search for the most efficient way to swap token A for token B. 6 | 7 | ## Development 8 | 9 | To develop on the Routing API you must have an AWS account where you can deploy your API for testing. 10 | 11 | ### Deploying the API 12 | 13 | The best way to develop and test the API is to deploy your own instance to AWS. 14 | 15 | 1. Install and configure [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) and [AWS CDK V1](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html). 16 | 2. Create .env file in the root directory of the project with : 17 | ``` 18 | THROTTLE_PER_FIVE_MINS = '' # Optional 19 | WEB3_RPC_{CHAIN ID} = { RPC Provider} 20 | # RPC Providers must be set for the following CHAIN IDs: 21 | # MAINNET = 1 22 | # ROPSTEN = 3 23 | # RINKEBY = 4 24 | # GOERLI = 5 25 | # KOVAN = 42 26 | # OPTIMISM = 10 27 | # OPTIMISTIC_KOVAN = 69 28 | # ARBITRUM_ONE = 42161 29 | # ARBITRUM_RINKEBY = 421611 30 | # POLYGON = 137 31 | # POLYGON_MUMBAI = 80001 32 | # BNB = 56 33 | # BASE = 8453 34 | # BLAST = 81457 35 | # ZORA = 7777777 36 | # ZKSYNC = 324 37 | TENDERLY_USER = '' # For enabling Tenderly simulations 38 | TENDERLY_PROJECT = '' # For enabling Tenderly simulations 39 | TENDERLY_ACCESS_KEY = '' # For enabling Tenderly simulations 40 | TENDERLY_NODE_API_KEY = '' # For enabling Tenderly node-level RPC access 41 | ALCHEMY_QUERY_KEY = '' # For Alchemy subgraph query access 42 | GQL_URL = '' # The GraphQL endpoint url, for Uniswap graphql query access 43 | GQL_H_ORGN = '' # The GraphQL header origin, for Uniswap graphql query access 44 | ``` 45 | 3. Install and build the package 46 | ``` 47 | npm install && npm run build 48 | ``` 49 | 4. To deploy the API run: 50 | ``` 51 | cdk deploy RoutingAPIStack 52 | ``` 53 | This will deploy to the default account your AWS CLI is configured for. Once complete it will output something like: 54 | ``` 55 | RoutingAPIStack.Url = https://... 56 | ``` 57 | You can then try it out: 58 | ``` 59 | curl --request GET '/quote?tokenInAddress=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2&tokenInChainId=1&tokenOutAddress=0x1f9840a85d5af5bf1d1762f925bdaddc4201f984&tokenOutChainId=1&amount=100&type=exactIn' 60 | ``` 61 | 62 | ### Tenderly Simulation 63 | 64 | 1. To get a more accurate estimate of the transaction's gas cost, request a tenderly simulation along with the swap. This is done by setting the optional query param "simulateFromAddress". For example: 65 | 66 | ``` 67 | curl --request GET '/quote?tokenInAddress=<0x...>&simulateFromAddress=&...' 68 | ``` 69 | 70 | 2. Tenderly simulates the transaction and returns to us the simulated gasLimit as 'gasUseEstimate'. We use this gasLimit to update all our gas estimate heuristics. In the response body, the 71 | 72 | ``` 73 | {'gasUseEstimate':string, 'gasUseEstimateQuote':string, 'quoteGasAdjusted':string, and 'gasUseEstimateUSD':string} 74 | ``` 75 | 76 | fields will be updated/calculated using tenderly gasLimit estimate. These fields are already present even without Tenderly simulation, however in that case they are simply heuristics. The Tenderly gas estimates will be more accurate. 77 | 78 | 3. If the simulation fails, there will be one more field present in the response body: 'simulationError'. If this field is set and it is set to true, that means the Tenderly Simulation failed. The 79 | 80 | ``` 81 | {'gasUseEstimate':string, 'gasUseEstimateQuote':string, 'quoteGasAdjusted':string, and 'gasUseEstimateUSD':string} 82 | ``` 83 | 84 | fields will still be included, however they will be heuristics rather then Tenderly estimates. These heuristic values are not reliable for sending transactions on chain. 85 | 86 | ### Testing 87 | 88 | #### Unit Tests 89 | 90 | Unit tests are invoked by running `npm run test:unit` in the root directory. A 'watch' mode is also supported by running `npm run test:unit:watch`. 91 | 92 | #### Integration Tests 93 | 94 | Integration tests run against a local DynamoDB node deployed using [dynamodb-local](https://github.com/rynop/dynamodb-local). Note that JDK 8 is a dependency of this package. Invoke the integration tests by running `npm run test:integ` in the root directory. 95 | 96 | #### End-to-end Tests 97 | 98 | The end-to-end tests fetch quotes from your deployed API, then execute the swaps on a Hardhat mainnet fork. 99 | 100 | 1. First deploy your test API using the instructions above. Then update your `.env` file with the URL of the API, and the RPC URL of an archive node: 101 | 102 | ``` 103 | UNISWAP_ROUTING_API='...' 104 | ARCHIVE_NODE_RPC='...' 105 | ``` 106 | 107 | 2. Run the tests with: 108 | ``` 109 | npm run test:e2e 110 | ``` 111 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --project=tsconfig.cdk.json bin/app.ts", 3 | "context": { 4 | "@aws-cdk/core:newStyleStackSynthesis": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers') 2 | require('dotenv').config() 3 | 4 | module.exports = { 5 | defaultNetwork: 'hardhat', 6 | networks: { 7 | hardhat: { 8 | chainId: 1, 9 | forking: { 10 | enabled: true, 11 | url: `${process.env.ARCHIVE_NODE_RPC}`, 12 | }, 13 | }, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest' 2 | 3 | const config: Config = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | verbose: true, 7 | roots: ['./test'], 8 | transform: { 9 | // Use swc to speed up ts-jest's sluggish compilation times. 10 | // Using this cuts the initial time to compile from 6-12 seconds to 11 | // ~1 second consistently. 12 | // Inspiration from: https://github.com/kulshekhar/ts-jest/issues/259#issuecomment-1332269911 13 | // 14 | // https://swc.rs/docs/usage/jest#usage 15 | '^.+\\.(t|j)s?$': '@swc/jest', 16 | }, 17 | } 18 | 19 | export default config 20 | -------------------------------------------------------------------------------- /lib/abis/erc20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [{ "name": "", "type": "string" }], 7 | "payable": false, 8 | "stateMutability": "view", 9 | "type": "function" 10 | }, 11 | { 12 | "constant": false, 13 | "inputs": [ 14 | { "name": "_spender", "type": "address" }, 15 | { "name": "_value", "type": "uint256" } 16 | ], 17 | "name": "approve", 18 | "outputs": [{ "name": "", "type": "bool" }], 19 | "payable": false, 20 | "stateMutability": "nonpayable", 21 | "type": "function" 22 | }, 23 | { 24 | "constant": true, 25 | "inputs": [], 26 | "name": "totalSupply", 27 | "outputs": [{ "name": "", "type": "uint256" }], 28 | "payable": false, 29 | "stateMutability": "view", 30 | "type": "function" 31 | }, 32 | { 33 | "constant": false, 34 | "inputs": [ 35 | { "name": "_from", "type": "address" }, 36 | { "name": "_to", "type": "address" }, 37 | { "name": "_value", "type": "uint256" } 38 | ], 39 | "name": "transferFrom", 40 | "outputs": [{ "name": "", "type": "bool" }], 41 | "payable": false, 42 | "stateMutability": "nonpayable", 43 | "type": "function" 44 | }, 45 | { 46 | "constant": true, 47 | "inputs": [], 48 | "name": "decimals", 49 | "outputs": [{ "name": "", "type": "uint8" }], 50 | "payable": false, 51 | "stateMutability": "view", 52 | "type": "function" 53 | }, 54 | { 55 | "constant": true, 56 | "inputs": [{ "name": "_owner", "type": "address" }], 57 | "name": "balanceOf", 58 | "outputs": [{ "name": "balance", "type": "uint256" }], 59 | "payable": false, 60 | "stateMutability": "view", 61 | "type": "function" 62 | }, 63 | { 64 | "constant": true, 65 | "inputs": [], 66 | "name": "symbol", 67 | "outputs": [{ "name": "", "type": "string" }], 68 | "payable": false, 69 | "stateMutability": "view", 70 | "type": "function" 71 | }, 72 | { 73 | "constant": false, 74 | "inputs": [ 75 | { "name": "_to", "type": "address" }, 76 | { "name": "_value", "type": "uint256" } 77 | ], 78 | "name": "transfer", 79 | "outputs": [{ "name": "", "type": "bool" }], 80 | "payable": false, 81 | "stateMutability": "nonpayable", 82 | "type": "function" 83 | }, 84 | { 85 | "constant": true, 86 | "inputs": [ 87 | { "name": "_owner", "type": "address" }, 88 | { "name": "_spender", "type": "address" } 89 | ], 90 | "name": "allowance", 91 | "outputs": [{ "name": "", "type": "uint256" }], 92 | "payable": false, 93 | "stateMutability": "view", 94 | "type": "function" 95 | }, 96 | { "payable": true, "stateMutability": "payable", "type": "fallback" }, 97 | { 98 | "anonymous": false, 99 | "inputs": [ 100 | { "indexed": true, "name": "owner", "type": "address" }, 101 | { "indexed": true, "name": "spender", "type": "address" }, 102 | { "indexed": false, "name": "value", "type": "uint256" } 103 | ], 104 | "name": "Approval", 105 | "type": "event" 106 | }, 107 | { 108 | "anonymous": false, 109 | "inputs": [ 110 | { "indexed": true, "name": "from", "type": "address" }, 111 | { "indexed": true, "name": "to", "type": "address" }, 112 | { "indexed": false, "name": "value", "type": "uint256" } 113 | ], 114 | "name": "Transfer", 115 | "type": "event" 116 | } 117 | ] 118 | -------------------------------------------------------------------------------- /lib/config/rpcProviderProdConfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "chainId": 42220, 4 | "useMultiProviderProb": 1, 5 | "latencyEvaluationSampleProb": 0.1, 6 | "healthCheckSampleProb": 0.1, 7 | "providerInitialWeights": [0.9, 0.1], 8 | "providerUrls": ["QUICKNODE_42220", "UNIRPC_0"], 9 | "providerNames": ["QUICKNODE", "UNIRPC"] 10 | }, 11 | { 12 | "chainId": 43114, 13 | "useMultiProviderProb": 1, 14 | "latencyEvaluationSampleProb": 0.1, 15 | "healthCheckSampleProb": 0.1, 16 | "providerInitialWeights": [0.9, 0.1], 17 | "providerUrls": ["QUICKNODE_43114", "UNIRPC_0"], 18 | "providerNames": ["QUICKNODE", "UNIRPC"] 19 | }, 20 | { 21 | "chainId": 56, 22 | "useMultiProviderProb": 1, 23 | "latencyEvaluationSampleProb": 0.01, 24 | "healthCheckSampleProb": 0.01, 25 | "providerInitialWeights": [0.9, 0.1], 26 | "providerUrls": ["QUICKNODE_56", "UNIRPC_0"], 27 | "providerNames": ["QUICKNODE", "UNIRPC"] 28 | }, 29 | { 30 | "chainId": 10, 31 | "useMultiProviderProb": 1, 32 | "latencyEvaluationSampleProb": 0.1, 33 | "healthCheckSampleProb": 0.01, 34 | "providerInitialWeights": [0.9, 0.1], 35 | "providerUrls": ["QUICKNODE_10", "UNIRPC_0"], 36 | "providerNames": ["QUICKNODE", "UNIRPC"] 37 | }, 38 | { 39 | "chainId": 11155111, 40 | "useMultiProviderProb": 1, 41 | "latencyEvaluationSampleProb": 0.01, 42 | "healthCheckSampleProb": 0.01, 43 | "providerInitialWeights": [0.9, 0.1], 44 | "providerUrls": ["ALCHEMY_11155111", "UNIRPC_0"], 45 | "providerNames": ["ALCHEMY", "UNIRPC"], 46 | "enableDbSync": true 47 | }, 48 | { 49 | "chainId": 137, 50 | "useMultiProviderProb": 1, 51 | "latencyEvaluationSampleProb": 0.25, 52 | "healthCheckSampleProb": 0.005, 53 | "providerInitialWeights": [0.9, 0.1], 54 | "providerUrls": ["QUICKNODE_137", "UNIRPC_0"], 55 | "providerNames": ["QUICKNODE", "UNIRPC"] 56 | }, 57 | { 58 | "chainId": 42161, 59 | "useMultiProviderProb": 1, 60 | "latencyEvaluationSampleProb": 0.1, 61 | "healthCheckSampleProb": 0.002, 62 | "providerInitialWeights": [0.95, 0.05], 63 | "providerUrls": ["QUICKNODE_42161", "UNIRPC_0"], 64 | "providerNames": ["QUICKNODE", "UNIRPC"] 65 | }, 66 | { 67 | "chainId": 8453, 68 | "useMultiProviderProb": 1, 69 | "latencyEvaluationSampleProb": 0.05, 70 | "healthCheckSampleProb": 0.0005, 71 | "providerInitialWeights": [0.95, 0.05], 72 | "providerUrls": ["QUICKNODE_8453", "UNIRPC_0"], 73 | "providerNames": ["QUICKNODE", "UNIRPC"] 74 | }, 75 | { 76 | "chainId": 1, 77 | "useMultiProviderProb": 1, 78 | "latencyEvaluationSampleProb": 0.05, 79 | "healthCheckSampleProb": 0.001, 80 | "providerInitialWeights": [0.99, 0.01, 0], 81 | "providerUrls": ["QUICKNODE_1", "UNIRPC_0", "QUICKNODERETH_1"], 82 | "providerNames": ["QUICKNODE", "UNIRPC", "QUICKNODERETH"] 83 | }, 84 | { 85 | "chainId": 81457, 86 | "useMultiProviderProb": 1, 87 | "latencyEvaluationSampleProb": 0.001, 88 | "healthCheckSampleProb": 0.001, 89 | "providerInitialWeights": [0.9, 0.1], 90 | "providerUrls": ["QUICKNODE_81457", "UNIRPC_0"], 91 | "providerNames": ["QUICKNODE", "UNIRPC"] 92 | }, 93 | { 94 | "chainId": 7777777, 95 | "useMultiProviderProb": 1, 96 | "latencyEvaluationSampleProb": 0.001, 97 | "healthCheckSampleProb": 0.001, 98 | "providerInitialWeights": [0.9, 0.1], 99 | "providerUrls": ["QUICKNODE_7777777", "UNIRPC_0"], 100 | "providerNames": ["QUICKNODE", "UNIRPC"] 101 | }, 102 | { 103 | "chainId": 324, 104 | "useMultiProviderProb": 1, 105 | "latencyEvaluationSampleProb": 0.01, 106 | "healthCheckSampleProb": 0.01, 107 | "providerInitialWeights": [0.9, 0.1], 108 | "providerUrls": ["QUICKNODE_324", "UNIRPC_0"], 109 | "providerNames": ["QUICKNODE", "UNIRPC"] 110 | }, 111 | { 112 | "chainId": 480, 113 | "useMultiProviderProb": 1, 114 | "latencyEvaluationSampleProb": 0.01, 115 | "healthCheckSampleProb": 0.01, 116 | "providerInitialWeights": [1], 117 | "providerUrls": ["ALCHEMY_480"], 118 | "providerNames": ["ALCHEMY"] 119 | }, 120 | { 121 | "chainId": 1301, 122 | "useMultiProviderProb": 1, 123 | "latencyEvaluationSampleProb": 0.01, 124 | "healthCheckSampleProb": 0.01, 125 | "providerInitialWeights": [1], 126 | "providerUrls": ["QUICKNODE_1301"], 127 | "providerNames": ["QUICKNODE"] 128 | } 129 | ] 130 | -------------------------------------------------------------------------------- /lib/cron/cache-pools-ipfs.ts: -------------------------------------------------------------------------------- 1 | import pinataSDK from '@pinata/sdk' 2 | import { ID_TO_NETWORK_NAME } from '@uniswap/smart-order-router' 3 | import { EventBridgeEvent, ScheduledHandler } from 'aws-lambda' 4 | import { Route53, STS } from 'aws-sdk' 5 | import { default as bunyan, default as Logger } from 'bunyan' 6 | import fs from 'fs' 7 | import _ from 'lodash' 8 | import path from 'path' 9 | import { chainProtocols } from './cache-config' 10 | 11 | const PARENT = '/tmp/temp/' 12 | 13 | const DIRECTORY = '/tmp/temp/v1/pools/' 14 | 15 | const pinata = pinataSDK(process.env.PINATA_API_KEY!, process.env.PINATA_API_SECRET!) 16 | 17 | const handler: ScheduledHandler = async (event: EventBridgeEvent) => { 18 | const log: Logger = bunyan.createLogger({ 19 | name: 'IPFSPoolCacheLambda', 20 | serializers: bunyan.stdSerializers, 21 | level: 'info', 22 | requestId: event.id, 23 | }) 24 | 25 | const sts = new STS() 26 | const stsParams = { 27 | RoleArn: process.env.ROLE_ARN!, 28 | RoleSessionName: `UpdateApiRoute53Role`, 29 | } 30 | 31 | // init route53 with credentials 32 | let data 33 | let route53 34 | try { 35 | data = await sts.assumeRole(stsParams).promise() 36 | } catch (err) { 37 | log.error({ err }, `Error assuming role`) 38 | throw err 39 | } 40 | 41 | log.info(`Role assumed`) 42 | try { 43 | const accessKeyId = data?.Credentials?.AccessKeyId 44 | const secretAccess = data?.Credentials?.SecretAccessKey 45 | const sessionKey = data?.Credentials?.SessionToken 46 | route53 = new Route53({ 47 | credentials: { 48 | accessKeyId: accessKeyId!, 49 | secretAccessKey: secretAccess!, 50 | sessionToken: sessionKey, 51 | }, 52 | }) 53 | } catch (err: any) { 54 | log.error({ err }, 'Route53 not initialized with correct credentials') 55 | throw err 56 | } 57 | 58 | await Promise.all( 59 | _.map(chainProtocols, async ({ protocol, chainId, provider }) => { 60 | const ipfsFilename = `${ID_TO_NETWORK_NAME(chainId)}.json` 61 | log.info(`Getting ${protocol} pools for chain ${chainId}`) 62 | const pools = await provider.getPools() 63 | log.info(`Got ${pools.length} ${protocol} pools for chain ${chainId}. Will save with filename ${ipfsFilename}`) 64 | const poolString = JSON.stringify(pools) 65 | 66 | // create directory and file for the chain and protocol 67 | // e.g: /tmp/temp/v1/pools/v3/mainnet.json 68 | const parentDirectory = path.join(DIRECTORY, protocol.toLowerCase()) 69 | const fullPath = path.join(DIRECTORY, protocol.toLowerCase(), ipfsFilename) 70 | fs.mkdirSync(parentDirectory, { recursive: true }) 71 | fs.writeFileSync(fullPath, poolString) 72 | }) 73 | ) 74 | 75 | // pins everything under '/tmp/` which should include mainnet.txt and rinkeby.txt 76 | // only have to pin once for all chains 77 | let result 78 | let hash 79 | try { 80 | log.info({ result }, `Pinning to pinata: ${PARENT}`) 81 | result = await pinata.pinFromFS(PARENT) 82 | const url = `https://ipfs.io/ipfs/${result.IpfsHash}` 83 | hash = result.IpfsHash 84 | 85 | log.info({ result }, `Successful pinning. IPFS hash: ${hash} and url : ${url}`) 86 | } catch (err) { 87 | log.error({ err }, 'Error pinning') 88 | throw err 89 | } 90 | 91 | // link resulting hash to DNS 92 | const domain = process.env.STAGE == 'prod' ? 'api.uniswap.org' : 'beta.api.uniswap.org' 93 | var params = { 94 | ChangeBatch: { 95 | Changes: [ 96 | { 97 | Action: 'UPSERT', 98 | ResourceRecordSet: { 99 | Name: domain, 100 | ResourceRecords: [ 101 | { 102 | Value: `\"dnslink=/ipfs/${hash}\"`, 103 | }, 104 | ], 105 | TTL: 60, 106 | Type: 'TXT', 107 | }, 108 | }, 109 | ], 110 | }, 111 | HostedZoneId: process.env.HOSTED_ZONE!, 112 | } 113 | try { 114 | log.info({ params }, `Updating record set`) 115 | const data = await route53.changeResourceRecordSets(params).promise() 116 | log.info(`Successful record update: ${data}`) 117 | } catch (err) { 118 | log.error({ err }, 'Error updating DNS') 119 | throw err 120 | } 121 | } 122 | module.exports = { handler } 123 | -------------------------------------------------------------------------------- /lib/cron/cache-token-lists.ts: -------------------------------------------------------------------------------- 1 | import { EventBridgeEvent, ScheduledHandler } from 'aws-lambda' 2 | import { S3 } from 'aws-sdk' 3 | import axios from 'axios' 4 | import { default as bunyan, default as Logger } from 'bunyan' 5 | 6 | const TOKEN_LISTS = [ 7 | 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json', 8 | 'https://tokens.coingecko.com/uniswap/all.json', 9 | 'https://gateway.ipfs.io/ipns/tokens.uniswap.org', 10 | ] 11 | 12 | const handler: ScheduledHandler = async (event: EventBridgeEvent) => { 13 | const log: Logger = bunyan.createLogger({ 14 | name: 'TokenListLambda', 15 | serializers: bunyan.stdSerializers, 16 | level: 'info', 17 | requestId: event.id, 18 | }) 19 | const s3 = new S3() 20 | 21 | for (const tokenListURI of TOKEN_LISTS) { 22 | log.info(`Getting tokenList from ${tokenListURI}.`) 23 | try { 24 | const { data: tokenList } = await axios.get(tokenListURI) 25 | log.info(`Got tokenList from ${tokenListURI}.`) 26 | 27 | await s3 28 | .putObject({ 29 | Bucket: process.env.TOKEN_LIST_CACHE_BUCKET!, 30 | Key: encodeURIComponent(tokenListURI), 31 | Body: JSON.stringify(tokenList), 32 | }) 33 | .promise() 34 | } catch (err) { 35 | log.error({ err }, `Could not get tokenlist ${tokenListURI}`) 36 | } 37 | } 38 | } 39 | 40 | module.exports = { handler } 41 | -------------------------------------------------------------------------------- /lib/cron/clean-pools-ipfs.ts: -------------------------------------------------------------------------------- 1 | import pinataSDK from '@pinata/sdk' 2 | import { EventBridgeEvent, ScheduledHandler } from 'aws-lambda' 3 | import { default as bunyan, default as Logger } from 'bunyan' 4 | 5 | function delay(ms: number) { 6 | return new Promise((resolve) => setTimeout(resolve, ms)) 7 | } 8 | 9 | const START_MONTHS_AGO = 2 10 | const END_MONTHS_AGO = 1 11 | 12 | const PAGE_SIZE = 1000 13 | 14 | const pinata = pinataSDK(process.env.PINATA_API_KEY!, process.env.PINATA_API_SECRET!) 15 | 16 | const handler: ScheduledHandler = async (event: EventBridgeEvent) => { 17 | const log: Logger = bunyan.createLogger({ 18 | name: 'CleanIPFSPoolCacheLambda', 19 | serializers: bunyan.stdSerializers, 20 | level: 'info', 21 | requestId: event.id, 22 | }) 23 | 24 | const now = new Date() 25 | const startDate = new Date(now.getFullYear(), now.getMonth() - START_MONTHS_AGO, now.getDate()) 26 | const endDate = new Date(now.getFullYear(), now.getMonth() - END_MONTHS_AGO, now.getDate()) 27 | 28 | let unpinned = 0 29 | let count = 1 30 | 31 | while (unpinned < count) { 32 | const filters = { 33 | status: 'pinned', 34 | pinStart: startDate.toISOString(), 35 | pinEnd: endDate.toISOString(), 36 | // retrieve only the pool data (called temp for now) 37 | metadata: { name: 'temp', keyvalues: {} }, 38 | pageLimit: PAGE_SIZE, 39 | // Do not need to change offset, getting new data from pinList each time. 40 | pageOffset: 0, 41 | } 42 | 43 | let result 44 | try { 45 | result = await pinata.pinList(filters) 46 | // 3 requests per second is max allowed by Pinata API. We ensure we do not exceed 2 requests per second to give a buffer. 47 | await delay(500) 48 | } catch (err) { 49 | log.error({ err }, `Error on pinList. ${JSON.stringify(err)}. Waiting one minute.`) 50 | await delay(60000) 51 | continue 52 | } 53 | 54 | if (count == 1) { 55 | // set count 56 | count = result.count 57 | log.info( 58 | { startDate, endDate }, 59 | `Overall pins count between ${startDate.toDateString()} and ${endDate.toDateString()}: ${count}` 60 | ) 61 | } 62 | 63 | for (let i = 0; i < result.rows.length; i += 1) { 64 | const { ipfs_pin_hash: hash, date_pinned: datePinned } = result.rows[i] 65 | 66 | try { 67 | const response = await pinata.unpin(hash) 68 | 69 | // 3 requests per second is max allowed by Pinata API. We ensure we do not exceed 2 requests per second to give a buffer. 70 | await delay(500) 71 | 72 | unpinned += 1 73 | log.info({ response, hash }, `Unpinned: ${hash} pinned at ${datePinned}`) 74 | } catch (err: any) { 75 | if (err.reason == 'CURRENT_USER_HAS_NOT_PINNED_CID') { 76 | log.error({ err }, `Error ${err.reason} - Unpinned ${unpinned} so far. Skipping current pin`) 77 | } else { 78 | log.error({ err }, `Error ${err.reason} - Unpinned ${unpinned} so far. Waiting one minute`) 79 | await delay(60000) 80 | // set i back one since it was an unsuccessful unpin 81 | i -= 1 82 | } 83 | } 84 | log.info(`Unpinned ${unpinned} out of ${result.rows.length} from current page.`) 85 | } 86 | } 87 | 88 | log.info(`Unpinned all ${unpinned} pins out of ${count} in the date range.`) 89 | } 90 | module.exports = { handler } 91 | -------------------------------------------------------------------------------- /lib/dashboards/core/model/widget.ts: -------------------------------------------------------------------------------- 1 | // Non-exhaustive list of Widget types, update list as we deem necessary 2 | export type WidgetType = 'text' | 'metric' | 'log' 3 | 4 | export type Widget = { 5 | type: WidgetType 6 | width: number 7 | height: number 8 | properties: any // TODO: Either find an SDK that already defines models for the widgets, or define them ourselves 9 | } 10 | -------------------------------------------------------------------------------- /lib/dashboards/core/widgets-factory.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from './model/widget' 2 | 3 | export interface WidgetsFactory { 4 | generateWidgets(): Widget[] 5 | } 6 | -------------------------------------------------------------------------------- /lib/dashboards/rpc-providers-widgets-factory.ts: -------------------------------------------------------------------------------- 1 | import { WidgetsFactory } from './core/widgets-factory' 2 | import { Widget } from './core/model/widget' 3 | import { ChainId } from '@uniswap/sdk-core' 4 | import _ from 'lodash' 5 | import { ID_TO_NETWORK_NAME } from '@uniswap/smart-order-router/build/main/util/chains' 6 | import { ProviderName } from '../handlers/evm/provider/ProviderName' 7 | 8 | const ID_TO_PROVIDER = (id: ChainId): string => { 9 | switch (id) { 10 | case ChainId.MAINNET: 11 | case ChainId.OPTIMISM: 12 | case ChainId.SEPOLIA: 13 | case ChainId.POLYGON: 14 | case ChainId.POLYGON_MUMBAI: 15 | case ChainId.ARBITRUM_ONE: 16 | case ChainId.ARBITRUM_GOERLI: 17 | case ChainId.AVALANCHE: 18 | case ChainId.GOERLI: 19 | return ProviderName.INFURA 20 | case ChainId.CELO: 21 | case ChainId.BNB: 22 | case ChainId.BASE: 23 | case ChainId.ASTROCHAIN_SEPOLIA: 24 | return ProviderName.QUIKNODE 25 | case ChainId.CELO_ALFAJORES: 26 | return ProviderName.FORNO 27 | case ChainId.WORLDCHAIN: 28 | return ProviderName.ALCHEMY 29 | default: 30 | return ProviderName.UNKNOWN 31 | } 32 | } 33 | 34 | export class RpcProvidersWidgetsFactory implements WidgetsFactory { 35 | region: string 36 | namespace: string 37 | chains: Array 38 | 39 | constructor(namespace: string, region: string, chains: Array) { 40 | this.namespace = namespace 41 | this.region = region 42 | this.chains = chains 43 | } 44 | 45 | generateWidgets(): Widget[] { 46 | return this.generateWidgetsForMethod('CALL') 47 | .concat(this.generateWidgetsForMethod('GETBLOCKNUMBER')) 48 | .concat(this.generateWidgetsForMethod('GETGASPRICE')) 49 | .concat(this.generateWidgetsForMethod('GETNETWORK')) 50 | .concat(this.generateWidgetsForMethod('RESOLVENAME')) 51 | } 52 | 53 | private generateWidgetsForMethod(rpcMethod: string): Widget[] { 54 | return this.generateRequestsWidgetForMethod(rpcMethod).concat(this.generateSuccessRateForMethod(rpcMethod)) 55 | } 56 | 57 | private generateSuccessRateForMethod(rpcMethod: string): Widget[] { 58 | const chainsWithIndices = this.chains.map((chainId, index) => { 59 | return { chainId: chainId, index: index } 60 | }) 61 | const metrics = _.flatMap(chainsWithIndices, (chainIdAndIndex) => { 62 | const chainId = chainIdAndIndex.chainId 63 | const index = chainIdAndIndex.index 64 | const providerName = ID_TO_PROVIDER(chainId) 65 | 66 | const metric1 = `m${index * 2 + 1}` 67 | const metric2 = `m${index * 2 + 2}` 68 | const expression = `e${index}` 69 | 70 | return [ 71 | [ 72 | { 73 | expression: `${metric1} / (${metric1} + ${metric2}) * 100`, 74 | label: `RPC ${providerName} Chain ${ID_TO_NETWORK_NAME(chainId)} ${rpcMethod} Success Rate`, 75 | id: expression, 76 | }, 77 | ], 78 | [ 79 | this.namespace, 80 | `RPC_${providerName}_${chainId}_${rpcMethod}_SUCCESS`, 81 | 'Service', 82 | 'RoutingAPI', 83 | { 84 | id: metric1, 85 | visible: false, 86 | }, 87 | ], 88 | [ 89 | this.namespace, 90 | `RPC_${providerName}_${chainId}_${rpcMethod}_FAILURE`, 91 | 'Service', 92 | 'RoutingAPI', 93 | { 94 | id: metric2, 95 | visible: false, 96 | }, 97 | ], 98 | ] 99 | }) 100 | 101 | return [ 102 | { 103 | height: 10, 104 | width: 12, 105 | type: 'metric', 106 | properties: { 107 | metrics: metrics, 108 | view: 'timeSeries', 109 | stacked: false, 110 | region: this.region, 111 | stat: 'SampleCount', 112 | period: 300, 113 | title: `RPC ${rpcMethod} Success Rate`, 114 | }, 115 | }, 116 | ] 117 | } 118 | 119 | private generateRequestsWidgetForMethod(rpcMethod: string): Widget[] { 120 | const chainsWithIndices = this.chains.map((chainId, index) => { 121 | return { chainId: chainId, index: index } 122 | }) 123 | const metrics = _.flatMap(chainsWithIndices, (chainIdAndIndex) => { 124 | const chainId = chainIdAndIndex.chainId 125 | const providerName = ID_TO_PROVIDER(chainId) 126 | 127 | return [ 128 | [this.namespace, `RPC_${providerName}_${chainId}_${rpcMethod}_SUCCESS`, 'Service', 'RoutingAPI'], 129 | [this.namespace, `RPC_${providerName}_${chainId}_${rpcMethod}_FAILURE`, 'Service', 'RoutingAPI'], 130 | ] 131 | }) 132 | 133 | return [ 134 | { 135 | height: 10, 136 | width: 12, 137 | type: 'metric', 138 | properties: { 139 | metrics: metrics, 140 | view: 'timeSeries', 141 | stacked: true, 142 | region: this.region, 143 | stat: 'SampleCount', 144 | period: 300, 145 | title: `RPC ${rpcMethod} Requests`, 146 | }, 147 | }, 148 | ] 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /lib/graphql/graphql-client.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' 2 | 3 | import { GraphQLResponse } from './graphql-schemas' 4 | 5 | /* Interface for accessing any GraphQL API */ 6 | export interface IGraphQLClient { 7 | fetchData(query: string, variables?: { [key: string]: any }): Promise 8 | } 9 | 10 | /* Implementation of the IGraphQLClient interface to give access to any GraphQL API */ 11 | export class GraphQLClient implements IGraphQLClient { 12 | constructor(private readonly endpoint: string, private readonly headers: Record) {} 13 | 14 | async fetchData(query: string, variables: { [key: string]: any } = {}): Promise { 15 | const requestConfig: AxiosRequestConfig = { 16 | method: 'POST', 17 | url: this.endpoint, 18 | headers: this.headers, 19 | data: { query, variables }, 20 | } 21 | 22 | try { 23 | const response: AxiosResponse> = await axios.request(requestConfig) 24 | const responseBody = response.data 25 | if (responseBody.errors) { 26 | throw new Error(`GraphQL error! ${JSON.stringify(responseBody.errors)}`) 27 | } 28 | 29 | return responseBody.data 30 | } catch (error) { 31 | if (axios.isAxiosError(error)) { 32 | throw new Error(`HTTP error! status: ${error.response?.status}`) 33 | } else { 34 | throw new Error(`Unexpected error: ${error}`) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/graphql/graphql-provider.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@uniswap/sdk-core' 2 | 3 | import { GraphQLClient, IGraphQLClient } from './graphql-client' 4 | import { 5 | GRAPHQL_QUERY_MULTIPLE_TOKEN_INFO_BY_CONTRACTS, 6 | GRAPHQL_QUERY_TOKEN_INFO_BY_ADDRESS_CHAIN, 7 | } from './graphql-queries' 8 | import { TokenInfoResponse, TokensInfoResponse } from './graphql-schemas' 9 | 10 | /* Interface for accessing Uniswap GraphQL API */ 11 | export interface IUniGraphQLProvider { 12 | /* Fetch token info for a given chain and address */ 13 | getTokenInfo(chainId: ChainId, address: string): Promise 14 | /* Fetch token info for multiple tokens given a chain and addresses */ 15 | getTokensInfo(chainId: ChainId, addresses: string[]): Promise 16 | // Add more methods here as needed. 17 | // - more details: https://github.com/Uniswap/data-api-graphql/blob/main/graphql/schema.graphql 18 | } 19 | 20 | /* Implementation of the UniGraphQLProvider interface to give access to Uniswap GraphQL API */ 21 | export class UniGraphQLProvider implements IUniGraphQLProvider { 22 | private readonly endpoint = process.env.GQL_URL! 23 | private readonly headers = { 24 | Origin: process.env.GQL_H_ORGN!, 25 | 'Content-Type': 'application/json', 26 | } 27 | private client: IGraphQLClient 28 | 29 | constructor() { 30 | this.client = new GraphQLClient(this.endpoint, this.headers) 31 | } 32 | 33 | /* Convert ChainId to a string recognized by data-graph-api graphql endpoint. 34 | * GraphQL Chain Enum located here: https://github.com/Uniswap/data-api-graphql/blob/main/graphql/schema.graphql#L155 35 | * */ 36 | private _chainIdToGraphQLChainName(chainId: ChainId): string | undefined { 37 | switch (chainId) { 38 | case ChainId.MAINNET: 39 | return 'ETHEREUM' 40 | case ChainId.ARBITRUM_ONE: 41 | return 'ARBITRUM' 42 | case ChainId.AVALANCHE: 43 | return 'AVALANCHE' 44 | case ChainId.OPTIMISM: 45 | return 'OPTIMISM' 46 | case ChainId.POLYGON: 47 | return 'POLYGON' 48 | case ChainId.CELO: 49 | return 'CELO' 50 | case ChainId.BNB: 51 | return 'BNB' 52 | case ChainId.BASE: 53 | return 'BASE' 54 | case ChainId.BLAST: 55 | return 'BLAST' 56 | case ChainId.ZORA: 57 | return 'ZORA' 58 | case ChainId.ZKSYNC: 59 | return 'ZKSYNC' 60 | case ChainId.WORLDCHAIN: 61 | return 'WORLDCHAIN' 62 | case ChainId.ASTROCHAIN_SEPOLIA: 63 | return 'ASTROCHAIN' 64 | default: 65 | throw new Error(`UniGraphQLProvider._chainIdToGraphQLChainName unsupported ChainId: ${chainId}`) 66 | } 67 | } 68 | 69 | async getTokenInfo(chainId: ChainId, address: string): Promise { 70 | const query = GRAPHQL_QUERY_TOKEN_INFO_BY_ADDRESS_CHAIN 71 | const variables = { chain: this._chainIdToGraphQLChainName(chainId), address: address } 72 | return this.client.fetchData(query, variables) 73 | } 74 | 75 | async getTokensInfo(chainId: ChainId, addresses: string[]): Promise { 76 | const query = GRAPHQL_QUERY_MULTIPLE_TOKEN_INFO_BY_CONTRACTS 77 | const contracts = addresses.map((address) => ({ 78 | chain: this._chainIdToGraphQLChainName(chainId), 79 | address: address, 80 | })) 81 | const variables = { contracts: contracts } 82 | return this.client.fetchData(query, variables) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/graphql/graphql-queries.ts: -------------------------------------------------------------------------------- 1 | /* Query to get the token info by address and chain */ 2 | export const GRAPHQL_QUERY_TOKEN_INFO_BY_ADDRESS_CHAIN = ` 3 | query Token($chain: Chain!, $address: String!) { 4 | token(chain: $chain, address: $address) { 5 | name 6 | chain 7 | address 8 | decimals 9 | symbol 10 | standard 11 | feeData { 12 | buyFeeBps 13 | sellFeeBps 14 | feeTakenOnTransfer 15 | externalTransferFailed 16 | sellReverted 17 | } 18 | } 19 | } 20 | ` 21 | 22 | /* Query to get the token info by multiple addresses and chain */ 23 | export const GRAPHQL_QUERY_MULTIPLE_TOKEN_INFO_BY_CONTRACTS = ` 24 | query Tokens($contracts: [ContractInput!]!) { 25 | tokens(contracts: $contracts) { 26 | name 27 | chain 28 | address 29 | decimals 30 | symbol 31 | standard 32 | feeData { 33 | buyFeeBps 34 | sellFeeBps 35 | feeTakenOnTransfer 36 | externalTransferFailed 37 | sellReverted 38 | } 39 | } 40 | } 41 | ` 42 | -------------------------------------------------------------------------------- /lib/graphql/graphql-schemas.ts: -------------------------------------------------------------------------------- 1 | export interface GraphQLResponse { 2 | data: T 3 | errors?: Array<{ message: string }> 4 | } 5 | 6 | export interface TokenInfoResponse { 7 | token: TokenInfo 8 | } 9 | 10 | export interface TokensInfoResponse { 11 | tokens: TokenInfo[] 12 | } 13 | 14 | export interface TokenInfo { 15 | name: string 16 | chain: string 17 | address: string 18 | decimals: number 19 | symbol: string 20 | standard: string 21 | feeData?: { 22 | buyFeeBps?: string 23 | sellFeeBps?: string 24 | feeTakenOnTransfer?: boolean 25 | externalTransferFailed?: boolean 26 | sellReverted?: boolean 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/graphql/graphql-token-fee-fetcher.ts: -------------------------------------------------------------------------------- 1 | import { ITokenFeeFetcher } from '@uniswap/smart-order-router/build/main/providers/token-fee-fetcher' 2 | import { IUniGraphQLProvider } from './graphql-provider' 3 | import { TokenFeeMap } from '@uniswap/smart-order-router/build/main/providers/token-fee-fetcher' 4 | import { ProviderConfig } from '@uniswap/smart-order-router/build/main/providers/provider' 5 | import { TokensInfoResponse } from './graphql-schemas' 6 | import { BigNumber } from 'ethers' 7 | import { ChainId } from '@uniswap/sdk-core' 8 | import { metric } from '@uniswap/smart-order-router/build/main/util/metric' 9 | import { log, MetricLoggerUnit } from '@uniswap/smart-order-router' 10 | import { TokenFeeUtils } from './token-fee-utils' 11 | 12 | /* Implementation of the ITokenFeeFetcher interface to give access to Uniswap GraphQL API token fee data. 13 | * This fetcher is used to get token fees from GraphQL API and fallback to OnChainTokenFeeFetcher if GraphQL API fails 14 | * or not all addresses could be fetched. 15 | * Note: OnChainTokenFeeFetcher takes into account the provided blocknumber when retrieving token fees (through providerConfig), 16 | * but GraphQLTokenFeeFetcher always returns the latest token fee (GraphQl doesn't keep historical data). 17 | * FOT tax doesn't change often, hence ok to not use blocknumber here. 18 | * */ 19 | export class GraphQLTokenFeeFetcher implements ITokenFeeFetcher { 20 | private readonly graphQLProvider: IUniGraphQLProvider 21 | private readonly onChainFeeFetcherFallback: ITokenFeeFetcher 22 | private readonly chainId: ChainId 23 | 24 | constructor( 25 | graphQLProvider: IUniGraphQLProvider, 26 | onChainTokenFeeFetcherFallback: ITokenFeeFetcher, 27 | chainId: ChainId 28 | ) { 29 | this.graphQLProvider = graphQLProvider 30 | this.onChainFeeFetcherFallback = onChainTokenFeeFetcherFallback 31 | this.chainId = chainId 32 | } 33 | 34 | async fetchFees(addresses: string[], providerConfig?: ProviderConfig): Promise { 35 | let tokenFeeMap: TokenFeeMap = {} 36 | 37 | // Use GraphQL only for tokens that are not dynamic FOT. For dynamic FOT, use fallback (on chain) as we need latest data. 38 | const addressesToFetchFeesWithGraphQL = addresses.filter((address) => !TokenFeeUtils.isDynamicFOT(address)) 39 | try { 40 | if (addressesToFetchFeesWithGraphQL.length > 0) { 41 | const tokenFeeResponse: TokensInfoResponse = await this.graphQLProvider.getTokensInfo( 42 | this.chainId, 43 | addressesToFetchFeesWithGraphQL 44 | ) 45 | tokenFeeResponse.tokens.forEach((token) => { 46 | if (token.feeData?.buyFeeBps || token.feeData?.sellFeeBps) { 47 | const buyFeeBps = token.feeData.buyFeeBps ? BigNumber.from(token.feeData.buyFeeBps) : undefined 48 | const sellFeeBps = token.feeData.sellFeeBps ? BigNumber.from(token.feeData.sellFeeBps) : undefined 49 | const feeTakenOnTransfer = token.feeData.feeTakenOnTransfer 50 | const externalTransferFailed = token.feeData.externalTransferFailed 51 | const sellReverted = token.feeData.sellReverted 52 | tokenFeeMap[token.address] = { 53 | buyFeeBps, 54 | sellFeeBps, 55 | feeTakenOnTransfer, 56 | externalTransferFailed, 57 | sellReverted, 58 | } 59 | } else { 60 | tokenFeeMap[token.address] = { 61 | buyFeeBps: undefined, 62 | sellFeeBps: undefined, 63 | feeTakenOnTransfer: false, 64 | externalTransferFailed: false, 65 | sellReverted: false, 66 | } 67 | } 68 | }) 69 | metric.putMetric('GraphQLTokenFeeFetcherFetchFeesSuccess', 1, MetricLoggerUnit.Count) 70 | } 71 | } catch (err) { 72 | log.error({ err }, `Error calling GraphQLTokenFeeFetcher for tokens: ${addressesToFetchFeesWithGraphQL}`) 73 | 74 | metric.putMetric('GraphQLTokenFeeFetcherFetchFeesFailure', 1, MetricLoggerUnit.Count) 75 | } 76 | 77 | // If we couldn't fetch all addresses from GraphQL then use fallback on chain fetcher for the rest. 78 | const addressesToFetchFeesWithFallbackFetcher = addresses.filter((address) => !tokenFeeMap[address]) 79 | if (addressesToFetchFeesWithFallbackFetcher.length > 0) { 80 | metric.putMetric('GraphQLTokenFeeFetcherOnChainCallbackRequest', 1, MetricLoggerUnit.Count) 81 | try { 82 | const tokenFeeMapFromFallback = await this.onChainFeeFetcherFallback.fetchFees( 83 | addressesToFetchFeesWithFallbackFetcher, 84 | providerConfig 85 | ) 86 | tokenFeeMap = { 87 | ...tokenFeeMap, 88 | ...tokenFeeMapFromFallback, 89 | } 90 | } catch (err) { 91 | log.error( 92 | { err }, 93 | `Error fetching fees for tokens ${addressesToFetchFeesWithFallbackFetcher} using onChain fallback` 94 | ) 95 | } 96 | } 97 | 98 | return tokenFeeMap 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/handlers/CurrencyLookup.ts: -------------------------------------------------------------------------------- 1 | import { Currency, Token } from '@uniswap/sdk-core' 2 | import { 3 | getAddress, 4 | ITokenListProvider, 5 | ITokenProvider, 6 | NATIVE_NAMES_BY_ID, 7 | nativeOnChain, 8 | } from '@uniswap/smart-order-router' 9 | import Logger from 'bunyan' 10 | import { isAddress } from '../util/isAddress' 11 | 12 | /** 13 | * CurrencyLookup searches native tokens, token lists, and on chain to determine 14 | * the token details (called Currency by the sdk) for an inputted string. 15 | */ 16 | export class CurrencyLookup { 17 | constructor( 18 | private readonly tokenListProvider: ITokenListProvider, 19 | private readonly tokenProvider: ITokenProvider, 20 | private readonly log: Logger 21 | ) {} 22 | 23 | public async searchForToken(tokenRaw: string, chainId: number): Promise { 24 | const nativeToken = this.checkIfNativeToken(tokenRaw, chainId) 25 | if (nativeToken) { 26 | return nativeToken 27 | } 28 | 29 | // At this point, we know this is not a NativeCurrency based on the check above, so we can explicitly cast to Token. 30 | const tokenFromTokenList: Token | undefined = await this.checkTokenLists(tokenRaw) 31 | if (tokenFromTokenList) { 32 | return tokenFromTokenList 33 | } 34 | 35 | return await this.checkOnChain(tokenRaw) 36 | } 37 | 38 | checkIfNativeToken = (tokenRaw: string, chainId: number): Currency | undefined => { 39 | if (!NATIVE_NAMES_BY_ID[chainId] || !NATIVE_NAMES_BY_ID[chainId].includes(tokenRaw)) { 40 | return undefined 41 | } 42 | 43 | const nativeToken = nativeOnChain(chainId) 44 | this.log.debug( 45 | { 46 | tokenAddress: getAddress(nativeToken), 47 | }, 48 | `Found native token ${tokenRaw} for chain ${chainId}: ${getAddress(nativeToken)}}` 49 | ) 50 | return nativeToken 51 | } 52 | 53 | checkTokenLists = async (tokenRaw: string): Promise => { 54 | let token: Token | undefined = undefined 55 | if (isAddress(tokenRaw)) { 56 | token = await this.tokenListProvider.getTokenByAddress(tokenRaw) 57 | } 58 | 59 | if (!token) { 60 | token = await this.tokenListProvider.getTokenBySymbol(tokenRaw) 61 | } 62 | 63 | if (token) { 64 | this.log.debug( 65 | { 66 | tokenAddress: token.address, 67 | }, 68 | `Found token ${tokenRaw} in token lists.` 69 | ) 70 | } 71 | 72 | return token 73 | } 74 | 75 | checkOnChain = async (tokenRaw: string): Promise => { 76 | this.log.debug(`Getting input token ${tokenRaw} from chain`) 77 | 78 | // The ITokenListProvider interface expects a list of addresses to lookup tokens. 79 | // If this isn't an address, we can't do the lookup. 80 | // https://github.com/Uniswap/smart-order-router/blob/71fac1905a32af369e30e9cbb52ea36e971ab279/src/providers/token-provider.ts#L23 81 | if (!isAddress(tokenRaw)) { 82 | return undefined 83 | } 84 | 85 | const tokenAccessor = await this.tokenProvider.getTokens([tokenRaw]) 86 | return tokenAccessor.getTokenByAddress(tokenRaw) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/handlers/evm/EVMClient.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | 3 | export interface EVMClient { 4 | getProvider(): ethers.providers.StaticJsonRpcProvider 5 | } 6 | 7 | export type EVMClientProps = { 8 | allProviders: Array 9 | } 10 | 11 | export class DefaultEVMClient implements EVMClient { 12 | private readonly allProviders: Array 13 | 14 | // delegate all non-private method calls 15 | constructor({ allProviders }: EVMClientProps) { 16 | this.allProviders = allProviders 17 | } 18 | 19 | getProvider(): ethers.providers.StaticJsonRpcProvider { 20 | // TODO: use strategy pattern to have heuristics selecting which provider 21 | return this.allProviders[0] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/handlers/evm/provider/ProviderName.ts: -------------------------------------------------------------------------------- 1 | export enum ProviderName { 2 | INFURA = 'INFURA', 3 | QUIKNODE = 'QUIKNODE', // quicknode doesn't have letter c in the RPC endpoint 4 | QUIKNODE_GETH = 'QUIKNODE_GETH', 5 | QUIKNODE_RETH = 'QUIKNODE_RETH', 6 | FORNO = 'FORNO', 7 | ALCHEMY = 'ALCHEMY', 8 | NIRVANA = 'NIRVANA', 9 | UNKNOWN = 'UNKNOWN', 10 | } 11 | 12 | export function deriveProviderName(url: string): ProviderName { 13 | for (const name in ProviderName) { 14 | if (url.toUpperCase().includes(name)) { 15 | if (url.toUpperCase().includes(ProviderName.QUIKNODE)) { 16 | if (url.toLowerCase().includes('solitary')) { 17 | return ProviderName.QUIKNODE_GETH 18 | } else if (url.toLowerCase().includes('ancient')) { 19 | return ProviderName.QUIKNODE_RETH 20 | } else { 21 | return ProviderName.QUIKNODE 22 | } 23 | } else { 24 | return ProviderName[name as keyof typeof ProviderName] 25 | } 26 | } 27 | } 28 | 29 | return ProviderName.UNKNOWN 30 | } 31 | -------------------------------------------------------------------------------- /lib/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { QuoteHandlerInjector } from './quote/injector' 2 | import { QuoteHandler } from './quote/quote' 3 | import { default as bunyan, default as Logger } from 'bunyan' 4 | 5 | const log: Logger = bunyan.createLogger({ 6 | name: 'Root', 7 | serializers: bunyan.stdSerializers, 8 | level: bunyan.INFO, 9 | }) 10 | 11 | let quoteHandler: QuoteHandler 12 | try { 13 | const quoteInjectorPromise = new QuoteHandlerInjector('quoteInjector').build() 14 | quoteHandler = new QuoteHandler('quote', quoteInjectorPromise) 15 | } catch (error) { 16 | log.fatal({ error }, 'Fatal error') 17 | throw error 18 | } 19 | 20 | module.exports = { 21 | quoteHandler: quoteHandler.handler, 22 | } 23 | -------------------------------------------------------------------------------- /lib/handlers/marshalling/cached-route-marshaller.ts: -------------------------------------------------------------------------------- 1 | import { CachedRoute, SupportedRoutes } from '@uniswap/smart-order-router' 2 | import { MarshalledRoute, RouteMarshaller } from './route-marshaller' 3 | 4 | export interface MarshalledCachedRoute { 5 | route: MarshalledRoute 6 | percent: number 7 | } 8 | 9 | export class CachedRouteMarshaller { 10 | public static marshal(cachedRoute: CachedRoute): MarshalledCachedRoute { 11 | return { 12 | route: RouteMarshaller.marshal(cachedRoute.route), 13 | percent: cachedRoute.percent, 14 | } 15 | } 16 | 17 | public static unmarshal(marshalledCachedRoute: MarshalledCachedRoute): CachedRoute { 18 | return new CachedRoute({ 19 | route: RouteMarshaller.unmarshal(marshalledCachedRoute.route), 20 | percent: marshalledCachedRoute.percent, 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/handlers/marshalling/cached-routes-marshaller.ts: -------------------------------------------------------------------------------- 1 | import { CachedRoutes } from '@uniswap/smart-order-router' 2 | import { ChainId, TradeType } from '@uniswap/sdk-core' 3 | import { Protocol } from '@uniswap/router-sdk' 4 | import { MarshalledCurrency, TokenMarshaller } from './token-marshaller' 5 | import { CachedRouteMarshaller, MarshalledCachedRoute } from './cached-route-marshaller' 6 | 7 | export interface MarshalledCachedRoutes { 8 | routes: MarshalledCachedRoute[] 9 | chainId: ChainId 10 | tokenIn: MarshalledCurrency 11 | tokenOut: MarshalledCurrency 12 | protocolsCovered: Protocol[] 13 | blockNumber: number 14 | tradeType: TradeType 15 | originalAmount: string 16 | blocksToLive: number 17 | } 18 | 19 | export class CachedRoutesMarshaller { 20 | public static marshal(cachedRoutes: CachedRoutes): MarshalledCachedRoutes { 21 | return { 22 | routes: cachedRoutes.routes.map((route) => CachedRouteMarshaller.marshal(route)), 23 | chainId: cachedRoutes.chainId, 24 | tokenIn: TokenMarshaller.marshal(cachedRoutes.currencyIn), 25 | tokenOut: TokenMarshaller.marshal(cachedRoutes.currencyOut), 26 | protocolsCovered: cachedRoutes.protocolsCovered, 27 | blockNumber: cachedRoutes.blockNumber, 28 | tradeType: cachedRoutes.tradeType, 29 | originalAmount: cachedRoutes.originalAmount, 30 | blocksToLive: cachedRoutes.blocksToLive, 31 | } 32 | } 33 | 34 | public static unmarshal(marshalledCachedRoutes: MarshalledCachedRoutes): CachedRoutes { 35 | return new CachedRoutes({ 36 | routes: marshalledCachedRoutes.routes.map((route) => CachedRouteMarshaller.unmarshal(route)), 37 | chainId: marshalledCachedRoutes.chainId, 38 | currencyIn: TokenMarshaller.unmarshal(marshalledCachedRoutes.tokenIn), 39 | currencyOut: TokenMarshaller.unmarshal(marshalledCachedRoutes.tokenOut), 40 | protocolsCovered: marshalledCachedRoutes.protocolsCovered, 41 | blockNumber: marshalledCachedRoutes.blockNumber, 42 | tradeType: marshalledCachedRoutes.tradeType, 43 | originalAmount: marshalledCachedRoutes.originalAmount, 44 | blocksToLive: marshalledCachedRoutes.blocksToLive, 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/handlers/marshalling/currency-amount-marshaller.ts: -------------------------------------------------------------------------------- 1 | import { Currency, CurrencyAmount } from '@uniswap/sdk-core' 2 | import { MarshalledCurrency, TokenMarshaller } from './token-marshaller' 3 | 4 | export interface MarshalledCurrencyAmount { 5 | currency: MarshalledCurrency 6 | numerator: string 7 | denominator: string 8 | } 9 | 10 | export class CurrencyAmountMarshaller { 11 | public static marshal(currencyAmount: CurrencyAmount): MarshalledCurrencyAmount { 12 | return { 13 | currency: TokenMarshaller.marshal(currencyAmount.currency), 14 | numerator: currencyAmount.numerator.toString(), 15 | denominator: currencyAmount.denominator.toString(), 16 | } 17 | } 18 | 19 | public static unmarshal(marshalledCurrencyAmount: MarshalledCurrencyAmount): CurrencyAmount { 20 | return CurrencyAmount.fromFractionalAmount( 21 | TokenMarshaller.unmarshal(marshalledCurrencyAmount.currency), 22 | marshalledCurrencyAmount.numerator, 23 | marshalledCurrencyAmount.denominator 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/handlers/marshalling/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cached-route-marshaller' 2 | export * from './cached-routes-marshaller' 3 | export * from './currency-amount-marshaller' 4 | export * from './pair-marshaller' 5 | export * from './v3/pool-marshaller' 6 | export * from './route-marshaller' 7 | export * from './token-marshaller' 8 | -------------------------------------------------------------------------------- /lib/handlers/marshalling/pair-marshaller.ts: -------------------------------------------------------------------------------- 1 | import { Pair } from '@uniswap/v2-sdk' 2 | import { CurrencyAmountMarshaller, MarshalledCurrencyAmount } from './currency-amount-marshaller' 3 | import { Protocol } from '@uniswap/router-sdk' 4 | 5 | export interface MarshalledPair { 6 | protocol: Protocol 7 | currencyAmountA: MarshalledCurrencyAmount 8 | tokenAmountB: MarshalledCurrencyAmount 9 | } 10 | 11 | export class PairMarshaller { 12 | public static marshal(pair: Pair): MarshalledPair { 13 | return { 14 | protocol: Protocol.V2, 15 | currencyAmountA: CurrencyAmountMarshaller.marshal(pair.reserve0), 16 | tokenAmountB: CurrencyAmountMarshaller.marshal(pair.reserve1), 17 | } 18 | } 19 | 20 | public static unmarshal(marshalledPair: MarshalledPair): Pair { 21 | return new Pair( 22 | CurrencyAmountMarshaller.unmarshal(marshalledPair.currencyAmountA).wrapped, 23 | CurrencyAmountMarshaller.unmarshal(marshalledPair.tokenAmountB).wrapped 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/handlers/marshalling/token-marshaller.ts: -------------------------------------------------------------------------------- 1 | import { Currency, Token } from '@uniswap/sdk-core' 2 | import { BigNumber } from 'ethers' 3 | import { getAddress, nativeOnChain } from '@uniswap/smart-order-router' 4 | import { isNativeCurrency } from '@uniswap/universal-router-sdk' 5 | 6 | export interface MarshalledCurrency { 7 | chainId: number 8 | address: string 9 | decimals: number 10 | symbol?: string 11 | name?: string 12 | buyFeeBps?: string 13 | sellFeeBps?: string 14 | } 15 | 16 | export class TokenMarshaller { 17 | public static marshal(currency: Currency): MarshalledCurrency { 18 | return { 19 | chainId: currency.chainId, 20 | address: getAddress(currency), 21 | decimals: currency.decimals, 22 | symbol: currency.symbol, 23 | name: currency.name, 24 | buyFeeBps: currency.isToken ? currency.buyFeeBps?.toString() : undefined, 25 | sellFeeBps: currency.isToken ? currency.sellFeeBps?.toString() : undefined, 26 | } 27 | } 28 | 29 | public static unmarshal(marshalledCurrency: MarshalledCurrency): Currency { 30 | return isNativeCurrency(marshalledCurrency.address) 31 | ? nativeOnChain(marshalledCurrency.chainId) 32 | : new Token( 33 | marshalledCurrency.chainId, 34 | marshalledCurrency.address, 35 | marshalledCurrency.decimals, 36 | marshalledCurrency.symbol, 37 | marshalledCurrency.name, 38 | true, // at this point we know it's valid token address 39 | marshalledCurrency.buyFeeBps ? BigNumber.from(marshalledCurrency.buyFeeBps) : undefined, 40 | marshalledCurrency.sellFeeBps ? BigNumber.from(marshalledCurrency.sellFeeBps) : undefined 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/handlers/marshalling/v3/pool-marshaller.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from '@uniswap/v3-sdk' 2 | import { FeeAmount } from '@uniswap/v3-sdk/dist/constants' 3 | import { MarshalledCurrency, TokenMarshaller } from '../token-marshaller' 4 | import { Protocol } from '@uniswap/router-sdk' 5 | 6 | export interface MarshalledPool { 7 | protocol: Protocol 8 | token0: MarshalledCurrency 9 | token1: MarshalledCurrency 10 | fee: FeeAmount 11 | sqrtRatioX96: string 12 | liquidity: string 13 | tickCurrent: number 14 | } 15 | 16 | export class PoolMarshaller { 17 | public static marshal(pool: Pool): MarshalledPool { 18 | return { 19 | protocol: Protocol.V3, 20 | token0: TokenMarshaller.marshal(pool.token0), 21 | token1: TokenMarshaller.marshal(pool.token1), 22 | fee: pool.fee, 23 | sqrtRatioX96: pool.sqrtRatioX96.toString(), 24 | liquidity: pool.liquidity.toString(), 25 | tickCurrent: pool.tickCurrent, 26 | } 27 | } 28 | 29 | public static unmarshal(marshalledPool: MarshalledPool): Pool { 30 | return new Pool( 31 | TokenMarshaller.unmarshal(marshalledPool.token0).wrapped, 32 | TokenMarshaller.unmarshal(marshalledPool.token1).wrapped, 33 | marshalledPool.fee, 34 | marshalledPool.sqrtRatioX96, 35 | marshalledPool.liquidity, 36 | marshalledPool.tickCurrent 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/handlers/marshalling/v4/pool-marshaller.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from '@uniswap/v4-sdk' 2 | import { FeeAmount } from '@uniswap/v3-sdk/dist/constants' 3 | import { MarshalledCurrency, TokenMarshaller } from '../token-marshaller' 4 | import { Protocol } from '@uniswap/router-sdk' 5 | 6 | export interface MarshalledPool { 7 | protocol: Protocol 8 | token0: MarshalledCurrency 9 | token1: MarshalledCurrency 10 | fee: FeeAmount 11 | tickSpacing: number 12 | hooks: string 13 | sqrtRatioX96: string 14 | liquidity: string 15 | tickCurrent: number 16 | } 17 | 18 | export class PoolMarshaller { 19 | public static marshal(pool: Pool): MarshalledPool { 20 | return { 21 | protocol: Protocol.V4, 22 | token0: TokenMarshaller.marshal(pool.token0), 23 | token1: TokenMarshaller.marshal(pool.token1), 24 | fee: pool.fee, 25 | tickSpacing: pool.tickSpacing, 26 | hooks: pool.hooks, 27 | sqrtRatioX96: pool.sqrtRatioX96.toString(), 28 | liquidity: pool.liquidity.toString(), 29 | tickCurrent: pool.tickCurrent, 30 | } 31 | } 32 | 33 | public static unmarshal(marshalledPool: MarshalledPool): Pool { 34 | return new Pool( 35 | TokenMarshaller.unmarshal(marshalledPool.token0), 36 | TokenMarshaller.unmarshal(marshalledPool.token1), 37 | marshalledPool.fee, 38 | marshalledPool.tickSpacing, 39 | marshalledPool.hooks, 40 | marshalledPool.sqrtRatioX96, 41 | marshalledPool.liquidity, 42 | marshalledPool.tickCurrent 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/handlers/pools/pool-caching/cache-dynamo.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDB } from 'aws-sdk' 2 | 3 | export interface IDynamoCache { 4 | get(partitionKey: TPKey, sortKey?: TSortKey): Promise 5 | set(value: TVal, partitionKey: TPKey, sortKey?: TSortKey): Promise 6 | } 7 | 8 | export interface DynamoCachingProps { 9 | tableName: string 10 | ttlMinutes?: number 11 | } 12 | 13 | export abstract class DynamoCaching implements IDynamoCache { 14 | protected readonly ddbClient: DynamoDB.DocumentClient 15 | protected readonly tableName: string 16 | protected readonly ttlMinutes: number 17 | 18 | protected constructor({ tableName, ttlMinutes = 2 }: DynamoCachingProps) { 19 | this.ddbClient = new DynamoDB.DocumentClient({ 20 | maxRetries: 1, 21 | retryDelayOptions: { 22 | base: 20, 23 | }, 24 | httpOptions: { 25 | timeout: 100, 26 | }, 27 | }) 28 | this.tableName = tableName 29 | this.ttlMinutes = ttlMinutes 30 | } 31 | 32 | abstract get(partitionKey: TPKey, sortKey?: TSortKey): Promise 33 | 34 | abstract set(value: TVal, partitionKey: TPKey, sortKey?: TSortKey): Promise 35 | } 36 | -------------------------------------------------------------------------------- /lib/handlers/pools/pool-caching/v3/cache-dynamo-pool.ts: -------------------------------------------------------------------------------- 1 | import { DynamoCaching, DynamoCachingProps } from '../cache-dynamo' 2 | import { Pool } from '@uniswap/v3-sdk' 3 | import { log, metric, MetricLoggerUnit } from '@uniswap/smart-order-router' 4 | import { PoolMarshaller } from '../../../marshalling/v3/pool-marshaller' 5 | 6 | interface DynamoCachingV3PoolProps extends DynamoCachingProps {} 7 | 8 | export class DynamoCachingV3Pool extends DynamoCaching { 9 | constructor({ tableName, ttlMinutes }: DynamoCachingV3PoolProps) { 10 | super({ tableName, ttlMinutes }) 11 | } 12 | 13 | override async get(partitionKey: string, sortKey?: number): Promise { 14 | if (sortKey) { 15 | const getParams = { 16 | TableName: this.tableName, 17 | Key: { 18 | poolAddress: partitionKey, 19 | blockNumber: sortKey, 20 | }, 21 | } 22 | 23 | const cachedPoolBinary: Buffer | undefined = ( 24 | await this.ddbClient 25 | .get(getParams) 26 | .promise() 27 | .catch((error) => { 28 | log.error({ error, getParams }, `[DynamoCachingV3Pool] Cached pool failed to get`) 29 | return undefined 30 | }) 31 | )?.Item?.item 32 | 33 | if (cachedPoolBinary) { 34 | metric.putMetric('V3_DYNAMO_CACHING_POOL_HIT_IN_TABLE', 1, MetricLoggerUnit.None) 35 | const cachedPoolBuffer: Buffer = Buffer.from(cachedPoolBinary) 36 | const marshalledPool = JSON.parse(cachedPoolBuffer.toString()) 37 | return PoolMarshaller.unmarshal(marshalledPool) 38 | } else { 39 | metric.putMetric('V3_DYNAMO_CACHING_POOL_MISS_NOT_IN_TABLE', 1, MetricLoggerUnit.None) 40 | return undefined 41 | } 42 | } else { 43 | metric.putMetric('V3_DYNAMO_CACHING_POOL_MISS_NO_BLOCK_NUMBER', 1, MetricLoggerUnit.None) 44 | return undefined 45 | } 46 | } 47 | 48 | override async set(pool: Pool, partitionKey: string, sortKey?: number): Promise { 49 | if (sortKey) { 50 | const marshalledPool = PoolMarshaller.marshal(pool) 51 | const binaryCachedPool: Buffer = Buffer.from(JSON.stringify(marshalledPool)) 52 | // TTL is minutes from now. multiply ttlMinutes times 60 to convert to seconds, since ttl is in seconds. 53 | const ttl = Math.floor(Date.now() / 1000) + 60 * this.ttlMinutes 54 | 55 | const putParams = { 56 | TableName: this.tableName, 57 | Item: { 58 | poolAddress: partitionKey, 59 | blockNumber: sortKey, 60 | item: binaryCachedPool, 61 | ttl: ttl, 62 | }, 63 | } 64 | 65 | await this.ddbClient 66 | .put(putParams) 67 | .promise() 68 | .catch((error) => { 69 | log.error({ error, putParams }, `[DynamoCachingV3Pool] Cached pool failed to insert`) 70 | return false 71 | }) 72 | 73 | return true 74 | } else { 75 | return false 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/handlers/pools/pool-caching/v3/dynamo-caching-pool-provider.ts: -------------------------------------------------------------------------------- 1 | import { IV3PoolProvider, V3PoolAccessor } from '@uniswap/smart-order-router' 2 | import { ChainId, Token } from '@uniswap/sdk-core' 3 | import { ProviderConfig } from '@uniswap/smart-order-router/build/main/providers/provider' 4 | import { FeeAmount, Pool } from '@uniswap/v3-sdk' 5 | import { IDynamoCache } from '../cache-dynamo' 6 | import { DynamoCachingV3Pool } from './cache-dynamo-pool' 7 | 8 | export class DynamoDBCachingV3PoolProvider implements IV3PoolProvider { 9 | private readonly dynamoCache: IDynamoCache 10 | private readonly POOL_CACHE_KEY = (chainId: ChainId, address: string) => `pool-${chainId}-${address}` 11 | 12 | constructor(protected chainId: ChainId, protected poolProvider: IV3PoolProvider, tableName: string) { 13 | this.dynamoCache = new DynamoCachingV3Pool({ tableName, ttlMinutes: 1 }) 14 | } 15 | 16 | public getPoolAddress( 17 | tokenA: Token, 18 | tokenB: Token, 19 | feeAmount: FeeAmount 20 | ): { 21 | poolAddress: string 22 | token0: Token 23 | token1: Token 24 | } { 25 | return this.poolProvider.getPoolAddress(tokenA, tokenB, feeAmount) 26 | } 27 | 28 | public async getPools( 29 | tokenPairs: [Token, Token, FeeAmount][], 30 | providerConfig?: ProviderConfig 31 | ): Promise { 32 | const poolAddressSet: Set = new Set() 33 | const poolsToGetTokenPairs: Array<[Token, Token, FeeAmount]> = [] 34 | const poolsToGetAddresses: string[] = [] 35 | const poolAddressToPool: { [poolAddress: string]: Pool } = {} 36 | const blockNumber: number | undefined = await providerConfig?.blockNumber 37 | 38 | for (const [tokenA, tokenB, feeAmount] of tokenPairs) { 39 | const { poolAddress, token0, token1 } = this.getPoolAddress(tokenA, tokenB, feeAmount) 40 | 41 | if (poolAddressSet.has(poolAddress)) { 42 | continue 43 | } 44 | 45 | poolAddressSet.add(poolAddress) 46 | 47 | const partitionKey = this.POOL_CACHE_KEY(this.chainId, poolAddress) 48 | const cachedPool = await this.dynamoCache.get(partitionKey, blockNumber) 49 | if (cachedPool) { 50 | poolAddressToPool[poolAddress] = cachedPool 51 | continue 52 | } 53 | 54 | poolsToGetTokenPairs.push([token0, token1, feeAmount]) 55 | poolsToGetAddresses.push(poolAddress) 56 | } 57 | 58 | if (poolsToGetAddresses.length > 0) { 59 | const poolAccessor = await this.poolProvider.getPools(poolsToGetTokenPairs, providerConfig) 60 | for (const address of poolsToGetAddresses) { 61 | const pool = poolAccessor.getPoolByAddress(address) 62 | if (pool) { 63 | poolAddressToPool[address] = pool 64 | 65 | const partitionKey = this.POOL_CACHE_KEY(this.chainId, address) 66 | await this.dynamoCache.set(pool, partitionKey, blockNumber) 67 | } 68 | } 69 | } 70 | 71 | return { 72 | getPool: (tokenA: Token, tokenB: Token, feeAmount: FeeAmount): Pool | undefined => { 73 | const { poolAddress } = this.getPoolAddress(tokenA, tokenB, feeAmount) 74 | return poolAddressToPool[poolAddress] 75 | }, 76 | getPoolByAddress: (address: string): Pool | undefined => poolAddressToPool[address], 77 | getAllPools: (): Pool[] => Object.values(poolAddressToPool), 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/handlers/pools/util/pool-provider-traffic-switch-configuration.ts: -------------------------------------------------------------------------------- 1 | export const POOL_PROVIDER_TRAFFIC_SWITCH_CONFIGURATION = { 2 | switchPercentage: 0.0, 3 | samplingPercentage: 0.0, 4 | } 5 | -------------------------------------------------------------------------------- /lib/handlers/quote/injector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AlphaRouter, 3 | AlphaRouterConfig, 4 | ID_TO_CHAIN_ID, 5 | IRouter, 6 | LegacyRoutingConfig, 7 | setGlobalLogger, 8 | setGlobalMetric, 9 | V3HeuristicGasModelFactory, 10 | } from '@uniswap/smart-order-router' 11 | import { MetricsLogger } from 'aws-embedded-metrics' 12 | import { APIGatewayProxyEvent, Context } from 'aws-lambda' 13 | import { default as bunyan, default as Logger } from 'bunyan' 14 | import { BigNumber } from 'ethers' 15 | import { ContainerInjected, InjectorSOR, RequestInjected } from '../injector-sor' 16 | import { AWSMetricsLogger } from '../router-entities/aws-metrics-logger' 17 | import { StaticGasPriceProvider } from '../router-entities/static-gas-price-provider' 18 | import { QuoteQueryParams } from './schema/quote-schema' 19 | export class QuoteHandlerInjector extends InjectorSOR< 20 | IRouter, 21 | QuoteQueryParams 22 | > { 23 | public async getRequestInjected( 24 | containerInjected: ContainerInjected, 25 | _requestBody: void, 26 | requestQueryParams: QuoteQueryParams, 27 | _event: APIGatewayProxyEvent, 28 | context: Context, 29 | log: Logger, 30 | metricsLogger: MetricsLogger 31 | ): Promise>> { 32 | const { dependencies, activityId } = containerInjected 33 | 34 | const requestId = context.awsRequestId 35 | const quoteId = requestId.substring(0, 5) 36 | // Sample 10% of all requests at the INFO log level for debugging purposes. 37 | // All other requests will only log warnings and errors. 38 | // Note that we use WARN as a default rather than ERROR 39 | // to capture Tapcompare logs in the smart-order-router. 40 | const logLevel = Math.random() < 0.1 ? bunyan.INFO : bunyan.WARN 41 | 42 | const { 43 | tokenInAddress, 44 | tokenInChainId, 45 | tokenOutAddress, 46 | amount, 47 | type, 48 | algorithm, 49 | gasPriceWei, 50 | quoteSpeed, 51 | intent, 52 | gasToken, 53 | } = requestQueryParams 54 | 55 | log = log.child({ 56 | serializers: bunyan.stdSerializers, 57 | level: logLevel, 58 | requestId, 59 | quoteId, 60 | tokenInAddress, 61 | chainId: tokenInChainId, 62 | tokenOutAddress, 63 | amount, 64 | type, 65 | algorithm, 66 | gasToken, 67 | activityId: activityId, 68 | }) 69 | setGlobalLogger(log) 70 | 71 | metricsLogger.setNamespace('Uniswap') 72 | metricsLogger.setDimensions({ Service: 'RoutingAPI' }) 73 | const metric = new AWSMetricsLogger(metricsLogger) 74 | setGlobalMetric(metric) 75 | 76 | // Today API is restricted such that both tokens must be on the same chain. 77 | const chainId = tokenInChainId 78 | const chainIdEnum = ID_TO_CHAIN_ID(chainId) 79 | 80 | if (!dependencies[chainIdEnum]) { 81 | // Request validation should prevent reject unsupported chains with 4xx already, so this should not be possible. 82 | throw new Error(`No container injected dependencies for chain: ${chainIdEnum}`) 83 | } 84 | 85 | const { 86 | provider, 87 | v4PoolProvider, 88 | v4SubgraphProvider, 89 | v3PoolProvider, 90 | multicallProvider, 91 | tokenProvider, 92 | tokenListProvider, 93 | v3SubgraphProvider, 94 | blockedTokenListProvider, 95 | v2PoolProvider, 96 | tokenValidatorProvider, 97 | tokenPropertiesProvider, 98 | v2QuoteProvider, 99 | v2SubgraphProvider, 100 | gasPriceProvider: gasPriceProviderOnChain, 101 | simulator, 102 | routeCachingProvider, 103 | v2Supported, 104 | } = dependencies[chainIdEnum]! 105 | 106 | let onChainQuoteProvider = dependencies[chainIdEnum]!.onChainQuoteProvider 107 | let gasPriceProvider = gasPriceProviderOnChain 108 | if (gasPriceWei) { 109 | const gasPriceWeiBN = BigNumber.from(gasPriceWei) 110 | gasPriceProvider = new StaticGasPriceProvider(gasPriceWeiBN) 111 | } 112 | 113 | let router 114 | switch (algorithm) { 115 | case 'alpha': 116 | default: 117 | router = new AlphaRouter({ 118 | chainId, 119 | provider, 120 | v4SubgraphProvider, 121 | v4PoolProvider, 122 | v3SubgraphProvider, 123 | multicall2Provider: multicallProvider, 124 | v3PoolProvider, 125 | onChainQuoteProvider, 126 | gasPriceProvider, 127 | v3GasModelFactory: new V3HeuristicGasModelFactory(provider), 128 | blockedTokenListProvider, 129 | tokenProvider, 130 | v2PoolProvider, 131 | v2QuoteProvider, 132 | v2SubgraphProvider, 133 | simulator, 134 | routeCachingProvider, 135 | tokenValidatorProvider, 136 | tokenPropertiesProvider, 137 | v2Supported, 138 | }) 139 | break 140 | } 141 | 142 | return { 143 | chainId: chainIdEnum, 144 | id: quoteId, 145 | log, 146 | metric, 147 | router, 148 | v4PoolProvider, 149 | v3PoolProvider, 150 | v2PoolProvider, 151 | tokenProvider, 152 | tokenListProvider, 153 | quoteSpeed, 154 | intent, 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/handlers/quote/schema/quote-schema.ts: -------------------------------------------------------------------------------- 1 | import BaseJoi from '@hapi/joi' 2 | import { SUPPORTED_CHAINS } from '../../injector-sor' 3 | 4 | const Joi = BaseJoi.extend((joi) => ({ 5 | base: joi.array(), 6 | type: 'stringArray', 7 | messages: { 8 | 'stringArray.type': '{{#label}} is not a valid string array', 9 | }, 10 | coerce: (value, helpers) => { 11 | if (typeof value !== 'string') { 12 | return { value: value, errors: [helpers.error('stringArray.type')] } 13 | } 14 | value = value.replace(/^\[|\]$/g, '').split(',') 15 | const ar = (value as string[]).map((val) => { 16 | return val.trim() 17 | }) 18 | return { value: ar } 19 | }, 20 | })) 21 | 22 | export const QuoteQueryParamsJoi = Joi.object({ 23 | tokenInAddress: Joi.string().alphanum().max(42).required(), 24 | tokenInChainId: Joi.number() 25 | .valid(...SUPPORTED_CHAINS.values()) 26 | .required(), 27 | tokenOutAddress: Joi.string().alphanum().max(42).required(), 28 | tokenOutChainId: Joi.number() 29 | .valid(...SUPPORTED_CHAINS.values()) 30 | .required(), 31 | amount: Joi.string() 32 | .pattern(/^[0-9]+$/) 33 | .max(77) // TODO: validate < 2**256 34 | .required(), 35 | type: Joi.string().valid('exactIn', 'exactOut').required(), 36 | recipient: Joi.string() 37 | .pattern(new RegExp(/^0x[a-fA-F0-9]{40}$/)) 38 | .optional(), 39 | slippageTolerance: Joi.number().min(0).max(20).precision(2).optional(), 40 | deadline: Joi.number().max(10800).optional(), // 180 mins, same as interface max 41 | algorithm: Joi.string().valid('alpha', 'legacy').optional(), 42 | gasPriceWei: Joi.string() 43 | .pattern(/^[0-9]+$/) 44 | .max(30) 45 | .optional(), 46 | minSplits: Joi.number().max(7).optional(), 47 | forceCrossProtocol: Joi.boolean().optional(), 48 | forceMixedRoutes: Joi.boolean().optional(), 49 | protocols: Joi.stringArray().items(Joi.string().valid('v2', 'v3', 'v4', 'mixed')).optional(), 50 | simulateFromAddress: Joi.string().alphanum().max(42).optional(), 51 | permitSignature: Joi.string().optional(), 52 | permitNonce: Joi.string().optional(), 53 | permitExpiration: Joi.number().optional(), 54 | permitAmount: Joi.string() 55 | .pattern(/^[0-9]+$/) 56 | .max(77), 57 | permitSigDeadline: Joi.number().optional(), 58 | // TODO: Remove once universal router is no longer behind a feature flag. 59 | enableUniversalRouter: Joi.boolean().optional().default(false), 60 | quoteSpeed: Joi.string().valid('fast', 'standard').optional().default('standard'), 61 | debugRoutingConfig: Joi.string().optional(), 62 | unicornSecret: Joi.string().optional(), 63 | intent: Joi.string().valid('quote', 'swap', 'caching', 'pricing').optional().default('quote'), 64 | enableFeeOnTransferFeeFetching: Joi.boolean().optional().default(false), 65 | portionBips: Joi.string() 66 | .pattern(/^[0-9]+$/) 67 | .max(5) // portionBips is a string type with the expectation of being parsable to integer between 0 and 10000 68 | .optional(), 69 | portionAmount: Joi.string() 70 | .pattern(/^[0-9]+$/) 71 | .optional(), 72 | portionRecipient: Joi.string().alphanum().max(42).optional(), 73 | source: Joi.string().max(20).optional(), 74 | gasToken: Joi.string().alphanum().max(42).optional(), 75 | }) 76 | 77 | // Future work: this TradeTypeParam can be converted into an enum and used in the 78 | // schema above and in the route QuoteHandler. 79 | export type TradeTypeParam = 'exactIn' | 'exactOut' 80 | 81 | export type QuoteQueryParams = { 82 | tokenInAddress: string 83 | tokenInChainId: number 84 | tokenOutAddress: string 85 | tokenOutChainId: number 86 | amount: string 87 | type: TradeTypeParam 88 | recipient?: string 89 | slippageTolerance?: string 90 | deadline?: string 91 | algorithm?: string 92 | gasPriceWei?: string 93 | minSplits?: number 94 | forceCrossProtocol?: boolean 95 | forceMixedRoutes?: boolean 96 | protocols?: string[] | string 97 | simulateFromAddress?: string 98 | permitSignature?: string 99 | permitNonce?: string 100 | permitExpiration?: string 101 | permitAmount?: string 102 | permitSigDeadline?: string 103 | enableUniversalRouter?: boolean 104 | quoteSpeed?: string 105 | debugRoutingConfig?: string 106 | unicornSecret?: string 107 | intent?: string 108 | enableFeeOnTransferFeeFetching?: boolean 109 | portionBips?: number 110 | portionAmount?: string 111 | portionRecipient?: string 112 | source?: string 113 | gasToken?: string 114 | } 115 | -------------------------------------------------------------------------------- /lib/handlers/quote/util/pairs-to-track.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, TradeType } from '@uniswap/sdk-core' 2 | 3 | export const PAIRS_TO_TRACK: Map> = new Map([ 4 | [ 5 | ChainId.MAINNET, 6 | new Map([ 7 | [ 8 | TradeType.EXACT_INPUT, 9 | ['WETH/USDC', 'USDC/WETH', 'USDT/WETH', 'WETH/USDT', 'WETH/*', 'USDC/*', 'USDT/*', 'DAI/*', 'WBTC/*'], 10 | ], 11 | [TradeType.EXACT_OUTPUT, ['USDC/WETH', '*/WETH', '*/USDC', '*/USDT', '*/DAI']], 12 | ]), 13 | ], 14 | [ 15 | ChainId.OPTIMISM, 16 | new Map([ 17 | [TradeType.EXACT_INPUT, ['WETH/USDC', 'USDC/WETH']], 18 | [TradeType.EXACT_OUTPUT, ['*/WETH']], 19 | ]), 20 | ], 21 | [ 22 | ChainId.ARBITRUM_ONE, 23 | new Map([ 24 | [TradeType.EXACT_INPUT, ['WETH/USDC', 'USDC/WETH']], 25 | [TradeType.EXACT_OUTPUT, ['*/WETH']], 26 | ]), 27 | ], 28 | [ 29 | ChainId.POLYGON, 30 | new Map([ 31 | [TradeType.EXACT_INPUT, ['WETH/USDC', 'USDC/WETH', 'WMATIC/USDC', 'USDC/WMATIC']], 32 | [TradeType.EXACT_OUTPUT, ['*/WMATIC']], 33 | ]), 34 | ], 35 | [ChainId.CELO, new Map([[TradeType.EXACT_OUTPUT, ['*/CELO']]])], 36 | ]) 37 | -------------------------------------------------------------------------------- /lib/handlers/quote/util/quote-provider-traffic-switch-configuration.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@uniswap/sdk-core' 2 | 3 | export type QuoteProviderTrafficSwitchConfiguration = { 4 | switchExactInPercentage: number 5 | samplingExactInPercentage: number 6 | switchExactOutPercentage: number 7 | samplingExactOutPercentage: number 8 | } 9 | 10 | export const QUOTE_PROVIDER_TRAFFIC_SWITCH_CONFIGURATION = ( 11 | chainId: ChainId 12 | ): QuoteProviderTrafficSwitchConfiguration => { 13 | switch (chainId) { 14 | // Mumbai was deprecated on April 13th. Do not sample at all 15 | case ChainId.POLYGON_MUMBAI: 16 | return { 17 | switchExactInPercentage: 0.0, 18 | samplingExactInPercentage: 0, 19 | switchExactOutPercentage: 0.0, 20 | samplingExactOutPercentage: 0, 21 | } as QuoteProviderTrafficSwitchConfiguration 22 | // Sepolia together have well below 50 RPM, so we can shadow sample 100% of traffic 23 | case ChainId.SEPOLIA: 24 | return { 25 | switchExactInPercentage: 0.0, 26 | samplingExactInPercentage: 100, 27 | switchExactOutPercentage: 0.0, 28 | samplingExactOutPercentage: 100, 29 | } as QuoteProviderTrafficSwitchConfiguration 30 | case ChainId.BASE: 31 | // Base RPC eth_call traffic is about double mainnet, so we can shadow sample 0.05% of traffic 32 | return { 33 | switchExactInPercentage: 100, 34 | samplingExactInPercentage: 0, 35 | switchExactOutPercentage: 100, 36 | samplingExactOutPercentage: 0, 37 | } as QuoteProviderTrafficSwitchConfiguration 38 | case ChainId.MAINNET: 39 | // Total RPM for 'QuoteTotalCallsToProvider' is around 20k-30k (across all chains), so 0.1% means 20-30 RPM shadow sampling 40 | return { 41 | switchExactInPercentage: 100, 42 | samplingExactInPercentage: 0, 43 | switchExactOutPercentage: 100, 44 | samplingExactOutPercentage: 0, 45 | } as QuoteProviderTrafficSwitchConfiguration 46 | case ChainId.ARBITRUM_ONE: 47 | // Arbitrum RPC eth_call traffic is about half of mainnet, so we can shadow sample 0.2% of traffic 48 | return { 49 | switchExactInPercentage: 100, 50 | samplingExactInPercentage: 0, 51 | switchExactOutPercentage: 100, 52 | samplingExactOutPercentage: 0, 53 | } as QuoteProviderTrafficSwitchConfiguration 54 | case ChainId.POLYGON: 55 | // Total RPM for 'QuoteTotalCallsToProvider' is around 20k-30k (across all chains), so 0.1% means 20-30 RPM shadow sampling 56 | return { 57 | switchExactInPercentage: 100, 58 | samplingExactInPercentage: 0, 59 | switchExactOutPercentage: 100, 60 | samplingExactOutPercentage: 0, 61 | } as QuoteProviderTrafficSwitchConfiguration 62 | case ChainId.OPTIMISM: 63 | // Optimism RPC eth_call traffic is about 1/10 of mainnet, so we can shadow sample 1% of traffic 64 | return { 65 | switchExactInPercentage: 100, 66 | samplingExactInPercentage: 0, 67 | switchExactOutPercentage: 100, 68 | samplingExactOutPercentage: 0, 69 | } as QuoteProviderTrafficSwitchConfiguration 70 | case ChainId.BLAST: 71 | // Blast RPC eth_call traffic is about 1/10 of mainnet, so we can shadow sample 1% of traffic 72 | return { 73 | switchExactInPercentage: 100, 74 | samplingExactInPercentage: 0, 75 | switchExactOutPercentage: 100, 76 | samplingExactOutPercentage: 0, 77 | } as QuoteProviderTrafficSwitchConfiguration 78 | case ChainId.BNB: 79 | // BNB RPC eth_call traffic is about 1/10 of mainnet, so we can shadow sample 1% of traffic 80 | return { 81 | switchExactInPercentage: 100, 82 | samplingExactInPercentage: 0, 83 | switchExactOutPercentage: 100, 84 | samplingExactOutPercentage: 0, 85 | } 86 | case ChainId.CELO: 87 | // Celo RPC eth_call traffic is about 1/100 of mainnet, so we can shadow sample 10% of traffic 88 | return { 89 | switchExactInPercentage: 100, 90 | samplingExactInPercentage: 0, 91 | switchExactOutPercentage: 100, 92 | samplingExactOutPercentage: 0, 93 | } as QuoteProviderTrafficSwitchConfiguration 94 | case ChainId.AVALANCHE: 95 | // Avalanche RPC eth_call traffic is about 1/100 of mainnet, so we can shadow sample 10% of traffic 96 | return { 97 | switchExactInPercentage: 100, 98 | samplingExactInPercentage: 0, 99 | switchExactOutPercentage: 100, 100 | samplingExactOutPercentage: 0, 101 | } as QuoteProviderTrafficSwitchConfiguration 102 | // worldchain and astrochain sepolia don't have the view-only quoter yet, so we can shadow sample 0.1% of traffic 103 | case ChainId.WORLDCHAIN: 104 | case ChainId.ASTROCHAIN_SEPOLIA: 105 | return { 106 | switchExactInPercentage: 0.0, 107 | samplingExactInPercentage: 0.1, 108 | switchExactOutPercentage: 0.0, 109 | samplingExactOutPercentage: 0.1, 110 | } as QuoteProviderTrafficSwitchConfiguration 111 | // If we accidentally switch a traffic, we have the protection to shadow sample only 0.1% of traffic 112 | default: 113 | return { 114 | switchExactInPercentage: 0.0, 115 | samplingExactInPercentage: 0.1, 116 | switchExactOutPercentage: 0.0, 117 | samplingExactOutPercentage: 0.1, 118 | } as QuoteProviderTrafficSwitchConfiguration 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/handlers/quote/util/simulation.ts: -------------------------------------------------------------------------------- 1 | import { SimulationStatus } from '@uniswap/smart-order-router' 2 | import Logger from 'bunyan' 3 | 4 | export enum RoutingApiSimulationStatus { 5 | UNATTEMPTED = 'UNATTEMPTED', 6 | SUCCESS = 'SUCCESS', 7 | FAILED = 'FAILED', 8 | INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE', 9 | NOT_SUPPORTED = 'NOT_SUPPORTED', 10 | NOT_APPROVED = 'NOT_APPROVED', 11 | UNKNOWN = '', 12 | } 13 | 14 | export const simulationStatusTranslation = ( 15 | simulationStatus: SimulationStatus | undefined, 16 | log: Logger 17 | ): RoutingApiSimulationStatus => { 18 | switch (simulationStatus) { 19 | case undefined: 20 | return RoutingApiSimulationStatus.UNATTEMPTED 21 | case SimulationStatus.Succeeded: 22 | return RoutingApiSimulationStatus.SUCCESS 23 | case SimulationStatus.Failed: 24 | return RoutingApiSimulationStatus.FAILED 25 | case SimulationStatus.InsufficientBalance: 26 | return RoutingApiSimulationStatus.INSUFFICIENT_BALANCE 27 | case SimulationStatus.NotSupported: 28 | return RoutingApiSimulationStatus.NOT_SUPPORTED 29 | case SimulationStatus.NotApproved: 30 | return RoutingApiSimulationStatus.NOT_APPROVED 31 | default: 32 | log.error(`Unknown simulation status ${simulationStatus}`) 33 | return RoutingApiSimulationStatus.UNKNOWN 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/handlers/router-entities/aws-metrics-logger.ts: -------------------------------------------------------------------------------- 1 | import { IMetric, MetricLoggerUnit } from '@uniswap/smart-order-router' 2 | import { MetricsLogger as AWSEmbeddedMetricsLogger } from 'aws-embedded-metrics' 3 | 4 | export class AWSMetricsLogger implements IMetric { 5 | constructor(private awsMetricLogger: AWSEmbeddedMetricsLogger) {} 6 | 7 | public putDimensions(dimensions: Record): void { 8 | this.awsMetricLogger.putDimensions(dimensions) 9 | } 10 | 11 | public putMetric(key: string, value: number, unit?: MetricLoggerUnit): void { 12 | this.awsMetricLogger.putMetric(key, value, unit) 13 | } 14 | 15 | public setProperty(key: string, value: unknown): void { 16 | this.awsMetricLogger.setProperty(key, value) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/handlers/router-entities/aws-token-list-provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CachingTokenListProvider, 3 | ITokenListProvider, 4 | ITokenProvider, 5 | log, 6 | NodeJSCache, 7 | } from '@uniswap/smart-order-router' 8 | import { ChainId } from '@uniswap/sdk-core' 9 | import { TokenList } from '@uniswap/token-lists' 10 | import S3 from 'aws-sdk/clients/s3' 11 | import NodeCache from 'node-cache' 12 | 13 | const TOKEN_LIST_CACHE = new NodeCache({ stdTTL: 600, useClones: false }) 14 | 15 | export class AWSTokenListProvider extends CachingTokenListProvider { 16 | public static async fromTokenListS3Bucket( 17 | chainId: ChainId, 18 | bucket: string, 19 | tokenListURI: string 20 | ): Promise { 21 | const s3 = new S3({ correctClockSkew: true, maxRetries: 3 }) 22 | 23 | const cachedTokenList = TOKEN_LIST_CACHE.get(tokenListURI) 24 | 25 | const tokenCache = new NodeCache({ stdTTL: 360, useClones: false }) 26 | 27 | if (cachedTokenList) { 28 | log.info(`Found token lists for ${tokenListURI} in local cache`) 29 | return super.fromTokenList(chainId, cachedTokenList, new NodeJSCache(tokenCache)) 30 | } 31 | 32 | try { 33 | log.info(`Getting tokenLists from s3.`) 34 | const tokenListResult = await s3.getObject({ Key: encodeURIComponent(tokenListURI), Bucket: bucket }).promise() 35 | 36 | const { Body: tokenListBuffer } = tokenListResult 37 | 38 | if (!tokenListBuffer) { 39 | return super.fromTokenListURI(chainId, tokenListURI, new NodeJSCache(tokenCache)) 40 | } 41 | 42 | const tokenList = JSON.parse(tokenListBuffer.toString('utf-8')) as TokenList 43 | 44 | log.info(`Got both tokenLists from s3. ${tokenList.tokens.length} tokens in main list.`) 45 | 46 | TOKEN_LIST_CACHE.set(tokenListURI, tokenList) 47 | 48 | return new CachingTokenListProvider(chainId, tokenList, new NodeJSCache(tokenCache)) 49 | } catch (err) { 50 | log.info({ err }, `Failed to get tokenLists from s3.`) 51 | return super.fromTokenListURI(chainId, tokenListURI, new NodeJSCache(tokenCache)) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/handlers/router-entities/route-caching/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../../marshalling' 2 | export * from './model' 3 | export * from './dynamo-route-caching-provider' 4 | -------------------------------------------------------------------------------- /lib/handlers/router-entities/route-caching/model/cached-routes-bucket.ts: -------------------------------------------------------------------------------- 1 | import { CacheMode } from '@uniswap/smart-order-router' 2 | 3 | interface CachedRoutesBucketsArgs { 4 | /** 5 | * The bucket for these parameters, this bucket is defined in total units. 6 | * e.g. if bucket = 1 and currency (in CachedRoutesStrategy) is WETH, then this is 1 WETH. 7 | */ 8 | bucket: number 9 | /** 10 | * For the cached route associated to this bucket, how many blocks should the cached route be valid for. 11 | */ 12 | blocksToLive?: number 13 | /** 14 | * The CacheMode associated to this bucket. Setting it to `Livemode` will enable caching the route for this bucket 15 | */ 16 | cacheMode: CacheMode 17 | /** 18 | * Defines the max number of splits allowed for a route to be cached. A value of 0 indicates that any splits are allowed 19 | * A value of 1 indicates that at most there can only be 1 split in the route in order to be cached. 20 | */ 21 | maxSplits?: number 22 | /** 23 | * When fetching the CachedRoutes, we could opt for using the last N routes, from the last N blocks 24 | * This way we would query the price for all the recent routes that have been cached as the best routes 25 | */ 26 | withLastNCachedRoutes?: number 27 | } 28 | 29 | export class CachedRoutesBucket { 30 | public readonly bucket: number 31 | public readonly blocksToLive: number 32 | public readonly cacheMode: CacheMode 33 | public readonly maxSplits: number 34 | public readonly withLastNCachedRoutes: number 35 | 36 | constructor({ 37 | bucket, 38 | blocksToLive = 2, 39 | cacheMode, 40 | maxSplits = 0, 41 | withLastNCachedRoutes = 4, 42 | }: CachedRoutesBucketsArgs) { 43 | this.bucket = bucket 44 | this.blocksToLive = blocksToLive // by default, we allow up to 2 blocks to live for a cached route 45 | this.cacheMode = cacheMode 46 | this.maxSplits = maxSplits // by default this value is 0, which means that any number of splits are allowed 47 | this.withLastNCachedRoutes = withLastNCachedRoutes // Fetching the last 4 cached routes by default 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/handlers/router-entities/route-caching/model/cached-routes-strategy.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' 2 | import { CachedRoutesBucket } from './cached-routes-bucket' 3 | import { CacheMode } from '@uniswap/smart-order-router' 4 | 5 | interface CachedRoutesStrategyArgs { 6 | pair: string 7 | tradeType: TradeType 8 | chainId: ChainId 9 | buckets: CachedRoutesBucket[] 10 | } 11 | 12 | /** 13 | * Models out the strategy for categorizing cached routes into buckets by amount traded 14 | */ 15 | export class CachedRoutesStrategy { 16 | readonly pair: string 17 | readonly _tradeType: TradeType 18 | readonly chainId: ChainId 19 | readonly willTapcompare: boolean 20 | private buckets: number[] 21 | private bucketsMap: Map 22 | 23 | /** 24 | * @param pair 25 | * @param tradeType 26 | * @param chainId 27 | * @param buckets 28 | */ 29 | constructor({ pair, tradeType, chainId, buckets }: CachedRoutesStrategyArgs) { 30 | this.pair = pair 31 | this._tradeType = tradeType 32 | this.chainId = chainId 33 | 34 | // Used for deciding to show metrics in the dashboard related to Tapcompare 35 | this.willTapcompare = buckets.find((bucket) => bucket.cacheMode == CacheMode.Tapcompare) != undefined 36 | 37 | // It is important that we sort the buckets in ascendant order for the algorithm to work correctly. 38 | // For a strange reason the `.sort()` function was comparing the number as strings, so I had to pass a compareFn. 39 | this.buckets = buckets.map((params) => params.bucket).sort((a, b) => a - b) 40 | 41 | // Create a Map for easy lookup once we find a bucket. 42 | this.bucketsMap = new Map(buckets.map((params) => [params.bucket, params])) 43 | } 44 | 45 | public get tradeType(): string { 46 | return this._tradeType == TradeType.EXACT_INPUT ? 'ExactIn' : 'ExactOut' 47 | } 48 | 49 | public readablePairTradeTypeChainId(): string { 50 | return `${this.pair.toUpperCase()}/${this.tradeType}/${this.chainId}` 51 | } 52 | 53 | public bucketPairs(): [number, number][] { 54 | if (this.buckets.length > 0) { 55 | const firstBucket: [number, number][] = [[0, this.buckets[0]]] 56 | const middleBuckets: [number, number][] = 57 | this.buckets.length > 1 58 | ? this.buckets.slice(0, -1).map((bucket, i): [number, number] => [bucket, this.buckets[i + 1]!]) 59 | : [] 60 | const lastBucket: [number, number][] = [[this.buckets.slice(-1)[0], -1]] 61 | 62 | return firstBucket.concat(middleBuckets).concat(lastBucket) 63 | } else { 64 | return [] 65 | } 66 | } 67 | 68 | /** 69 | * Given an amount, we will search the bucket that has a cached route for that amount based on the CachedRoutesBucket array 70 | * @param amount 71 | */ 72 | public getCachingBucket(amount: CurrencyAmount): CachedRoutesBucket | undefined { 73 | // Find the first bucket which is greater or equal than the amount. 74 | // If no bucket is found it means it's not supposed to be cached. 75 | // e.g. let buckets = [10, 50, 100, 500, 1000] 76 | // e.g.1. if amount = 0.10, then bucket = 10 77 | // e.g.2. if amount = 501, then bucket = 1000 78 | // e.g.3. If amount = 1001 then bucket = undefined 79 | const bucket = this.buckets.find((bucket: number) => { 80 | // Create a CurrencyAmount object to compare the amount with the bucket. 81 | const bucketCurrency = CurrencyAmount.fromRawAmount(amount.currency, bucket * 10 ** amount.currency.decimals) 82 | 83 | // Given that the array of buckets is sorted, we want to find the first bucket that makes the amount lessThanOrEqual to the bucket 84 | // refer to the examples above 85 | return amount.lessThan(bucketCurrency) || amount.equalTo(bucketCurrency) 86 | }) 87 | 88 | if (bucket) { 89 | // if a bucket was found, return the CachedRoutesBucket associated to that bucket. 90 | return this.bucketsMap.get(bucket) 91 | } 92 | 93 | return undefined 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/handlers/router-entities/route-caching/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cached-routes-bucket' 2 | export * from './cached-routes-strategy' 3 | export * from './pair-trade-type-chain-id' 4 | -------------------------------------------------------------------------------- /lib/handlers/router-entities/route-caching/model/pair-trade-type-chain-id.ts: -------------------------------------------------------------------------------- 1 | import { Protocol } from '@uniswap/router-sdk' 2 | import { ChainId, Currency, TradeType } from '@uniswap/sdk-core' 3 | import { CachedRoutes, getAddress } from '@uniswap/smart-order-router' 4 | 5 | interface PairTradeTypeChainIdArgs { 6 | currencyIn: string 7 | currencyOut: string 8 | tradeType: TradeType 9 | chainId: ChainId 10 | } 11 | 12 | /** 13 | * Class used to model the partition key of the CachedRoutes cache database and configuration. 14 | */ 15 | export class PairTradeTypeChainId { 16 | public readonly currencyIn: string 17 | public readonly currencyOut: string 18 | public readonly tradeType: TradeType 19 | public readonly chainId: ChainId 20 | 21 | constructor({ currencyIn, currencyOut, tradeType, chainId }: PairTradeTypeChainIdArgs) { 22 | this.currencyIn = currencyIn.toLowerCase() // All currency addresses should be lower case for normalization. 23 | this.currencyOut = currencyOut.toLowerCase() // All currency addresses should be lower case for normalization. 24 | this.tradeType = tradeType 25 | this.chainId = chainId 26 | } 27 | 28 | public toString(): string { 29 | return `${this.currencyIn}/${this.currencyOut}/${this.tradeType}/${this.chainId}` 30 | } 31 | 32 | public static fromCachedRoutes(cachedRoutes: CachedRoutes): PairTradeTypeChainId { 33 | const includesV4Pool = cachedRoutes.routes.some((route) => route.protocol === Protocol.V4) 34 | 35 | return new PairTradeTypeChainId({ 36 | currencyIn: PairTradeTypeChainId.deriveCurrencyAddress(includesV4Pool, cachedRoutes.currencyIn), 37 | currencyOut: PairTradeTypeChainId.deriveCurrencyAddress(includesV4Pool, cachedRoutes.currencyOut), 38 | tradeType: cachedRoutes.tradeType, 39 | chainId: cachedRoutes.chainId, 40 | }) 41 | } 42 | 43 | public static deriveCurrencyAddress(includesV4Pool: boolean, currency: Currency): string { 44 | return includesV4Pool ? getAddress(currency) : currency.wrapped.address 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/handlers/router-entities/route-caching/model/protocols-bucket-block-number.ts: -------------------------------------------------------------------------------- 1 | import { Protocol } from '@uniswap/router-sdk' 2 | 3 | interface ProtocolsBucketBlockNumberArgs { 4 | protocols: Protocol[] 5 | bucket: number 6 | blockNumber?: number 7 | } 8 | 9 | /** 10 | * Class used to model the sort key of the CachedRoutes cache database. 11 | */ 12 | export class ProtocolsBucketBlockNumber { 13 | public readonly protocols: Protocol[] 14 | public readonly bucket: number 15 | public readonly blockNumber?: number 16 | 17 | constructor({ protocols, bucket, blockNumber }: ProtocolsBucketBlockNumberArgs) { 18 | this.protocols = protocols.sort() 19 | this.bucket = bucket 20 | this.blockNumber = blockNumber 21 | } 22 | 23 | public fullKey(): string { 24 | if (this.blockNumber === undefined) { 25 | throw Error('BlockNumber is necessary to create a fullKey') 26 | } 27 | 28 | return `${this.protocols}/${this.bucket}/${this.blockNumber}` 29 | } 30 | 31 | public protocolsBucketPartialKey(): string { 32 | return `${this.protocols}/${this.bucket}/` 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/handlers/router-entities/static-gas-price-provider.ts: -------------------------------------------------------------------------------- 1 | import { GasPrice, IGasPriceProvider } from '@uniswap/smart-order-router' 2 | import { BigNumber } from 'ethers' 3 | 4 | export class StaticGasPriceProvider implements IGasPriceProvider { 5 | constructor(private gasPriceWei: BigNumber) {} 6 | async getGasPrice(): Promise { 7 | return { gasPriceWei: this.gasPriceWei } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/handlers/router-entities/v3-aws-subgraph-provider.ts: -------------------------------------------------------------------------------- 1 | import { IV3SubgraphProvider, log, V3SubgraphPool, V3SubgraphProvider } from '@uniswap/smart-order-router' 2 | import { S3 } from 'aws-sdk' 3 | import { ChainId } from '@uniswap/sdk-core' 4 | import _ from 'lodash' 5 | import NodeCache from 'node-cache' 6 | 7 | const POOL_CACHE = new NodeCache({ stdTTL: 240, useClones: false }) 8 | const POOL_CACHE_KEY = (chainId: ChainId) => `pools${chainId}` 9 | 10 | export class V3AWSSubgraphProviderWithFallback extends V3SubgraphProvider implements IV3SubgraphProvider { 11 | private key: string 12 | 13 | constructor(private chain: ChainId, private bucket: string, key: string) { 14 | super(chain) 15 | this.key = `${key}${chain != ChainId.MAINNET ? `-${chain}` : ''}` 16 | } 17 | 18 | public async getPools(): Promise { 19 | log.info(`In legacy AWS subgraph provider for protocol V3`) 20 | 21 | const s3 = new S3() 22 | 23 | const cachedPools = POOL_CACHE.get(POOL_CACHE_KEY(this.chain)) 24 | 25 | if (cachedPools) { 26 | log.info( 27 | { subgraphPoolsSample: cachedPools.slice(0, 5) }, 28 | `Subgraph pools fetched from local cache. Num: ${cachedPools.length}` 29 | ) 30 | 31 | return cachedPools 32 | } 33 | 34 | log.info( 35 | { bucket: this.bucket, key: this.key }, 36 | `Subgraph pools local cache miss. Getting subgraph pools from S3 ${this.bucket}/${this.key}` 37 | ) 38 | try { 39 | const result = await s3.getObject({ Key: this.key, Bucket: this.bucket }).promise() 40 | 41 | const { Body: poolsBuffer } = result 42 | 43 | if (!poolsBuffer) { 44 | throw new Error('Could not get subgraph pool cache from S3') 45 | } 46 | 47 | let pools = JSON.parse(poolsBuffer.toString('utf-8')) 48 | 49 | if (pools[0].totalValueLockedETH) { 50 | pools = _.map( 51 | pools, 52 | (pool) => 53 | ({ 54 | ...pool, 55 | id: pool.id.toLowerCase(), 56 | token0: { 57 | id: pool.token0.id.toLowerCase(), 58 | }, 59 | token1: { 60 | id: pool.token1.id.toLowerCase(), 61 | }, 62 | tvlETH: parseFloat(pool.totalValueLockedETH), 63 | tvlUSD: parseFloat(pool.totalValueLockedUSD), 64 | } as V3SubgraphPool) 65 | ) 66 | log.info({ sample: pools.slice(0, 5) }, 'Converted legacy schema to new schema') 67 | } 68 | 69 | log.info( 70 | { bucket: this.bucket, key: this.key, sample: pools.slice(0, 3) }, 71 | `Got subgraph pools from S3. Num: ${pools.length}` 72 | ) 73 | 74 | POOL_CACHE.set(POOL_CACHE_KEY(this.chain), pools) 75 | 76 | return pools 77 | } catch (err) { 78 | log.info( 79 | { bucket: this.bucket, key: this.key }, 80 | `Failed to get subgraph pools from S3 ${this.bucket}/${this.key}.` 81 | ) 82 | 83 | return super.getPools() 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/handlers/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from '@hapi/joi' 2 | import { MethodParameters } from '@uniswap/smart-order-router' 3 | import { RoutingApiSimulationStatus } from './quote/util/simulation' 4 | 5 | export type TokenInRoute = { 6 | address: string 7 | chainId: number 8 | symbol: string 9 | decimals: string 10 | buyFeeBps?: string 11 | sellFeeBps?: string 12 | } 13 | 14 | export type SupportedPoolInRoute = V2PoolInRoute | V3PoolInRoute | V4PoolInRoute 15 | 16 | export type V4PoolInRoute = { 17 | type: 'v4-pool' 18 | address: string 19 | tokenIn: TokenInRoute 20 | tokenOut: TokenInRoute 21 | sqrtRatioX96: string 22 | liquidity: string 23 | tickCurrent: string 24 | fee: string 25 | tickSpacing: string 26 | hooks: string 27 | amountIn?: string 28 | amountOut?: string 29 | } 30 | 31 | export type V3PoolInRoute = { 32 | type: 'v3-pool' 33 | address: string 34 | tokenIn: TokenInRoute 35 | tokenOut: TokenInRoute 36 | sqrtRatioX96: string 37 | liquidity: string 38 | tickCurrent: string 39 | fee: string 40 | amountIn?: string 41 | amountOut?: string 42 | } 43 | 44 | export type V2Reserve = { 45 | token: TokenInRoute 46 | quotient: string 47 | } 48 | 49 | export type V2PoolInRoute = { 50 | type: 'v2-pool' 51 | address: string 52 | tokenIn: TokenInRoute 53 | tokenOut: TokenInRoute 54 | reserve0: V2Reserve 55 | reserve1: V2Reserve 56 | amountIn?: string 57 | amountOut?: string 58 | } 59 | 60 | export const QuoteResponseSchemaJoi = Joi.object().keys({ 61 | quoteId: Joi.string().required(), 62 | amount: Joi.string().required(), 63 | amountDecimals: Joi.string().required(), 64 | quote: Joi.string().required(), 65 | quoteDecimals: Joi.string().required(), 66 | quoteGasAdjusted: Joi.string().required(), 67 | quoteGasAdjustedDecimals: Joi.string().required(), 68 | gasUseEstimateQuote: Joi.string().required(), 69 | gasUseEstimateQuoteDecimals: Joi.string().required(), 70 | gasUseEstimateGasToken: Joi.string().optional(), 71 | gasUseEstimateGasTokenDecimals: Joi.string().optional(), 72 | quoteGasAndPortionAdjusted: Joi.string().optional(), 73 | quoteGasAndPortionAdjustedDecimals: Joi.string().optional(), 74 | gasUseEstimate: Joi.string().required(), 75 | gasUseEstimateUSD: Joi.string().required(), 76 | simulationError: Joi.boolean().optional(), 77 | simulationStatus: Joi.string().required(), 78 | gasPriceWei: Joi.string().required(), 79 | blockNumber: Joi.string().required(), 80 | route: Joi.array().items(Joi.any()).required(), 81 | routeString: Joi.string().required(), 82 | methodParameters: Joi.object({ 83 | calldata: Joi.string().required(), 84 | value: Joi.string().required(), 85 | to: Joi.string().required(), 86 | }).optional(), 87 | hitsCachedRoutes: Joi.boolean().optional(), 88 | portionBips: Joi.number().optional(), 89 | portionRecipient: Joi.string().optional(), 90 | portionAmount: Joi.string().optional(), 91 | portionAmountDecimals: Joi.string().optional(), 92 | priceImpact: Joi.string().optional(), 93 | }) 94 | 95 | export type QuoteResponse = { 96 | quoteId: string 97 | amount: string 98 | amountDecimals: string 99 | quote: string 100 | quoteDecimals: string 101 | quoteGasAdjusted: string 102 | quoteGasAdjustedDecimals: string 103 | quoteGasAndPortionAdjusted?: string 104 | quoteGasAndPortionAdjustedDecimals?: string 105 | gasUseEstimate: string 106 | gasUseEstimateQuote: string 107 | gasUseEstimateQuoteDecimals: string 108 | gasUseEstimateGasToken?: string 109 | gasUseEstimateGasTokenDecimals?: string 110 | gasUseEstimateUSD: string 111 | simulationError?: boolean 112 | simulationStatus: RoutingApiSimulationStatus 113 | gasPriceWei: string 114 | blockNumber: string 115 | route: Array 116 | routeString: string 117 | methodParameters?: MethodParameters 118 | hitsCachedRoutes?: boolean 119 | portionBips?: number 120 | portionRecipient?: string 121 | portionAmount?: string 122 | portionAmountDecimals?: string 123 | priceImpact?: string 124 | } 125 | -------------------------------------------------------------------------------- /lib/rpc/GlobalRpcProviders.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@uniswap/sdk-core' 2 | import { SingleJsonRpcProvider } from './SingleJsonRpcProvider' 3 | import { UniJsonRpcProvider } from './UniJsonRpcProvider' 4 | import Logger from 'bunyan' 5 | import { 6 | DEFAULT_SINGLE_PROVIDER_CONFIG, 7 | DEFAULT_UNI_PROVIDER_CONFIG, 8 | SingleJsonRpcProviderConfig, 9 | UniJsonRpcProviderConfig, 10 | } from './config' 11 | import { ProdConfig, ProdConfigJoi } from './ProdConfig' 12 | import { chainIdToNetworkName, generateProviderUrl } from './utils' 13 | import PROD_CONFIG from '../config/rpcProviderProdConfig.json' 14 | 15 | export class GlobalRpcProviders { 16 | private static SINGLE_RPC_PROVIDERS: Map | null = null 17 | 18 | private static UNI_RPC_PROVIDERS: Map | null = null 19 | 20 | private static validateProdConfig(config?: object): ProdConfig { 21 | const prodConfigInput = config !== undefined ? config : PROD_CONFIG 22 | const validation = ProdConfigJoi.validate(prodConfigInput) 23 | if (validation.error) { 24 | throw new Error( 25 | `ProdConfig failed data validation: Value: ${prodConfigInput}, Error: ${validation.error.message}` 26 | ) 27 | } 28 | const prodConfig: ProdConfig = validation.value as ProdConfig 29 | for (let chainConfig of prodConfig) { 30 | if (!chainConfig.providerUrls) { 31 | continue 32 | } 33 | for (let i = 0; i < chainConfig.providerUrls!.length; i++) { 34 | const urlEnvVar = chainConfig.providerUrls[i] 35 | if (process.env[urlEnvVar] === undefined) { 36 | throw new Error(`Environmental variable ${urlEnvVar} isn't defined!`) 37 | } 38 | chainConfig.providerUrls[i] = generateProviderUrl(urlEnvVar, process.env[urlEnvVar]!, chainConfig.chainId) 39 | } 40 | } 41 | return prodConfig 42 | } 43 | 44 | private static initGlobalSingleRpcProviders( 45 | log: Logger, 46 | prodConfig: ProdConfig, 47 | singleConfig: SingleJsonRpcProviderConfig 48 | ) { 49 | GlobalRpcProviders.SINGLE_RPC_PROVIDERS = new Map() 50 | for (const chainConfig of prodConfig) { 51 | const chainId = chainConfig.chainId as ChainId 52 | if (Math.random() < chainConfig.useMultiProviderProb) { 53 | let providers: SingleJsonRpcProvider[] = [] 54 | 55 | for (let i = 0; i < chainConfig.providerUrls!.length; i++) { 56 | // For unirpc provider, pass the service id in the header. 57 | const providerUrl = chainConfig.providerUrls![i] 58 | const headers = 59 | chainConfig.providerNames![i] === 'UNIRPC' 60 | ? { 61 | 'x-uni-service-id': 'routing_api', 62 | } 63 | : undefined 64 | 65 | providers.push( 66 | new SingleJsonRpcProvider( 67 | { name: chainIdToNetworkName(chainId), chainId }, 68 | providerUrl, 69 | headers, 70 | log, 71 | singleConfig, 72 | chainConfig.enableDbSync!, 73 | chainConfig.dbSyncSampleProb! 74 | ) 75 | ) 76 | } 77 | GlobalRpcProviders.SINGLE_RPC_PROVIDERS.set(chainId, providers) 78 | } 79 | } 80 | } 81 | 82 | private static initGlobalUniRpcProviders( 83 | log: Logger, 84 | prodConfig: ProdConfig, 85 | uniConfig: UniJsonRpcProviderConfig, 86 | singleConfig: SingleJsonRpcProviderConfig 87 | ) { 88 | if (GlobalRpcProviders.SINGLE_RPC_PROVIDERS === null) { 89 | GlobalRpcProviders.initGlobalSingleRpcProviders(log, prodConfig, singleConfig) 90 | } 91 | 92 | GlobalRpcProviders.UNI_RPC_PROVIDERS = new Map() 93 | for (const chainConfig of prodConfig) { 94 | const chainId = chainConfig.chainId as ChainId 95 | if (!GlobalRpcProviders.SINGLE_RPC_PROVIDERS!.has(chainId)) { 96 | continue 97 | } 98 | GlobalRpcProviders.UNI_RPC_PROVIDERS.set( 99 | chainId, 100 | new UniJsonRpcProvider( 101 | chainId, 102 | GlobalRpcProviders.SINGLE_RPC_PROVIDERS!.get(chainId)!, 103 | log, 104 | uniConfig, 105 | chainConfig.latencyEvaluationSampleProb!, 106 | chainConfig.healthCheckSampleProb!, 107 | chainConfig.providerInitialWeights, 108 | true 109 | ) 110 | ) 111 | } 112 | return GlobalRpcProviders.UNI_RPC_PROVIDERS 113 | } 114 | 115 | static getGlobalSingleRpcProviders( 116 | log: Logger, 117 | singleConfig: SingleJsonRpcProviderConfig = DEFAULT_SINGLE_PROVIDER_CONFIG 118 | ): Map { 119 | const prodConfig = GlobalRpcProviders.validateProdConfig() 120 | if (GlobalRpcProviders.SINGLE_RPC_PROVIDERS === null) { 121 | GlobalRpcProviders.initGlobalSingleRpcProviders(log, prodConfig, singleConfig) 122 | } 123 | return GlobalRpcProviders.SINGLE_RPC_PROVIDERS! 124 | } 125 | 126 | static getGlobalUniRpcProviders( 127 | log: Logger, 128 | uniConfig: UniJsonRpcProviderConfig = DEFAULT_UNI_PROVIDER_CONFIG, 129 | singleConfig: SingleJsonRpcProviderConfig = DEFAULT_SINGLE_PROVIDER_CONFIG, 130 | prodConfigJson?: any 131 | ): Map { 132 | const prodConfig = GlobalRpcProviders.validateProdConfig(prodConfigJson) 133 | if (GlobalRpcProviders.UNI_RPC_PROVIDERS === null) { 134 | GlobalRpcProviders.initGlobalUniRpcProviders(log, prodConfig, uniConfig, singleConfig) 135 | } 136 | return GlobalRpcProviders.UNI_RPC_PROVIDERS! 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/rpc/ProdConfig.ts: -------------------------------------------------------------------------------- 1 | import Joi from '@hapi/joi' 2 | import { ChainId } from '@uniswap/sdk-core' 3 | import PROD_CONFIG from '../config/rpcProviderProdConfig.json' 4 | 5 | export interface ChainConfig { 6 | chainId: number 7 | useMultiProviderProb: number 8 | sessionAllowProviderFallbackWhenUnhealthy?: boolean 9 | providerInitialWeights?: number[] 10 | providerUrls?: string[] 11 | providerNames?: string[] 12 | dbSyncSampleProb?: number 13 | latencyEvaluationSampleProb?: number 14 | healthCheckSampleProb?: number 15 | enableDbSync?: boolean 16 | } 17 | 18 | export type ProdConfig = ChainConfig[] 19 | 20 | export const ProdConfigJoi = Joi.array().items( 21 | Joi.object({ 22 | chainId: Joi.number().required(), 23 | useMultiProviderProb: Joi.number().required(), 24 | sessionAllowProviderFallbackWhenUnhealthy: Joi.boolean().optional(), 25 | providerInitialWeights: Joi.array().items(Joi.number()).optional(), 26 | providerUrls: Joi.array().items(Joi.string()).optional(), 27 | providerNames: Joi.array().items(Joi.string()).optional(), 28 | dbSyncSampleProb: Joi.number().min(0.0).max(1.0).optional().default(1.0), 29 | latencyEvaluationSampleProb: Joi.number().min(0.0).max(1.0).optional().default(1.0), 30 | healthCheckSampleProb: Joi.number().min(0.0).max(1.0).optional().default(1.0), 31 | enableDbSync: Joi.boolean().optional().default(false), 32 | }) 33 | ) 34 | 35 | // Return a map of chain id and its provider names 36 | export function getRpcGatewayEnabledChains(config?: object): Map { 37 | const prodConfigInput = config ?? PROD_CONFIG 38 | const validation = ProdConfigJoi.validate(prodConfigInput) 39 | if (validation.error) { 40 | throw new Error(`ProdConfig failed data validation: Value: ${prodConfigInput}, Error: ${validation.error.message}`) 41 | } 42 | const prodConfig: ProdConfig = validation.value as ProdConfig 43 | let result = new Map() 44 | for (const chainConfig of prodConfig) { 45 | if (chainConfig.providerUrls) { 46 | result.set( 47 | chainConfig.chainId, 48 | // Extract the provider name. For example, from "INFURA_8453" to "INFURA" 49 | chainConfig.providerUrls.map((url: string) => url.substring(0, url.indexOf('_'))) 50 | ) 51 | } 52 | } 53 | return result 54 | } 55 | -------------------------------------------------------------------------------- /lib/rpc/ProviderHealthState.ts: -------------------------------------------------------------------------------- 1 | export enum ProviderHealthiness { 2 | HEALTHY = 'HEALTHY', 3 | UNHEALTHY = 'UNHEALTHY', 4 | } 5 | 6 | export interface ProviderHealthState { 7 | healthiness: ProviderHealthiness 8 | ongoingAlarms: string[] 9 | version: number 10 | } 11 | -------------------------------------------------------------------------------- /lib/rpc/ProviderHealthStateDynamoDbRepository.ts: -------------------------------------------------------------------------------- 1 | import { ProviderHealthStateRepository } from './ProviderHealthStateRepository' 2 | import Logger from 'bunyan' 3 | import { DynamoDB } from 'aws-sdk' 4 | import { DocumentClient } from 'aws-sdk/clients/dynamodb' 5 | import { ProviderHealthState } from './ProviderHealthState' 6 | 7 | // Table item assignment 8 | const UPDATE_EXPRESSION = `SET 9 | #healthiness = :healthiness, 10 | #ongoingAlarms = :ongoingAlarms, 11 | #version = :version` 12 | 13 | // Table column names 14 | const EXPRESSION_ATTRIBUTE_NAMES = { 15 | '#healthiness': 'healthiness', 16 | '#ongoingAlarms': 'ongoingAlarms', 17 | '#version': 'version', 18 | } 19 | 20 | const CONDITION_EXPRESSION = '#version = :baseVersion' 21 | 22 | export class ProviderHealthStateDynamoDbRepository implements ProviderHealthStateRepository { 23 | private ddbClient: DynamoDB.DocumentClient 24 | 25 | constructor(private dbTableName: string, private log: Logger) { 26 | this.ddbClient = new DynamoDB.DocumentClient() 27 | } 28 | 29 | async read(providerId: string): Promise { 30 | const getParams = { 31 | TableName: this.dbTableName, 32 | Key: { chainIdProviderName: providerId }, 33 | } 34 | try { 35 | const result = await this.ddbClient.get(getParams).promise() 36 | const item = result.Item 37 | if (item === undefined) { 38 | this.log.debug(`No health state found for ${providerId}`) 39 | return null 40 | } 41 | return { 42 | healthiness: item.healthiness, 43 | ongoingAlarms: item.ongoingAlarms, 44 | version: item.version, 45 | } 46 | } catch (error: any) { 47 | this.log.error(`Failed to read health state from DB: ${JSON.stringify(error)}`) 48 | throw error 49 | } 50 | } 51 | 52 | async write(providerId: string, state: ProviderHealthState): Promise { 53 | const putParams: DocumentClient.PutItemInput = { 54 | TableName: this.dbTableName, 55 | Item: { 56 | chainIdProviderName: providerId, 57 | healthiness: state.healthiness, 58 | ongoingAlarms: state.ongoingAlarms, 59 | version: state.version, 60 | }, 61 | } 62 | await this.ddbClient.put(putParams).promise() 63 | return 64 | } 65 | 66 | async update(providerId: string, state: ProviderHealthState): Promise { 67 | const updateParams: DocumentClient.UpdateItemInput = { 68 | TableName: this.dbTableName, 69 | Key: { chainIdProviderName: providerId }, 70 | UpdateExpression: UPDATE_EXPRESSION, 71 | ExpressionAttributeNames: EXPRESSION_ATTRIBUTE_NAMES, 72 | ExpressionAttributeValues: this.getExpressionAttributeValues(state), 73 | // Use conditional update in combination with increasing version number to detect concurrent write conflicts. 74 | // If write conflicts is detected, the later write will be dropped. But the invocation of this lambda will be 75 | // retried for a maximum of 2 times, at 60 seconds delay per retry. 76 | ConditionExpression: CONDITION_EXPRESSION, 77 | } 78 | await this.ddbClient.update(updateParams).promise() 79 | } 80 | 81 | private getExpressionAttributeValues(state: ProviderHealthState): { [key: string]: any } { 82 | let attributes: { [key: string]: any } = {} 83 | attributes[':healthiness'] = state.healthiness 84 | attributes[':ongoingAlarms'] = state.ongoingAlarms 85 | attributes[':baseVersion'] = state.version - 1 86 | attributes[':version'] = state.version 87 | return attributes 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/rpc/ProviderHealthStateRepository.ts: -------------------------------------------------------------------------------- 1 | import { ProviderHealthState } from './ProviderHealthState' 2 | 3 | export abstract class ProviderHealthStateRepository { 4 | abstract read(providerId: string): Promise 5 | abstract write(providerId: string, providerHealthState: ProviderHealthState): Promise 6 | abstract update(providerId: string, providerHealthState: ProviderHealthState): Promise 7 | } 8 | -------------------------------------------------------------------------------- /lib/rpc/config.ts: -------------------------------------------------------------------------------- 1 | export interface CommonConfig { 2 | // For an unhealthy provider, if it hasn't been used for some time, we can 3 | // test it out to check its recovery. This defines the time it needs to wait 4 | // before being tested again, in milliseconds. 5 | HEALTH_EVALUATION_WAIT_PERIOD_IN_S: number 6 | // Wait time for recording next latency evaluation result. 7 | LATENCY_EVALUATION_WAIT_PERIOD_IN_S: number 8 | } 9 | 10 | // Config here applies to all chains. 11 | export interface UniJsonRpcProviderConfig extends CommonConfig { 12 | // Do shadow calls on other non-selected healthy providers to monitor their latencies 13 | ENABLE_SHADOW_LATENCY_EVALUATION: boolean 14 | // Default initial provider's weight, if not specified. 15 | DEFAULT_INITIAL_WEIGHT: 1000 16 | } 17 | 18 | // Config here applies to all chains. 19 | export interface SingleJsonRpcProviderConfig extends CommonConfig { 20 | ERROR_PENALTY: number 21 | HIGH_LATENCY_PENALTY: number 22 | // If a healthy provider's score drop below this, it will become unhealthy. 23 | HEALTH_SCORE_FALLBACK_THRESHOLD: number 24 | // If an unhealthy provider's score raise above this, it will become healthy. 25 | HEALTH_SCORE_RECOVER_THRESHOLD: number 26 | // Latency exceeds this will be considered as error. 27 | MAX_LATENCY_ALLOWED_IN_MS: number 28 | // As time passes, provider's health score will automatically increase, 29 | // but will not exceed 0. This defines the score increased every millisecond. 30 | RECOVER_SCORE_PER_MS: number 31 | // This is added to prevent an unhealthy provider gain too much recovery score only by 32 | // waiting a long time to be evaluated. 33 | RECOVER_MAX_WAIT_TIME_TO_ACKNOWLEDGE_IN_MS: number 34 | // Time interval to sync with health states from DB 35 | DB_SYNC_INTERVAL_IN_S: number 36 | // The length of latency history window to consider. 37 | LATENCY_STAT_HISTORY_WINDOW_LENGTH_IN_S: number 38 | } 39 | 40 | export const DEFAULT_UNI_PROVIDER_CONFIG: UniJsonRpcProviderConfig = { 41 | HEALTH_EVALUATION_WAIT_PERIOD_IN_S: 60, 42 | ENABLE_SHADOW_LATENCY_EVALUATION: true, 43 | LATENCY_EVALUATION_WAIT_PERIOD_IN_S: 60, 44 | DEFAULT_INITIAL_WEIGHT: 1000, 45 | } 46 | 47 | // Health score needs to drop below a certain threshold to trigger circuit break (all potentially fallback to other 48 | // providers). If we set the threshold to the lowest possible, there will never be a circuit break. 49 | const NEVER_FALLBACK = Number.MIN_SAFE_INTEGER 50 | 51 | export const DEFAULT_SINGLE_PROVIDER_CONFIG: SingleJsonRpcProviderConfig = { 52 | ERROR_PENALTY: -50, 53 | HIGH_LATENCY_PENALTY: -20, 54 | HEALTH_SCORE_FALLBACK_THRESHOLD: NEVER_FALLBACK, 55 | HEALTH_SCORE_RECOVER_THRESHOLD: -200, 56 | MAX_LATENCY_ALLOWED_IN_MS: 4000, 57 | RECOVER_SCORE_PER_MS: 0.01, 58 | RECOVER_MAX_WAIT_TIME_TO_ACKNOWLEDGE_IN_MS: 60000, 59 | DB_SYNC_INTERVAL_IN_S: 60, 60 | LATENCY_STAT_HISTORY_WINDOW_LENGTH_IN_S: 180, 61 | HEALTH_EVALUATION_WAIT_PERIOD_IN_S: 60, 62 | LATENCY_EVALUATION_WAIT_PERIOD_IN_S: 60, 63 | } 64 | 65 | export enum ProviderSpecialWeight { 66 | // Provider will never receive any traffic. 67 | // However, it's still being perceived as one of available healthy provider. 68 | // This is useful when we want to do shadow calls to collect performance metrics. 69 | NEVER = 0, 70 | 71 | // Provider will be able to serve as a fallback. For detailed logic, please see the TSDoc for 72 | // UniJsonRpcProvider's constructor 73 | AS_FALLBACK = -1, 74 | } 75 | -------------------------------------------------------------------------------- /lib/rpc/handler/index.ts: -------------------------------------------------------------------------------- 1 | import { FallbackHandler } from './FallbackHandler' 2 | 3 | import { default as bunyan, default as Logger } from 'bunyan' 4 | 5 | const log: Logger = bunyan.createLogger({ 6 | name: 'Root', 7 | serializers: bunyan.stdSerializers, 8 | level: bunyan.ERROR, 9 | }) 10 | 11 | let fallbackHandler: FallbackHandler 12 | try { 13 | fallbackHandler = new FallbackHandler(log) 14 | } catch (error) { 15 | log.fatal({ error }, 'Unable to construct FallbackHandler') 16 | throw error 17 | } 18 | 19 | module.exports = { 20 | fallbackHandler: fallbackHandler.handler, 21 | } 22 | -------------------------------------------------------------------------------- /lib/rpc/utils.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@uniswap/sdk-core' 2 | 3 | export function chainIdToNetworkName(networkId: ChainId): string { 4 | switch (networkId) { 5 | case ChainId.MAINNET: 6 | return 'ethereum' 7 | case ChainId.ARBITRUM_ONE: 8 | return 'arbitrum' 9 | case ChainId.OPTIMISM: 10 | return 'optimism' 11 | case ChainId.POLYGON: 12 | return 'polygon' 13 | case ChainId.BNB: 14 | return 'smartchain' 15 | case ChainId.CELO: 16 | return 'celo' 17 | case ChainId.AVALANCHE: 18 | return 'avalanchec' 19 | case ChainId.BASE: 20 | return 'base' 21 | case ChainId.WORLDCHAIN: 22 | return 'worldchain' 23 | case ChainId.ASTROCHAIN_SEPOLIA: 24 | return 'astrochain-sepolia' 25 | default: 26 | return 'ethereum' 27 | } 28 | } 29 | 30 | export function generateProviderUrl(key: string, value: string, chainId: number): string { 31 | if (key === 'UNIRPC_0') { 32 | // UNIRPC_0 is a special case for the Uniswap RPC 33 | // - env value will contain the generic unirpc endpoint - no trailing '/' 34 | return `${value}/rpc/${chainId}` 35 | } 36 | 37 | const tokens = value.split(',') 38 | switch (key) { 39 | // Infura 40 | case 'INFURA_43114': { 41 | return `https://avalanche-mainnet.infura.io/v3/${tokens[0]}` 42 | } 43 | case 'INFURA_10': { 44 | return `https://optimism-mainnet.infura.io/v3/${tokens[0]}` 45 | } 46 | case 'INFURA_42220': { 47 | return `https://celo-mainnet.infura.io/v3/${tokens[0]}` 48 | } 49 | case 'INFURA_137': { 50 | return `https://polygon-mainnet.infura.io/v3/${tokens[0]}` 51 | } 52 | case 'INFURA_8453': { 53 | return `https://base-mainnet.infura.io/v3/${tokens[0]}` 54 | } 55 | case 'INFURA_11155111': { 56 | return `https://sepolia.infura.io/v3/${tokens[0]}` 57 | } 58 | case 'INFURA_42161': { 59 | return `https://arbitrum-mainnet.infura.io/v3/${tokens[0]}` 60 | } 61 | case 'INFURA_1': { 62 | return `https://mainnet.infura.io/v3/${tokens[0]}` 63 | } 64 | case 'INFURA_81457': { 65 | return `https://blast-mainnet.infura.io/v3/${tokens[0]}` 66 | } 67 | // Quicknode 68 | case 'QUICKNODE_43114': { 69 | return `https://${tokens[0]}.avalanche-mainnet.quiknode.pro/${tokens[1]}/ext/bc/C/rpc/` 70 | } 71 | case 'QUICKNODE_10': { 72 | return `https://${tokens[0]}.optimism.quiknode.pro/${tokens[1]}` 73 | } 74 | case 'QUICKNODE_42220': { 75 | return `https://${tokens[0]}.celo-mainnet.quiknode.pro/${tokens[1]}` 76 | } 77 | case 'QUICKNODE_56': { 78 | return `https://${tokens[0]}.bsc.quiknode.pro/${tokens[1]}` 79 | } 80 | case 'QUICKNODE_137': { 81 | return `https://${tokens[0]}.matic.quiknode.pro/${tokens[1]}` 82 | } 83 | case 'QUICKNODE_8453': { 84 | return `https://${tokens[0]}.base-mainnet.quiknode.pro/${tokens[1]}` 85 | } 86 | case 'QUICKNODE_42161': { 87 | return `https://${tokens[0]}.arbitrum-mainnet.quiknode.pro/${tokens[1]}` 88 | } 89 | case 'QUICKNODE_1': { 90 | return `https://${tokens[0]}.quiknode.pro/${tokens[1]}` 91 | } 92 | case 'QUICKNODE_81457': { 93 | return `https://${tokens[0]}.blast-mainnet.quiknode.pro/${tokens[1]}` 94 | } 95 | case 'QUICKNODE_7777777': { 96 | return `https://${tokens[0]}.zora-mainnet.quiknode.pro/${tokens[1]}` 97 | } 98 | case 'QUICKNODE_324': { 99 | return `https://${tokens[0]}.zksync-mainnet.quiknode.pro/${tokens[1]}` 100 | } 101 | case 'QUICKNODE_1301': { 102 | return `${tokens[0]}` 103 | } 104 | // QuickNode RETH 105 | case 'QUICKNODERETH_1': { 106 | return `https://${tokens[0]}.quiknode.pro/${tokens[1]}` 107 | } 108 | // Alchemy 109 | case 'ALCHEMY_10': { 110 | return `https://opt-mainnet-fast.g.alchemy.com/v2/${tokens[0]}` 111 | } 112 | case 'ALCHEMY_137': { 113 | return `https://polygon-mainnet-fast.g.alchemy.com/v2/${tokens[0]}` 114 | } 115 | case 'ALCHEMY_8453': { 116 | return `https://base-mainnet-fast.g.alchemy.com/v2/${tokens[0]}` 117 | } 118 | case 'ALCHEMY_11155111': { 119 | return `https://eth-sepolia-fast.g.alchemy.com/v2/${tokens[0]}` 120 | } 121 | case 'ALCHEMY_42161': { 122 | return `https://arb-mainnet-fast.g.alchemy.com/v2/${tokens[0]}` 123 | } 124 | case 'ALCHEMY_1': { 125 | return `https://eth-mainnet-fast.g.alchemy.com/v2/${tokens[0]}` 126 | } 127 | case 'ALCHEMY_324': { 128 | return `https://zksync-mainnet.g.alchemy.com/v2/${tokens[0]}` 129 | } 130 | case 'ALCHEMY_480': { 131 | return `https://worldchain-mainnet.g.alchemy.com/v2/${tokens[0]}` 132 | } 133 | } 134 | throw new Error(`Unknown provider-chainId pair: ${key}`) 135 | } 136 | 137 | export function getProviderId(chainId: ChainId, providerName: string): string { 138 | return `${chainId.toString()}_${providerName}` 139 | } 140 | -------------------------------------------------------------------------------- /lib/util/alpha-config-measurement.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Currency, CurrencyAmount } from '@uniswap/sdk-core' 2 | import { Protocol } from '@uniswap/router-sdk' 3 | import { log, metric, MetricLoggerUnit, SwapRoute } from '@uniswap/smart-order-router' 4 | 5 | export const getDistribution = (distributionPercent: number) => { 6 | const percents: Array = new Array() 7 | 8 | for (let i = 1; i <= 100 / distributionPercent; i++) { 9 | percents.push(i * distributionPercent) 10 | } 11 | 12 | return percents 13 | } 14 | 15 | export const measureDistributionPercentChangeImpact = ( 16 | distributionPercentBefore: number, 17 | distributionPercentAfter: number, 18 | bestSwapRoute: SwapRoute, 19 | currencyIn: Currency, 20 | currencyOut: Currency, 21 | tradeType: string, 22 | chainId: ChainId, 23 | amount: CurrencyAmount 24 | ) => { 25 | const routesImpacted: Array = new Array() 26 | 27 | const percentDistributionBefore = getDistribution(distributionPercentBefore) 28 | const percentDistributionAfter = getDistribution(distributionPercentAfter) 29 | 30 | bestSwapRoute.route.forEach((route) => { 31 | switch (route.protocol) { 32 | case Protocol.MIXED: 33 | case Protocol.V3: 34 | if (percentDistributionBefore.includes(route.percent) && !percentDistributionAfter.includes(route.percent)) { 35 | routesImpacted.push(route.toString()) 36 | } 37 | break 38 | case Protocol.V2: 39 | // if it's v2, there's no distribution, skip the current route 40 | break 41 | } 42 | }) 43 | 44 | if (routesImpacted.length > 0) { 45 | log.warn(`Distribution percent change impacted the routes ${routesImpacted.join(',')}, 46 | for currency ${currencyIn.symbol} 47 | amount ${amount.toExact()} 48 | quote currency ${currencyOut.symbol} 49 | trade type ${tradeType} 50 | chain id ${chainId}`) 51 | metric.putMetric('BEST_SWAP_ROUTE_DISTRIBUTION_PERCENT_CHANGE_IMPACTED', 1, MetricLoggerUnit.Count) 52 | metric.putMetric( 53 | 'ROUTES_WITH_VALID_QUOTE_DISTRIBUTION_PERCENT_CHANGE_IMPACTED', 54 | routesImpacted.length, 55 | MetricLoggerUnit.Count 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/util/estimateGasUsed.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber' 2 | import { ChainId } from '@uniswap/sdk-core' 3 | import { CHAIN_TO_GAS_LIMIT_MAP } from './gasLimit' 4 | import { TENDERLY_NOT_SUPPORTED_CHAINS } from '@uniswap/smart-order-router' 5 | 6 | export function adhocCorrectGasUsed(estimatedGasUsed: BigNumber, chainId: ChainId): BigNumber { 7 | const shouldCorrectGas = TENDERLY_NOT_SUPPORTED_CHAINS.includes(chainId) 8 | 9 | if (!shouldCorrectGas) { 10 | return estimatedGasUsed 11 | } 12 | 13 | if (estimatedGasUsed.gt(CHAIN_TO_GAS_LIMIT_MAP[chainId])) { 14 | return estimatedGasUsed 15 | } 16 | 17 | return CHAIN_TO_GAS_LIMIT_MAP[chainId] 18 | } 19 | -------------------------------------------------------------------------------- /lib/util/estimateGasUsedUSD.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber' 2 | import { ChainId, Currency, CurrencyAmount } from '@uniswap/sdk-core' 3 | import { CHAIN_TO_GAS_LIMIT_MAP } from './gasLimit' 4 | import JSBI from 'jsbi' 5 | import { TENDERLY_NOT_SUPPORTED_CHAINS } from '@uniswap/smart-order-router' 6 | 7 | export function adhocCorrectGasUsedUSD( 8 | estimatedGasUsed: BigNumber, 9 | estimatedGasUsedUSD: CurrencyAmount, 10 | chainId: ChainId 11 | ): CurrencyAmount { 12 | const shouldCorrectGas = TENDERLY_NOT_SUPPORTED_CHAINS.includes(chainId) 13 | 14 | if (!shouldCorrectGas) { 15 | return estimatedGasUsedUSD 16 | } 17 | 18 | if (estimatedGasUsed.gt(CHAIN_TO_GAS_LIMIT_MAP[chainId])) { 19 | // this is a check to ensure that we don't return the gas used smaller than upper swap gas limit, 20 | // although this is unlikely 21 | return estimatedGasUsedUSD 22 | } 23 | 24 | const correctedEstimateGasUsedUSD = JSBI.divide( 25 | JSBI.multiply(estimatedGasUsedUSD.quotient, JSBI.BigInt(CHAIN_TO_GAS_LIMIT_MAP[chainId])), 26 | JSBI.BigInt(estimatedGasUsed) 27 | ) 28 | return CurrencyAmount.fromRawAmount(estimatedGasUsedUSD.currency, correctedEstimateGasUsedUSD) 29 | } 30 | -------------------------------------------------------------------------------- /lib/util/eth_feeHistory.ts: -------------------------------------------------------------------------------- 1 | export type EthFeeHistory = { 2 | oldestBlock: string 3 | reward: string[] 4 | baseFeePerGas: string[] 5 | gasUsedRatio: number[] 6 | baseFeePerBlobGas: string[] 7 | blobGasUsedRatio: number[] 8 | } 9 | -------------------------------------------------------------------------------- /lib/util/gasLimit.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber' 2 | import { ChainId } from '@uniswap/sdk-core' 3 | 4 | export const ZKSYNC_UPPER_SWAP_GAS_LIMIT = BigNumber.from(6000000) 5 | // CELO high gas limit from SOR https://github.com/Uniswap/smart-order-router/blob/main/src/routers/alpha-router/alpha-router.ts#L670 6 | export const CELO_UPPER_SWAP_GAS_LIMIT = BigNumber.from(5000000) 7 | // https://github.com/Uniswap/routing-api/blob/fe410751985995cb2904837e24f22da7dca1f518/lib/util/onChainQuoteProviderConfigs.ts#L340 divivde by 10 8 | export const WORLDCHAIN_UPPER_SWAP_GAS_LIMIT = BigNumber.from(300000) 9 | // https://github.com/Uniswap/routing-api/blob/fe410751985995cb2904837e24f22da7dca1f518/lib/util/onChainQuoteProviderConfigs.ts#L344 divivde by 10 10 | export const ASTROCHAIN_SEPOLIA_UPPER_SWAP_GAS_LIMIT = BigNumber.from(300000) 11 | 12 | export const CHAIN_TO_GAS_LIMIT_MAP: { [chainId: number]: BigNumber } = { 13 | [ChainId.ZKSYNC]: ZKSYNC_UPPER_SWAP_GAS_LIMIT, 14 | [ChainId.CELO]: CELO_UPPER_SWAP_GAS_LIMIT, 15 | [ChainId.CELO_ALFAJORES]: CELO_UPPER_SWAP_GAS_LIMIT, 16 | [ChainId.ASTROCHAIN_SEPOLIA]: ASTROCHAIN_SEPOLIA_UPPER_SWAP_GAS_LIMIT, 17 | } 18 | -------------------------------------------------------------------------------- /lib/util/isAddress.ts: -------------------------------------------------------------------------------- 1 | export const isAddress = (s: string) => { 2 | // Ethereum addresses are 42-character long hex strings that start with 3 | // 0x. This function can be improved in the future by validating that the string 4 | // only contains 0-9 and A-F. 5 | return s.length === 42 && s.startsWith('0x') 6 | } 7 | -------------------------------------------------------------------------------- /lib/util/pool-cache-key.ts: -------------------------------------------------------------------------------- 1 | import { Protocol } from '@uniswap/router-sdk' 2 | import { ChainId } from '@uniswap/sdk-core' 3 | 4 | export const S3_POOL_CACHE_KEY = (baseKey: string, chain: ChainId, protocol: Protocol) => 5 | `${baseKey}-${chain}-${protocol}` 6 | -------------------------------------------------------------------------------- /lib/util/poolCachingFilePrefixes.ts: -------------------------------------------------------------------------------- 1 | export const PoolCachingFilePrefixes = { 2 | PlainText: 'poolCache.json', 3 | GzipText: 'poolCacheGzip.json', 4 | } 5 | -------------------------------------------------------------------------------- /lib/util/requestSources.ts: -------------------------------------------------------------------------------- 1 | export const REQUEST_SOURCES = [ 2 | 'undefined', 3 | 'unknown', 4 | 'uniswap-ios', 5 | 'uniswap-android', 6 | 'uniswap-web', 7 | 'external-api', 8 | 'routing-api', 9 | 'uniswap-extension', 10 | ] 11 | -------------------------------------------------------------------------------- /lib/util/stage.ts: -------------------------------------------------------------------------------- 1 | export enum STAGE { 2 | BETA = 'beta', 3 | PROD = 'prod', 4 | LOCAL = 'local', 5 | } 6 | -------------------------------------------------------------------------------- /lib/util/supportedProtocolVersions.ts: -------------------------------------------------------------------------------- 1 | import { Protocol } from '@uniswap/router-sdk' 2 | import { UniversalRouterVersion } from '@uniswap/universal-router-sdk' 3 | 4 | export const SUPPORTED_PROTOCOL_VERSIONS = [Protocol.V2, Protocol.V3, Protocol.V4] 5 | 6 | export function convertStringRouterVersionToEnum(routerVersion?: string): UniversalRouterVersion { 7 | const validVersions = Object.values(UniversalRouterVersion) 8 | return validVersions.find((v) => v === routerVersion) || UniversalRouterVersion.V1_2 9 | } 10 | 11 | export type URVersionsToProtocolVersionsMapping = { 12 | readonly [universalRouterVersion in UniversalRouterVersion]: Array 13 | } 14 | 15 | export const URVersionsToProtocolVersions: URVersionsToProtocolVersionsMapping = { 16 | [UniversalRouterVersion.V1_2]: [Protocol.V2, Protocol.V3], 17 | [UniversalRouterVersion.V2_0]: [Protocol.V2, Protocol.V3, Protocol.V4], 18 | } 19 | 20 | export function protocolVersionsToBeExcludedFromMixed(universalRouterVersion: UniversalRouterVersion): Protocol[] { 21 | return SUPPORTED_PROTOCOL_VERSIONS.filter( 22 | (protocol) => !URVersionsToProtocolVersions[universalRouterVersion].includes(protocol) 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /lib/util/testNets.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@uniswap/sdk-core' 2 | 3 | export const TESTNETS = [ 4 | ChainId.ARBITRUM_GOERLI, 5 | ChainId.POLYGON_MUMBAI, 6 | ChainId.GOERLI, 7 | ChainId.SEPOLIA, 8 | ChainId.CELO_ALFAJORES, 9 | ChainId.BASE_GOERLI, 10 | ChainId.OPTIMISM_SEPOLIA, 11 | ChainId.OPTIMISM_GOERLI, 12 | ChainId.ARBITRUM_SEPOLIA, 13 | ChainId.ARBITRUM_GOERLI, 14 | ChainId.ASTROCHAIN_SEPOLIA, 15 | ] 16 | -------------------------------------------------------------------------------- /lib/util/traffic-switch/traffic-switcher-i-token-fee-fetcher.ts: -------------------------------------------------------------------------------- 1 | import { TrafficSwitcher } from './traffic-switcher' 2 | import { ITokenFeeFetcher, TokenFeeMap } from '@uniswap/smart-order-router/build/main/providers/token-fee-fetcher' 3 | import { ProviderConfig } from '@uniswap/smart-order-router/build/main/providers/provider' 4 | import { log } from '@uniswap/smart-order-router' 5 | import { BigNumber } from 'ethers' 6 | 7 | type Address = string 8 | 9 | export class TrafficSwitcherITokenFeeFetcher extends TrafficSwitcher implements ITokenFeeFetcher { 10 | async fetchFees(addresses: Address[], providerConfig?: ProviderConfig): Promise { 11 | return this.trafficSwitchMethod( 12 | () => this.props.control.fetchFees(addresses, providerConfig), 13 | () => this.props.treatment.fetchFees(addresses, providerConfig), 14 | this.fetchFees.name, 15 | {}, 16 | this.compareResultsForFetchFees.bind(this) 17 | ) 18 | } 19 | 20 | private compareResultsForFetchFees(resultA: TokenFeeMap | undefined, resultB: TokenFeeMap | undefined): void { 21 | // Check if both results are undefined or only one of them is. If so, log and return 22 | if (!resultA && !resultB) { 23 | this.logComparisonResult(this.fetchFees.name, 'IDENTICAL', true) 24 | return 25 | } 26 | if (!resultA) { 27 | this.logComparisonResult(this.fetchFees.name, this.props.aliasControl + '_IS_UNDEFINED', true) 28 | return 29 | } 30 | if (!resultB) { 31 | this.logComparisonResult(this.fetchFees.name, this.props.aliasTreatment + '_IS_UNDEFINED', true) 32 | return 33 | } 34 | 35 | // We have results from both implementations, compare them as a whole. 36 | // Before comparison, do some cleaning and keep only entries with a fee != 0. 37 | // This is needed as different implementations can return empty/null entry, or entry with 0 fees (buy/sellFeeBps or both). 38 | const cleanResult = (result: TokenFeeMap): TokenFeeMap => 39 | Object.entries(result) 40 | .filter( 41 | ([_, v]) => 42 | (v.buyFeeBps !== undefined && !v.buyFeeBps.eq(0)) || (v.sellFeeBps !== undefined && !v.sellFeeBps.eq(0)) 43 | ) 44 | .reduce( 45 | (acc, [k, v]) => ({ 46 | ...acc, 47 | [k]: { 48 | buyFeeBps: v.buyFeeBps ?? BigNumber.from(0), 49 | sellFeeBps: v.sellFeeBps ?? BigNumber.from(0), 50 | }, 51 | }), 52 | {} 53 | ) 54 | const cleanedResultA = cleanResult(resultA) 55 | const cleanedResultB = cleanResult(resultB) 56 | 57 | const identical = JSON.stringify(cleanedResultA) === JSON.stringify(cleanedResultB) 58 | this.logComparisonResult(this.fetchFees.name, 'IDENTICAL', identical) 59 | 60 | // Go deeper and let's do more granular custom comparisons 61 | if (!identical) { 62 | // Compare the number of results 63 | const comparisonResultLength = Object.keys(cleanedResultA).length === Object.keys(cleanedResultB).length 64 | this.logComparisonResult(this.fetchFees.name, 'LENGTHS_MATCH', comparisonResultLength) 65 | 66 | // find and log the differences: what's missing in A, what's missing in B, and what's different 67 | const keysA = Object.keys(cleanedResultA) 68 | const keysB = Object.keys(cleanedResultB) 69 | const missingInA = keysB.filter((k) => !keysA.includes(k)) 70 | const missingInB = keysA.filter((k) => !keysB.includes(k)) 71 | missingInA.forEach((k) => 72 | this.logMetric(this.fetchFees.name, 'MISSING_IN_' + this.props.aliasControl + '__Address__' + k) 73 | ) 74 | missingInB.forEach((k) => 75 | this.logMetric(this.fetchFees.name, 'MISSING_IN_' + this.props.aliasTreatment + '__Address__' + k) 76 | ) 77 | // find common keys with diffs 78 | const commonKeys = keysA.filter((k) => keysB.includes(k)) 79 | const commonKeysWithDifferentFees = commonKeys.filter( 80 | (k) => JSON.stringify(cleanedResultA[k]) !== JSON.stringify(cleanedResultB[k]) 81 | ) 82 | commonKeysWithDifferentFees.forEach((k) => { 83 | this.logMetric(this.fetchFees.name, 'DIFFERENT_FEE_FOR__Address__' + k) 84 | log.warn( 85 | `TrafficSwitcherITokenFeeFetcher compareResultsForFetchFees: Different fee for address ${k}: in control: ${JSON.stringify( 86 | cleanedResultA[k] 87 | )} and treatment: ${JSON.stringify(cleanedResultB[k])}` 88 | ) 89 | }) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uniswap/routing-api", 3 | "version": "0.1.0", 4 | "bin": { 5 | "app": "./dist/bin/app.js" 6 | }, 7 | "license": "GPL", 8 | "repository": "https://github.com/Uniswap/routing-api", 9 | "scripts": { 10 | "compile-external-types": "npx typechain --target ethers-v5 --out-dir lib/types/ext 'lib/abis/**/*.json'", 11 | "compile-v3-contract-types": "npx typechain --target ethers-v5 --out-dir lib/types/v3 './node_modules/@uniswap/?(v3-core|v3-periphery)/artifacts/contracts/**/*.json'", 12 | "build": "run-p compile-external-types compile-v3-contract-types && tsc", 13 | "clean": "rm -rf dist cdk.out", 14 | "watch": "tsc -w", 15 | "test:unit": "jest test/jest/unit/ && ts-mocha -p tsconfig.cdk.json test/mocha/unit/**/*", 16 | "test:unit:watch": "jest test/jest/unit/ --watch", 17 | "test:integ": "ts-mocha -p tsconfig.cdk.json -r test/mocha/dynamoDBLocalFixture.ts test/mocha/integ --recursive --extension .test.ts", 18 | "test:e2e": "ts-mocha -p tsconfig.cdk.json -r dotenv/config test/mocha/e2e/**/*.test.ts", 19 | "cdk": "cdk", 20 | "fix": "run-s fix:*", 21 | "fix:prettier": "prettier \"./**/*.ts\" --write", 22 | "fix:lint": "eslint lib --ext .ts --fix" 23 | }, 24 | "devDependencies": { 25 | "@jest/globals": "^29.5.0", 26 | "@nomiclabs/hardhat-ethers": "^2.0.2", 27 | "@nomiclabs/hardhat-waffle": "^2.0.1", 28 | "@swc/core": "^1.3.101", 29 | "@swc/jest": "^0.2.29", 30 | "@types/aws-lambda": "^8.10.77", 31 | "@types/bunyan": "^1.8.6", 32 | "@types/chai-as-promised": "^7.1.4", 33 | "@types/expect": "^24.3.0", 34 | "@types/hapi__joi": "^17.1.6", 35 | "@types/http-errors": "^1.8.0", 36 | "@types/lodash": "^4.14.182", 37 | "@types/mocha": "^8.2.3", 38 | "@types/node": "^14.0.27", 39 | "@types/uuid": "^8.3.0", 40 | "@typescript-eslint/eslint-plugin": "^4.0.1", 41 | "@typescript-eslint/parser": "^4.0.1", 42 | "axios-retry": "^3.2.4", 43 | "chai": "^4.3.4", 44 | "chai-as-promised": "^7.1.1", 45 | "chai-subset": "^1.6.0", 46 | "dynamodb-local": "^0.0.32", 47 | "esbuild": "^0.12.8", 48 | "eslint": "^7.29.0", 49 | "eslint-config-prettier": "^6.11.0", 50 | "eslint-plugin-eslint-comments": "^3.2.0", 51 | "eslint-plugin-import": "^2.22.0", 52 | "ethereum-waffle": "^3.4.0", 53 | "ethers": "^5.5.1", 54 | "fishery": "^2.2.2", 55 | "hardhat": "2.20.0", 56 | "jest": "^29.5.0", 57 | "json-schema-to-ts": "^1.6.4", 58 | "mocha": "^8.4.0", 59 | "npm-run-all": "^4.1.5", 60 | "nyc": "^15.1.0", 61 | "prettier": "^2.1.1", 62 | "prettier-plugin-organize-imports": "^2.1.0", 63 | "reflect-metadata": "^0.1.13", 64 | "sinon": "^12.0.1", 65 | "ts-jest": "^29.1.0", 66 | "ts-node": "^8.10.2", 67 | "typechain": "^5.0.0", 68 | "typescript": "^4.2.3" 69 | }, 70 | "dependencies": { 71 | "@hapi/joi": "^17.1.1", 72 | "@middy/core": "^2.4.1", 73 | "@middy/http-error-handler": "^2.4.1", 74 | "@middy/http-json-body-parser": "^2.4.1", 75 | "@middy/http-urlencode-path-parser": "^2.4.1", 76 | "@middy/input-output-logger": "^2.4.1", 77 | "@middy/validator": "^2.4.1", 78 | "@pinata/sdk": "^1.1.23", 79 | "@typechain/ethers-v5": "^7.0.1", 80 | "@types/async-retry": "^1.4.2", 81 | "@types/chai-subset": "^1.3.3", 82 | "@types/qs": "^6.9.7", 83 | "@types/sinon": "^10.0.6", 84 | "@types/stats-lite": "^2.2.0", 85 | "@uniswap/default-token-list": "^11.13.0", 86 | "@uniswap/permit2-sdk": "^1.3.0", 87 | "@uniswap/router-sdk": "^1.14.0", 88 | "@uniswap/sdk-core": "^5.8.2", 89 | "@types/semver": "^7.5.8", 90 | "@uniswap/smart-order-router": "4.6.1", 91 | "@uniswap/token-lists": "^1.0.0-beta.33", 92 | "@uniswap/universal-router-sdk": "^4.4.2", 93 | "@uniswap/v2-sdk": "^4.6.1", 94 | "@uniswap/v3-periphery": "^1.4.4", 95 | "@uniswap/v3-sdk": "^3.17.1", 96 | "@uniswap/v4-sdk": "^1.10.0", 97 | "async-retry": "^1.3.1", 98 | "aws-cdk-lib": "^2.137.0", 99 | "aws-embedded-metrics": "^2.0.6", 100 | "aws-sdk": "^2.927.0", 101 | "aws-xray-sdk": "^3.3.3", 102 | "axios": "^0.21.1", 103 | "bunyan": "^1.8.15", 104 | "constructs": "^10.0.0", 105 | "dotenv": "^16.0.1", 106 | "esm": "^3.2.25", 107 | "got": "^11.8.2", 108 | "graphql": "^16.5.0", 109 | "graphql-request": "^3.4.0", 110 | "http-errors": "^1.7.3", 111 | "http-json-errors": "^1.2.10", 112 | "lodash": "^4.17.21", 113 | "lru-cache": "^10.1.0", 114 | "node-cache": "^5.1.2", 115 | "punycode": "^2.1.1", 116 | "qs": "^6.10.1", 117 | "semver": "^7.6.0", 118 | "source-map-support": "^0.5.19", 119 | "stats-lite": "^2.2.0", 120 | "ts-mocha": "^8.0.0", 121 | "upgrade": "^1.1.0", 122 | "uuid": "^3.4.0" 123 | }, 124 | "prettier": { 125 | "printWidth": 120, 126 | "semi": false, 127 | "singleQuote": true 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /scripts/get_quote.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ts-node --project=tsconfig.cdk.json scripts/get_quote.ts 3 | */ 4 | import axios, { AxiosResponse } from 'axios' 5 | import dotenv from 'dotenv' 6 | import { QuoteQueryParams } from '../lib/handlers/quote/schema/quote-schema' 7 | import { QuoteResponse } from '../lib/handlers/schema' 8 | dotenv.config() 9 | ;(async function () { 10 | const quotePost: QuoteQueryParams = { 11 | tokenInAddress: 'MKR', 12 | tokenInChainId: 1, 13 | tokenOutAddress: 'GRT', 14 | tokenOutChainId: 1, 15 | amount: '50', 16 | type: 'exactIn', 17 | recipient: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B', 18 | slippageTolerance: '5', 19 | deadline: '360', 20 | algorithm: 'alpha', 21 | } 22 | 23 | const response: AxiosResponse = await axios.post( 24 | process.env.UNISWAP_ROUTING_API! + 'quote', 25 | quotePost 26 | ) 27 | 28 | console.log({ response }) 29 | })() 30 | -------------------------------------------------------------------------------- /test/jest/unit/dashboards/cached-routes-widgets-factory.test.ts: -------------------------------------------------------------------------------- 1 | import { CachedRoutesWidgetsFactory } from '../../../../lib/dashboards/cached-routes-widgets-factory' 2 | import { describe, it, expect } from '@jest/globals' 3 | 4 | const widgetsFactory = new CachedRoutesWidgetsFactory('Uniswap', 'us-west-1', 'lambda') 5 | 6 | describe('CachedRoutesWidgetsFactory', () => { 7 | it('works', () => { 8 | const widgets = widgetsFactory.generateWidgets() 9 | // It's hard to write a meaningful test here. 10 | expect(widgets.length).toBeGreaterThan(0) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /test/jest/unit/dashboards/quote-amounts-widgets-factory.test.ts: -------------------------------------------------------------------------------- 1 | import { QuoteAmountsWidgetsFactory } from '../../../../lib/dashboards/quote-amounts-widgets-factory' 2 | import { describe, it, expect } from '@jest/globals' 3 | 4 | const quoteAmountsWidgets = new QuoteAmountsWidgetsFactory('Uniswap', 'us-west-1') 5 | 6 | describe('Test widgets', () => { 7 | it('works', () => { 8 | const widgets = quoteAmountsWidgets.generateWidgets() 9 | // It's hard to write a meaningful test here. 10 | expect(widgets.length).toBeGreaterThan(0) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /test/jest/unit/handlers/CurrencyLookup.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, jest } from '@jest/globals' 2 | import { ExtendedEther } from '@uniswap/smart-order-router' 3 | import { ChainId, Token } from '@uniswap/sdk-core' 4 | import { CurrencyLookup } from '../../../../lib/handlers/CurrencyLookup' 5 | 6 | const address = '0x0000000000000000000000000000000000000001' 7 | const token = new Token(ChainId.MAINNET, address, 18, 'FOO', 'Foo') 8 | 9 | describe('CurrencyLookup', () => { 10 | it('Returns the native token if tokenRaw is a native token string', async () => { 11 | const tokenLookup = new CurrencyLookup( 12 | {} as any, // tokenListProvider 13 | {} as any, // tokenProvider, 14 | { 15 | debug: jest.fn(), 16 | } as any // log 17 | ) 18 | const result = await tokenLookup.searchForToken('ETH', ChainId.MAINNET) 19 | 20 | expect(result).toBeDefined() 21 | expect(result).toEqual(ExtendedEther.onChain(ChainId.MAINNET)) 22 | }) 23 | 24 | it('Finds the token in the token list when the input is an address', async () => { 25 | const tokenLookup = new CurrencyLookup( 26 | { 27 | getTokenByAddress: (inputAddress: string) => (inputAddress === address ? token : undefined), 28 | } as any, // tokenListProvider 29 | {} as any, // tokenProvider, 30 | { 31 | debug: jest.fn(), 32 | } as any // log 33 | ) 34 | const result = await tokenLookup.searchForToken(address, ChainId.MAINNET) 35 | 36 | expect(result).toBeDefined() 37 | expect(result).toEqual(token) 38 | }) 39 | 40 | it('Finds the token in the token list when the input is a symbol', async () => { 41 | const tokenLookup = new CurrencyLookup( 42 | { 43 | getTokenBySymbol: (inputSymbol: string) => (inputSymbol === 'FOO' ? token : undefined), 44 | } as any, // tokenListProvider 45 | {} as any, // tokenProvider, 46 | { 47 | debug: jest.fn(), 48 | } as any // log 49 | ) 50 | const result = await tokenLookup.searchForToken('FOO', ChainId.MAINNET) 51 | 52 | expect(result).toBeDefined() 53 | expect(result).toEqual(token) 54 | }) 55 | 56 | it('Returns the token if the on-chain lookup by address is successful', async () => { 57 | const tokenLookup = new CurrencyLookup( 58 | { 59 | // Both token list lookups return null 60 | getTokenBySymbol: () => undefined, 61 | getTokenByAddress: () => undefined, 62 | } as any, // tokenListProvider 63 | { 64 | getTokens: () => { 65 | return { 66 | getTokenByAddress: (inputAddress: string) => (inputAddress === address ? token : undefined), 67 | } 68 | }, 69 | } as any, // tokenProvider, 70 | { 71 | debug: jest.fn(), 72 | } as any // log 73 | ) 74 | const result = await tokenLookup.searchForToken(address, ChainId.MAINNET) 75 | 76 | // Because the input address was a symbol and not an address, expect the result to be undefined. 77 | expect(result).toBe(token) 78 | }) 79 | 80 | it('Returns undefined if the on-chain lookup is by symbol', async () => { 81 | const tokenLookup = new CurrencyLookup( 82 | { 83 | // Both token list lookups return null 84 | getTokenBySymbol: () => undefined, 85 | getTokenByAddress: () => undefined, 86 | } as any, // tokenListProvider 87 | { 88 | getTokens: () => { 89 | return { 90 | getTokenByAddress: (inputAddress: string) => (inputAddress === address ? token : undefined), 91 | } 92 | }, 93 | } as any, // tokenProvider, 94 | { 95 | debug: jest.fn(), 96 | } as any // log 97 | ) 98 | const result = await tokenLookup.searchForToken('FOO', ChainId.MAINNET) 99 | 100 | // Because the input address was a symbol and not an address, expect the result to be undefined. 101 | expect(result).toBeUndefined() 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /test/jest/unit/handlers/quote.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from '@jest/globals' 2 | import { QuoteHandler } from '../../../../lib/handlers/quote/quote' 3 | import { ChainId } from '@uniswap/sdk-core' 4 | import { Protocol } from '@uniswap/router-sdk' 5 | import { UniversalRouterVersion } from '@uniswap/universal-router-sdk' 6 | 7 | describe('QuoteHandler', () => { 8 | describe('.protocolsFromRequest', () => { 9 | it('returns V3 when no protocols are requested', () => { 10 | expect( 11 | QuoteHandler.protocolsFromRequest(ChainId.MAINNET, UniversalRouterVersion.V1_2, undefined, undefined) 12 | ).toEqual([Protocol.V3]) 13 | }) 14 | 15 | it('returns V3 when forceCrossProtocol is false', () => { 16 | expect(QuoteHandler.protocolsFromRequest(ChainId.MAINNET, UniversalRouterVersion.V1_2, undefined, false)).toEqual( 17 | [Protocol.V3] 18 | ) 19 | }) 20 | 21 | it('returns empty when forceCrossProtocol is true', () => { 22 | expect(QuoteHandler.protocolsFromRequest(ChainId.MAINNET, UniversalRouterVersion.V1_2, undefined, true)).toEqual( 23 | [] 24 | ) 25 | }) 26 | 27 | it('returns requested protocols', () => { 28 | expect( 29 | QuoteHandler.protocolsFromRequest( 30 | ChainId.MAINNET, 31 | UniversalRouterVersion.V1_2, 32 | ['v2', 'v3', 'mixed'], 33 | undefined 34 | ) 35 | ).toEqual([Protocol.V2, Protocol.V3, Protocol.MIXED]) 36 | }) 37 | 38 | it('returns a different set of requested protocols', () => { 39 | expect( 40 | QuoteHandler.protocolsFromRequest(ChainId.MAINNET, UniversalRouterVersion.V1_2, ['v3', 'mixed'], undefined) 41 | ).toEqual([Protocol.V3, Protocol.MIXED]) 42 | }) 43 | 44 | it('works with other chains', () => { 45 | expect( 46 | QuoteHandler.protocolsFromRequest(ChainId.BASE, UniversalRouterVersion.V1_2, ['v2', 'v3', 'mixed'], undefined) 47 | ).toEqual([Protocol.V2, Protocol.V3, Protocol.MIXED]) 48 | }) 49 | 50 | it('returns undefined when a requested protocol is invalid', () => { 51 | expect( 52 | QuoteHandler.protocolsFromRequest( 53 | ChainId.BASE, 54 | UniversalRouterVersion.V1_2, 55 | ['v2', 'v3', 'mixed', 'miguel'], 56 | undefined 57 | ) 58 | ).toBeUndefined() 59 | }) 60 | 61 | it('returns v2, v3, mixed when universal router version is v1.2', () => { 62 | expect( 63 | QuoteHandler.protocolsFromRequest( 64 | ChainId.MAINNET, 65 | UniversalRouterVersion.V1_2, 66 | ['v2', 'v3', 'v4', 'mixed'], 67 | undefined 68 | ) 69 | ).toEqual([Protocol.V2, Protocol.V3, Protocol.MIXED]) 70 | }) 71 | 72 | it('returns v2, v3, v4, mixed when universal router version is v2.0', () => { 73 | expect( 74 | QuoteHandler.protocolsFromRequest( 75 | ChainId.MAINNET, 76 | UniversalRouterVersion.V2_0, 77 | ['v2', 'v3', 'v4', 'mixed'], 78 | undefined 79 | ) 80 | ).toEqual([Protocol.V2, Protocol.V3, Protocol.V4, Protocol.MIXED]) 81 | }) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /test/jest/unit/handlers/router-entities/route-caching/model/cached-routes-strategy.test.ts: -------------------------------------------------------------------------------- 1 | import { CacheMode } from '@uniswap/smart-order-router' 2 | import { ChainId, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' 3 | import { 4 | CachedRoutesBucket, 5 | CachedRoutesStrategy, 6 | } from '../../../../../../../lib/handlers/router-entities/route-caching' 7 | import { describe, it, expect } from '@jest/globals' 8 | 9 | describe('CachedRoutesStrategy', () => { 10 | const WETH = new Token(ChainId.MAINNET, '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 18, 'WETH') 11 | let strategy: CachedRoutesStrategy 12 | 13 | beforeEach(() => { 14 | strategy = new CachedRoutesStrategy({ 15 | pair: 'WETH/USD', 16 | tradeType: TradeType.EXACT_INPUT, 17 | chainId: ChainId.MAINNET, 18 | buckets: [ 19 | new CachedRoutesBucket({ bucket: 1, blocksToLive: 2, cacheMode: CacheMode.Tapcompare }), 20 | new CachedRoutesBucket({ bucket: 5, blocksToLive: 2, cacheMode: CacheMode.Tapcompare }), 21 | new CachedRoutesBucket({ bucket: 10, blocksToLive: 1, cacheMode: CacheMode.Tapcompare }), 22 | new CachedRoutesBucket({ bucket: 50, blocksToLive: 1, cacheMode: CacheMode.Tapcompare }), 23 | new CachedRoutesBucket({ bucket: 100, blocksToLive: 1, cacheMode: CacheMode.Tapcompare }), 24 | new CachedRoutesBucket({ bucket: 500, blocksToLive: 1, cacheMode: CacheMode.Tapcompare }), 25 | ], 26 | }) 27 | }) 28 | 29 | describe('#getCachingBucket', () => { 30 | it('find the first bucket that fits the amount', () => { 31 | const currencyAmount = CurrencyAmount.fromRawAmount(WETH, 1 * 10 ** WETH.decimals) 32 | const cachingParameters = strategy.getCachingBucket(currencyAmount) 33 | 34 | expect(cachingParameters).toBeDefined() 35 | expect(cachingParameters?.bucket).toBe(1) 36 | }) 37 | 38 | it('find the bucket, searching in the middle amounts', () => { 39 | const currencyAmount = CurrencyAmount.fromRawAmount(WETH, 42 * 10 ** WETH.decimals) 40 | const cachingParameters = strategy.getCachingBucket(currencyAmount) 41 | 42 | expect(cachingParameters).toBeDefined() 43 | expect(cachingParameters?.bucket).toBe(50) 44 | }) 45 | 46 | it('looks for bucket in higher amounts', () => { 47 | const currencyAmount = CurrencyAmount.fromRawAmount(WETH, 500 * 10 ** WETH.decimals) 48 | const cachingParameters = strategy.getCachingBucket(currencyAmount) 49 | 50 | expect(cachingParameters).toBeDefined() 51 | expect(cachingParameters?.bucket).toBe(500) 52 | }) 53 | 54 | it('returns undefined once we are out of range', () => { 55 | const currencyAmount = CurrencyAmount.fromRawAmount(WETH, 501 * 10 ** WETH.decimals) 56 | const cachingParameters = strategy.getCachingBucket(currencyAmount) 57 | 58 | expect(cachingParameters).toBeUndefined() 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /test/jest/unit/handlers/router-entities/route-caching/model/pair-trade-type-chain-id.test.ts: -------------------------------------------------------------------------------- 1 | import { PairTradeTypeChainId } from '../../../../../../../lib/handlers/router-entities/route-caching' 2 | import { ChainId, TradeType } from '@uniswap/sdk-core' 3 | import { describe, it, expect } from '@jest/globals' 4 | 5 | describe('PairTradeTypeChainId', () => { 6 | const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' 7 | const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' 8 | 9 | describe('toString', () => { 10 | it('returns a stringified version of the object', () => { 11 | const pairTradeTypeChainId = new PairTradeTypeChainId({ 12 | currencyIn: WETH, 13 | currencyOut: USDC, 14 | tradeType: TradeType.EXACT_INPUT, 15 | chainId: ChainId.MAINNET, 16 | }) 17 | 18 | expect(pairTradeTypeChainId.toString()).toBe( 19 | `${WETH.toLowerCase()}/${USDC.toLowerCase()}/${TradeType.EXACT_INPUT}/${ChainId.MAINNET}` 20 | ) 21 | }) 22 | 23 | it('token addresses are converted to lowercase', () => { 24 | const pairTradeTypeChainId = new PairTradeTypeChainId({ 25 | currencyIn: WETH.toUpperCase(), 26 | currencyOut: USDC.toUpperCase(), 27 | tradeType: TradeType.EXACT_INPUT, 28 | chainId: ChainId.MAINNET, 29 | }) 30 | 31 | expect(pairTradeTypeChainId.toString()).toBe( 32 | `${WETH.toLowerCase()}/${USDC.toLowerCase()}/${TradeType.EXACT_INPUT}/${ChainId.MAINNET}` 33 | ) 34 | }) 35 | 36 | it('works with ExactOutput too', () => { 37 | const pairTradeTypeChainId = new PairTradeTypeChainId({ 38 | currencyIn: WETH.toUpperCase(), 39 | currencyOut: USDC.toUpperCase(), 40 | tradeType: TradeType.EXACT_OUTPUT, 41 | chainId: ChainId.MAINNET, 42 | }) 43 | 44 | expect(pairTradeTypeChainId.toString()).toBe( 45 | `${WETH.toLowerCase()}/${USDC.toLowerCase()}/${TradeType.EXACT_OUTPUT}/${ChainId.MAINNET}` 46 | ) 47 | }) 48 | 49 | it('works with other chains', () => { 50 | const pairTradeTypeChainId = new PairTradeTypeChainId({ 51 | currencyIn: WETH.toUpperCase(), 52 | currencyOut: USDC.toUpperCase(), 53 | tradeType: TradeType.EXACT_OUTPUT, 54 | chainId: ChainId.ARBITRUM_ONE, 55 | }) 56 | 57 | expect(pairTradeTypeChainId.toString()).toBe( 58 | `${WETH.toLowerCase()}/${USDC.toLowerCase()}/${TradeType.EXACT_OUTPUT}/${ChainId.ARBITRUM_ONE}` 59 | ) 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /test/jest/unit/handlers/router-entities/route-caching/model/protocols-bucket-block-number.test.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolsBucketBlockNumber } from '../../../../../../../lib/handlers/router-entities/route-caching/model/protocols-bucket-block-number' 2 | import { Protocol } from '@uniswap/router-sdk' 3 | import { describe, it, expect } from '@jest/globals' 4 | 5 | describe('ProtocolsBucketBlockNumber', () => { 6 | describe('#fullKey', () => { 7 | it('returns a string-ified version of the object', () => { 8 | const protocolsBucketBlockNumber = new ProtocolsBucketBlockNumber({ 9 | protocols: [Protocol.MIXED, Protocol.V2, Protocol.V3], 10 | bucket: 5, 11 | blockNumber: 12345, 12 | }) 13 | 14 | expect(protocolsBucketBlockNumber.fullKey()).toBe('MIXED,V2,V3/5/12345') 15 | }) 16 | 17 | it('protocols are sorted, even if the original array is not', () => { 18 | const protocolsBucketBlockNumber = new ProtocolsBucketBlockNumber({ 19 | protocols: [Protocol.V3, Protocol.MIXED, Protocol.V2], 20 | bucket: 5, 21 | blockNumber: 12345, 22 | }) 23 | 24 | expect(protocolsBucketBlockNumber.fullKey()).toBe('MIXED,V2,V3/5/12345') 25 | }) 26 | 27 | it('throws an error when the bucketNumber is undefined', () => { 28 | const protocolsBucketBlockNumber = new ProtocolsBucketBlockNumber({ 29 | protocols: [Protocol.V3, Protocol.MIXED, Protocol.V2], 30 | bucket: 5, 31 | }) 32 | 33 | expect(() => protocolsBucketBlockNumber.fullKey()).toThrow('BlockNumber is necessary to create a fullKey') 34 | }) 35 | }) 36 | 37 | describe('#protocolsBucketPartialKey', () => { 38 | it('returns a string-ified version of the object without the blockNumber', () => { 39 | const protocolsBucketBlockNumber = new ProtocolsBucketBlockNumber({ 40 | protocols: [Protocol.MIXED, Protocol.V2, Protocol.V3], 41 | bucket: 5, 42 | blockNumber: 12345, 43 | }) 44 | 45 | expect(protocolsBucketBlockNumber.protocolsBucketPartialKey()).toBe('MIXED,V2,V3/5/') 46 | }) 47 | 48 | it('protocols are sorted, even if the original array is not, without the blockNumber', () => { 49 | const protocolsBucketBlockNumber = new ProtocolsBucketBlockNumber({ 50 | protocols: [Protocol.V3, Protocol.MIXED, Protocol.V2], 51 | bucket: 5, 52 | blockNumber: 12345, 53 | }) 54 | 55 | expect(protocolsBucketBlockNumber.protocolsBucketPartialKey()).toBe('MIXED,V2,V3/5/') 56 | }) 57 | 58 | it('returns the partial key even if blockNumber is undefined', () => { 59 | const protocolsBucketBlockNumber = new ProtocolsBucketBlockNumber({ 60 | protocols: [Protocol.V3, Protocol.MIXED, Protocol.V2], 61 | bucket: 5, 62 | }) 63 | 64 | expect(protocolsBucketBlockNumber.protocolsBucketPartialKey()).toBe('MIXED,V2,V3/5/') 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /test/jest/unit/handlers/router-entities/route-caching/model/token-marshaller.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals' 2 | import { TokenMarshaller } from '../../../../../../../lib/handlers/router-entities/route-caching' 3 | import { ChainId } from '@uniswap/sdk-core' 4 | import { nativeOnChain, WETH9 } from '@uniswap/smart-order-router' 5 | 6 | describe('TokenMarshaller', () => { 7 | it('returns native currency', () => { 8 | const marshalledCurrency = TokenMarshaller.marshal(nativeOnChain(ChainId.MAINNET)) 9 | const currency = TokenMarshaller.unmarshal(marshalledCurrency) 10 | expect(currency).toEqual(nativeOnChain(ChainId.MAINNET)) 11 | }) 12 | 13 | it('returns token currency', () => { 14 | const marshalledCurrency = TokenMarshaller.marshal(WETH9[ChainId.MAINNET]) 15 | const currency = TokenMarshaller.unmarshal(marshalledCurrency) 16 | expect(currency).toEqual(WETH9[ChainId.MAINNET]) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/jest/unit/handlers/shared.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect } from '@jest/globals' 2 | import { 3 | computePortionAmount, 4 | parseDeadline, 5 | parseFeeOptions, 6 | parseFlatFeeOptions, 7 | parsePortionPercent, 8 | populateFeeOptions, 9 | } from '../../../../lib/handlers/shared' 10 | import { getAmount } from '../../../utils/tokens' 11 | import { CurrencyAmount, Percent } from '@uniswap/sdk-core' 12 | import { DAI_MAINNET, SwapOptions, SwapType } from '@uniswap/smart-order-router' 13 | import { UniversalRouterVersion } from '@uniswap/universal-router-sdk' 14 | 15 | describe('shared', () => { 16 | it('parsePortionPercent', () => { 17 | const percent = parsePortionPercent(10000) 18 | expect(percent.quotient.toString()).toEqual('1') 19 | }) 20 | 21 | it('parseFeePortions', () => { 22 | const feeOptions = parseFeeOptions(10000, '0x123') 23 | expect(feeOptions).toBeDefined() 24 | 25 | if (feeOptions) { 26 | expect(feeOptions.fee.quotient.toString()).toEqual('1') 27 | expect(feeOptions.recipient).toEqual('0x123') 28 | } 29 | }) 30 | 31 | it('parseFlatFeePortions', () => { 32 | const flatFeeOptionsent = parseFlatFeeOptions('35', '0x123') 33 | expect(flatFeeOptionsent).toBeDefined() 34 | 35 | if (flatFeeOptionsent) { 36 | expect(flatFeeOptionsent.amount.toString()).toEqual('35') 37 | expect(flatFeeOptionsent.recipient).toEqual('0x123') 38 | } 39 | }) 40 | 41 | it('computePortionAmount', async () => { 42 | const amount = await getAmount(1, 'EXACT_OUTPUT', 'ETH', 'DAI', '1') 43 | const daiAmount = CurrencyAmount.fromRawAmount(DAI_MAINNET, amount) 44 | const portionAmount = computePortionAmount(daiAmount, 15) 45 | 46 | if (portionAmount) { 47 | expect(portionAmount).toEqual(daiAmount.multiply(parsePortionPercent(15)).quotient.toString()) 48 | } 49 | }) 50 | 51 | it('populateFeeOptions exact in', () => { 52 | const allFeeOptions = populateFeeOptions('exactIn', 15, '0x123') 53 | 54 | const swapParams: SwapOptions = { 55 | type: SwapType.UNIVERSAL_ROUTER, 56 | version: UniversalRouterVersion.V1_2, 57 | deadlineOrPreviousBlockhash: parseDeadline('1800'), 58 | recipient: '0x123', 59 | slippageTolerance: new Percent(5), 60 | ...allFeeOptions, 61 | } 62 | 63 | expect(swapParams.fee).toBeDefined() 64 | expect(swapParams.fee!.fee.equalTo(parsePortionPercent(15))).toBe(true) 65 | expect(swapParams.fee!.recipient).toEqual('0x123') 66 | 67 | expect(swapParams.flatFee).toBeUndefined() 68 | }) 69 | 70 | it('populateFeeOptions exact out', () => { 71 | const allFeeOptions = populateFeeOptions('exactOut', undefined, '0x123', '35') 72 | 73 | const swapParams: SwapOptions = { 74 | type: SwapType.UNIVERSAL_ROUTER, 75 | version: UniversalRouterVersion.V1_2, 76 | deadlineOrPreviousBlockhash: parseDeadline('1800'), 77 | recipient: '0x123', 78 | slippageTolerance: new Percent(5), 79 | ...allFeeOptions, 80 | } 81 | 82 | expect(swapParams.flatFee).toBeDefined() 83 | expect(swapParams.flatFee!.amount.toString()).toEqual('35') 84 | expect(swapParams.flatFee!.recipient).toEqual('0x123') 85 | 86 | expect(swapParams.fee).toBeUndefined() 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /test/jest/unit/handlers/util/estimateGasUsed.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from '@jest/globals' 2 | import { ChainId } from '@uniswap/sdk-core' 3 | import { adhocCorrectGasUsed } from '../../../../../lib/util/estimateGasUsed' 4 | import { BigNumber } from '@ethersproject/bignumber' 5 | import { CELO_UPPER_SWAP_GAS_LIMIT, ZKSYNC_UPPER_SWAP_GAS_LIMIT } from '../../../../../lib/util/gasLimit' 6 | 7 | describe('estimateGasUsed', () => { 8 | it('returns normal gas for mainnet', () => { 9 | const estimatedGasUsed = BigNumber.from(100) 10 | expect(adhocCorrectGasUsed(estimatedGasUsed, ChainId.MAINNET)).toBe(estimatedGasUsed) 11 | }) 12 | 13 | it('returns normal gas for zkSync on mobile', () => { 14 | const estimatedGasUsed = ZKSYNC_UPPER_SWAP_GAS_LIMIT.add(1) 15 | expect(adhocCorrectGasUsed(estimatedGasUsed, ChainId.ZKSYNC)).toBe(estimatedGasUsed) 16 | }) 17 | 18 | it('returns normal gas for zkSync on extension', () => { 19 | const estimatedGasUsed = ZKSYNC_UPPER_SWAP_GAS_LIMIT.add(1) 20 | expect(adhocCorrectGasUsed(estimatedGasUsed, ChainId.ZKSYNC)).toBe(estimatedGasUsed) 21 | }) 22 | 23 | it('returns upper limit gas for zkSync on mobile', () => { 24 | const estimatedGasUsed = ZKSYNC_UPPER_SWAP_GAS_LIMIT.sub(1) 25 | expect(adhocCorrectGasUsed(estimatedGasUsed, ChainId.ZKSYNC)).toBe(ZKSYNC_UPPER_SWAP_GAS_LIMIT) 26 | }) 27 | 28 | it('returns upper limit gas for zkSync on extension', () => { 29 | const estimatedGasUsed = ZKSYNC_UPPER_SWAP_GAS_LIMIT.sub(1) 30 | expect(adhocCorrectGasUsed(estimatedGasUsed, ChainId.ZKSYNC)).toBe(ZKSYNC_UPPER_SWAP_GAS_LIMIT) 31 | }) 32 | 33 | it('returns upper limit gas for celo on mobile', () => { 34 | const estimatedGasUsed = CELO_UPPER_SWAP_GAS_LIMIT.sub(1) 35 | expect(adhocCorrectGasUsed(estimatedGasUsed, ChainId.CELO)).toBe(CELO_UPPER_SWAP_GAS_LIMIT) 36 | }) 37 | 38 | it('returns upper limit gas for celo on extension', () => { 39 | const estimatedGasUsed = CELO_UPPER_SWAP_GAS_LIMIT.sub(1) 40 | expect(adhocCorrectGasUsed(estimatedGasUsed, ChainId.CELO)).toBe(CELO_UPPER_SWAP_GAS_LIMIT) 41 | }) 42 | 43 | it('returns normal gas for celo on mobile', () => { 44 | const estimatedGasUsed = CELO_UPPER_SWAP_GAS_LIMIT.add(1) 45 | expect(adhocCorrectGasUsed(estimatedGasUsed, ChainId.CELO)).toBe(estimatedGasUsed) 46 | }) 47 | 48 | it('returns normal gas for celo on extension', () => { 49 | const estimatedGasUsed = CELO_UPPER_SWAP_GAS_LIMIT.add(1) 50 | expect(adhocCorrectGasUsed(estimatedGasUsed, ChainId.CELO)).toBe(estimatedGasUsed) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/jest/unit/handlers/util/isAddress.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from '@jest/globals' 2 | import { isAddress } from '../../../../../lib/util/isAddress' 3 | 4 | describe('isAddress', () => { 5 | it('returns true for a valid address', () => { 6 | const uniTokenAddress = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984' 7 | expect(isAddress(uniTokenAddress)).toBe(true) 8 | }) 9 | 10 | it('returns true for the zero address', () => { 11 | const zeroAddress = '0x0000000000000000000000000000000000000000' 12 | expect(isAddress(zeroAddress)).toBe(true) 13 | }) 14 | 15 | it('returns false if there are less than 42 characters', () => { 16 | const invalidAddress = '0xabc' 17 | expect(isAddress(invalidAddress)).toBe(false) 18 | }) 19 | 20 | it('returns false if the address is not prefixed with 0x', () => { 21 | const uniTokenAddressNo0x = '__1f9840a85d5aF5bf1D1762F925BDADdC4201F984' 22 | expect(isAddress(uniTokenAddressNo0x)).toBe(false) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/jest/unit/handlers/util/simulation.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals' 2 | import { log, SimulationStatus } from '@uniswap/smart-order-router' 3 | import { 4 | RoutingApiSimulationStatus, 5 | simulationStatusTranslation, 6 | } from '../../../../../lib/handlers/quote/util/simulation' 7 | 8 | describe('simulation', () => { 9 | it('returns unattempted for undefined simulation status', () => { 10 | const status = simulationStatusTranslation(undefined, log) 11 | expect(status).toStrictEqual(RoutingApiSimulationStatus.UNATTEMPTED) 12 | }) 13 | 14 | it('returns success for succeeded simulation status', () => { 15 | const status = simulationStatusTranslation(SimulationStatus.Succeeded, log) 16 | expect(status).toStrictEqual(RoutingApiSimulationStatus.SUCCESS) 17 | }) 18 | 19 | it('returns failed for failed simulation status', () => { 20 | const status = simulationStatusTranslation(SimulationStatus.Failed, log) 21 | expect(status).toStrictEqual(RoutingApiSimulationStatus.FAILED) 22 | }) 23 | 24 | it('returns insufficient balance for insufficient balance simulation status', () => { 25 | const status = simulationStatusTranslation(SimulationStatus.InsufficientBalance, log) 26 | expect(status).toStrictEqual(RoutingApiSimulationStatus.INSUFFICIENT_BALANCE) 27 | }) 28 | 29 | it('returns not supported for not supported simulation status', () => { 30 | const status = simulationStatusTranslation(SimulationStatus.NotSupported, log) 31 | expect(status).toStrictEqual(RoutingApiSimulationStatus.NOT_SUPPORTED) 32 | }) 33 | 34 | it('returns not approved for not approved simulation status', () => { 35 | const status = simulationStatusTranslation(SimulationStatus.NotApproved, log) 36 | expect(status).toStrictEqual(RoutingApiSimulationStatus.NOT_APPROVED) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/mocha/dbSetup.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDB } from 'aws-sdk' 2 | import { dbConnectionSetup } from './dynamoDBLocalFixture' 3 | 4 | const createTable = async (table: DynamoDB.Types.CreateTableInput) => { 5 | const ddb = getDdbOrDie() 6 | 7 | await ddb.createTable(table).promise() 8 | } 9 | 10 | const getDdbOrDie = (): DynamoDB => { 11 | const ddb = (global as any)['__DYNAMODB_CLIENT__'] as DynamoDB 12 | 13 | if (ddb === undefined) { 14 | throw new Error() 15 | } 16 | 17 | return ddb 18 | } 19 | 20 | export const deleteAllTables = async () => { 21 | const ddb = getDdbOrDie() 22 | const { TableNames: tableNames } = await ddb.listTables().promise() 23 | 24 | if (tableNames === undefined) { 25 | return 26 | } 27 | 28 | await Promise.all(tableNames.map((t) => ddb.deleteTable({ TableName: t }).promise())) 29 | } 30 | 31 | export const setupTables = (...tables: DynamoDB.Types.CreateTableInput[]) => { 32 | dbConnectionSetup() 33 | beforeEach(async () => { 34 | await Promise.all(tables.map(createTable)) 35 | }) 36 | 37 | afterEach(async () => { 38 | await deleteAllTables() 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /test/mocha/dynamoDBLocalFixture.ts: -------------------------------------------------------------------------------- 1 | import AWS, { DynamoDB } from 'aws-sdk' 2 | import { ChildProcess } from 'child_process' 3 | import DDBLocal from 'dynamodb-local' 4 | import { deleteAllTables } from './dbSetup' 5 | 6 | process.env.AWS_ACCESS_KEY_ID = 'myaccesskey' 7 | process.env.AWS_SECRET_ACCESS_KEY = 'mysecretkey' 8 | 9 | const dbPort = Number(process.env.DYNAMODB_LOCAL_PORT || 8000) 10 | 11 | let dbInstance: ChildProcess | undefined 12 | ;(global as any)['__DYNAMODB_LOCAL__'] = true 13 | 14 | export const mochaGlobalSetup = async () => { 15 | try { 16 | console.log('Starting DynamoDB') 17 | dbInstance = await DDBLocal.launch(dbPort, null) 18 | console.log('Started DynamoDB') 19 | 20 | const ddb = new DynamoDB({ 21 | endpoint: `localhost:${dbPort}`, 22 | sslEnabled: false, 23 | region: 'local', 24 | }) 25 | 26 | dbConnectionSetup() 27 | 28 | exportDDBInstance(ddb) 29 | 30 | await deleteAllTables() 31 | } catch (e) { 32 | console.log('Error instantiating DynamoDB', e) 33 | } 34 | } 35 | 36 | // Overrides the default config to use the local instance of DynamoDB in tests 37 | export const dbConnectionSetup = () => { 38 | const config: any = AWS.config 39 | 40 | const dynamoLocalPort = Number(process.env.DYNAMODB_LOCAL_PORT || 8000) 41 | config.endpoint = `localhost:${dynamoLocalPort}` 42 | config.sslEnabled = false 43 | config.region = 'local' 44 | } 45 | 46 | const exportDDBInstance = (ddb: DynamoDB) => { 47 | ;(global as any)['__DYNAMODB_CLIENT__'] = ddb 48 | } 49 | 50 | export const mochaGlobalTeardown = async () => { 51 | console.log('Stopping DynamoDB') 52 | if (dbInstance !== undefined) { 53 | await DDBLocal.stopChild(dbInstance) 54 | } 55 | console.log('Stopped DynamoDB') 56 | } 57 | -------------------------------------------------------------------------------- /test/mocha/integ/graphql/graphql-provider.test.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@uniswap/sdk-core' 2 | import { expect } from 'chai' 3 | import { UniGraphQLProvider } from '../../../../lib/graphql/graphql-provider' 4 | import dotenv from 'dotenv' 5 | 6 | dotenv.config() 7 | 8 | describe('integration test for UniGraphQLProvider', () => { 9 | let provider: UniGraphQLProvider 10 | 11 | beforeEach(() => { 12 | provider = new UniGraphQLProvider() 13 | }) 14 | 15 | it('should fetch Ethereum token info', async () => { 16 | const address = '0xBbE460dC4ac73f7C13A2A2feEcF9aCF6D5083F9b' 17 | const chainId = ChainId.MAINNET 18 | const tokenInfoResponse = await provider.getTokenInfo(chainId, address) 19 | 20 | expect(tokenInfoResponse?.token).to.not.be.undefined 21 | expect(tokenInfoResponse.token.address).equals(address) 22 | expect(tokenInfoResponse.token.name).equals('Wick Finance') 23 | expect(tokenInfoResponse.token.symbol).equals('WICK') 24 | expect(tokenInfoResponse.token.feeData?.buyFeeBps).to.not.be.undefined 25 | }) 26 | 27 | it('should fetch Ethereum low traffic token info', async () => { 28 | const address = '0x4a500ed6add5994569e66426588168705fcc9767' 29 | const chainId = ChainId.MAINNET 30 | const tokenInfoResponse = await provider.getTokenInfo(chainId, address) 31 | 32 | expect(tokenInfoResponse?.token).to.not.be.undefined 33 | expect(tokenInfoResponse.token.address).equals(address) 34 | expect(tokenInfoResponse.token.symbol).equals('BITBOY') 35 | expect(tokenInfoResponse.token.feeData?.buyFeeBps).to.not.be.undefined 36 | expect(tokenInfoResponse.token.feeData?.sellFeeBps).to.not.be.undefined 37 | }) 38 | 39 | it('should fetch multiple Ethereum token info', async () => { 40 | const chainId = ChainId.MAINNET 41 | const addresses = ['0xBbE460dC4ac73f7C13A2A2feEcF9aCF6D5083F9b', '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'] 42 | const tokensInfoResponse = await provider.getTokensInfo(chainId, addresses) 43 | 44 | expect(tokensInfoResponse?.tokens).to.not.be.undefined 45 | expect(tokensInfoResponse.tokens.length == 2) 46 | const token1 = tokensInfoResponse.tokens.find((tokenInfo) => tokenInfo.address === addresses[0]) 47 | const token2 = tokensInfoResponse.tokens.find((tokenInfo) => tokenInfo.address === addresses[1]) 48 | expect(token1).to.not.be.undefined 49 | expect(token2).to.not.be.undefined 50 | 51 | expect(token1?.symbol).equals('WICK') 52 | expect(token1?.feeData?.buyFeeBps).to.not.be.undefined 53 | expect(token1?.feeData?.sellFeeBps).to.not.be.undefined 54 | expect(token2?.symbol).equals('UNI') 55 | expect(token2?.feeData?.buyFeeBps).to.be.null 56 | expect(token2?.feeData?.sellFeeBps).to.be.null 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/mocha/integ/handlers/pools/pool-caching/v3/dynamo-caching-pool-provider.test.ts: -------------------------------------------------------------------------------- 1 | import { setupTables } from '../../../../../dbSetup' 2 | import { DynamoDBCachingV3PoolProvider } from '../../../../../../../lib/handlers/pools/pool-caching/v3/dynamo-caching-pool-provider' 3 | import { getMockedV3PoolProvider, TEST_ROUTE_TABLE } from '../../../../../../test-utils/mocked-dependencies' 4 | import { SUPPORTED_POOLS } from '../../../../../../test-utils/mocked-data' 5 | import { ChainId, Token } from '@uniswap/sdk-core' 6 | import { FeeAmount, Pool } from '@uniswap/v3-sdk' 7 | import { ProviderConfig } from '@uniswap/smart-order-router/build/main/providers/provider' 8 | import { expect } from 'chai' 9 | import { DynamoCachingV3Pool } from '../../../../../../../lib/handlers/pools/pool-caching/v3/cache-dynamo-pool' 10 | import { log } from '@uniswap/smart-order-router' 11 | 12 | describe('DynamoDBCachingV3PoolProvider', async () => { 13 | setupTables(TEST_ROUTE_TABLE) 14 | 15 | it('caches pools properly with a given block number', async () => { 16 | const dynamoPoolCache = new DynamoDBCachingV3PoolProvider( 17 | ChainId.GOERLI, 18 | getMockedV3PoolProvider(), 19 | TEST_ROUTE_TABLE.TableName 20 | ) 21 | const dynamoCache = new DynamoCachingV3Pool({ tableName: TEST_ROUTE_TABLE.TableName }) 22 | 23 | const providerConfig: ProviderConfig = { blockNumber: 111 } 24 | const blockNumber = await providerConfig.blockNumber 25 | 26 | // First ensure the dynamo cache doesn't have the pools yet 27 | for (const pool of SUPPORTED_POOLS) { 28 | const poolAddress = getMockedV3PoolProvider().getPoolAddress(pool.token0, pool.token1, pool.fee).poolAddress 29 | const hasCachedPool = await dynamoCache.get(`pool-${ChainId.GOERLI}-${poolAddress}`, blockNumber) 30 | expect(hasCachedPool).to.not.exist 31 | } 32 | 33 | const tokenPairs: [Token, Token, FeeAmount][] = SUPPORTED_POOLS.map((pool: Pool) => { 34 | return [pool.token0, pool.token1, pool.fee] 35 | }) 36 | await dynamoPoolCache.getPools(tokenPairs, providerConfig) 37 | 38 | // Then ensure the dynamo cache has the pools yet 39 | for (const pool of SUPPORTED_POOLS) { 40 | const poolAddress = getMockedV3PoolProvider().getPoolAddress(pool.token0, pool.token1, pool.fee).poolAddress 41 | const hasCachedPool = await dynamoCache.get(`pool-${ChainId.GOERLI}-${poolAddress}`, blockNumber) 42 | expect(hasCachedPool).to.exist 43 | 44 | expect(hasCachedPool?.token0.chainId).equals(pool.token0.chainId) 45 | expect(hasCachedPool?.token0.decimals).equals(pool.token0.decimals) 46 | expect(hasCachedPool?.token0.address).equals(pool.token0.address) 47 | 48 | expect(hasCachedPool?.token1.chainId).equals(pool.token1.chainId) 49 | expect(hasCachedPool?.token1.decimals).equals(pool.token1.decimals) 50 | expect(hasCachedPool?.token1.address).equals(pool.token1.address) 51 | 52 | expect(hasCachedPool?.fee).equals(pool.fee) 53 | 54 | expect(hasCachedPool?.sqrtRatioX96.toString()).equals(pool.sqrtRatioX96.toString()) 55 | 56 | expect(hasCachedPool?.liquidity.toString()).equals(pool.liquidity.toString()) 57 | 58 | expect(hasCachedPool?.tickCurrent).equals(pool.tickCurrent) 59 | } 60 | }) 61 | 62 | it('caches do not cache when no block number', async () => { 63 | const dynamoPoolCache = new DynamoDBCachingV3PoolProvider( 64 | ChainId.GOERLI, 65 | getMockedV3PoolProvider(), 66 | TEST_ROUTE_TABLE.TableName 67 | ) 68 | const dynamoCache = new DynamoCachingV3Pool({ tableName: TEST_ROUTE_TABLE.TableName }) 69 | 70 | const providerConfig: ProviderConfig = { blockNumber: undefined } 71 | const blockNumber = await providerConfig.blockNumber 72 | 73 | // First ensure the dynamo cache doesn't have the pools yet 74 | for (const pool of SUPPORTED_POOLS) { 75 | const poolAddress = getMockedV3PoolProvider().getPoolAddress(pool.token0, pool.token1, pool.fee).poolAddress 76 | log.info(`check if pool pool-${ChainId.GOERLI}-${poolAddress} block ${blockNumber} contains the cache`) 77 | const hasCachedPool = await dynamoCache.get(`pool-${ChainId.GOERLI}-${poolAddress}`, blockNumber) 78 | expect(hasCachedPool).to.not.exist 79 | } 80 | 81 | const tokenPairs: [Token, Token, FeeAmount][] = SUPPORTED_POOLS.map((pool: Pool) => { 82 | return [pool.token0, pool.token1, pool.fee] 83 | }) 84 | await dynamoPoolCache.getPools(tokenPairs, providerConfig) 85 | 86 | // Then ensure the dynamo cache won't have the pools 87 | for (const pool of SUPPORTED_POOLS) { 88 | const poolAddress = getMockedV3PoolProvider().getPoolAddress(pool.token0, pool.token1, pool.fee).poolAddress 89 | log.info(`check if pool pool-${ChainId.GOERLI}-${poolAddress} block ${blockNumber} contains the cache`) 90 | const hasCachedPool = await dynamoCache.get(`pool-${ChainId.GOERLI}-${poolAddress}`, blockNumber) 91 | expect(hasCachedPool).to.not.equals 92 | } 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /test/mocha/integ/rpc/ProviderHealthStateDynamoDbRepository.test.ts: -------------------------------------------------------------------------------- 1 | import { setupTables } from '../../dbSetup' 2 | import { DynamoDBTableProps } from '../../../../bin/stacks/routing-database-stack' 3 | import { default as bunyan } from 'bunyan' 4 | import chai, { expect } from 'chai' 5 | import { ProviderHealthStateRepository } from '../../../../lib/rpc/ProviderHealthStateRepository' 6 | import { ProviderHealthStateDynamoDbRepository } from '../../../../lib/rpc/ProviderHealthStateDynamoDbRepository' 7 | import { ProviderHealthiness, ProviderHealthState } from '../../../../lib/rpc/ProviderHealthState' 8 | import chaiAsPromised from 'chai-as-promised' 9 | 10 | chai.use(chaiAsPromised) 11 | 12 | const DB_TABLE = { 13 | TableName: DynamoDBTableProps.RpcProviderHealthStateDbTable.Name, 14 | KeySchema: [ 15 | { 16 | AttributeName: 'chainIdProviderName', 17 | KeyType: 'HASH', 18 | }, 19 | ], 20 | AttributeDefinitions: [ 21 | { 22 | AttributeName: 'chainIdProviderName', 23 | AttributeType: 'S', 24 | }, 25 | ], 26 | ProvisionedThroughput: { 27 | ReadCapacityUnits: 1, 28 | WriteCapacityUnits: 1, 29 | }, 30 | } 31 | 32 | const PROVIDER_ID = 'provider_id' 33 | 34 | const log = bunyan.createLogger({ 35 | name: 'ProviderHealthStateDynamoDbRepositoryTest', 36 | serializers: bunyan.stdSerializers, 37 | level: bunyan.DEBUG, 38 | }) 39 | 40 | describe('ProviderHealthStateDynamoDbRepository', () => { 41 | setupTables(DB_TABLE) 42 | const storage: ProviderHealthStateRepository = new ProviderHealthStateDynamoDbRepository( 43 | DynamoDBTableProps.RpcProviderHealthStateDbTable.Name, 44 | log 45 | ) 46 | 47 | it('write state to DB then read from it, empty case', async () => { 48 | let readState: ProviderHealthState | null = await storage.read(PROVIDER_ID) 49 | expect(readState).to.be.null 50 | }) 51 | 52 | it('write state to DB then read from it, new item', async () => { 53 | await storage.write(PROVIDER_ID, { 54 | healthiness: ProviderHealthiness.HEALTHY, 55 | ongoingAlarms: [], 56 | version: 1, 57 | }) 58 | const readState = await storage.read(PROVIDER_ID) 59 | expect(readState).deep.equals({ 60 | healthiness: ProviderHealthiness.HEALTHY, 61 | ongoingAlarms: [], 62 | version: 1, 63 | }) 64 | }) 65 | 66 | it('write state can overwrite existing DB item', async () => { 67 | await storage.write(PROVIDER_ID, { 68 | healthiness: ProviderHealthiness.HEALTHY, 69 | ongoingAlarms: [], 70 | version: 1, 71 | }) 72 | await storage.write(PROVIDER_ID, { 73 | healthiness: ProviderHealthiness.UNHEALTHY, 74 | ongoingAlarms: ['alarm1'], 75 | version: 1, 76 | }) 77 | const readState = await storage.read(PROVIDER_ID) 78 | expect(readState).deep.equals({ 79 | healthiness: ProviderHealthiness.UNHEALTHY, 80 | ongoingAlarms: ['alarm1'], 81 | version: 1, 82 | }) 83 | }) 84 | 85 | it('Update item, with expected base version', async () => { 86 | await storage.write(PROVIDER_ID, { 87 | healthiness: ProviderHealthiness.HEALTHY, 88 | ongoingAlarms: [], 89 | version: 1, 90 | }) 91 | await storage.update(PROVIDER_ID, { 92 | healthiness: ProviderHealthiness.UNHEALTHY, 93 | ongoingAlarms: ['alarm1', 'alarm2'], 94 | version: 2, 95 | }) 96 | const readState = await storage.read(PROVIDER_ID) 97 | expect(readState).deep.equals({ 98 | healthiness: ProviderHealthiness.UNHEALTHY, 99 | ongoingAlarms: ['alarm1', 'alarm2'], 100 | version: 2, 101 | }) 102 | }) 103 | 104 | it('Update item, with unexpected base version', async () => { 105 | await storage.write(PROVIDER_ID, { 106 | healthiness: ProviderHealthiness.HEALTHY, 107 | ongoingAlarms: [], 108 | version: 2, 109 | }) 110 | // This update should fail, because it expects the version of existing item is 1. 111 | await expect( 112 | storage.update(PROVIDER_ID, { 113 | healthiness: ProviderHealthiness.UNHEALTHY, 114 | ongoingAlarms: ['alarm1'], 115 | version: 2, 116 | }) 117 | ).to.be.rejectedWith(Error) 118 | // DB entry isn't updated 119 | const readState = await storage.read(PROVIDER_ID) 120 | expect(readState).deep.equals({ 121 | healthiness: ProviderHealthiness.HEALTHY, 122 | ongoingAlarms: [], 123 | version: 2, 124 | }) 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /test/mocha/unit/graphql/graphql-provider.test.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@uniswap/sdk-core' 2 | import { IUniGraphQLProvider, UniGraphQLProvider } from '../../../../lib/graphql/graphql-provider' 3 | import sinon from 'sinon' 4 | import { TokensInfoResponse } from '../../../../lib/graphql/graphql-schemas' 5 | import { expect } from 'chai' 6 | 7 | describe('UniGraphQLProvider', () => { 8 | let mockUniGraphQLProvider: sinon.SinonStubbedInstance 9 | 10 | beforeEach(() => { 11 | mockUniGraphQLProvider = sinon.createStubInstance(UniGraphQLProvider) 12 | 13 | mockUniGraphQLProvider.getTokenInfo.callsFake(async (_: ChainId, address: string) => { 14 | return { 15 | token: { 16 | address: address, 17 | decimals: 18, 18 | name: `Wick Finance`, 19 | symbol: 'WICK', 20 | standard: 'ERC20', 21 | chain: 'ETHEREUM', 22 | feeData: { 23 | buyFeeBps: '213', 24 | sellFeeBps: '800', 25 | }, 26 | }, 27 | } 28 | }) 29 | 30 | mockUniGraphQLProvider.getTokensInfo.callsFake(async (_: ChainId, addresses: string[]) => { 31 | const tokensInfoResponse: TokensInfoResponse = { 32 | tokens: [ 33 | { 34 | address: addresses[0], 35 | decimals: 18, 36 | name: 'Wick Finance', 37 | symbol: 'WICK', 38 | standard: 'ERC20', 39 | chain: 'ETHEREUM', 40 | feeData: { 41 | buyFeeBps: '213', 42 | sellFeeBps: '800', 43 | }, 44 | }, 45 | { 46 | address: addresses[1], 47 | decimals: 18, 48 | name: 'Uniswap', 49 | symbol: 'UNI', 50 | standard: 'ERC20', 51 | chain: 'ETHEREUM', 52 | feeData: { 53 | buyFeeBps: '213', 54 | sellFeeBps: '800', 55 | }, 56 | }, 57 | ], 58 | } 59 | return tokensInfoResponse 60 | }) 61 | }) 62 | 63 | it('should fetch Ethereum token info', async () => { 64 | const address = '0xBbE460dC4ac73f7C13A2A2feEcF9aCF6D5083F9b' 65 | const chainId = ChainId.MAINNET 66 | 67 | const tokenInfoResponse = await mockUniGraphQLProvider.getTokenInfo(chainId, address) 68 | 69 | expect(tokenInfoResponse?.token).to.not.be.undefined 70 | expect(tokenInfoResponse.token.address).equals(address) 71 | expect(tokenInfoResponse.token.name).equals('Wick Finance') 72 | expect(tokenInfoResponse.token.symbol).equals('WICK') 73 | }) 74 | 75 | it('should fetch multiple Ethereum token info', async () => { 76 | const chainId = ChainId.MAINNET 77 | const addresses = ['0xBbE460dC4ac73f7C13A2A2feEcF9aCF6D5083F9b', '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'] 78 | 79 | const tokensInfoResponse = await mockUniGraphQLProvider.getTokensInfo(chainId, addresses) 80 | 81 | expect(tokensInfoResponse?.tokens).to.not.be.undefined 82 | expect(tokensInfoResponse.tokens.length == 2) 83 | const token1 = tokensInfoResponse.tokens.find((tokenInfo) => tokenInfo.address === addresses[0]) 84 | const token2 = tokensInfoResponse.tokens.find((tokenInfo) => tokenInfo.address === addresses[1]) 85 | expect(token1).to.not.be.undefined 86 | expect(token2).to.not.be.undefined 87 | 88 | expect(token1?.symbol).equals('WICK') 89 | expect(token1?.feeData?.buyFeeBps).to.not.be.undefined 90 | expect(token2?.symbol).equals('UNI') 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /test/mocha/unit/rpc/ProdConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { getRpcGatewayEnabledChains, ProdConfig, ProdConfigJoi } from '../../../../lib/rpc/ProdConfig' 3 | import TEST_PROD_CONFIG from './rpcProviderTestProdConfig.json' 4 | import PROD_CONFIG from '../../../../lib/config/rpcProviderProdConfig.json' 5 | import { ChainId } from '@uniswap/sdk-core' 6 | 7 | describe('ProdConfig', () => { 8 | it('test generate json string from ProdConfig', () => { 9 | const prodConfig: ProdConfig = [ 10 | { 11 | chainId: 1, 12 | useMultiProviderProb: 0, 13 | }, 14 | { 15 | chainId: 43114, 16 | useMultiProviderProb: 1, 17 | sessionAllowProviderFallbackWhenUnhealthy: true, 18 | providerInitialWeights: [-1, -1], 19 | providerUrls: ['url1', 'url2'], 20 | }, 21 | ] 22 | 23 | const jsonStr = JSON.stringify(prodConfig) 24 | expect(jsonStr).equal( 25 | '[{"chainId":1,"useMultiProviderProb":0},{"chainId":43114,"useMultiProviderProb":1,"sessionAllowProviderFallbackWhenUnhealthy":true,"providerInitialWeights":[-1,-1],"providerUrls":["url1","url2"]}]' 26 | ) 27 | }) 28 | 29 | it('test parse json string into ProdConfig with validation, good case', () => { 30 | const jsonStr = 31 | '[{"chainId":1,"useMultiProviderProb":0},{"chainId":43114,"useMultiProviderProb":1,"sessionAllowProviderFallbackWhenUnhealthy":true,"providerInitialWeights":[-1,-1],"providerUrls":["url1","url2"],"enableDbSync": true}]' 32 | const object = JSON.parse(jsonStr) 33 | const validation = ProdConfigJoi.validate(object) 34 | if (validation.error) { 35 | throw new Error(`Fail to decode or validate json str: ${validation.error}`) 36 | } 37 | const prodConfig: ProdConfig = validation.value as ProdConfig 38 | expect(prodConfig.length).equal(2) 39 | expect(prodConfig[0]).deep.equal({ 40 | chainId: 1, 41 | useMultiProviderProb: 0, 42 | dbSyncSampleProb: 1, 43 | healthCheckSampleProb: 1, 44 | latencyEvaluationSampleProb: 1, 45 | enableDbSync: false, 46 | }) 47 | expect(prodConfig[1]).deep.equal({ 48 | chainId: 43114, 49 | useMultiProviderProb: 1, 50 | sessionAllowProviderFallbackWhenUnhealthy: true, 51 | providerInitialWeights: [-1, -1], 52 | providerUrls: ['url1', 'url2'], 53 | dbSyncSampleProb: 1, 54 | healthCheckSampleProb: 1, 55 | latencyEvaluationSampleProb: 1, 56 | enableDbSync: true, 57 | }) 58 | }) 59 | 60 | it('test parse json string into ProdConfig with validation, bad cases', () => { 61 | let jsonStr = '[{"yummy": "yummy"}]' 62 | let object = JSON.parse(jsonStr) 63 | let validation = ProdConfigJoi.validate(object) 64 | expect(validation.error !== undefined) 65 | 66 | jsonStr = 67 | '[{"chainId":123,"useMultiProvider":false},{"chainId":43114,"useMultiProvider":true,"sessionAllowProviderFallbackWhenUnhealthy":true,"providerInitialWeights":["x","y"],"providerUrls":["url1","url2"]}]' 68 | object = JSON.parse(jsonStr) 69 | validation = ProdConfigJoi.validate(object) 70 | expect(validation.error !== undefined) 71 | 72 | jsonStr = 73 | '[{"chainId":123},{"chainId":43114,"useMultiProvider":true,"sessionAllowProviderFallbackWhenUnhealthy":true,"providerInitialWeights":["x","y"],"providerUrls":["url1","url2"]}]' 74 | object = JSON.parse(jsonStr) 75 | validation = ProdConfigJoi.validate(object) 76 | expect(validation.error !== undefined) 77 | }) 78 | 79 | it('test getRpcGatewayEnabledChainIdProviderNamePairs', () => { 80 | const enabledChains: Map = getRpcGatewayEnabledChains(TEST_PROD_CONFIG) 81 | expect(enabledChains.get(ChainId.CELO)).to.have.members(['QUICKNODE', 'INFURA']) 82 | expect(enabledChains.get(ChainId.AVALANCHE)).to.have.members(['INFURA', 'QUICKNODE']) 83 | expect(enabledChains.get(ChainId.BNB)).to.have.members(['QUICKNODE']) 84 | expect(enabledChains.get(ChainId.OPTIMISM)).to.have.members(['INFURA', 'QUICKNODE', 'ALCHEMY']) 85 | expect(enabledChains.get(ChainId.SEPOLIA)).to.have.members(['INFURA', 'ALCHEMY']) 86 | expect(enabledChains.get(ChainId.POLYGON)).to.have.members(['QUICKNODE', 'INFURA', 'ALCHEMY']) 87 | expect(enabledChains.get(ChainId.ARBITRUM_ONE)).to.have.members(['QUICKNODE', 'INFURA', 'ALCHEMY']) 88 | expect(enabledChains.get(ChainId.BASE)).to.have.members(['QUICKNODE', 'INFURA', 'ALCHEMY']) 89 | expect(enabledChains.get(ChainId.MAINNET)).to.have.members(['QUICKNODE', 'INFURA', 'ALCHEMY', 'UNIRPC']) 90 | expect(enabledChains.get(ChainId.BLAST)).to.have.members(['QUICKNODE', 'INFURA']) 91 | }) 92 | 93 | it('validates prod config', () => { 94 | for (const entry of PROD_CONFIG) { 95 | expect(entry.providerUrls.length === entry.providerInitialWeights.length).equals(true) 96 | expect(entry.providerUrls.length === entry.providerNames.length).equals(true) 97 | } 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /test/mocha/unit/rpc/rpcProviderTestProdConfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "chainId": 42220, 4 | "useMultiProviderProb": 1, 5 | "providerInitialWeights": [1, 0], 6 | "providerUrls": ["QUICKNODE_42220", "INFURA_42220"], 7 | "providerNames": ["QUICKNODE", "INFURA"] 8 | }, 9 | { 10 | "chainId": 43114, 11 | "useMultiProviderProb": 1, 12 | "providerInitialWeights": [1, 0], 13 | "providerUrls": ["INFURA_43114", "QUICKNODE_43114"], 14 | "providerNames": ["INFURA", "QUICKNODE"] 15 | }, 16 | { 17 | "chainId": 56, 18 | "useMultiProviderProb": 1, 19 | "providerInitialWeights": [1], 20 | "providerUrls": ["QUICKNODE_56"], 21 | "providerNames": ["QUICKNODE"] 22 | }, 23 | { 24 | "chainId": 10, 25 | "useMultiProviderProb": 1, 26 | "providerInitialWeights": [1, 0, 0], 27 | "providerUrls": ["INFURA_10", "QUICKNODE_10", "ALCHEMY_10"], 28 | "providerNames": ["INFURA", "QUICKNODE", "ALCHEMY"] 29 | }, 30 | { 31 | "chainId": 11155111, 32 | "useMultiProviderProb": 1, 33 | "providerInitialWeights": [1, 0], 34 | "providerUrls": ["INFURA_11155111", "ALCHEMY_11155111"], 35 | "providerNames": ["INFURA", "ALCHEMY"] 36 | }, 37 | { 38 | "chainId": 137, 39 | "useMultiProviderProb": 0.01, 40 | "providerInitialWeights": [1, 0, 0], 41 | "providerUrls": ["QUICKNODE_137", "INFURA_137", "ALCHEMY_137"], 42 | "providerNames": ["QUICKNODE", "INFURA", "ALCHEMY"] 43 | }, 44 | { 45 | "chainId": 42161, 46 | "useMultiProviderProb": 1, 47 | "providerInitialWeights": [1, 1, 1], 48 | "providerUrls": ["INFURA_42161", "QUICKNODE_42161", "ALCHEMY_42161"], 49 | "providerNames": ["INFURA", "QUICKNODE", "ALCHEMY"] 50 | }, 51 | { 52 | "chainId": 8453, 53 | "useMultiProviderProb": 1, 54 | "providerInitialWeights": [1, 0, 0], 55 | "providerUrls": ["QUICKNODE_8453", "INFURA_8453", "ALCHEMY_8453"], 56 | "providerNames": ["QUICKNODE", "INFURA", "ALCHEMY"] 57 | }, 58 | { 59 | "chainId": 1, 60 | "useMultiProviderProb": 1, 61 | "providerInitialWeights": [1, 0, 0, 0], 62 | "providerUrls": ["INFURA_1", "QUICKNODE_1", "ALCHEMY_1", "UNIRPC_0"], 63 | "providerNames": ["INFURA", "QUICKNODE", "ALCHEMY", "UNIRPC"] 64 | }, 65 | { 66 | "chainId": 81457, 67 | "useMultiProviderProb": 1, 68 | "providerInitialWeights": [1, 0], 69 | "providerUrls": ["QUICKNODE_81457", "INFURA_81457"], 70 | "providerNames": ["QUICKNODE", "INFURA"] 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /test/mocha/unit/util/supportedProtocolVersions.test.ts: -------------------------------------------------------------------------------- 1 | import { UniversalRouterVersion } from '@uniswap/universal-router-sdk' 2 | import { 3 | convertStringRouterVersionToEnum, 4 | protocolVersionsToBeExcludedFromMixed, 5 | } from '../../../../lib/util/supportedProtocolVersions' 6 | import { Protocol } from '@uniswap/router-sdk' 7 | import { expect } from 'chai' 8 | 9 | describe('supported protocol versions', () => { 10 | it('should convert string router version to enum', async () => { 11 | expect(convertStringRouterVersionToEnum('1.2')).to.eq(UniversalRouterVersion.V1_2) 12 | expect(convertStringRouterVersionToEnum('2.0')).to.eq(UniversalRouterVersion.V2_0) 13 | expect(convertStringRouterVersionToEnum('3.0')).to.eq(UniversalRouterVersion.V1_2) 14 | expect(convertStringRouterVersionToEnum(undefined)).to.eq(UniversalRouterVersion.V1_2) 15 | }) 16 | 17 | it('should return protocol versions to be excluded from mixed', async () => { 18 | expect(protocolVersionsToBeExcludedFromMixed(UniversalRouterVersion.V1_2)).to.deep.eq([Protocol.V4]) 19 | expect(protocolVersionsToBeExcludedFromMixed(UniversalRouterVersion.V2_0)).to.deep.eq([]) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/test-utils/mocked-data.ts: -------------------------------------------------------------------------------- 1 | import { encodeSqrtRatioX96, Pool } from '@uniswap/v3-sdk' 2 | import { FeeAmount } from '../utils/ticks' 3 | import { 4 | DAI_MAINNET as DAI, 5 | USDC_MAINNET as USDC, 6 | USDT_MAINNET as USDT, 7 | WRAPPED_NATIVE_CURRENCY, 8 | } from '@uniswap/smart-order-router/build/main/index' 9 | import { V3PoolAccessor } from '@uniswap/smart-order-router/build/main/providers/v3/pool-provider' 10 | import _ from 'lodash' 11 | import { ChainId, Currency, Ether, WETH9 } from '@uniswap/sdk-core' 12 | import { DAI_ON, USDC_ON, USDT_ON } from '../utils/tokens' 13 | import { WBTC_MAINNET } from '@uniswap/smart-order-router' 14 | 15 | export const USDC_DAI_LOW = new Pool(USDC, DAI, FeeAmount.LOW, encodeSqrtRatioX96(1, 1), 10, 0) 16 | export const USDC_DAI_MEDIUM = new Pool(USDC, DAI, FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 8, 0) 17 | export const USDC_WETH_LOW = new Pool( 18 | USDC, 19 | WRAPPED_NATIVE_CURRENCY[1]!, 20 | FeeAmount.LOW, 21 | encodeSqrtRatioX96(1, 1), 22 | 500, 23 | 0 24 | ) 25 | export const WETH9_USDT_LOW = new Pool( 26 | WRAPPED_NATIVE_CURRENCY[1]!, 27 | USDT, 28 | FeeAmount.LOW, 29 | encodeSqrtRatioX96(1, 1), 30 | 200, 31 | 0 32 | ) 33 | export const DAI_USDT_LOW = new Pool(DAI, USDT, FeeAmount.LOW, encodeSqrtRatioX96(1, 1), 10, 0) 34 | export const SUPPORTED_POOLS: Pool[] = [USDC_DAI_LOW, USDC_DAI_MEDIUM, USDC_WETH_LOW, WETH9_USDT_LOW, DAI_USDT_LOW] 35 | 36 | export const buildMockV3PoolAccessor: (pools: Pool[]) => V3PoolAccessor = (pools: Pool[]) => { 37 | return { 38 | getAllPools: () => pools, 39 | getPoolByAddress: (address: string) => 40 | _.find(pools, (p) => Pool.getAddress(p.token0, p.token1, p.fee).toLowerCase() == address.toLowerCase()), 41 | getPool: (tokenA, tokenB, fee) => 42 | _.find(pools, (p) => Pool.getAddress(p.token0, p.token1, p.fee) == Pool.getAddress(tokenA, tokenB, fee)), 43 | } 44 | } 45 | 46 | export type Portion = { 47 | bips: number 48 | recipient: string 49 | type: string 50 | } 51 | 52 | export const PORTION_BIPS = 12 53 | export const PORTION_RECIPIENT = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' 54 | export const PORTION_TYPE = 'flat' 55 | 56 | export const FLAT_PORTION: Portion = { 57 | bips: PORTION_BIPS, 58 | recipient: PORTION_RECIPIENT, 59 | type: PORTION_TYPE, 60 | } 61 | 62 | export const GREENLIST_TOKEN_PAIRS: Array<[Currency, Currency]> = [ 63 | [Ether.onChain(ChainId.MAINNET), USDC_ON(ChainId.MAINNET)], 64 | [WETH9[ChainId.MAINNET], USDT_ON(ChainId.MAINNET)], 65 | [DAI_ON(ChainId.MAINNET), WBTC_MAINNET], 66 | ] 67 | -------------------------------------------------------------------------------- /test/test-utils/mocked-dependencies.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OnChainQuoteProvider, 3 | RouteWithQuotes, 4 | USDC_MAINNET, 5 | V3PoolProvider, 6 | WRAPPED_NATIVE_CURRENCY, 7 | } from '@uniswap/smart-order-router' 8 | import { Pool } from '@uniswap/v3-sdk' 9 | import { 10 | buildMockV3PoolAccessor, 11 | DAI_USDT_LOW, 12 | USDC_DAI_LOW, 13 | USDC_DAI_MEDIUM, 14 | USDC_WETH_LOW, 15 | WETH9_USDT_LOW, 16 | } from './mocked-data' 17 | import sinon from 'sinon' 18 | import { V3Route } from '@uniswap/smart-order-router/build/main/routers' 19 | import { ChainId, CurrencyAmount } from '@uniswap/sdk-core' 20 | import { AmountQuote } from '@uniswap/smart-order-router/build/main/providers/on-chain-quote-provider' 21 | import { BigNumber } from 'ethers' 22 | 23 | export function getMockedV3PoolProvider( 24 | pools: Pool[] = [USDC_DAI_LOW, USDC_DAI_MEDIUM, USDC_WETH_LOW, WETH9_USDT_LOW, DAI_USDT_LOW] 25 | ): V3PoolProvider { 26 | const mockV3PoolProvider = sinon.createStubInstance(V3PoolProvider) 27 | 28 | mockV3PoolProvider.getPools.resolves(buildMockV3PoolAccessor(pools)) 29 | mockV3PoolProvider.getPoolAddress.callsFake((tA, tB, fee) => ({ 30 | poolAddress: Pool.getAddress(tA, tB, fee), 31 | token0: tA, 32 | token1: tB, 33 | })) 34 | 35 | return mockV3PoolProvider 36 | } 37 | 38 | export function getMockedOnChainQuoteProvider(): sinon.SinonStubbedInstance { 39 | const mockedQuoteProvider = sinon.createStubInstance(OnChainQuoteProvider) 40 | const route = new V3Route([USDC_WETH_LOW], WRAPPED_NATIVE_CURRENCY[ChainId.MAINNET], USDC_MAINNET) 41 | const quotes: AmountQuote[] = [ 42 | { 43 | amount: CurrencyAmount.fromRawAmount(WRAPPED_NATIVE_CURRENCY[ChainId.MAINNET], '1000000000000000000'), 44 | quote: BigNumber.from('1000000000000000000'), 45 | sqrtPriceX96AfterList: [BigNumber.from(100)], 46 | initializedTicksCrossedList: [1, 1, 1], 47 | gasEstimate: BigNumber.from(10000), 48 | gasLimit: BigNumber.from(1000000), 49 | }, 50 | ] 51 | const routesWithQuotes: RouteWithQuotes[] = [[route, quotes]] 52 | mockedQuoteProvider.getQuotesManyExactIn.resolves({ 53 | routesWithQuotes: routesWithQuotes, 54 | blockNumber: BigNumber.from(0), 55 | }) 56 | mockedQuoteProvider.getQuotesManyExactOut.resolves({ 57 | routesWithQuotes: routesWithQuotes, 58 | blockNumber: BigNumber.from(0), 59 | }) 60 | 61 | return mockedQuoteProvider 62 | } 63 | 64 | export const TEST_ROUTE_TABLE = { 65 | TableName: 'PoolCachingV3', 66 | KeySchema: [ 67 | { 68 | AttributeName: 'poolAddress', 69 | KeyType: 'HASH', 70 | }, 71 | { 72 | AttributeName: 'blockNumber', 73 | KeyType: 'RANGE', 74 | }, 75 | ], 76 | AttributeDefinitions: [ 77 | { 78 | AttributeName: 'poolAddress', 79 | AttributeType: 'S', 80 | }, 81 | { 82 | AttributeName: 'blockNumber', 83 | AttributeType: 'N', 84 | }, 85 | ], 86 | ProvisionedThroughput: { 87 | ReadCapacityUnits: 1, 88 | WriteCapacityUnits: 1, 89 | }, 90 | } 91 | -------------------------------------------------------------------------------- /test/utils/absoluteValue.ts: -------------------------------------------------------------------------------- 1 | import { Fraction } from '@uniswap/sdk-core' 2 | import JSBI from 'jsbi' 3 | 4 | export function absoluteValue(fraction: Fraction): Fraction { 5 | const numeratorAbs = JSBI.lessThan(fraction.numerator, JSBI.BigInt(0)) 6 | ? JSBI.unaryMinus(fraction.numerator) 7 | : fraction.numerator 8 | const denominatorAbs = JSBI.lessThan(fraction.denominator, JSBI.BigInt(0)) 9 | ? JSBI.unaryMinus(fraction.denominator) 10 | : fraction.denominator 11 | return new Fraction(numeratorAbs, denominatorAbs) 12 | } 13 | -------------------------------------------------------------------------------- /test/utils/forkAndFund.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' 2 | import { Currency, CurrencyAmount } from '@uniswap/sdk-core' 3 | import hre from 'hardhat' 4 | import { Erc20, Erc20__factory } from '../../lib/types/ext' 5 | 6 | const WHALES = [ 7 | '0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8', 8 | '0x6555e1cc97d3cba6eaddebbcd7ca51d75771e0b8', 9 | '0x08638ef1a205be6762a8b935f5da9b700cf7322c', 10 | '0xe8e8f41ed29e46f34e206d7d2a7d6f735a3ff2cb', 11 | '0x72a53cdbbcc1b9efa39c834a540550e23463aacb', 12 | '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', 13 | '0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf', 14 | '0x8eb8a3b98659cce290402893d0123abb75e3ab28', 15 | '0x1e3d6eab4bcf24bcd04721caa11c478a2e59852d', 16 | '0x28C6c06298d514Db089934071355E5743bf21d60', 17 | '0xF977814e90dA44bFA03b6295A0616a897441aceC', 18 | '0x5d3a536e4d6dbd6114cc1ead35777bab948e3643', 19 | '0x2775b1c75658be0f640272ccb8c72ac986009e38', 20 | '0x28c6c06298d514db089934071355e5743bf21d60', 21 | '0x47ac0fb4f2d84898e4d9e7b4dab3c24507a6d503', 22 | '0x06601571aa9d3e8f5f7cdd5b993192618964bab5', 23 | '0x171d311eAcd2206d21Cb462d661C33F0eddadC03', 24 | ] 25 | 26 | const { ethers } = hre 27 | 28 | export const resetAndFundAtBlock = async ( 29 | alice: SignerWithAddress, 30 | blockNumber: number, 31 | currencyAmounts: CurrencyAmount[] 32 | ): Promise => { 33 | await hre.network.provider.request({ 34 | method: 'hardhat_reset', 35 | params: [ 36 | { 37 | forking: { 38 | jsonRpcUrl: process.env.ARCHIVE_NODE_RPC, 39 | blockNumber, 40 | }, 41 | }, 42 | ], 43 | }) 44 | 45 | for (const whale of WHALES) { 46 | await hre.network.provider.request({ 47 | method: 'hardhat_impersonateAccount', 48 | params: [whale], 49 | }) 50 | } 51 | 52 | for (const currencyAmount of currencyAmounts) { 53 | const currency = currencyAmount.currency 54 | const amount = currencyAmount.toExact() 55 | 56 | if (currency.isNative) { 57 | // Requested funding was for ETH. Hardhat prefunds Alice with 1000 Eth. 58 | return alice 59 | } 60 | 61 | for (let i = 0; i < WHALES.length; i++) { 62 | const whale = WHALES[i] 63 | const whaleAccount = ethers.provider.getSigner(whale) 64 | try { 65 | // Send native ETH from hardhat alice test address, so that whale accounts have sufficient ETH to pay for gas 66 | await alice.sendTransaction({ 67 | to: whale, 68 | value: ethers.utils.parseEther('0.1'), // Sends exactly 0.1 ether 69 | }) 70 | 71 | const whaleToken: Erc20 = Erc20__factory.connect(currency.wrapped.address, whaleAccount) 72 | 73 | await whaleToken.transfer(alice.address, ethers.utils.parseUnits(amount, currency.decimals)) 74 | 75 | break 76 | } catch (err) { 77 | if (i == WHALES.length - 1) { 78 | throw new Error( 79 | `Could not fund ${amount} ${currency.symbol} from any whales. Original error ${JSON.stringify(err)}` 80 | ) 81 | } 82 | } 83 | } 84 | } 85 | 86 | return alice 87 | } 88 | -------------------------------------------------------------------------------- /test/utils/getBalanceAndApprove.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' 2 | import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' 3 | import { constants } from 'ethers' 4 | import { Erc20 } from '../../lib/types/ext/Erc20' 5 | import { Erc20__factory } from '../../lib/types/ext/factories/Erc20__factory' 6 | 7 | export const getBalance = async (alice: SignerWithAddress, currency: Currency): Promise> => { 8 | if (!currency.isToken) { 9 | return CurrencyAmount.fromRawAmount(currency, (await alice.getBalance()).toString()) 10 | } 11 | 12 | const aliceTokenIn: Erc20 = Erc20__factory.connect(currency.address, alice) 13 | 14 | return CurrencyAmount.fromRawAmount(currency, (await aliceTokenIn.balanceOf(alice.address)).toString()) 15 | } 16 | 17 | export const getBalanceOfAddress = async ( 18 | alice: SignerWithAddress, 19 | address: string, 20 | currency: Token 21 | ): Promise> => { 22 | // tokens / WETH only. 23 | const token: Erc20 = Erc20__factory.connect(currency.address, alice) 24 | 25 | return CurrencyAmount.fromRawAmount(currency, (await token.balanceOf(address)).toString()) 26 | } 27 | 28 | export const getBalanceAndApprove = async ( 29 | alice: SignerWithAddress, 30 | approveTarget: string, 31 | currency: Currency 32 | ): Promise> => { 33 | if (currency.isToken) { 34 | const aliceTokenIn: Erc20 = Erc20__factory.connect(currency.address, alice) 35 | 36 | if (currency.symbol == 'USDT') { 37 | await (await aliceTokenIn.approve(approveTarget, 0)).wait() 38 | } 39 | await (await aliceTokenIn.approve(approveTarget, constants.MaxUint256)).wait() 40 | } 41 | 42 | return getBalance(alice, currency) 43 | } 44 | -------------------------------------------------------------------------------- /test/utils/minimumAmountOut.ts: -------------------------------------------------------------------------------- 1 | import { Currency, CurrencyAmount, Fraction, Percent } from '@uniswap/sdk-core' 2 | import JSBI from 'jsbi' 3 | import invariant from 'tiny-invariant' 4 | 5 | export const minimumAmountOut = ( 6 | slippageTolerance: Percent, 7 | amountOut: CurrencyAmount 8 | ): CurrencyAmount => { 9 | invariant(!slippageTolerance.lessThan(JSBI.BigInt(0)), 'SLIPPAGE_TOLERANCE') 10 | const slippageAdjustedAmountOut = new Fraction(JSBI.BigInt(1)) 11 | .add(slippageTolerance) 12 | .invert() 13 | .multiply(amountOut.quotient).quotient 14 | return CurrencyAmount.fromRawAmount(amountOut.currency, slippageAdjustedAmountOut) 15 | } 16 | -------------------------------------------------------------------------------- /test/utils/ticks.ts: -------------------------------------------------------------------------------- 1 | export enum FeeAmount { 2 | LOW = 500, 3 | MEDIUM = 3000, 4 | HIGH = 10000, 5 | } 6 | 7 | export const TICK_SPACINGS: { [amount in FeeAmount]: number } = { 8 | [FeeAmount.LOW]: 10, 9 | [FeeAmount.MEDIUM]: 60, 10 | [FeeAmount.HIGH]: 200, 11 | } 12 | 13 | export const getMinTick = (tickSpacing: number) => Math.ceil(-887272 / tickSpacing) * tickSpacing 14 | export const getMaxTick = (tickSpacing: number) => Math.floor(887272 / tickSpacing) * tickSpacing 15 | -------------------------------------------------------------------------------- /test/utils/tokens.ts: -------------------------------------------------------------------------------- 1 | import DEFAULT_TOKEN_LIST from '@uniswap/default-token-list' 2 | import { ChainId, Token } from '@uniswap/sdk-core' 3 | import { 4 | CachingTokenListProvider, 5 | DAI_ARBITRUM, 6 | DAI_AVAX, 7 | DAI_BNB, 8 | DAI_GOERLI, 9 | DAI_MAINNET, 10 | DAI_OPTIMISM, 11 | DAI_OPTIMISM_GOERLI, 12 | DAI_POLYGON, 13 | DAI_POLYGON_MUMBAI, 14 | DAI_SEPOLIA, 15 | log, 16 | NodeJSCache, 17 | USDC_ARBITRUM, 18 | USDC_ASTROCHAIN_SEPOLIA, 19 | USDC_AVAX, 20 | USDC_BASE, 21 | USDC_BASE_GOERLI, 22 | USDC_BNB, 23 | USDC_GOERLI, 24 | USDC_MAINNET, 25 | USDC_OPTIMISM, 26 | USDC_OPTIMISM_GOERLI, 27 | USDC_POLYGON, 28 | USDC_POLYGON_MUMBAI, 29 | USDC_SEPOLIA, 30 | USDC_WORLDCHAIN, 31 | USDC_ZORA, 32 | USDCE_ZKSYNC, 33 | USDT_ARBITRUM, 34 | USDT_BNB, 35 | USDT_GOERLI, 36 | USDT_MAINNET, 37 | USDT_OPTIMISM, 38 | WRAPPED_NATIVE_CURRENCY, 39 | } from '@uniswap/smart-order-router' 40 | import { ethers } from 'ethers' 41 | import NodeCache from 'node-cache' 42 | 43 | export const getTokenListProvider = (id: ChainId) => { 44 | return new CachingTokenListProvider(id, DEFAULT_TOKEN_LIST, new NodeJSCache(new NodeCache())) 45 | } 46 | 47 | export const getAmount = async (id: ChainId, type: string, symbolIn: string, symbolOut: string, amount: string) => { 48 | const tokenListProvider = getTokenListProvider(id) 49 | const decimals = (await tokenListProvider.getTokenBySymbol(type == 'exactIn' ? symbolIn : symbolOut))!.decimals 50 | log.info(decimals) 51 | return ethers.utils.parseUnits(amount, decimals).toString() 52 | } 53 | 54 | export const getAmountFromToken = async (type: string, tokenIn: Token, tokenOut: Token, amount: string) => { 55 | const decimals = (type == 'exactIn' ? tokenIn : tokenOut).decimals 56 | return ethers.utils.parseUnits(amount, decimals).toString() 57 | } 58 | 59 | export const UNI_MAINNET = new Token( 60 | ChainId.MAINNET, 61 | '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', 62 | 18, 63 | 'UNI', 64 | 'Uniswap' 65 | ) 66 | 67 | export const UNI_GORLI = new Token(ChainId.GOERLI, '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', 18, 'UNI', 'Uni token') 68 | 69 | export const DAI_ON = (chainId: ChainId): Token => { 70 | switch (chainId) { 71 | case ChainId.MAINNET: 72 | return DAI_MAINNET 73 | case ChainId.GOERLI: 74 | return DAI_GOERLI 75 | case ChainId.SEPOLIA: 76 | return DAI_SEPOLIA 77 | case ChainId.OPTIMISM: 78 | return DAI_OPTIMISM 79 | case ChainId.OPTIMISM_GOERLI: 80 | return DAI_OPTIMISM_GOERLI 81 | case ChainId.ARBITRUM_ONE: 82 | return DAI_ARBITRUM 83 | case ChainId.POLYGON: 84 | return DAI_POLYGON 85 | case ChainId.POLYGON_MUMBAI: 86 | return DAI_POLYGON_MUMBAI 87 | case ChainId.BNB: 88 | return DAI_BNB 89 | case ChainId.AVALANCHE: 90 | return DAI_AVAX 91 | default: 92 | throw new Error(`Chain id: ${chainId} not supported`) 93 | } 94 | } 95 | 96 | export const USDT_ON = (chainId: ChainId): Token => { 97 | switch (chainId) { 98 | case ChainId.MAINNET: 99 | return USDT_MAINNET 100 | case ChainId.GOERLI: 101 | return USDT_GOERLI 102 | case ChainId.OPTIMISM: 103 | return USDT_OPTIMISM 104 | case ChainId.ARBITRUM_ONE: 105 | return USDT_ARBITRUM 106 | case ChainId.BNB: 107 | return USDT_BNB 108 | default: 109 | throw new Error(`Chain id: ${chainId} not supported`) 110 | } 111 | } 112 | 113 | export const USDC_ON = (chainId: ChainId): Token => { 114 | switch (chainId) { 115 | case ChainId.MAINNET: 116 | return USDC_MAINNET 117 | case ChainId.GOERLI: 118 | return USDC_GOERLI 119 | case ChainId.SEPOLIA: 120 | return USDC_SEPOLIA 121 | case ChainId.OPTIMISM: 122 | return USDC_OPTIMISM 123 | case ChainId.OPTIMISM_GOERLI: 124 | return USDC_OPTIMISM_GOERLI 125 | case ChainId.ARBITRUM_ONE: 126 | return USDC_ARBITRUM 127 | case ChainId.POLYGON: 128 | return USDC_POLYGON 129 | case ChainId.POLYGON_MUMBAI: 130 | return USDC_POLYGON_MUMBAI 131 | case ChainId.BNB: 132 | return USDC_BNB 133 | case ChainId.AVALANCHE: 134 | return USDC_AVAX 135 | case ChainId.BASE: 136 | return USDC_BASE 137 | case ChainId.BASE_GOERLI: 138 | return USDC_BASE_GOERLI 139 | case ChainId.ZORA: 140 | return USDC_ZORA 141 | case ChainId.ZKSYNC: 142 | return USDCE_ZKSYNC 143 | case ChainId.WORLDCHAIN: 144 | return USDC_WORLDCHAIN 145 | case ChainId.ASTROCHAIN_SEPOLIA: 146 | return USDC_ASTROCHAIN_SEPOLIA 147 | default: 148 | throw new Error(`Chain id: ${chainId} not supported`) 149 | } 150 | } 151 | 152 | export const WNATIVE_ON = (chainId: ChainId): Token => { 153 | return WRAPPED_NATIVE_CURRENCY[chainId] 154 | } 155 | -------------------------------------------------------------------------------- /tsconfig.cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "outDir": "dist", 23 | "typeRoots": ["./node_modules/@types"] 24 | }, 25 | "files": ["./hardhat.config.js"], 26 | "exclude": ["cdk.out", "./dist/**/*"] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": ["es2018"], 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": true /* Report errors on unused locals. */, 16 | "noUnusedParameters": true /* Report errors on unused parameters. */, 17 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 18 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "outDir": "./dist", 24 | "allowJs": true, 25 | "typeRoots": ["./node_modules/@types"], 26 | "forceConsistentCasingInFileNames": true 27 | }, 28 | "exclude": ["cdk.out", "./dist/**/*"] 29 | } 30 | --------------------------------------------------------------------------------