├── .env.default ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── lint.yml │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .yarnrc ├── .yarnrc.yml ├── README.md ├── commitlint.config.js ├── hardhat.config.ts ├── jest.config.js ├── package.json ├── prettier.config.js ├── scripts └── log-sources-per-chain.ts ├── src ├── chains.ts ├── index.ts ├── sdk │ ├── builders │ │ ├── allowance-builder.ts │ │ ├── balance-builder.ts │ │ ├── blocks-builder.ts │ │ ├── dca-builder.ts │ │ ├── earn-builder.ts │ │ ├── fetch-builder.ts │ │ ├── gas-builder.ts │ │ ├── index.ts │ │ ├── logs-builder.ts │ │ ├── metadata-builder.ts │ │ ├── permit2-builder.ts │ │ ├── price-builder.ts │ │ ├── provider-builder.ts │ │ └── quote-builder.ts │ ├── index.ts │ ├── sdk-builder.ts │ └── types.ts ├── services │ ├── allowances │ │ ├── allowance-service.ts │ │ ├── allowance-sources │ │ │ ├── cached-allowance-source.ts │ │ │ └── rpc-allowance-source.ts │ │ ├── index.ts │ │ └── types.ts │ ├── balances │ │ ├── balance-service.ts │ │ ├── balance-sources │ │ │ ├── cached-balance-source.ts │ │ │ ├── fastest-balance-source.ts │ │ │ ├── rpc-balance-source.ts │ │ │ └── utils.ts │ │ ├── index.ts │ │ └── types.ts │ ├── blocks │ │ ├── block-service.ts │ │ ├── block-sources │ │ │ └── defi-llama-block-source.ts │ │ ├── index.ts │ │ └── types.ts │ ├── dca │ │ ├── config.ts │ │ ├── dca-service.ts │ │ ├── index.ts │ │ └── types.ts │ ├── earn │ │ ├── config.ts │ │ ├── earn-service.ts │ │ ├── index.ts │ │ └── types.ts │ ├── fetch │ │ ├── fetch-service.ts │ │ ├── index.ts │ │ └── types.ts │ ├── gas │ │ ├── gas-price-sources │ │ │ ├── aggregator-gas-price-source.ts │ │ │ ├── cached-gas-price-source.ts │ │ │ ├── etherscan-gas-price-source.ts │ │ │ ├── fastest-gas-price-source-combinator.ts │ │ │ ├── open-ocean-gas-price-source.ts │ │ │ ├── owlracle-gas-price-source.ts │ │ │ ├── paraswap-gas-price-source.ts │ │ │ ├── polygon-gas-station-gas-price-source.ts │ │ │ ├── prioritized-gas-price-source-combinator.ts │ │ │ └── rpc-gas-price-source.ts │ │ ├── gas-service.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── index.ts │ ├── logs │ │ ├── index.ts │ │ ├── loggers │ │ │ └── console-logger.ts │ │ ├── logs-service.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── metadata │ │ ├── index.ts │ │ ├── metadata-service.ts │ │ ├── metadata-sources │ │ │ ├── cached-metadata-source.ts │ │ │ ├── defi-llama-metadata-source.ts │ │ │ ├── fallback-metadata-source.ts │ │ │ └── rpc-metadata-source.ts │ │ └── types.ts │ ├── permit2 │ │ ├── README.md │ │ ├── index.ts │ │ ├── permit2-arbitrary-service.ts │ │ ├── permit2-quote-service.ts │ │ ├── permit2-service.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── config.ts │ │ │ └── eip712-types.ts │ ├── prices │ │ ├── index.ts │ │ ├── price-service.ts │ │ ├── price-sources │ │ │ ├── aggregator-price-source.ts │ │ │ ├── alchemy-price-source.ts │ │ │ ├── balmy-price-source.ts │ │ │ ├── cached-price-source.ts │ │ │ ├── codex-price-source.ts │ │ │ ├── coingecko-price-source.ts │ │ │ ├── defi-llama-price-source.ts │ │ │ ├── fastest-price-source.ts │ │ │ ├── odos-price-source.ts │ │ │ ├── prioritized-price-source.ts │ │ │ └── utils.ts │ │ └── types.ts │ ├── providers │ │ ├── index.ts │ │ ├── provider-service.ts │ │ ├── provider-sources │ │ │ ├── alchemy-provider.ts │ │ │ ├── ankr-provider.ts │ │ │ ├── base │ │ │ │ ├── base-http-provider.ts │ │ │ │ └── base-web-socket-provider.ts │ │ │ ├── blast-provider.ts │ │ │ ├── drpc-provider.ts │ │ │ ├── fallback-provider.ts │ │ │ ├── get-block-provider.ts │ │ │ ├── http-provider.ts │ │ │ ├── index.ts │ │ │ ├── infura-provider.ts │ │ │ ├── llama-nodes-provider.ts │ │ │ ├── load-balance-provider.ts │ │ │ ├── moralis-provider.ts │ │ │ ├── node-real-provider.ts │ │ │ ├── on-finality-provider.ts │ │ │ ├── one-rpc-provider.ts │ │ │ ├── prioritized-provider-source-combinator.ts │ │ │ ├── public-rpcs-provider.ts │ │ │ ├── tenderly-provider.ts │ │ │ ├── third-web-provider.ts │ │ │ └── web-sockets-provider.ts │ │ ├── types.ts │ │ └── utils.ts │ └── quotes │ │ ├── errors.ts │ │ ├── index.ts │ │ ├── quote-compare.ts │ │ ├── quote-service.ts │ │ ├── quote-sources │ │ ├── 0x-quote-source.ts │ │ ├── 1inch-quote-source.ts │ │ ├── balancer-quote-source.ts │ │ ├── balmy-quote-source.ts │ │ ├── barter-quote-source.ts │ │ ├── base │ │ │ └── always-valid-source.ts │ │ ├── bebop-quote-source.ts │ │ ├── braindex-quote-source.ts │ │ ├── changelly-quote-source.ts │ │ ├── conveyor-quote-source.ts │ │ ├── dodo-quote-source.ts │ │ ├── enso-quote-source.ts │ │ ├── fly-trade-quote-source.ts │ │ ├── kyberswap-quote-source.ts │ │ ├── li-fi-quote-source.ts │ │ ├── odos-quote-source.ts │ │ ├── oku-quote-source.ts │ │ ├── okx-dex-quote-source.ts │ │ ├── open-ocean-quote-source.ts │ │ ├── paraswap-quote-source.ts │ │ ├── portals-fi-quote-source.ts │ │ ├── rango-quote-source.ts │ │ ├── sovryn-quote-source.ts │ │ ├── squid-quote-source.ts │ │ ├── sushiswap-quote-source.ts │ │ ├── swing-quote-source.ts │ │ ├── types.ts │ │ ├── uniswap-quote-source.ts │ │ ├── utils.ts │ │ ├── wrappers │ │ │ ├── buy-to-sell-order-wrapper.ts │ │ │ └── forced-timeout-wrapper.ts │ │ └── xy-finance-quote-source.ts │ │ ├── source-lists │ │ ├── batch-api-source-list.ts │ │ ├── index.ts │ │ ├── local-source-list.ts │ │ ├── overridable-source-list.ts │ │ ├── types.ts │ │ └── utils.ts │ │ ├── source-registry.ts │ │ └── types.ts ├── shared │ ├── abis │ │ ├── companion.ts │ │ ├── dca-hub.ts │ │ ├── earn-delayed-withdrawal-manager.ts │ │ ├── earn-strategy-router.ts │ │ ├── earn-strategy.ts │ │ ├── earn-vault-companion.ts │ │ ├── earn-vault.ts │ │ ├── erc20.ts │ │ ├── erc721.ts │ │ ├── permit2-adapter.ts │ │ └── permit2.ts │ ├── alchemy.ts │ ├── auto-update-cache.ts │ ├── concurrent-lru-cache.ts │ ├── constants.ts │ ├── contracts.ts │ ├── deferred.ts │ ├── defi-llama.ts │ ├── index.ts │ ├── requirements-and-support.ts │ ├── timeouts.ts │ ├── triggerable-promise.ts │ ├── utils.ts │ ├── viem.ts │ └── wait.ts ├── types.ts └── utility-types.ts ├── test ├── integration │ ├── services │ │ ├── allowances │ │ │ └── allowance-sources.spec.ts │ │ ├── balances │ │ │ └── balance-sources.spec.ts │ │ ├── blocks │ │ │ └── block-sources.spec.ts │ │ ├── gas │ │ │ └── gas-price-sources.spec.ts │ │ ├── metadata │ │ │ ├── metadata-sources.spec.ts │ │ │ └── metadata-sources │ │ │ │ └── rpc-metadata-source.spec.ts │ │ ├── permit2 │ │ │ ├── permit2-arbitrary-service.spec.ts │ │ │ └── permit2-quote-service.spec.ts │ │ ├── prices │ │ │ └── price-sources.spec.ts │ │ └── quotes │ │ │ ├── failing-quote.spec.ts │ │ │ ├── quote-tests-config.ts │ │ │ └── sources │ │ │ └── quote-sources.spec.ts │ └── utils │ │ ├── erc20.ts │ │ ├── evm.ts │ │ └── other.ts ├── tsconfig.json ├── unit │ ├── services │ │ ├── balances │ │ │ └── balance-service.spec.ts │ │ ├── fetch │ │ │ └── fetch-service.spec.ts │ │ ├── gas │ │ │ └── gas-price-sources │ │ │ │ ├── aggregator-gas-price-source.spec.ts │ │ │ │ ├── cached-gas-price-source.spec.ts │ │ │ │ ├── fastest-gas-price-source-combinator.spec.ts │ │ │ │ └── prioritized-gas-price-source-combinator.spec.ts │ │ ├── metadata │ │ │ └── metadata-sources │ │ │ │ ├── cached-metadata-source.spec.ts │ │ │ │ └── fallback-metadata-source.spec.ts │ │ ├── prices │ │ │ └── price-sources │ │ │ │ ├── aggregator-price-source.spec.ts │ │ │ │ ├── fastest-price-source.spec.ts │ │ │ │ └── prioritized-price-source.spec.ts │ │ ├── providers │ │ │ └── provider-sources │ │ │ │ └── prioritized-provider-source-combinator.spec.ts │ │ └── quotes │ │ │ ├── quote-service.spec.ts │ │ │ ├── source-lists │ │ │ └── local-source-list.spec.ts │ │ │ └── sources │ │ │ └── quote-sources.spec.ts │ └── shared │ │ └── requirements-and-support.spec.ts └── utils │ └── bdd.ts ├── tsconfig.json └── yarn.lock /.env.default: -------------------------------------------------------------------------------- 1 | #Needs to be set for integration tests 2 | 3 | ALCHEMY_API_KEY= 4 | DODO_API_KEY= 5 | BARTER_AUTH_HEADER= 6 | BARTER_CUSTOM_SUBDOMAIN= -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out github repository 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 1 18 | 19 | - name: Install node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: '18.x' 23 | cache: yarn 24 | cache-dependency-path: '**/yarn.lock' 25 | 26 | - name: Install dependencies 27 | run: yarn --frozen-lockfile 28 | 29 | - name: Build 30 | run: yarn build 31 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | - 'dev' 8 | pull_request: 9 | 10 | jobs: 11 | files: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out github repository 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 1 19 | 20 | - name: Install node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '18.x' 24 | cache: yarn 25 | cache-dependency-path: '**/yarn.lock' 26 | 27 | - name: Install dependencies 28 | run: yarn --frozen-lockfile 29 | 30 | - name: Run linter 31 | run: yarn lint:check 32 | 33 | commits: 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - name: Check out github repository 38 | uses: actions/checkout@v3 39 | with: 40 | fetch-depth: 0 41 | 42 | - name: Run commitlint 43 | uses: wagoid/commitlint-github-action@v5 44 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | inputs: 8 | release-type: 9 | description: 'Release type (one of): patch, minor, major' 10 | required: true 11 | 12 | permissions: write-all 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | build-and-publish: 16 | # The type of runner that the job will run on 17 | runs-on: ubuntu-latest 18 | if: github.ref == 'refs/heads/main' 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v3 24 | 25 | - name: Use Node.js 16.x 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: '18.x' 29 | registry-url: https://registry.npmjs.org/ 30 | - name: Git configuration 31 | run: | 32 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 33 | git config --global user.name "GitHub Actions" 34 | - name: Install dependencies 35 | run: yarn install --pure-lockfile 36 | - name: Build 37 | run: yarn build 38 | - name: Unit tests 39 | run: yarn test:unit 40 | - name: Bump release version 41 | run: | 42 | echo "NEW_VERSION=$(npm --no-git-tag-version version $RELEASE_TYPE)" >> $GITHUB_ENV 43 | echo "RELEASE_TAG=latest" >> $GITHUB_ENV 44 | env: 45 | RELEASE_TYPE: ${{ github.event.inputs.release-type }} 46 | - name: Commit report 47 | run: | 48 | git commit -am "chore: release ${{ env.NEW_VERSION }}" 49 | git tag ${{ env.NEW_VERSION }} 50 | - name: Publish 51 | run: npm publish --tag ${{ env.RELEASE_TAG }} 52 | env: 53 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }} 54 | - name: Push changes to repository 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | run: | 58 | git push origin && git push --tags 59 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | unit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out github repository 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 1 17 | 18 | - name: Install node 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: '18.x' 22 | cache: yarn 23 | cache-dependency-path: '**/yarn.lock' 24 | 25 | - name: Install dependencies 26 | run: yarn --frozen-lockfile 27 | 28 | - name: Run unit tests 29 | run: yarn test:unit 30 | 31 | integration: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Check out github repository 35 | uses: actions/checkout@v3 36 | with: 37 | fetch-depth: 1 38 | 39 | - name: Install node 40 | uses: actions/setup-node@v3 41 | with: 42 | node-version: '18.x' 43 | cache: yarn 44 | cache-dependency-path: '**/yarn.lock' 45 | 46 | - name: Install dependencies 47 | run: yarn --frozen-lockfile 48 | 49 | - name: Run integration tests 50 | run: yarn test:integration 51 | timeout-minutes: 15 52 | env: 53 | ALCHEMY_API_KEY: ${{secrets.ALCHEMY_API_KEY}} 54 | BARTER_AUTH_HEADER: ${{secrets.BARTER_AUTH_HEADER}} 55 | BARTER_CUSTOM_SUBDOMAIN: ${{secrets.BARTER_CUSTOM_SUBDOMAIN}} 56 | CODEX_API_KEY: ${{secrets.CODEX_API_KEY}} 57 | # integration-quotes: 58 | # needs: ['integration'] 59 | # runs-on: ubuntu-latest 60 | # steps: 61 | # - name: Check out github repository 62 | # uses: actions/checkout@v3 63 | # with: 64 | # fetch-depth: 1 65 | 66 | # - name: Install node 67 | # uses: actions/setup-node@v3 68 | # with: 69 | # node-version: '18.x' 70 | # cache: yarn 71 | # cache-dependency-path: '**/yarn.lock' 72 | 73 | # - name: Install dependencies 74 | # run: yarn --frozen-lockfile 75 | 76 | # - name: Run integration tests 77 | # run: yarn test:integration:quotes 78 | # timeout-minutes: 15 79 | # env: 80 | # ALCHEMY_API_KEY: ${{secrets.ALCHEMY_API_KEY}} 81 | # RANGO_API_KEY: ${{secrets.RANGO_API_KEY}} 82 | # BARTER_AUTH_HEADER: ${{secrets.BARTER_AUTH_HEADER}} 83 | # BARTER_CUSTOM_SUBDOMAIN: ${{secrets.BARTER_CUSTOM_SUBDOMAIN}} 84 | # CI_CONTEXT: true 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | **/.DS_Store 4 | coverage 5 | .env 6 | newrelic_agent.log 7 | yarn-error.log 8 | cache 9 | .idea 10 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # General 2 | dist 3 | workspace.code-workspace 4 | CHANGELOG.md 5 | .DS_STORE 6 | .husky 7 | .gitignore 8 | .gitattributes 9 | .versionrc 10 | .env 11 | .env.example 12 | yarn-error.log 13 | **/LICENSE 14 | cache 15 | 16 | # JS 17 | node_modules 18 | yarn.lock 19 | 20 | # Coverage 21 | coverage 22 | coverage.json 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "restart": true, 12 | "skipFiles": ["/**/*.js", "node_modules/**/*.js"], 13 | "runtimeExecutable": "node", 14 | "sourceMaps": true, 15 | "showAsyncStacks": true, 16 | "autoAttachChildProcesses": true, 17 | "runtimeArgs": ["--preserve-symlinks"], 18 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 19 | "program": "${workspaceFolder}/dist/index.js", 20 | "preLaunchTask": "npm: build" 21 | }, 22 | { 23 | "name": "Debug Jest Tests", 24 | "type": "node", 25 | "request": "launch", 26 | "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand"], 27 | "console": "integratedTerminal", 28 | "internalConsoleOptions": "neverOpen" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.git": true, 4 | "**/node_modules": true, 5 | "**/coverage": true, 6 | "**/dist": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "options": { 8 | "env": { 9 | "NODE_ENV": "development" 10 | } 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | }, 16 | "problemMatcher": ["$tsc"], 17 | "label": "npm: build", 18 | "detail": "build" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix: '' 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | defaultSemverRangePrefix: '' 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from 'hardhat/types'; 2 | import '@nomiclabs/hardhat-ethers'; 3 | import '@nomicfoundation/hardhat-chai-matchers'; 4 | import 'tsconfig-paths/register'; 5 | import dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | const config: HardhatUserConfig = { 9 | defaultNetwork: 'hardhat', 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest'); 2 | const { compilerOptions } = require('./tsconfig'); 3 | 4 | module.exports = { 5 | globals: {}, 6 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }), 7 | moduleFileExtensions: ['ts', 'js'], 8 | transform: { 9 | '^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: 'test/tsconfig.json' }], 10 | }, 11 | coverageDirectory: 'coverage', 12 | collectCoverageFrom: ['src/**/*.ts', 'src/**/*.js'], 13 | testMatch: ['**/*.spec.(ts)'], 14 | testEnvironment: 'node', 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@balmy/sdk", 3 | "version": "0.9.0", 4 | "contributors": [ 5 | { 6 | "name": "Nicolás Chamo", 7 | "email": "nchamo@balmy.xyz", 8 | "url": "https://github.com/nchamo" 9 | }, 10 | { 11 | "name": "fiboape", 12 | "email": "fiboape@balmy.xyz", 13 | "url": "https://github.com/fiboape" 14 | }, 15 | { 16 | "name": "0xged", 17 | "email": "ged@balmy.xyz", 18 | "url": "https://github.com/0xged" 19 | }, 20 | { 21 | "name": "Sam Bugs", 22 | "email": "sam@balmy.xyz", 23 | "url": "https://github.com/0xsambugs" 24 | } 25 | ], 26 | "main": "./dist/index.js", 27 | "files": [ 28 | "dist/**" 29 | ], 30 | "scripts": { 31 | "build": "tsc -p tsconfig.json && tsconfig-replace-paths -s src -p tsconfig.json", 32 | "lint:check": "prettier --check .", 33 | "lint:fix": "sort-package-json && prettier --write .", 34 | "prepare": "husky install", 35 | "script": "ts-node -r tsconfig-paths/register", 36 | "test": "jest --forceExit --detectOpenHandles --verbose", 37 | "test:integration": "jest --forceExit --detectOpenHandles --verbose --testPathPattern=test/integration -t \"^(?!.*\\[External Quotes\\]).*$\"", 38 | "test:integration:quotes": "jest --forceExit --detectOpenHandles --verbose -t \".*\\[External Quotes\\].*\"", 39 | "test:unit": "jest --forceExit --detectOpenHandles --verbose --testPathPattern=test/unit" 40 | }, 41 | "lint-staged": { 42 | "*.{js,css,md,ts,sol}": "prettier --write", 43 | "package.json": "sort-package-json" 44 | }, 45 | "dependencies": { 46 | "cross-fetch": "3.1.5", 47 | "crypto-js": "4.2.0", 48 | "deepmerge": "4.3.1", 49 | "lru-cache": "9.0.3", 50 | "ms": "3.0.0-canary.1", 51 | "qs": "6.11.2", 52 | "viem": "2.28.0" 53 | }, 54 | "devDependencies": { 55 | "@commitlint/cli": "17.8.0", 56 | "@commitlint/config-conventional": "17.8.0", 57 | "@nomicfoundation/hardhat-chai-matchers": "1.0.6", 58 | "@nomicfoundation/hardhat-network-helpers": "1.0.8", 59 | "@nomicfoundation/hardhat-toolbox": "2.0.2", 60 | "@nomiclabs/hardhat-ethers": "2.2.3", 61 | "@types/crypto-js": "4.1.2", 62 | "@types/jest": "29.5.6", 63 | "@types/node": "18.16.3", 64 | "@types/qs": "6.9.10", 65 | "@types/ws": "8.5.10", 66 | "chai": "4.3.7", 67 | "dotenv": "16.3.1", 68 | "ethers": "5.7.2", 69 | "hardhat": "2.23.0", 70 | "husky": "8.0.3", 71 | "jest": "29.7.0", 72 | "lint-staged": "13.2.2", 73 | "patch-package": "8.0.0", 74 | "prettier": "2.8.8", 75 | "sort-package-json": "2.6.0", 76 | "ts-jest": "29.1.1", 77 | "ts-node": "10.9.1", 78 | "tsconfig-paths": "4.2.0", 79 | "tsconfig-replace-paths": "0.0.14", 80 | "typescript": "5.4.2" 81 | }, 82 | "publishConfig": { 83 | "access": "public" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overrides: [ 3 | { 4 | files: ['**.ts', '**.js', '**.yml'], 5 | options: { 6 | printWidth: 145, 7 | tabWidth: 2, 8 | semi: true, 9 | singleQuote: true, 10 | useTabs: false, 11 | endOfLine: 'auto', 12 | }, 13 | }, 14 | { 15 | files: '**.json', 16 | options: { 17 | tabWidth: 2, 18 | printWidth: 200, 19 | }, 20 | }, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /scripts/log-sources-per-chain.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../src/types'; 2 | import { getChainByKeyOrFail } from '../src/chains'; 3 | import { QUOTE_SOURCES } from '../src/services/quotes/source-registry'; 4 | 5 | function main() { 6 | const allSources: Record = {}; 7 | 8 | for (const source of Object.values(QUOTE_SOURCES)) { 9 | const metadata = source.getMetadata(); 10 | for (const chainId of metadata.supports.chains) { 11 | if (chainId in allSources) { 12 | allSources[chainId].push(metadata.name); 13 | } else { 14 | allSources[chainId] = [metadata.name]; 15 | } 16 | } 17 | } 18 | 19 | console.log('Sources per chain:'); 20 | for (const [chainId, sources] of Object.entries(allSources)) { 21 | console.log(`- ${getChainByKeyOrFail(chainId).name} (${chainId}):`); 22 | for (const source of sources) { 23 | console.log(` - ${source}`); 24 | } 25 | } 26 | } 27 | 28 | main(); 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chains'; 2 | export * from './sdk'; 3 | export * from './services'; 4 | export * from './shared'; 5 | export { 6 | TimeString, 7 | Chain, 8 | ChainId, 9 | ContractCall, 10 | Address, 11 | TokenAddress, 12 | SupportRecord, 13 | SupportInChain, 14 | FieldRequirementOptions, 15 | FieldsRequirements, 16 | BigIntish, 17 | Timestamp, 18 | InputTransaction, 19 | BuiltTransaction, 20 | AmountsOfToken, 21 | } from './types'; 22 | export { WithRequired } from './utility-types'; 23 | -------------------------------------------------------------------------------- /src/sdk/builders/allowance-builder.ts: -------------------------------------------------------------------------------- 1 | import { IFetchService } from '@services/fetch'; 2 | import { CacheConfig } from '@shared/concurrent-lru-cache'; 3 | import { IAllowanceService, IAllowanceSource } from '@services/allowances/types'; 4 | import { RPCAllowanceSource } from '@services/allowances/allowance-sources/rpc-allowance-source'; 5 | import { AllowanceService } from '@services/allowances/allowance-service'; 6 | import { CachedAllowanceSource } from '@services/allowances/allowance-sources/cached-allowance-source'; 7 | import { IProviderService } from '@services/providers'; 8 | 9 | export type AllowanceSourceInput = 10 | | { type: 'rpc-multicall' } 11 | | { type: 'cached'; underlyingSource: AllowanceSourceInput; config: CacheConfig } 12 | | { type: 'custom'; instance: IAllowanceSource }; 13 | export type BuildAllowanceParams = { source: AllowanceSourceInput }; 14 | 15 | export function buildAllowanceService( 16 | params: BuildAllowanceParams | undefined, 17 | fetchService: IFetchService, 18 | providerService: IProviderService 19 | ): IAllowanceService { 20 | const source = buildSource(params?.source, { fetchService, providerService }); 21 | return new AllowanceService(source); 22 | } 23 | 24 | function buildSource( 25 | source: AllowanceSourceInput | undefined, 26 | { fetchService, providerService }: { fetchService: IFetchService; providerService: IProviderService } 27 | ): IAllowanceSource { 28 | switch (source?.type) { 29 | case undefined: 30 | case 'rpc-multicall': 31 | return new RPCAllowanceSource(providerService); 32 | case 'cached': 33 | const underlying = buildSource(source.underlyingSource, { fetchService, providerService }); 34 | return new CachedAllowanceSource(underlying, source.config); 35 | case 'custom': 36 | return source.instance; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/sdk/builders/balance-builder.ts: -------------------------------------------------------------------------------- 1 | import { IBalanceService, IBalanceSource } from '@services/balances/types'; 2 | import { CacheConfig } from '@shared/concurrent-lru-cache'; 3 | import { RPCBalanceSource, RPCBalanceSourceConfig } from '@services/balances/balance-sources/rpc-balance-source'; 4 | import { IProviderService } from '@services/providers'; 5 | import { BalanceService } from '@services/balances/balance-service'; 6 | import { IFetchService } from '@services/fetch'; 7 | import { ILogsService } from '@services/logs'; 8 | import { CachedBalanceSource } from '@services/balances/balance-sources/cached-balance-source'; 9 | import { FastestBalanceSource } from '@services/balances/balance-sources/fastest-balance-source'; 10 | import { BuildProviderParams, buildProviderService } from './provider-builder'; 11 | 12 | export type BalanceSourceInput = 13 | | { type: 'rpc-multicall'; config?: RPCBalanceSourceConfig; customProvider?: BuildProviderParams } 14 | | { type: 'cached'; underlyingSource: BalanceSourceInput; config: CacheConfig } 15 | | { type: 'custom'; instance: IBalanceSource } 16 | | { type: 'fastest'; sources: BalanceSourceInput[] }; 17 | export type BuildBalancesParams = { source: BalanceSourceInput }; 18 | 19 | export function buildBalanceService( 20 | params: BuildBalancesParams | undefined, 21 | fetchService: IFetchService, 22 | providerService: IProviderService, 23 | logsService: ILogsService 24 | ): IBalanceService { 25 | const source = buildSource(params?.source, { fetchService, providerService, logsService }); 26 | return new BalanceService(source); 27 | } 28 | 29 | function buildSource( 30 | source: BalanceSourceInput | undefined, 31 | { fetchService, providerService, logsService }: { fetchService: IFetchService; providerService: IProviderService; logsService: ILogsService } 32 | ): IBalanceSource { 33 | switch (source?.type) { 34 | case undefined: 35 | case 'rpc-multicall': 36 | const provider = source?.customProvider ? buildProviderService(source.customProvider) : providerService; 37 | return new RPCBalanceSource(provider, logsService, source?.config); 38 | case 'cached': 39 | const underlying = buildSource(source.underlyingSource, { fetchService, providerService, logsService }); 40 | return new CachedBalanceSource(underlying, source.config); 41 | case 'custom': 42 | return source.instance; 43 | case 'fastest': 44 | return new FastestBalanceSource( 45 | source.sources.map((source) => buildSource(source, { fetchService, providerService, logsService })), 46 | logsService 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/sdk/builders/blocks-builder.ts: -------------------------------------------------------------------------------- 1 | import { IFetchService } from '@services/fetch'; 2 | import { IBlocksService, IBlocksSource } from '@services/blocks'; 3 | import { DefiLlamaBlockSource } from '@services/blocks/block-sources/defi-llama-block-source'; 4 | import { BlocksService } from '@services/blocks/block-service'; 5 | import { IProviderService } from '@services/providers'; 6 | 7 | export type BlocksSourceInput = { type: 'custom'; instance: IBlocksSource } | { type: 'defi-llama' }; 8 | export type BuildBlocksParams = { source: BlocksSourceInput }; 9 | 10 | export function buildBlocksService( 11 | params: BuildBlocksParams | undefined, 12 | fetchService: IFetchService, 13 | providerService: IProviderService 14 | ): IBlocksService { 15 | const source = buildSource(params?.source, { fetchService, providerService }); 16 | return new BlocksService(source); 17 | } 18 | 19 | function buildSource( 20 | source: BlocksSourceInput | undefined, 21 | { fetchService, providerService }: { fetchService: IFetchService; providerService: IProviderService } 22 | ): IBlocksSource { 23 | switch (source?.type) { 24 | case undefined: 25 | case 'defi-llama': 26 | return new DefiLlamaBlockSource(fetchService, providerService); 27 | case 'custom': 28 | return source.instance; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/sdk/builders/dca-builder.ts: -------------------------------------------------------------------------------- 1 | import { DCAService } from '@services/dca/dca-service'; 2 | import { IFetchService } from '@services/fetch'; 3 | import { IPermit2Service } from '@services/permit2'; 4 | import { IPriceService } from '@services/prices'; 5 | import { IProviderService } from '@services/providers'; 6 | import { IQuoteService } from '@services/quotes'; 7 | 8 | export type BuildDCAParams = { customAPIUrl?: string }; 9 | type Dependencies = { 10 | providerService: IProviderService; 11 | permit2Service: IPermit2Service; 12 | quoteService: IQuoteService; 13 | fetchService: IFetchService; 14 | priceService: IPriceService; 15 | }; 16 | export function buildDCAService( 17 | params: BuildDCAParams | undefined, 18 | { providerService, permit2Service, quoteService, fetchService, priceService }: Dependencies 19 | ) { 20 | return new DCAService( 21 | params?.customAPIUrl ?? 'https://api.balmy.xyz', 22 | providerService, 23 | permit2Service, 24 | quoteService, 25 | fetchService, 26 | priceService 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/sdk/builders/earn-builder.ts: -------------------------------------------------------------------------------- 1 | import { IAllowanceService } from '@services/allowances'; 2 | import { IBalanceService } from '@services/balances'; 3 | import { EarnService } from '@services/earn/earn-service'; 4 | import { IFetchService } from '@services/fetch'; 5 | import { IPermit2Service } from '@services/permit2'; 6 | import { IProviderService } from '@services/providers'; 7 | import { IQuoteService } from '@services/quotes'; 8 | 9 | export type BuildEarnParams = { customAPIUrl?: string }; 10 | type Dependencies = { 11 | permit2Service: IPermit2Service; 12 | quoteService: IQuoteService; 13 | providerService: IProviderService; 14 | allowanceService: IAllowanceService; 15 | fetchService: IFetchService; 16 | balanceService: IBalanceService; 17 | }; 18 | export function buildEarnService( 19 | params: BuildEarnParams | undefined, 20 | { permit2Service, quoteService, providerService, allowanceService, fetchService, balanceService }: Dependencies 21 | ) { 22 | return new EarnService( 23 | params?.customAPIUrl ?? 'https://api.balmy.xyz', 24 | permit2Service, 25 | quoteService, 26 | providerService, 27 | allowanceService, 28 | fetchService, 29 | balanceService 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/sdk/builders/fetch-builder.ts: -------------------------------------------------------------------------------- 1 | import { Fetch } from '@services/fetch/types'; 2 | import { FetchService } from '@services/fetch/fetch-service'; 3 | 4 | export type BuildFetchParams = { fetch?: Fetch }; 5 | 6 | export function buildFetchService(params?: BuildFetchParams) { 7 | return new FetchService(params?.fetch); 8 | } 9 | -------------------------------------------------------------------------------- /src/sdk/builders/index.ts: -------------------------------------------------------------------------------- 1 | export { BuildFetchParams } from './fetch-builder'; 2 | export { BuildGasParams, GasSourceInput } from './gas-builder'; 3 | export { BuildProviderParams, ProviderSourceInput, ProviderConfig } from './provider-builder'; 4 | export { BuildQuoteParams, QuoteSourceListInput } from './quote-builder'; 5 | export { BuildMetadataParams, MetadataSourceInput } from './metadata-builder'; 6 | export { BuildBalancesParams, BalanceSourceInput } from './balance-builder'; 7 | export { BuildPriceParams, PriceSourceInput } from './price-builder'; 8 | export { BuildAllowanceParams, AllowanceSourceInput } from './allowance-builder'; 9 | export { BuildLogsParams } from './logs-builder'; 10 | export { BuildBlocksParams, BlocksSourceInput } from './blocks-builder'; 11 | -------------------------------------------------------------------------------- /src/sdk/builders/logs-builder.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from '@services/logs'; 2 | import { LogsService } from '@services/logs/logs-service'; 3 | 4 | export type BuildLogsParams = { level?: LogLevel }; 5 | 6 | export function buildLogsService(params: BuildLogsParams | undefined) { 7 | return new LogsService(params?.level ?? 'WARN'); 8 | } 9 | -------------------------------------------------------------------------------- /src/sdk/builders/permit2-builder.ts: -------------------------------------------------------------------------------- 1 | import { IGasService } from '@services/gas'; 2 | import { Permit2Service } from '@services/permit2/permit2-service'; 3 | import { IProviderService } from '@services/providers'; 4 | import { IQuoteService } from '@services/quotes'; 5 | 6 | export function buildPermit2Service(quoteService: IQuoteService, providerService: IProviderService, gasService: IGasService) { 7 | return new Permit2Service(providerService, quoteService, gasService); 8 | } 9 | -------------------------------------------------------------------------------- /src/sdk/index.ts: -------------------------------------------------------------------------------- 1 | export { buildSDK, BuildParams } from './sdk-builder'; 2 | export { ISDK } from './types'; 3 | export * from './builders'; 4 | -------------------------------------------------------------------------------- /src/sdk/sdk-builder.ts: -------------------------------------------------------------------------------- 1 | import { buildLogsService, BuildLogsParams } from './builders/logs-builder'; 2 | import { BuildFetchParams, buildFetchService } from './builders/fetch-builder'; 3 | import { BuildProviderParams, buildProviderService } from './builders/provider-builder'; 4 | import { buildGasService, BuildGasParams, CalculateGasValuesFromSourceParams } from './builders/gas-builder'; 5 | import { BuildMetadataParams, buildMetadataService, CalculateMetadataFromSourceParams } from './builders/metadata-builder'; 6 | import { BuildQuoteParams, buildQuoteService } from './builders/quote-builder'; 7 | import { buildBalanceService, BuildBalancesParams } from './builders/balance-builder'; 8 | import { buildAllowanceService, BuildAllowanceParams } from './builders/allowance-builder'; 9 | import { ISDK } from './types'; 10 | import { BuildPriceParams, buildPriceService } from './builders/price-builder'; 11 | import { buildPermit2Service } from './builders/permit2-builder'; 12 | import { BuildDCAParams, buildDCAService } from './builders/dca-builder'; 13 | import { BuildBlocksParams, buildBlocksService } from './builders/blocks-builder'; 14 | import { BuildEarnParams, buildEarnService } from './builders/earn-builder'; 15 | 16 | export function buildSDK( 17 | params?: Params 18 | ): ISDK, CalculateGasValuesFromSourceParams> { 19 | const logsService = buildLogsService(params?.logs); 20 | const fetchService = buildFetchService(params?.fetch); 21 | const providerService = buildProviderService(params?.provider); 22 | const blocksService = buildBlocksService(params?.blocks, fetchService, providerService); 23 | const balanceService = buildBalanceService(params?.balances, fetchService, providerService, logsService); 24 | const allowanceService = buildAllowanceService(params?.allowances, fetchService, providerService); 25 | const gasService = buildGasService(params?.gas, logsService, fetchService, providerService); 26 | const metadataService = buildMetadataService(params?.metadata, fetchService, providerService); 27 | const priceService = buildPriceService(params?.price, fetchService); 28 | const quoteService = buildQuoteService(params?.quotes, providerService, fetchService, gasService as any, metadataService as any, priceService); 29 | const permit2Service = buildPermit2Service(quoteService, providerService, gasService as any); 30 | const dcaService = buildDCAService(params?.dca, { providerService, permit2Service, quoteService, fetchService, priceService }); 31 | const earnService = buildEarnService(params?.earn, { 32 | permit2Service, 33 | quoteService, 34 | providerService, 35 | allowanceService, 36 | fetchService, 37 | balanceService, 38 | }); 39 | 40 | return { 41 | providerService, 42 | fetchService, 43 | allowanceService, 44 | balanceService, 45 | gasService, 46 | metadataService, 47 | priceService, 48 | quoteService, 49 | logsService, 50 | permit2Service, 51 | dcaService, 52 | earnService, 53 | blocksService, 54 | }; 55 | } 56 | 57 | export type BuildParams = { 58 | fetch?: BuildFetchParams; 59 | provider?: BuildProviderParams; 60 | balances?: BuildBalancesParams; 61 | allowances?: BuildAllowanceParams; 62 | gas?: BuildGasParams; 63 | dca?: BuildDCAParams; 64 | earn?: BuildEarnParams; 65 | metadata?: BuildMetadataParams; 66 | price?: BuildPriceParams; 67 | quotes?: BuildQuoteParams; 68 | logs?: BuildLogsParams; 69 | blocks?: BuildBlocksParams; 70 | }; 71 | -------------------------------------------------------------------------------- /src/sdk/types.ts: -------------------------------------------------------------------------------- 1 | import { IAllowanceService } from '@services/allowances'; 2 | import { IBalanceService } from '@services/balances/types'; 3 | import { IFetchService } from '@services/fetch/types'; 4 | import { IGasService, SupportedGasValues } from '@services/gas/types'; 5 | import { IPriceService } from '@services/prices'; 6 | import { IProviderService } from '@services/providers'; 7 | import { IQuoteService } from '@services/quotes/types'; 8 | import { IMetadataService } from '@services/metadata/types'; 9 | import { CalculateMetadataFromSourceParams } from './builders/metadata-builder'; 10 | import { CalculateGasValuesFromSourceParams } from './builders/gas-builder'; 11 | import { ILogsService } from '@services/logs'; 12 | import { IPermit2Service } from '@services/permit2'; 13 | import { IDCAService } from '@services/dca'; 14 | import { IBlocksService } from '@services/blocks'; 15 | import { IEarnService } from '@services/earn'; 16 | 17 | export type ISDK< 18 | TokenMetadata extends object = CalculateMetadataFromSourceParams, 19 | GasValues extends SupportedGasValues = CalculateGasValuesFromSourceParams 20 | > = { 21 | providerService: IProviderService; 22 | fetchService: IFetchService; 23 | gasService: IGasService; 24 | allowanceService: IAllowanceService; 25 | balanceService: IBalanceService; 26 | quoteService: IQuoteService; 27 | priceService: IPriceService; 28 | logsService: ILogsService; 29 | metadataService: IMetadataService; 30 | permit2Service: IPermit2Service; 31 | dcaService: IDCAService; 32 | earnService: IEarnService; 33 | blocksService: IBlocksService; 34 | }; 35 | -------------------------------------------------------------------------------- /src/services/allowances/allowance-service.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, TimeString, TokenAddress } from '@types'; 2 | import { AllowanceInput, IAllowanceService, IAllowanceSource, OwnerAddress, SpenderAddress } from './types'; 3 | import { timeoutPromise } from '@shared/timeouts'; 4 | 5 | export class AllowanceService implements IAllowanceService { 6 | constructor(private readonly source: IAllowanceSource) {} 7 | 8 | supportedChains(): ChainId[] { 9 | return this.source.supportedChains(); 10 | } 11 | 12 | async getAllowanceInChain({ 13 | chainId, 14 | token, 15 | owner, 16 | spender, 17 | config, 18 | }: { 19 | chainId: ChainId; 20 | token: TokenAddress; 21 | owner: OwnerAddress; 22 | spender: SpenderAddress; 23 | config?: { timeout?: TimeString }; 24 | }): Promise { 25 | const result = await this.getAllowancesInChain({ 26 | chainId, 27 | allowances: [{ token, owner, spender }], 28 | config, 29 | }); 30 | return result[token][owner][spender]; 31 | } 32 | 33 | async getAllowancesInChain({ 34 | chainId, 35 | allowances, 36 | config, 37 | }: { 38 | chainId: ChainId; 39 | allowances: Omit[]; 40 | config?: { timeout?: TimeString }; 41 | }) { 42 | const result = await this.getAllowances({ 43 | allowances: allowances.map((allowance) => ({ chainId, ...allowance })), 44 | config, 45 | }); 46 | return result[chainId] ?? {}; 47 | } 48 | 49 | async getAllowances({ allowances, config }: { allowances: AllowanceInput[]; config?: { timeout?: TimeString } }) { 50 | return timeoutPromise( 51 | this.source.getAllowances({ 52 | allowances, 53 | config, 54 | }), 55 | config?.timeout 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/services/allowances/allowance-sources/cached-allowance-source.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, TimeString, TokenAddress } from '@types'; 2 | import { CacheConfig, ConcurrentLRUCache } from '@shared/concurrent-lru-cache'; 3 | import { AllowanceInput, IAllowanceSource, OwnerAddress, SpenderAddress } from '../types'; 4 | 5 | export class CachedAllowanceSource implements IAllowanceSource { 6 | private readonly cache: ConcurrentLRUCache; 7 | 8 | constructor(private readonly source: IAllowanceSource, config: CacheConfig) { 9 | this.cache = new ConcurrentLRUCache({ 10 | calculate: (ownerSpendersInChain) => this.fetchTokens(ownerSpendersInChain), 11 | config, 12 | }); 13 | } 14 | 15 | supportedChains(): ChainId[] { 16 | return this.source.supportedChains(); 17 | } 18 | 19 | async getAllowances({ 20 | allowances, 21 | config, 22 | }: { 23 | allowances: AllowanceInput[]; 24 | config?: { timeout?: TimeString }; 25 | }): Promise>>>> { 26 | const keys = allowanceChecksToKeys(allowances); 27 | const result = await this.cache.getOrCalculate({ keys, timeout: config?.timeout }); 28 | return keyResultsToResult(result); 29 | } 30 | 31 | private async fetchTokens(keys: Key[]): Promise> { 32 | const allowances = keysToAllowanceChecks(keys); 33 | const result = await this.source.getAllowances({ allowances }); 34 | return resultsToKeyResults(result); 35 | } 36 | } 37 | 38 | type Key = `${ChainId}-${TokenAddress}-${OwnerAddress}-${SpenderAddress}`; 39 | 40 | function allowanceChecksToKeys(allowances: AllowanceInput[]): Key[] { 41 | return allowances.map(({ chainId, token, owner, spender }) => toKey(chainId, token, owner, spender)); 42 | } 43 | 44 | function resultsToKeyResults(result: Record>>>) { 45 | const keyResults: Record = {}; 46 | for (const chainId in result) { 47 | for (const token in result[chainId]) { 48 | for (const owner in result[chainId][token]) { 49 | for (const spender in result[chainId][token][owner]) { 50 | const key = toKey(Number(chainId), token, owner, spender); 51 | keyResults[key] = result[chainId][token][owner][spender]; 52 | } 53 | } 54 | } 55 | } 56 | return keyResults; 57 | } 58 | 59 | function keysToAllowanceChecks(keys: Key[]) { 60 | return keys.map(fromKey); 61 | } 62 | 63 | function keyResultsToResult(keyResults: Record) { 64 | const result: Record>>> = {}; 65 | for (const [key, amount] of Object.entries(keyResults)) { 66 | const { chainId, token, owner, spender } = fromKey(key as Key); 67 | if (!(chainId in result)) result[chainId] = {}; 68 | if (!(token in result[chainId])) result[chainId][token] = {}; 69 | if (!(owner in result[chainId][token])) result[chainId][token][owner] = {}; 70 | result[chainId][token][owner][spender] = amount; 71 | } 72 | return result; 73 | } 74 | 75 | function toKey(chainId: ChainId, token: TokenAddress, owner: OwnerAddress, spender: SpenderAddress): Key { 76 | return `${chainId}-${token}-${owner}-${spender}`; 77 | } 78 | 79 | function fromKey(key: Key): { chainId: ChainId; token: TokenAddress; owner: OwnerAddress; spender: SpenderAddress } { 80 | const [chainId, token, owner, spender] = key.split('-'); 81 | return { chainId: Number(chainId), token, owner, spender }; 82 | } 83 | -------------------------------------------------------------------------------- /src/services/allowances/allowance-sources/rpc-allowance-source.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, TimeString, TokenAddress } from '@types'; 2 | import { AllowanceInput, IAllowanceSource, OwnerAddress, SpenderAddress } from '../types'; 3 | import { timeoutPromise } from '@shared/timeouts'; 4 | import { filterRejectedResults, groupByChain } from '@shared/utils'; 5 | import ERC20_ABI from '@shared/abis/erc20'; 6 | import { IProviderService } from '@services/providers'; 7 | import { Address as ViemAddress } from 'viem'; 8 | import { MULTICALL_CONTRACT } from '@services/providers/utils'; 9 | 10 | export class RPCAllowanceSource implements IAllowanceSource { 11 | constructor(private readonly providerService: IProviderService) {} 12 | 13 | supportedChains(): ChainId[] { 14 | return this.providerService.supportedChains(); 15 | } 16 | 17 | async getAllowances({ 18 | allowances, 19 | config, 20 | }: { 21 | allowances: AllowanceInput[]; 22 | config?: { timeout?: TimeString }; 23 | }): Promise>>>> { 24 | const groupedByChain = groupByChain(allowances); 25 | 26 | const promises = Object.entries(groupedByChain).map(async ([chainId, checks]) => [ 27 | Number(chainId), 28 | await timeoutPromise(this.getAllowancesInChain(Number(chainId), checks), config?.timeout, { reduceBy: '100' }), 29 | ]); 30 | return Object.fromEntries(await filterRejectedResults(promises)); 31 | } 32 | 33 | private async getAllowancesInChain(chainId: ChainId, checks: Omit[]) { 34 | const contracts = checks.map(({ token, owner, spender }) => ({ 35 | address: token as ViemAddress, 36 | abi: ERC20_ABI, 37 | functionName: 'allowance', 38 | args: [owner, spender], 39 | })); 40 | const multicallResults = contracts.length 41 | ? await this.providerService 42 | .getViemPublicClient({ chainId }) 43 | .multicall({ multicallAddress: MULTICALL_CONTRACT.address(chainId), contracts, batchSize: 0 }) 44 | : []; 45 | const result: Record>> = {}; 46 | for (let i = 0; i < multicallResults.length; i++) { 47 | const multicallResult = multicallResults[i]; 48 | if (multicallResult.status === 'failure') continue; 49 | const { token, owner, spender } = checks[i]; 50 | if (!(token in result)) result[token] = {}; 51 | if (!(owner in result[token])) result[token][owner] = {}; 52 | result[token][owner][spender] = multicallResult.result as unknown as bigint; 53 | } 54 | return result; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/services/allowances/index.ts: -------------------------------------------------------------------------------- 1 | export { IAllowanceService, IAllowanceSource, AllowanceInput } from './types'; 2 | -------------------------------------------------------------------------------- /src/services/allowances/types.ts: -------------------------------------------------------------------------------- 1 | import { Address, ChainId, TimeString, TokenAddress } from '@types'; 2 | 3 | export type OwnerAddress = Address; 4 | export type SpenderAddress = Address; 5 | 6 | export type IAllowanceService = { 7 | supportedChains(): ChainId[]; 8 | getAllowanceInChain(_: { 9 | chainId: ChainId; 10 | token: TokenAddress; 11 | owner: OwnerAddress; 12 | spender: SpenderAddress; 13 | config?: { timeout?: TimeString }; 14 | }): Promise; 15 | getAllowancesInChain(_: { 16 | chainId: ChainId; 17 | allowances: Omit[]; 18 | config?: { timeout?: TimeString }; 19 | }): Promise>>>; 20 | getAllowances(_: { 21 | allowances: AllowanceInput[]; 22 | config?: { timeout?: TimeString }; 23 | }): Promise>>>>; 24 | }; 25 | 26 | export type IAllowanceSource = { 27 | supportedChains(): ChainId[]; 28 | getAllowances(_: { 29 | allowances: AllowanceInput[]; 30 | config?: { timeout?: TimeString }; 31 | }): Promise>>>>; 32 | }; 33 | 34 | export type AllowanceInput = { 35 | chainId: ChainId; 36 | token: TokenAddress; 37 | owner: Address; 38 | spender: Address; 39 | }; 40 | -------------------------------------------------------------------------------- /src/services/balances/balance-service.ts: -------------------------------------------------------------------------------- 1 | import { Address, ChainId, TimeString, TokenAddress } from '@types'; 2 | import { BalanceInput, IBalanceService, IBalanceSource } from './types'; 3 | import { timeoutPromise } from '@shared/timeouts'; 4 | 5 | export class BalanceService implements IBalanceService { 6 | constructor(private readonly source: IBalanceSource) {} 7 | 8 | supportedChains(): ChainId[] { 9 | return this.source.supportedChains(); 10 | } 11 | 12 | async getBalancesForAccountInChain({ 13 | chainId, 14 | account, 15 | tokens, 16 | config, 17 | }: { 18 | chainId: ChainId; 19 | account: Address; 20 | tokens: TokenAddress[]; 21 | config?: { timeout?: TimeString }; 22 | }): Promise> { 23 | const result = await this.getBalancesForAccount({ 24 | account, 25 | tokens: tokens.map((token) => ({ chainId, token })), 26 | config, 27 | }); 28 | return result[chainId] ?? {}; 29 | } 30 | 31 | async getBalancesForAccount({ 32 | account, 33 | tokens, 34 | config, 35 | }: { 36 | account: Address; 37 | tokens: Omit[]; 38 | config?: { timeout?: TimeString }; 39 | }): Promise>> { 40 | const result = await this.getBalances({ tokens: tokens.map((token) => ({ account, ...token })), config }); 41 | const entries = Object.entries(result) 42 | .map<[ChainId, Record | undefined]>(([chainId, result]) => [Number(chainId), result[account]]) 43 | .filter((entry): entry is [ChainId, Record] => entry[1] !== undefined); 44 | return Object.fromEntries(entries); 45 | } 46 | 47 | async getBalancesInChain({ 48 | chainId, 49 | tokens, 50 | config, 51 | }: { 52 | chainId: ChainId; 53 | tokens: Omit[]; 54 | config?: { timeout?: TimeString }; 55 | }) { 56 | const result = await this.getBalances({ tokens: tokens.map((token) => ({ chainId, ...token })), config }); 57 | return result[chainId] ?? {}; 58 | } 59 | 60 | getBalances({ tokens, config }: { tokens: BalanceInput[]; config?: { timeout?: TimeString } }) { 61 | return timeoutPromise(this.source.getBalances({ tokens, config }), config?.timeout); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/services/balances/balance-sources/cached-balance-source.ts: -------------------------------------------------------------------------------- 1 | import { Address, ChainId, TimeString, TokenAddress } from '@types'; 2 | import { CacheConfig, ConcurrentLRUCacheWithContext } from '@shared/concurrent-lru-cache'; 3 | import { BalanceInput, IBalanceSource } from '../types'; 4 | 5 | type Config = { timeout?: TimeString } | undefined; 6 | export class CachedBalanceSource implements IBalanceSource { 7 | private readonly cache: ConcurrentLRUCacheWithContext; 8 | 9 | constructor(private readonly source: IBalanceSource, config: CacheConfig) { 10 | this.cache = new ConcurrentLRUCacheWithContext({ 11 | calculate: (config, keysTokenInChain) => this.fetchBalancesForTokens(keysTokenInChain, config), 12 | config, 13 | }); 14 | } 15 | 16 | supportedChains() { 17 | return this.source.supportedChains(); 18 | } 19 | 20 | async getBalances({ 21 | tokens, 22 | config, 23 | }: { 24 | tokens: BalanceInput[]; 25 | config?: { timeout?: TimeString }; 26 | }): Promise>>> { 27 | const keys = tokens.map(({ chainId, account, token }) => toKeyTokenInChain(chainId, account, token)); 28 | const amountsInChain = await this.cache.getOrCalculate({ 29 | keys, 30 | timeout: config?.timeout, 31 | context: config, 32 | }); 33 | 34 | const result: Record>> = {}; 35 | for (const key in amountsInChain) { 36 | const { chainId, account, token } = fromKeyTokenInChain(key as KeyTokenInChain); 37 | if (!(chainId in result)) result[chainId] = {}; 38 | if (!(account in result[chainId])) result[chainId][account] = {}; 39 | result[chainId][account][token] = amountsInChain[key as KeyTokenInChain]; 40 | } 41 | 42 | return result; 43 | } 44 | 45 | private async fetchBalancesForTokens(keys: KeyTokenInChain[], config: Config): Promise> { 46 | const tokens = keys.map((key) => fromKeyTokenInChain(key)); 47 | const balances = await this.source.getBalances({ tokens, config }); 48 | 49 | const result: Record = {}; 50 | for (const key of keys) { 51 | const { chainId, account, token } = fromKeyTokenInChain(key as KeyTokenInChain); 52 | const balance = balances?.[chainId]?.[account]?.[token]; 53 | if (balance !== undefined) { 54 | result[key as KeyTokenInChain] = balance; 55 | } 56 | } 57 | return result; 58 | } 59 | } 60 | 61 | type KeyTokenInChain = `${ChainId}-${Address}-${TokenAddress}`; 62 | 63 | function toKeyTokenInChain(chainId: ChainId, account: Address, token: TokenAddress): KeyTokenInChain { 64 | return `${chainId}-${account}-${token}`; 65 | } 66 | 67 | function fromKeyTokenInChain(key: KeyTokenInChain): { chainId: ChainId; account: Address; token: TokenAddress } { 68 | const [chainId, account, token] = key.split('-'); 69 | return { chainId: Number(chainId), account, token }; 70 | } 71 | -------------------------------------------------------------------------------- /src/services/balances/balance-sources/fastest-balance-source.ts: -------------------------------------------------------------------------------- 1 | import { reduceTimeout, timeoutPromise } from '@shared/timeouts'; 2 | import { Address, ChainId, TimeString, TokenAddress } from '@types'; 3 | import { chainsUnion } from '@chains'; 4 | import { ILogger, ILogsService } from '@services/logs'; 5 | import { filterRequestForSource, fillResponseWithNewResult, doesResponseFulfilRequest, getSourcesThatSupportRequestOrFail } from './utils'; 6 | import { IBalanceSource, BalanceInput } from '../types'; 7 | 8 | // This source will take a list of sources and combine the results of each one to try to fulfil 9 | // the request. As soon as there there is a response that is valid for the request, it will be returned 10 | export class FastestBalanceSource implements IBalanceSource { 11 | private readonly logger: ILogger; 12 | 13 | constructor(private readonly sources: IBalanceSource[], logs: ILogsService) { 14 | if (sources.length === 0) throw new Error('No sources were specified'); 15 | this.logger = logs.getLogger({ name: 'FastestBalanceSource' }); 16 | } 17 | 18 | supportedChains() { 19 | return chainsUnion(this.sources.map((source) => source.supportedChains())); 20 | } 21 | 22 | getBalances({ 23 | tokens, 24 | config, 25 | }: { 26 | tokens: BalanceInput[]; 27 | config?: { timeout?: TimeString }; 28 | }): Promise>>> { 29 | const sourcesInChains = getSourcesThatSupportRequestOrFail(tokens, this.sources); 30 | const reducedTimeout = reduceTimeout(config?.timeout, '100'); 31 | return new Promise>>>(async (resolve, reject) => { 32 | const result: Record>> = {}; 33 | const allPromises = sourcesInChains.map((source) => 34 | timeoutPromise( 35 | source.getBalances({ 36 | tokens: filterRequestForSource(tokens, source), 37 | config: { timeout: reducedTimeout }, 38 | }), 39 | reducedTimeout 40 | ).then((response) => { 41 | fillResponseWithNewResult(result, response); 42 | if (doesResponseFulfilRequest(result, tokens).ok) { 43 | resolve(result); 44 | } 45 | }) 46 | ); 47 | 48 | Promise.allSettled(allPromises).then(() => { 49 | const isOk = doesResponseFulfilRequest(result, tokens); 50 | if (!isOk.ok) { 51 | // We couldn't fulfil the request, so we know we didn't resolve. We will revert then 52 | const missingText = isOk.missing.map(({ chainId, account, token }) => `${chainId}-${account}-${token}`).join(','); 53 | reject(new Error(`Failed to fulfil request: missing: ${missingText}`)); 54 | } 55 | }); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/services/balances/balance-sources/utils.ts: -------------------------------------------------------------------------------- 1 | import { Address, ChainId, TokenAddress } from '@types'; 2 | import { IBalanceSource, BalanceInput } from '../types'; 3 | 4 | export function fillResponseWithNewResult( 5 | result: Record>>, 6 | newResult: Record>> 7 | ) { 8 | for (const chainId in newResult) { 9 | if (!(chainId in result)) result[chainId] = {}; 10 | for (const address in newResult[chainId]) { 11 | if (!(address in result[chainId])) result[chainId][address] = {}; 12 | for (const token in newResult[chainId][address]) { 13 | if (!result[chainId]?.[address]?.[token]) { 14 | result[chainId][address][token] = newResult[chainId][address][token]; 15 | } 16 | } 17 | } 18 | } 19 | } 20 | 21 | export function doesResponseFulfilRequest( 22 | result: Record>>, 23 | request: BalanceInput[] 24 | ): { ok: true } | { ok: false; missing: { chainId: ChainId; account: Address; token: TokenAddress }[] } { 25 | const missing: { chainId: ChainId; account: Address; token: TokenAddress }[] = []; 26 | for (const { chainId, token, account } of request) { 27 | if (typeof result[chainId]?.[account]?.[token] === 'undefined') { 28 | missing.push({ chainId, account, token }); 29 | } 30 | } 31 | return missing.length > 0 ? { ok: false, missing } : { ok: true }; 32 | } 33 | 34 | export function filterRequestForSource(request: BalanceInput[], source: IBalanceSource) { 35 | const support = source.supportedChains(); 36 | return request.filter(({ chainId }) => support.includes(chainId)); 37 | } 38 | 39 | export function getSourcesThatSupportRequestOrFail(request: BalanceInput[], sources: IBalanceSource[]) { 40 | const chainsInRequest = new Set(request.map(({ chainId }) => chainId)); 41 | const sourcesInChain = sources.filter((source) => source.supportedChains().some((chainId) => chainsInRequest.has(chainId))); 42 | if (sourcesInChain.length === 0) throw new Error('Operation not supported'); 43 | return sourcesInChain; 44 | } 45 | -------------------------------------------------------------------------------- /src/services/balances/index.ts: -------------------------------------------------------------------------------- 1 | export { IBalanceService, IBalanceSource, BalanceInput } from './types'; 2 | -------------------------------------------------------------------------------- /src/services/balances/types.ts: -------------------------------------------------------------------------------- 1 | import { Address, ChainId, TimeString, TokenAddress } from '@types'; 2 | 3 | type Account = Address; 4 | 5 | export type IBalanceService = { 6 | supportedChains(): ChainId[]; 7 | getBalancesForAccountInChain(_: { 8 | chainId: ChainId; 9 | account: Address; 10 | tokens: TokenAddress[]; 11 | config?: { timeout?: TimeString }; 12 | }): Promise>; 13 | getBalancesForAccount(_: { 14 | account: Address; 15 | tokens: Omit[]; 16 | config?: { timeout?: TimeString }; 17 | }): Promise>>; 18 | getBalancesInChain(_: { 19 | chainId: ChainId; 20 | tokens: Omit[]; 21 | config?: { timeout?: TimeString }; 22 | }): Promise>>; 23 | getBalances(_: { 24 | tokens: BalanceInput[]; 25 | config?: { timeout?: TimeString }; 26 | }): Promise>>>; 27 | }; 28 | 29 | export type IBalanceSource = { 30 | supportedChains(): ChainId[]; 31 | getBalances(_: { 32 | tokens: BalanceInput[]; 33 | config?: { timeout?: TimeString }; 34 | }): Promise>>>; 35 | }; 36 | 37 | export type BalanceInput = { 38 | chainId: ChainId; 39 | account: Account; 40 | token: TokenAddress; 41 | }; 42 | -------------------------------------------------------------------------------- /src/services/blocks/block-service.ts: -------------------------------------------------------------------------------- 1 | import { StringValue } from 'ms'; 2 | import { ChainId, TimeString, Timestamp } from '@types'; 3 | import { timeoutPromise } from '@shared/timeouts'; 4 | import { BlockInput, IBlocksService, IBlocksSource } from './types'; 5 | 6 | export class BlocksService implements IBlocksService { 7 | constructor(private readonly source: IBlocksSource) {} 8 | 9 | supportedChains(): ChainId[] { 10 | return this.source.supportedChains(); 11 | } 12 | 13 | async getBlockClosestToTimestampInChain({ 14 | chainId, 15 | timestamp, 16 | config, 17 | }: { 18 | chainId: ChainId; 19 | timestamp: Timestamp; 20 | config?: { timeout?: TimeString }; 21 | }) { 22 | const result = await this.getBlocksClosestToTimestampsInChain({ chainId, timestamps: [timestamp], config }); 23 | return result[timestamp]; 24 | } 25 | 26 | async getBlocksClosestToTimestampsInChain({ 27 | chainId, 28 | timestamps, 29 | config, 30 | }: { 31 | chainId: ChainId; 32 | timestamps: Timestamp[]; 33 | config?: { timeout?: StringValue }; 34 | }) { 35 | const result = await this.getBlocksClosestToTimestamps({ timestamps: timestamps.map((timestamp) => ({ chainId, timestamp })), config }); 36 | return result[chainId]; 37 | } 38 | 39 | async getBlocksClosestToTimestamps({ timestamps, config }: { timestamps: BlockInput[]; config?: { timeout?: TimeString } }) { 40 | if (timestamps.length === 0) return {}; 41 | return timeoutPromise(this.source.getBlocksClosestToTimestamps({ timestamps, config }), config?.timeout); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/services/blocks/block-sources/defi-llama-block-source.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Timestamp } from '@types'; 2 | import { IFetchService } from '@services/fetch'; 3 | import { DefiLlamaClient } from '@shared/defi-llama'; 4 | import { BlockInput, BlockResult, IBlocksSource } from '../types'; 5 | import { IProviderService } from '@services/providers'; 6 | 7 | export class DefiLlamaBlockSource implements IBlocksSource { 8 | private readonly defiLlama: DefiLlamaClient; 9 | private readonly providerService: IProviderService; 10 | 11 | constructor(fetch: IFetchService, providerService: IProviderService) { 12 | this.defiLlama = new DefiLlamaClient(fetch); 13 | this.providerService = providerService; 14 | } 15 | 16 | supportedChains(): ChainId[] { 17 | return this.defiLlama.supportedChains(); 18 | } 19 | 20 | async getBlocksClosestToTimestamps({ timestamps }: { timestamps: BlockInput[] }): Promise>> { 21 | const result: Record> = {}; 22 | const promises: Promise[] = []; 23 | for (const { chainId, timestamp } of timestamps) { 24 | if (!(chainId in result)) result[chainId] = {}; 25 | const promise = this.defiLlama 26 | .getClosestBlock(chainId, timestamp) 27 | .then((block) => (result[chainId][timestamp] = block)) 28 | .catch(async (e) => { 29 | const provider = this.providerService.getViemPublicClient({ chainId }); 30 | 31 | // We're getting a timestamp value of 0n for genesis block, so we will use block 1n for now 32 | const blockOne = await provider.getBlock({ blockNumber: 1n }); 33 | const blockTimestamp = Number(blockOne.timestamp); 34 | if (timestamp < blockTimestamp) { 35 | result[chainId][timestamp] = { block: 1n, timestamp: blockTimestamp }; 36 | } else { 37 | throw e; 38 | } 39 | }); 40 | promises.push(promise); 41 | } 42 | await Promise.all(promises); 43 | return result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/services/blocks/index.ts: -------------------------------------------------------------------------------- 1 | export { IBlocksService, IBlocksSource, BlockResult, BlockInput } from './types'; 2 | -------------------------------------------------------------------------------- /src/services/blocks/types.ts: -------------------------------------------------------------------------------- 1 | import { BlockNumber } from 'viem'; 2 | import { ChainId, TimeString, Timestamp } from '@types'; 3 | 4 | export type BlockResult = { block: BlockNumber; timestamp: Timestamp }; 5 | 6 | export type IBlocksService = { 7 | supportedChains(): ChainId[]; 8 | getBlockClosestToTimestampInChain(_: { chainId: ChainId; timestamp: Timestamp; config?: { timeout?: TimeString } }): Promise; 9 | getBlocksClosestToTimestampsInChain(_: { 10 | chainId: ChainId; 11 | timestamps: Timestamp[]; 12 | config?: { timeout?: TimeString }; 13 | }): Promise>; 14 | getBlocksClosestToTimestamps(_: { 15 | timestamps: BlockInput[]; 16 | config?: { timeout?: TimeString }; 17 | }): Promise>>; 18 | }; 19 | 20 | export type IBlocksSource = { 21 | supportedChains(): ChainId[]; 22 | getBlocksClosestToTimestamps(_: { 23 | timestamps: BlockInput[]; 24 | config?: { timeout?: TimeString }; 25 | }): Promise>>; 26 | }; 27 | 28 | export type BlockInput = { 29 | chainId: ChainId; 30 | timestamp: Timestamp; 31 | }; 32 | -------------------------------------------------------------------------------- /src/services/dca/config.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { PERMIT2_ADAPTER_CONTRACT } from '@services/permit2/utils/config'; 3 | import { Contract } from '@shared/contracts'; 4 | 5 | export const DCA_HUB_CONTRACT = Contract.with({ defaultAddress: '0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345' }) 6 | .and({ address: '0x8CC0Df843610cefF7f4AFa01100B6abf6756Bdf2', onChain: Chains.ROOTSTOCK.chainId }) 7 | .build(); 8 | export const DCA_PERMISSION_MANAGER_CONTRACT = Contract.with({ defaultAddress: '0x20bdAE1413659f47416f769a4B27044946bc9923' }) 9 | .and({ address: '0x1EE410Fc840cC13C4e1b17DC6f93E245a918c19e', onChain: Chains.ROOTSTOCK.chainId }) 10 | .build(); 11 | export const COMPANION_CONTRACT = Contract.with({ defaultAddress: '0x6C615481E96806edBd9987B6E522A4Ea85d13659' }) 12 | .and({ address: '0x5872E8D5Ec9Dbf67949FdD4B5e05707644D60876', onChain: Chains.ROOTSTOCK.chainId }) 13 | .and({ address: '0x73376120621b243bC59c988cf172d23D5093e9F9', onChain: Chains.AVALANCHE.chainId }) 14 | .build(); 15 | export const COMPANION_SWAPPER_CONTRACT = PERMIT2_ADAPTER_CONTRACT; 16 | -------------------------------------------------------------------------------- /src/services/dca/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | IDCAService, 3 | DCAPermission, 4 | DCASwapInterval, 5 | DCAPermissionSet, 6 | CreateDCAPositionParams, 7 | IncreaseDCAPositionParams, 8 | ReduceDCAPositionParams, 9 | ReduceToBuyDCAPositionParams, 10 | WithdrawDCAPositionParams, 11 | TerminateDCAPositionParams, 12 | DCAActionSwapConfig, 13 | DCAPermissionPermit, 14 | AddFunds, 15 | SupportedPair, 16 | PairInChain, 17 | SwapIntervalData, 18 | TokenVariantPair, 19 | SupportedDCAToken, 20 | TokenVariant, 21 | TokenVariantOriginal, 22 | TokenVariantWrapper, 23 | TokenVariantYield, 24 | TokenVariantId, 25 | PositionId, 26 | PositionSummary, 27 | ActionTypeAction, 28 | PositionFunds, 29 | DCAPositionToken, 30 | PlatformMessage, 31 | DCAPositionAction, 32 | ActionType, 33 | CreatedAction, 34 | ModifiedAction, 35 | WithdrawnAction, 36 | TerminatedAction, 37 | TransferredAction, 38 | PermissionsModifiedAction, 39 | SwappedAction, 40 | DCATransaction, 41 | TokenInPair, 42 | DCASwap, 43 | } from './types'; 44 | -------------------------------------------------------------------------------- /src/services/earn/config.ts: -------------------------------------------------------------------------------- 1 | import { PERMIT2_ADAPTER_CONTRACT } from '@services/permit2/utils/config'; 2 | import { Contract } from '@shared/contracts'; 3 | 4 | export const EARN_VAULT = Contract.with({ defaultAddress: '0x0990a4a641636D437Af9aa214a1A580377eF1954' }).build(); 5 | 6 | export const EARN_VAULT_COMPANION = Contract.with({ defaultAddress: '0x5cb7667A29D2029aC2e38aA43F0608b620FAd087' }).build(); 7 | 8 | export const COMPANION_SWAPPER_CONTRACT = PERMIT2_ADAPTER_CONTRACT; 9 | 10 | export const DELAYED_WITHDRAWAL_MANAGER = Contract.with({ defaultAddress: '0x0ed7f185b12f8C5Cb91daA16edDb1778E404d5D0' }).build(); 11 | 12 | export const EARN_STRATEGY_ROUTER = Contract.with({ defaultAddress: '0xaA2e04112B149Dc415d3F29fc53dD97647ddeE30' }).build(); 13 | 14 | export const EXTERNAL_FIREWALL = Contract.with({ defaultAddress: '0xdcD1B12ab4941D1D2761119cd5f9B0C4a58e8eda' }).build(); 15 | -------------------------------------------------------------------------------- /src/services/earn/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | IEarnService, 3 | CreateEarnPositionParams, 4 | IncreaseEarnPositionParams, 5 | WithdrawEarnPositionParams, 6 | EarnPermission, 7 | EarnPermissionSet, 8 | EarnPermissionPermit, 9 | AddFundsEarn, 10 | EarnActionSwapConfig, 11 | Token, 12 | DepositToken, 13 | } from './types'; 14 | -------------------------------------------------------------------------------- /src/services/fetch/fetch-service.ts: -------------------------------------------------------------------------------- 1 | import ms from 'ms'; 2 | import crossFetch from 'cross-fetch'; 3 | import { Fetch, IFetchService, RequestInit } from './types'; 4 | import { TimeoutError } from '@shared/timeouts'; 5 | import { wait } from '@shared/wait'; 6 | 7 | export class FetchService implements IFetchService { 8 | constructor(private readonly realFetch: Fetch = crossFetch) {} 9 | 10 | async fetch(url: RequestInfo | URL, init?: RequestInit) { 11 | const { retries = 0, retryDelay = 1000, retryWhenTimeouted = true, timeout: timeoutText = '5m', ...restInit } = init ?? {}; 12 | 13 | const errors: Error[] = []; 14 | 15 | for (let attempt = 0; attempt <= retries; attempt++) { 16 | const controller = new AbortController(); 17 | let timeouted = false; 18 | const timeoutId = setTimeout(() => { 19 | timeouted = true; 20 | controller.abort(); 21 | }, ms(timeoutText)); 22 | 23 | try { 24 | const response = await this.realFetch(url, { 25 | ...restInit, 26 | signal: controller.signal as AbortSignal, 27 | }); 28 | return response; 29 | } catch (error: any) { 30 | if (timeouted) { 31 | const timeoutError = new TimeoutError(`Request to ${url}`, timeoutText); 32 | errors.push(timeoutError); 33 | if (!retryWhenTimeouted || attempt === retries) { 34 | throw new AggregateError(errors); 35 | } 36 | } else { 37 | errors.push(error); 38 | if (attempt === retries) { 39 | throw new AggregateError(errors); 40 | } 41 | } 42 | 43 | // Calculate delay with exponential backoff: delay * 2^attempt 44 | const backoffDelay = retryDelay * Math.pow(2, attempt + 1); 45 | await wait(backoffDelay); 46 | } finally { 47 | clearTimeout(timeoutId); 48 | } 49 | } 50 | 51 | throw new AggregateError(errors, 'Multiple fetch attempts failed'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/services/fetch/index.ts: -------------------------------------------------------------------------------- 1 | export { RequestInit, IFetchService } from './types'; 2 | -------------------------------------------------------------------------------- /src/services/fetch/types.ts: -------------------------------------------------------------------------------- 1 | import { TimeString } from '@types'; 2 | 3 | export type RequestInit = globalThis.RequestInit & { 4 | timeout?: TimeString; 5 | retries?: number; 6 | retryDelay?: number; 7 | retryWhenTimeouted?: boolean; 8 | }; 9 | 10 | export type IFetchService = { 11 | fetch(input: RequestInfo | URL, init?: RequestInit): Promise; 12 | }; 13 | 14 | export type Fetch = typeof global.fetch; 15 | -------------------------------------------------------------------------------- /src/services/gas/gas-price-sources/etherscan-gas-price-source.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, FieldsRequirements, SupportRecord, TimeString } from '@types'; 2 | import { IGasPriceSource, GasPrice, GasPriceResult, GasValueForVersions } from '@services/gas/types'; 3 | import { IFetchService } from '@services/fetch/types'; 4 | import { Chains } from '@chains'; 5 | import { parseUnits } from 'viem'; 6 | 7 | const CHAINS = { 8 | [Chains.ETHEREUM.chainId]: 'etherscan.io', 9 | [Chains.POLYGON.chainId]: 'polygonscan.com', 10 | [Chains.BNB_CHAIN.chainId]: 'bscscan.com', 11 | [Chains.FANTOM.chainId]: 'ftmscan.com', 12 | }; 13 | 14 | type GasValues = GasValueForVersions<'standard' | 'fast' | 'instant'>; 15 | export class EtherscanGasPriceSource implements IGasPriceSource { 16 | constructor(private readonly fetchService: IFetchService, private readonly apiKeys?: Record) {} 17 | 18 | supportedSpeeds() { 19 | const support: SupportRecord = { standard: 'present', fast: 'present', instant: 'present' }; 20 | return Object.fromEntries(Object.keys(CHAINS).map((chainId) => [Number(chainId), support])); 21 | } 22 | 23 | async getGasPrice>({ 24 | chainId, 25 | config, 26 | }: { 27 | chainId: ChainId; 28 | config?: { timeout?: TimeString }; 29 | }) { 30 | let url = `https://api.${CHAINS[chainId]}/api?module=gastracker&action=gasoracle`; 31 | if (this.apiKeys?.[chainId]) { 32 | url += `&apikey=${this.apiKeys[chainId]} `; 33 | } 34 | 35 | const response = await this.fetchService.fetch(url, { timeout: config?.timeout }); 36 | const { 37 | result: { SafeGasPrice, ProposeGasPrice, FastGasPrice, suggestBaseFee }, 38 | }: { result: { SafeGasPrice: `${number}`; ProposeGasPrice: `${number}`; FastGasPrice: `${number}`; suggestBaseFee?: `${number}` } } = 39 | await response.json(); 40 | return { 41 | standard: calculateGas(SafeGasPrice, suggestBaseFee), 42 | fast: calculateGas(ProposeGasPrice, suggestBaseFee), 43 | instant: calculateGas(FastGasPrice, suggestBaseFee), 44 | } as GasPriceResult; 45 | } 46 | } 47 | 48 | function calculateGas(price: `${number}`, baseFee?: `${number}`): GasPrice { 49 | const gasPrice = parseUnits(price, 9); 50 | if (!baseFee) return { gasPrice }; 51 | const base = parseUnits(baseFee, 9); 52 | return { 53 | maxFeePerGas: gasPrice, 54 | maxPriorityFeePerGas: gasPrice - base, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/services/gas/gas-price-sources/fastest-gas-price-source-combinator.ts: -------------------------------------------------------------------------------- 1 | import { couldSupportMeetRequirements, combineSourcesSupport, doesResponseMeetRequirements } from '@shared/requirements-and-support'; 2 | import { ChainId, FieldsRequirements, TimeString } from '@types'; 3 | import { IGasPriceSource, MergeGasValues, GasPriceResult } from '../types'; 4 | 5 | // This source will take a list of sources, and try to calculate the gas price on all of them, returning 6 | // the one that resolves first 7 | export class FastestGasPriceSourceCombinator[] | []> 8 | implements IGasPriceSource> 9 | { 10 | constructor(private readonly sources: Sources) { 11 | if (sources.length === 0) throw new Error('No sources were specified'); 12 | } 13 | 14 | supportedSpeeds() { 15 | return combineSourcesSupport, MergeGasValues>(this.sources, (source) => source.supportedSpeeds()); 16 | } 17 | 18 | async getGasPrice>>({ 19 | chainId, 20 | config, 21 | }: { 22 | chainId: ChainId; 23 | config?: { fields?: Requirements; timeout?: TimeString }; 24 | }) { 25 | const sourcesInChain = this.sources.filter( 26 | (source) => chainId in source.supportedSpeeds() && couldSupportMeetRequirements(source.supportedSpeeds()[chainId], config?.fields) 27 | ); 28 | if (sourcesInChain.length === 0) throw new Error(`Chain with id ${chainId} cannot support the given requirements`); 29 | const gasResults = sourcesInChain.map((source) => 30 | source 31 | .getGasPrice({ chainId, config }) 32 | .then((response) => 33 | failIfResponseDoesNotMeetRequirements(response, config?.fields ?? ({} as FieldsRequirements>)) 34 | ) 35 | ); 36 | return Promise.any(gasResults) as Promise, Requirements>>; 37 | } 38 | } 39 | 40 | function failIfResponseDoesNotMeetRequirements>( 41 | response: GasPriceResult, 42 | requirements: Requirements | undefined 43 | ) { 44 | if (!doesResponseMeetRequirements(response, requirements)) { 45 | throw new Error('Failed to meet requirements'); 46 | } 47 | return response; 48 | } 49 | -------------------------------------------------------------------------------- /src/services/gas/gas-price-sources/open-ocean-gas-price-source.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, FieldsRequirements, SupportRecord, TimeString } from '@types'; 2 | import { IGasPriceSource, GasSpeed, GasPriceResult, GasValueForVersions } from '@services/gas/types'; 3 | import { IFetchService } from '@services/fetch/types'; 4 | import { Chains } from '@chains'; 5 | 6 | const SUPPORTED_CHAINS = [ 7 | Chains.ETHEREUM, 8 | Chains.POLYGON, 9 | Chains.BNB_CHAIN, 10 | Chains.FANTOM, 11 | Chains.AVALANCHE, 12 | Chains.HECO, 13 | Chains.OKC, 14 | Chains.GNOSIS, 15 | Chains.ARBITRUM, 16 | Chains.OPTIMISM, 17 | Chains.CRONOS, 18 | Chains.MOONRIVER, 19 | Chains.BOBA, 20 | Chains.POLYGON_ZKEVM, 21 | Chains.KAVA, 22 | Chains.CELO, 23 | Chains.BASE, 24 | ]; 25 | 26 | type GasValues = GasValueForVersions<'standard' | 'fast' | 'instant'>; 27 | export class OpenOceanGasPriceSource implements IGasPriceSource { 28 | constructor(private readonly fetchService: IFetchService) {} 29 | 30 | supportedSpeeds() { 31 | const support: SupportRecord = { standard: 'present', fast: 'present', instant: 'present' }; 32 | return Object.fromEntries(SUPPORTED_CHAINS.map(({ chainId }) => [Number(chainId), support])); 33 | } 34 | 35 | async getGasPrice>({ 36 | chainId, 37 | config, 38 | }: { 39 | chainId: ChainId; 40 | config?: { timeout?: TimeString }; 41 | }) { 42 | const response = await this.fetchService.fetch(`https://ethapi.openocean.finance/v2/${chainId}/gas-price`, { timeout: config?.timeout }); 43 | const body = await response.json(); 44 | const result = 45 | typeof body.standard === 'string' || typeof body.standard === 'number' 46 | ? { 47 | standard: stringToLegacyGasPrice(body, 'standard'), 48 | fast: stringToLegacyGasPrice(body, 'fast'), 49 | instant: stringToLegacyGasPrice(body, 'instant'), 50 | } 51 | : { 52 | standard: toEip1159GasPrice(body, 'standard'), 53 | fast: toEip1159GasPrice(body, 'fast'), 54 | instant: toEip1159GasPrice(body, 'instant'), 55 | }; 56 | return result as GasPriceResult; 57 | } 58 | } 59 | 60 | function toEip1159GasPrice(body: any, key: GasSpeed) { 61 | const { maxPriorityFeePerGas, maxFeePerGas } = body[key]; 62 | return { 63 | maxFeePerGas: BigInt(maxFeePerGas), 64 | maxPriorityFeePerGas: BigInt(maxPriorityFeePerGas), 65 | }; 66 | } 67 | 68 | function stringToLegacyGasPrice(body: any, key: GasSpeed) { 69 | return { gasPrice: BigInt(body[key]) }; 70 | } 71 | -------------------------------------------------------------------------------- /src/services/gas/gas-price-sources/owlracle-gas-price-source.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, FieldsRequirements, SupportRecord, TimeString } from '@types'; 2 | import { IGasPriceSource, GasPrice, GasPriceResult, GasValueForVersions } from '@services/gas/types'; 3 | import { IFetchService } from '@services/fetch/types'; 4 | import { Chains } from '@chains'; 5 | 6 | const CHAINS: Record = { 7 | [Chains.ETHEREUM.chainId]: 'eth', 8 | [Chains.AVALANCHE.chainId]: 'avax', 9 | [Chains.BNB_CHAIN.chainId]: 'bsc', 10 | [Chains.POLYGON.chainId]: 'poly', 11 | [Chains.ARBITRUM.chainId]: 'arb', 12 | [Chains.OPTIMISM.chainId]: 'opt', 13 | [Chains.CRONOS.chainId]: 'cro', 14 | [Chains.FANTOM.chainId]: 'ftm', 15 | [Chains.AURORA.chainId]: 'aurora', 16 | [Chains.MOONRIVER.chainId]: 'movr', 17 | [Chains.HECO.chainId]: 'ht', 18 | [Chains.CELO.chainId]: 'celo', 19 | [Chains.HARMONY_SHARD_0.chainId]: 'one', 20 | [Chains.FUSE.chainId]: 'fuse', 21 | }; 22 | const DEFAULT_CONFIG: Config = { 23 | blocks: 200, 24 | percentile: 0.3, 25 | accept: { 26 | standard: 60, 27 | fast: 90, 28 | instant: 95, 29 | }, 30 | }; 31 | type GasValues = GasValueForVersions<'standard' | 'fast' | 'instant'>; 32 | export class OwlracleGasPriceSource implements IGasPriceSource { 33 | private readonly config: Config; 34 | 35 | constructor(private readonly fetchService: IFetchService, private readonly apiKey: string, config?: Partial) { 36 | this.config = { ...DEFAULT_CONFIG, ...config }; 37 | } 38 | 39 | supportedSpeeds() { 40 | const support: SupportRecord = { standard: 'present', fast: 'present', instant: 'present' }; 41 | return Object.fromEntries(Object.keys(CHAINS).map((chainId) => [Number(chainId), support])); 42 | } 43 | 44 | async getGasPrice>({ 45 | chainId, 46 | config, 47 | }: { 48 | chainId: ChainId; 49 | config?: { timeout?: TimeString }; 50 | }) { 51 | const key = CHAINS[chainId]; 52 | const accept = [this.config.accept.standard, this.config.accept.fast, this.config.accept.instant].join(','); 53 | const response = await this.fetchService.fetch( 54 | `https://api.owlracle.info/v3/${key}/gas` + 55 | `?apikey=${this.apiKey}` + 56 | `&reportwei=true` + 57 | `&feeinusd=false` + 58 | `&accept=${accept}` + 59 | `&percentile=${this.config.percentile}` + 60 | `&blocks=${this.config.blocks}`, 61 | { timeout: config?.timeout } 62 | ); 63 | const { speeds }: { speeds: GasPrice[] } = await response.json(); 64 | const [standard, fast, instant] = speeds; 65 | return { 66 | standard: filterOutExtraData(standard), 67 | fast: filterOutExtraData(fast), 68 | instant: filterOutExtraData(instant), 69 | } as GasPriceResult; 70 | } 71 | } 72 | 73 | function filterOutExtraData(result: any): GasPrice { 74 | return 'maxFeePerGas' in result 75 | ? { maxFeePerGas: result.maxFeePerGas, maxPriorityFeePerGas: result.maxPriorityFeePerGas } 76 | : { gasPrice: result.gasPrice }; 77 | } 78 | 79 | type Config = { 80 | blocks: number; 81 | percentile: number; 82 | accept: { 83 | standard: number; 84 | fast: number; 85 | instant: number; 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /src/services/gas/gas-price-sources/paraswap-gas-price-source.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, FieldsRequirements, SupportRecord, TimeString } from '@types'; 2 | import { IGasPriceSource, GasPriceResult, GasValueForVersions, GasValueForVersion, LegacyGasPrice } from '@services/gas/types'; 3 | import { IFetchService } from '@services/fetch/types'; 4 | import { Chains } from '@chains'; 5 | 6 | const SUPPORTED_CHAINS = [Chains.ETHEREUM, Chains.POLYGON, Chains.BNB_CHAIN, Chains.AVALANCHE, Chains.ARBITRUM, Chains.OPTIMISM, Chains.BASE]; 7 | 8 | type GasValues = GasValueForVersion<'standard' | 'fast' | 'instant', LegacyGasPrice>; 9 | export class ParaswapGasPriceSource implements IGasPriceSource { 10 | constructor(private readonly fetchService: IFetchService) {} 11 | 12 | supportedSpeeds() { 13 | const support: SupportRecord = { standard: 'present', fast: 'present', instant: 'present' }; 14 | return Object.fromEntries(SUPPORTED_CHAINS.map(({ chainId }) => [Number(chainId), support])); 15 | } 16 | 17 | async getGasPrice>({ 18 | chainId, 19 | config, 20 | }: { 21 | chainId: ChainId; 22 | config?: { timeout?: TimeString }; 23 | }) { 24 | const response = await this.fetchService.fetch(`https://api.paraswap.io/prices/gas/${chainId}?eip1559=false`, { timeout: config?.timeout }); 25 | const body = await response.json(); 26 | return { 27 | standard: stringToLegacyGasPrice(body, 'average'), 28 | fast: stringToLegacyGasPrice(body, 'fast'), 29 | instant: stringToLegacyGasPrice(body, 'fastest'), 30 | } as GasPriceResult; 31 | } 32 | } 33 | 34 | function stringToLegacyGasPrice(body: any, key: string) { 35 | return { gasPrice: BigInt(body[key]) }; 36 | } 37 | -------------------------------------------------------------------------------- /src/services/gas/gas-price-sources/polygon-gas-station-gas-price-source.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, FieldsRequirements, SupportRecord, TimeString } from '@types'; 2 | import { IGasPriceSource, EIP1159GasPrice, GasPriceResult, GasValueForVersion } from '@services/gas/types'; 3 | import { IFetchService } from '@services/fetch/types'; 4 | import { Chains } from '@chains'; 5 | import { parseUnits } from 'viem'; 6 | 7 | type GasValues = GasValueForVersion<'standard' | 'fast' | 'instant', EIP1159GasPrice>; 8 | export class PolygonGasStationGasPriceSource implements IGasPriceSource { 9 | constructor(private readonly fetchService: IFetchService) {} 10 | 11 | supportedSpeeds() { 12 | const support: SupportRecord = { standard: 'present', fast: 'present', instant: 'present' }; 13 | return { [Chains.POLYGON.chainId]: support }; 14 | } 15 | 16 | async getGasPrice>({ 17 | chainId, 18 | config, 19 | }: { 20 | chainId: ChainId; 21 | config?: { timeout?: TimeString }; 22 | }) { 23 | const response = await this.fetchService.fetch('https://gasstation-mainnet.matic.network/v2', { timeout: config?.timeout }); 24 | const { safeLow, standard, fast }: { safeLow: Gas; standard: Gas; fast: Gas } = await response.json(); 25 | return { 26 | standard: calculateGas(safeLow), 27 | fast: calculateGas(standard), 28 | instant: calculateGas(fast), 29 | } as GasPriceResult; 30 | } 31 | } 32 | 33 | function calculateGas(gas: Gas): EIP1159GasPrice { 34 | return { 35 | maxFeePerGas: parseUnits(gas.maxFee.toFixed(9) as `${number}`, 9), 36 | maxPriorityFeePerGas: parseUnits(gas.maxPriorityFee.toFixed(9) as `${number}`, 9), 37 | }; 38 | } 39 | 40 | type Gas = { 41 | maxPriorityFee: number; 42 | maxFee: number; 43 | }; 44 | -------------------------------------------------------------------------------- /src/services/gas/gas-price-sources/prioritized-gas-price-source-combinator.ts: -------------------------------------------------------------------------------- 1 | import { couldSupportMeetRequirements, combineSourcesSupport, doesResponseMeetRequirements } from '@shared/requirements-and-support'; 2 | import { ChainId, FieldsRequirements, TimeString } from '@types'; 3 | import { GasPriceResult, IGasPriceSource, MergeGasValues } from '../types'; 4 | 5 | // This source will take a list of sources, sorted by priority, and use the first one possible 6 | // that supports the given chain 7 | export class PrioritizedGasPriceSourceCombinator[] | []> 8 | implements IGasPriceSource> 9 | { 10 | constructor(private readonly sources: Sources) { 11 | if (sources.length === 0) throw new Error('No sources were specified'); 12 | } 13 | 14 | supportedSpeeds() { 15 | return combineSourcesSupport, MergeGasValues>(this.sources, (source) => source.supportedSpeeds()); 16 | } 17 | 18 | async getGasPrice>>({ 19 | chainId, 20 | config, 21 | }: { 22 | chainId: ChainId; 23 | config?: { fields?: Requirements; timeout?: TimeString }; 24 | }) { 25 | const sourcesInChain = this.sources.filter( 26 | (source) => chainId in source.supportedSpeeds() && couldSupportMeetRequirements(source.supportedSpeeds()[chainId], config?.fields) 27 | ); 28 | if (sourcesInChain.length === 0) throw new Error(`Chain with id ${chainId} cannot support the given requirements`); 29 | const gasResults = sourcesInChain.map((source) => source.getGasPrice({ chainId, config }).catch(() => ({}))); 30 | return new Promise, Requirements>>(async (resolve, reject) => { 31 | for (let i = 0; i < gasResults.length; i++) { 32 | const response = await gasResults[i]; 33 | if (Object.keys(response).length > 0 && doesResponseMeetRequirements(response, config?.fields)) { 34 | resolve(response as GasPriceResult, Requirements>); 35 | } 36 | } 37 | reject(new Error('Could not fetch gas prices that met the given requirements')); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/services/gas/gas-price-sources/rpc-gas-price-source.ts: -------------------------------------------------------------------------------- 1 | import { PublicClient } from 'viem'; 2 | import { ChainId, FieldsRequirements, SupportRecord, TimeString } from '@types'; 3 | import { IProviderService } from '@services/providers/types'; 4 | import { GasPriceResult, GasValueForVersions, IGasPriceSource } from '@services/gas/types'; 5 | import { timeoutPromise } from '@shared/timeouts'; 6 | 7 | // We are using the provider here to calculate the gas price 8 | type GasValues = GasValueForVersions<'standard'>; 9 | export class RPCGasPriceSource implements IGasPriceSource { 10 | constructor(private readonly providerService: IProviderService) {} 11 | 12 | supportedSpeeds() { 13 | const support: SupportRecord = { standard: 'present' }; 14 | return Object.fromEntries(this.providerService.supportedChains().map((chainId) => [Number(chainId), support])); 15 | } 16 | 17 | getGasPrice>({ chainId, config }: { chainId: ChainId; config?: { timeout?: TimeString } }) { 18 | const client = this.providerService.getViemPublicClient({ chainId }); 19 | const promise = calculatePrice(client); 20 | return timeoutPromise(promise, config?.timeout) as Promise>; 21 | } 22 | } 23 | 24 | async function calculatePrice(client: PublicClient) { 25 | // We need to specify a type, or viem will default to eip1559 and fail if the chain doesn't support it. So we've looked into what viem does 26 | // and we realized that it will fetch a block, and then return the latest price. So we fetch it first to determine the time, and then pass 27 | // it to viem, even though it doesn't accept it "publicly". But it does use it nevertheless and it doesn't fetch a new one 28 | const block = await client.getBlock(); 29 | const type = typeof block.baseFeePerGas === 'bigint' ? 'eip1559' : 'legacy'; 30 | const feeData = await client.estimateFeesPerGas({ block, type } as any); 31 | const gasPrice = 32 | feeData.maxFeePerGas !== undefined && feeData.maxPriorityFeePerGas !== undefined 33 | ? { standard: { maxFeePerGas: feeData.maxFeePerGas, maxPriorityFeePerGas: feeData.maxPriorityFeePerGas } } 34 | : { standard: { gasPrice: feeData.gasPrice! } }; 35 | return gasPrice; 36 | } 37 | -------------------------------------------------------------------------------- /src/services/gas/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | AVAILABLE_GAS_SPEEDS, 3 | GasPrice, 4 | LegacyGasPrice, 5 | EIP1159GasPrice, 6 | GasSpeed, 7 | GasEstimation, 8 | IGasService, 9 | IGasPriceSource, 10 | IQuickGasCostCalculator, 11 | } from './types'; 12 | export { GasPriceAggregationMethod } from './gas-price-sources/aggregator-gas-price-source'; 13 | -------------------------------------------------------------------------------- /src/services/gas/utils.ts: -------------------------------------------------------------------------------- 1 | import { DefaultRequirements, FieldsRequirements } from '@types'; 2 | import { isBigIntish } from '@shared/utils'; 3 | import { EIP1159GasPrice, GasPriceResult, SupportedGasValues } from './types'; 4 | 5 | export function isEIP1159Compatible< 6 | GasValues extends SupportedGasValues, 7 | Requirements extends FieldsRequirements = DefaultRequirements 8 | >(gasPriceForSpeed: GasPriceResult): gasPriceForSpeed is GasPriceResult, Requirements> { 9 | const keys = Object.keys(gasPriceForSpeed); 10 | if (keys.length === 0) { 11 | throw new Error(`Found a gas price result with nothing on it. This shouldn't happen`); 12 | } 13 | const gasPrice = (gasPriceForSpeed as any)[keys[0]]; 14 | if ('maxFeePerGas' in gasPrice && isBigIntish(gasPrice.maxFeePerGas)) { 15 | return true; 16 | } 17 | return false; 18 | } 19 | 20 | type OnlyEIP1559 = { 21 | [K in keyof GasValues]: GasValues[K] extends EIP1159GasPrice ? GasValues[K] : never; 22 | }; 23 | 24 | export function isValidGasPriceValue(value: any) { 25 | return ('maxFeePerGas' in value && isBigIntish(value.maxFeePerGas)) || ('gasPrice' in value && isBigIntish(value.gasPrice)); 26 | } 27 | 28 | export function filterOutInvalidSpeeds(result: GasPriceResult) { 29 | return Object.fromEntries(Object.entries(result).filter(([, value]) => isValidGasPriceValue(value))); 30 | } 31 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './balances'; 2 | export * from './allowances'; 3 | export * from './fetch'; 4 | export * from './gas'; 5 | export * from './providers'; 6 | export * from './quotes'; 7 | export * from './metadata'; 8 | export * from './prices'; 9 | export * from './logs'; 10 | export * from './permit2'; 11 | export * from './dca'; 12 | export * from './blocks'; 13 | -------------------------------------------------------------------------------- /src/services/logs/index.ts: -------------------------------------------------------------------------------- 1 | export { LogLevel, ILogger, ILogsService } from './types'; 2 | -------------------------------------------------------------------------------- /src/services/logs/loggers/console-logger.ts: -------------------------------------------------------------------------------- 1 | import { ILogger, LogLevel } from '../types'; 2 | import { shouldPrintLog } from '../utils'; 3 | 4 | export class ConsoleLogger implements ILogger { 5 | constructor(private readonly name: string, private readonly level: LogLevel) {} 6 | 7 | log(message?: any, ...optionalParams: any[]): void { 8 | this.print('LOG', message, optionalParams); 9 | } 10 | 11 | debug(message?: any, ...optionalParams: any[]): void { 12 | this.print('DEBUG', message, optionalParams); 13 | } 14 | 15 | info(message?: any, ...optionalParams: any[]): void { 16 | this.print('INFO', message, optionalParams); 17 | } 18 | 19 | warn(message?: any, ...optionalParams: any[]): void { 20 | this.print('WARN', message, optionalParams); 21 | } 22 | 23 | error(message?: any, ...optionalParams: any[]): void { 24 | this.print('ERROR', message, optionalParams); 25 | } 26 | 27 | private print(level: LogLevel, message: any | undefined, optionalParams: any[]) { 28 | if (shouldPrintLog(level, this.level)) { 29 | if (optionalParams.length > 1) { 30 | return console.log(new Date().toISOString() + ' [' + level + '] (' + this.name + '): ' + (message ?? ''), ...optionalParams); 31 | } else { 32 | return console.log(new Date().toISOString() + ' [' + level + '] (' + this.name + '): ' + (message ?? '')); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/services/logs/logs-service.ts: -------------------------------------------------------------------------------- 1 | import { ILogger, ILogsService, LogLevel } from './types'; 2 | import { ConsoleLogger } from './loggers/console-logger'; 3 | 4 | export class LogsService implements ILogsService { 5 | constructor(private readonly defaultLevel: LogLevel) {} 6 | 7 | getLogger({ name }: { name: string }): ILogger { 8 | return new ConsoleLogger(name, this.defaultLevel); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/services/logs/types.ts: -------------------------------------------------------------------------------- 1 | export type LogLevel = 'ALL' | 'LOG' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'OFF'; 2 | 3 | export type ILogsService = { 4 | getLogger({ name }: { name: string }): ILogger; 5 | }; 6 | 7 | export type ILogger = { 8 | log(message?: any, ...optionalParams: any[]): void; 9 | debug(message?: any, ...optionalParams: any[]): void; 10 | info(message?: any, ...optionalParams: any[]): void; 11 | warn(message?: any, ...optionalParams: any[]): void; 12 | error(message?: any, ...optionalParams: any[]): void; 13 | }; 14 | -------------------------------------------------------------------------------- /src/services/logs/utils.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from './types'; 2 | 3 | export function shouldPrintLog(logType: LogLevel, currentLevel: LogLevel) { 4 | return getLogPriority(logType) >= getLogPriority(currentLevel); 5 | } 6 | 7 | function getLogPriority(level: LogLevel): number { 8 | switch (level) { 9 | case 'ALL': 10 | return 0; 11 | case 'LOG': 12 | case 'DEBUG': 13 | return 10; 14 | case 'INFO': 15 | return 20; 16 | case 'WARN': 17 | return 30; 18 | case 'ERROR': 19 | return 40; 20 | case 'OFF': 21 | return Infinity; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/services/metadata/index.ts: -------------------------------------------------------------------------------- 1 | export { IMetadataService, IMetadataSource, MetadataResult, MetadataInput } from './types'; 2 | -------------------------------------------------------------------------------- /src/services/metadata/metadata-service.ts: -------------------------------------------------------------------------------- 1 | import { doesResponseMeetRequirements, validateRequirements } from '@shared/requirements-and-support'; 2 | import { timeoutPromise } from '@shared/timeouts'; 3 | import { ChainId, DefaultRequirements, FieldsRequirements, TimeString, TokenAddress } from '@types'; 4 | import { IMetadataService, IMetadataSource, MetadataInput } from './types'; 5 | 6 | export class MetadataService implements IMetadataService { 7 | constructor(private readonly metadataSource: IMetadataSource) {} 8 | 9 | supportedChains(): ChainId[] { 10 | return Object.keys(this.supportedProperties()).map(Number); 11 | } 12 | 13 | supportedProperties() { 14 | return this.metadataSource.supportedProperties(); 15 | } 16 | 17 | async getMetadataInChain = DefaultRequirements>({ 18 | chainId, 19 | tokens, 20 | config, 21 | }: { 22 | chainId: ChainId; 23 | tokens: TokenAddress[]; 24 | config?: { fields?: Requirements; timeout?: TimeString }; 25 | }) { 26 | const result = await this.getMetadata({ tokens: tokens.map((token) => ({ chainId, token })), config }); 27 | return result[chainId] ?? {}; 28 | } 29 | 30 | async getMetadata = DefaultRequirements>({ 31 | tokens, 32 | config, 33 | }: { 34 | tokens: MetadataInput[]; 35 | config?: { fields?: Requirements; timeout?: TimeString }; 36 | }) { 37 | if (tokens.length === 0) return {}; 38 | const chains = [...new Set(tokens.map(({ chainId }) => chainId))]; 39 | validateRequirements(this.supportedProperties(), chains, config?.fields); 40 | const response = await timeoutPromise(this.metadataSource.getMetadata({ tokens, config }), config?.timeout); 41 | validateResponse(tokens, response, config?.fields); 42 | return response; 43 | } 44 | } 45 | 46 | function validateResponse>( 47 | request: MetadataInput[], 48 | response: Record>, 49 | requirements: Requirements | undefined 50 | ) { 51 | for (const { chainId, token } of request) { 52 | if (!doesResponseMeetRequirements(response[chainId]?.[token], requirements)) { 53 | throw new Error('Failed to fetch metadata that meets the given requirements'); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/services/metadata/metadata-sources/defi-llama-metadata-source.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, FieldsRequirements, SupportInChain, TimeString, TokenAddress } from '@types'; 2 | import { IFetchService } from '@services/fetch/types'; 3 | import { BaseTokenMetadata, IMetadataSource, MetadataInput, MetadataResult } from '../types'; 4 | import { DefiLlamaClient } from '@shared/defi-llama'; 5 | 6 | export class DefiLlamaMetadataSource implements IMetadataSource { 7 | private readonly defiLlama: DefiLlamaClient; 8 | 9 | constructor(fetch: IFetchService) { 10 | this.defiLlama = new DefiLlamaClient(fetch); 11 | } 12 | 13 | async getMetadata>(params: { 14 | tokens: MetadataInput[]; 15 | config?: { timeout?: TimeString }; 16 | }) { 17 | const result: Record> = {}; 18 | const data = await this.defiLlama.getCurrentTokenData(params); 19 | for (const [chainIdString, tokens] of Object.entries(data)) { 20 | const chainId = Number(chainIdString); 21 | result[chainId] = {}; 22 | for (const [address, { confidence, timestamp, price, ...metadata }] of Object.entries(tokens)) { 23 | result[chainId][address] = metadata; 24 | } 25 | } 26 | return result as Record>>; 27 | } 28 | 29 | supportedProperties() { 30 | const properties: SupportInChain = { symbol: 'present', decimals: 'present' }; 31 | return Object.fromEntries(this.defiLlama.supportedChains().map((chainId) => [chainId, properties])); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/services/metadata/types.ts: -------------------------------------------------------------------------------- 1 | import { BasedOnRequirements, ChainId, DefaultRequirements, FieldsRequirements, SupportInChain, TimeString, TokenAddress } from '@types'; 2 | import { UnionMerge } from '@utility-types'; 3 | 4 | export type IMetadataService = { 5 | supportedChains(): ChainId[]; 6 | supportedProperties(): Record>; 7 | getMetadataInChain = DefaultRequirements>(_: { 8 | chainId: ChainId; 9 | tokens: TokenAddress[]; 10 | config?: { fields?: Requirements; timeout?: TimeString }; 11 | }): Promise>>; 12 | getMetadata = DefaultRequirements>(_: { 13 | tokens: MetadataInput[]; 14 | config?: { fields?: Requirements; timeout?: TimeString }; 15 | }): Promise>>>; 16 | }; 17 | 18 | export type IMetadataSource = { 19 | getMetadata = DefaultRequirements>(_: { 20 | tokens: MetadataInput[]; 21 | config?: { fields?: Requirements; timeout?: TimeString }; 22 | }): Promise>>>; 23 | supportedProperties(): Record>; 24 | }; 25 | 26 | export type MetadataResult< 27 | TokenMetadata extends object, 28 | Requirements extends FieldsRequirements = DefaultRequirements 29 | > = BasedOnRequirements; 30 | 31 | export type BaseTokenMetadata = { 32 | symbol: string; 33 | decimals: number; 34 | }; 35 | 36 | export type ExtractMetadata> = Source extends IMetadataSource ? R : never; 37 | 38 | export type MergeMetadata[] | []> = UnionMerge< 39 | { [K in keyof Sources]: ExtractMetadata }[number] 40 | >; 41 | 42 | export type MetadataInput = { 43 | chainId: ChainId; 44 | token: TokenAddress; 45 | }; 46 | -------------------------------------------------------------------------------- /src/services/permit2/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | IPermit2Service, 3 | IPermit2ArbitraryService, 4 | IPermit2QuoteService, 5 | SinglePermitParams, 6 | BatchPermitParams, 7 | PermitData, 8 | BatchPermitData, 9 | ArbitraryCallWithPermitParams, 10 | ArbitraryCallWithBatchPermitParams, 11 | ArbitraryCallWithoutPermitParams, 12 | GenericContractCall, 13 | DistributionTarget, 14 | EstimatedQuoteResponseWithTx, 15 | } from './types'; 16 | -------------------------------------------------------------------------------- /src/services/permit2/utils/config.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { Contract } from '@shared/contracts'; 3 | 4 | export const PERMIT2_CONTRACT = Contract.with({ defaultAddress: '0x000000000022d473030f116ddee9f6b43ac78ba3' }).build(); 5 | export const PERMIT2_ADAPTER_CONTRACT = Contract.with({ defaultAddress: '0xED306e38BB930ec9646FF3D917B2e513a97530b1' }) 6 | .and({ address: '0xd4c28318bf51e823bAE1C4FEC562b80C53E66467', onChain: Chains.MANTLE }) 7 | .and({ address: '0xd4c28318bf51e823bAE1C4FEC562b80C53E66467', onChain: Chains.CRONOS }) 8 | .and({ address: '0xd4c28318bf51e823bAE1C4FEC562b80C53E66467', onChain: Chains.SONIC }) 9 | .build(); 10 | export const WORDS_FOR_NONCE_CALCULATION = 10; 11 | export const PERMIT2_SUPPORTED_CHAINS = [ 12 | Chains.ETHEREUM, 13 | Chains.POLYGON, 14 | Chains.BNB_CHAIN, 15 | Chains.AVALANCHE, 16 | Chains.FANTOM, 17 | Chains.ARBITRUM, 18 | Chains.OPTIMISM, 19 | Chains.BASE, 20 | Chains.MOONRIVER, 21 | Chains.MOONBEAM, 22 | Chains.FUSE, 23 | Chains.EVMOS, 24 | Chains.CELO, 25 | Chains.GNOSIS, 26 | Chains.KAVA, 27 | Chains.POLYGON_ZKEVM, 28 | Chains.OKC, 29 | Chains.LINEA, 30 | Chains.ROOTSTOCK, 31 | Chains.BLAST, 32 | Chains.SCROLL, 33 | Chains.MODE, 34 | Chains.MANTLE, 35 | Chains.CRONOS, 36 | Chains.SONIC, 37 | ].map(({ chainId }) => chainId); 38 | -------------------------------------------------------------------------------- /src/services/permit2/utils/eip712-types.ts: -------------------------------------------------------------------------------- 1 | export const PERMIT2_TRANSFER_FROM_TYPES = { 2 | PermitTransferFrom: [ 3 | { type: 'TokenPermissions', name: 'permitted' }, 4 | { type: 'address', name: 'spender' }, 5 | { type: 'uint256', name: 'nonce' }, 6 | { type: 'uint256', name: 'deadline' }, 7 | ], 8 | TokenPermissions: [ 9 | { type: 'address', name: 'token' }, 10 | { type: 'uint256', name: 'amount' }, 11 | ], 12 | }; 13 | 14 | export const PERMIT2_BATCH_TRANSFER_FROM_TYPES = { 15 | PermitBatchTransferFrom: [ 16 | { type: 'TokenPermissions[]', name: 'permitted' }, 17 | { type: 'address', name: 'spender' }, 18 | { type: 'uint256', name: 'nonce' }, 19 | { type: 'uint256', name: 'deadline' }, 20 | ], 21 | TokenPermissions: [ 22 | { type: 'address', name: 'token' }, 23 | { type: 'uint256', name: 'amount' }, 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /src/services/prices/index.ts: -------------------------------------------------------------------------------- 1 | export { IPriceService, IPriceSource, TokenPrice, PriceResult, PriceInput } from './types'; 2 | -------------------------------------------------------------------------------- /src/services/prices/price-sources/defi-llama-price-source.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, TimeString, Timestamp, TokenAddress } from '@types'; 2 | import { IFetchService } from '@services/fetch/types'; 3 | import { PriceResult, IPriceSource, PricesQueriesSupport, PriceInput } from '../types'; 4 | import { DefiLlamaClient } from '@shared/defi-llama'; 5 | 6 | export class DefiLlamaPriceSource implements IPriceSource { 7 | private readonly defiLlama: DefiLlamaClient; 8 | 9 | constructor(fetch: IFetchService) { 10 | this.defiLlama = new DefiLlamaClient(fetch); 11 | } 12 | 13 | supportedQueries() { 14 | const support: PricesQueriesSupport = { getCurrentPrices: true, getHistoricalPrices: true, getBulkHistoricalPrices: true, getChart: true }; 15 | const entries = this.defiLlama.supportedChains().map((chainId) => [chainId, support]); 16 | return Object.fromEntries(entries); 17 | } 18 | 19 | async getCurrentPrices(params: { 20 | tokens: PriceInput[]; 21 | config: { timeout?: TimeString } | undefined; 22 | }): Promise>> { 23 | const result: Record> = {}; 24 | const data = await this.defiLlama.getCurrentTokenData(params); 25 | for (const [chainIdString, tokens] of Object.entries(data)) { 26 | const chainId = Number(chainIdString); 27 | result[chainId] = {}; 28 | for (const [address, token] of Object.entries(tokens)) { 29 | result[chainId][address] = { price: token.price, closestTimestamp: token.timestamp }; 30 | } 31 | } 32 | return result; 33 | } 34 | 35 | async getHistoricalPrices(params: { 36 | tokens: PriceInput[]; 37 | timestamp: Timestamp; 38 | searchWidth: TimeString | undefined; 39 | config: { timeout?: TimeString } | undefined; 40 | }): Promise>> { 41 | const result: Record> = {}; 42 | const data = await this.defiLlama.getHistoricalTokenData(params); 43 | for (const [chainIdString, tokens] of Object.entries(data)) { 44 | const chainId = Number(chainIdString); 45 | result[chainId] = {}; 46 | for (const [address, { price, timestamp }] of Object.entries(tokens)) { 47 | result[chainId][address] = { price, closestTimestamp: timestamp }; 48 | } 49 | } 50 | return result; 51 | } 52 | 53 | async getBulkHistoricalPrices({ 54 | tokens, 55 | searchWidth, 56 | config, 57 | }: { 58 | tokens: { chainId: ChainId; token: TokenAddress; timestamp: Timestamp }[]; 59 | searchWidth: TimeString | undefined; 60 | config: { timeout?: TimeString } | undefined; 61 | }): Promise>>> { 62 | return this.defiLlama.getBulkHistoricalTokenData({ tokens, searchWidth, config }); 63 | } 64 | 65 | async getChart({ 66 | tokens, 67 | span, 68 | period, 69 | bound, 70 | searchWidth, 71 | config, 72 | }: { 73 | tokens: PriceInput[]; 74 | span: number; 75 | period: TimeString; 76 | bound: { from: Timestamp } | { upTo: Timestamp | 'now' }; 77 | searchWidth?: TimeString; 78 | config: { timeout?: TimeString } | undefined; 79 | }): Promise>> { 80 | return this.defiLlama.getChart({ tokens, span, period, bound, searchWidth, config }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/services/prices/price-sources/utils.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, TokenAddress } from '@types'; 2 | import { IPriceSource, PriceInput, PricesQueriesSupport } from '../types'; 3 | 4 | export function fillResponseWithNewResult( 5 | result: Record>, 6 | newResult: Record> 7 | ) { 8 | for (const chainId in newResult) { 9 | for (const address in newResult[chainId]) { 10 | if (!result[chainId]?.[address]) { 11 | if (!(chainId in result)) { 12 | result[chainId] = {}; 13 | } 14 | result[chainId][address] = newResult[chainId][address]; 15 | } 16 | } 17 | } 18 | } 19 | 20 | export function doesResponseFulfillRequest(result: Record>, request: Record) { 21 | for (const chainId in request) { 22 | for (const address of request[chainId]) { 23 | if (typeof result[chainId]?.[address] === 'undefined') { 24 | return false; 25 | } 26 | } 27 | } 28 | return true; 29 | } 30 | 31 | function doesSourceSupportQueryInAnyOfTheChains(source: IPriceSource, query: keyof PricesQueriesSupport, chains: ChainId[]) { 32 | const support = source.supportedQueries(); 33 | return chains.some((chainId) => support[chainId]?.[query]); 34 | } 35 | 36 | export function filterRequestForSource( 37 | request: T[], 38 | query: keyof PricesQueriesSupport, 39 | source: IPriceSource 40 | ): T[] { 41 | const support = source.supportedQueries(); 42 | return request.filter(({ chainId }) => support[chainId]?.[query]); 43 | } 44 | 45 | export function combineSupport(sources: IPriceSource[]): Record { 46 | const result: Record = {}; 47 | for (const source of sources) { 48 | for (const [chainIdString, support] of Object.entries(source.supportedQueries())) { 49 | const chainId = Number(chainIdString); 50 | const current = result[chainId] ?? { 51 | getCurrentPrices: false, 52 | getHistoricalPrices: false, 53 | getBulkHistoricalPrices: false, 54 | getChart: false, 55 | }; 56 | result[chainId] = { 57 | getCurrentPrices: current.getCurrentPrices || support.getCurrentPrices, 58 | getHistoricalPrices: current.getHistoricalPrices || support.getHistoricalPrices, 59 | getBulkHistoricalPrices: current.getBulkHistoricalPrices || support.getBulkHistoricalPrices, 60 | getChart: current.getChart || support.getChart, 61 | }; 62 | } 63 | } 64 | return result; 65 | } 66 | 67 | export function getSourcesThatSupportRequestOrFail( 68 | request: T[], 69 | sources: IPriceSource[], 70 | query: keyof PricesQueriesSupport 71 | ) { 72 | const chainsInRequest = [...new Set(request.map(({ chainId }) => chainId))]; 73 | const sourcesInChain = sources.filter((source) => doesSourceSupportQueryInAnyOfTheChains(source, query, chainsInRequest)); 74 | if (sourcesInChain.length === 0) throw new Error(`Current price sources can't support all the given chains`); 75 | return sourcesInChain; 76 | } 77 | 78 | export function nowInSeconds() { 79 | return Math.floor(Date.now() / 1000); 80 | } 81 | -------------------------------------------------------------------------------- /src/services/prices/types.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, TimeString, Timestamp, TokenAddress } from '@types'; 2 | 3 | export type TokenPrice = number; 4 | 5 | export type IPriceService = { 6 | supportedChains(): ChainId[]; 7 | supportedQueries(): Record; 8 | getCurrentPricesInChain(_: { 9 | chainId: ChainId; 10 | tokens: TokenAddress[]; 11 | config?: { timeout?: TimeString }; 12 | }): Promise>; 13 | getCurrentPrices(_: { tokens: PriceInput[]; config?: { timeout?: TimeString } }): Promise>>; 14 | getHistoricalPricesInChain(_: { 15 | chainId: ChainId; 16 | tokens: TokenAddress[]; 17 | timestamp: Timestamp; 18 | searchWidth?: TimeString; 19 | config?: { timeout?: TimeString }; 20 | }): Promise>; 21 | getHistoricalPrices(_: { 22 | tokens: PriceInput[]; 23 | timestamp: Timestamp; 24 | searchWidth?: TimeString; 25 | config?: { timeout?: TimeString }; 26 | }): Promise>>; 27 | getBulkHistoricalPrices(_: { 28 | tokens: { chainId: ChainId; token: TokenAddress; timestamp: Timestamp }[]; 29 | searchWidth?: TimeString; 30 | config?: { timeout?: TimeString }; 31 | }): Promise>>>; 32 | getChart(_: { 33 | tokens: PriceInput[]; 34 | span: number; 35 | period: TimeString; 36 | bound: { from: Timestamp } | { upTo: Timestamp | 'now' }; 37 | searchWidth?: TimeString; 38 | }): Promise>>; 39 | }; 40 | 41 | export type PricesQueriesSupport = { 42 | getBulkHistoricalPrices: boolean; 43 | getHistoricalPrices: boolean; 44 | getCurrentPrices: true; 45 | getChart: boolean; 46 | }; 47 | 48 | export type PriceResult = { price: TokenPrice; closestTimestamp: Timestamp }; 49 | 50 | export type IPriceSource = { 51 | supportedQueries(): Record; 52 | getCurrentPrices(_: { 53 | tokens: PriceInput[]; 54 | config: { timeout?: TimeString } | undefined; 55 | }): Promise>>; 56 | getHistoricalPrices(_: { 57 | tokens: PriceInput[]; 58 | timestamp: Timestamp; 59 | searchWidth: TimeString | undefined; 60 | config: { timeout?: TimeString } | undefined; 61 | }): Promise>>; 62 | getBulkHistoricalPrices(_: { 63 | tokens: { chainId: ChainId; token: TokenAddress; timestamp: Timestamp }[]; 64 | searchWidth: TimeString | undefined; 65 | config: { timeout?: TimeString } | undefined; 66 | }): Promise>>>; 67 | getChart(_: { 68 | tokens: PriceInput[]; 69 | span: number; 70 | period: TimeString; 71 | bound: { from: Timestamp } | { upTo: Timestamp | 'now' }; 72 | searchWidth?: TimeString; 73 | config: { timeout?: TimeString } | undefined; 74 | }): Promise>>; 75 | }; 76 | 77 | export type PriceInput = { 78 | chainId: ChainId; 79 | token: TokenAddress; 80 | }; 81 | -------------------------------------------------------------------------------- /src/services/providers/index.ts: -------------------------------------------------------------------------------- 1 | export { IProviderSource, IProviderService } from './types'; 2 | export * from './provider-sources'; 3 | -------------------------------------------------------------------------------- /src/services/providers/provider-service.ts: -------------------------------------------------------------------------------- 1 | import { PublicClient, PublicClientConfig, Transport, createPublicClient } from 'viem'; 2 | import { ChainId } from '@types'; 3 | import { IProviderService, IProviderSource } from './types'; 4 | import { getViemChain } from './utils'; 5 | 6 | export type ProviderConfig = Pick; 7 | 8 | export class ProviderService implements IProviderService { 9 | // Viem clients have a lot of state and they even do some polling at regular intervals 10 | // That's why we'll only create one client per chain, and then re-use it 11 | private readonly viemPublicClients: Map = new Map(); 12 | private readonly source: IProviderSource; 13 | private readonly config: ProviderConfig | undefined; 14 | 15 | constructor({ source, config }: { source: IProviderSource; config?: ProviderConfig }) { 16 | this.source = source; 17 | this.config = config; 18 | } 19 | 20 | supportedChains(): ChainId[] { 21 | return this.source.supportedChains(); 22 | } 23 | 24 | getViemPublicClient({ chainId }: { chainId: ChainId }): PublicClient { 25 | if (!this.viemPublicClients.has(chainId)) { 26 | const transport = this.getViemTransport({ chainId }); 27 | const chain = getViemChain(chainId); 28 | const client = createPublicClient({ ...this.config, chain, transport }); 29 | this.viemPublicClients.set(chainId, client as PublicClient); 30 | } 31 | return this.viemPublicClients.get(chainId)!; 32 | } 33 | 34 | getViemTransport({ chainId }: { chainId: number }): Transport { 35 | return this.source.getViemTransport({ chainId }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/alchemy-provider.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@types'; 2 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 3 | import { ALCHEMY_NETWORKS } from '@shared/alchemy'; 4 | 5 | export type AlchemySupportedChains = AlchemyDefaultChains | ChainId[]; 6 | type AlchemyDefaultChains = { allInTier: 'free tier' | 'paid tier'; except?: ChainId[] }; 7 | 8 | export class AlchemyProviderSource extends BaseHttpProvider { 9 | private readonly key: string; 10 | private readonly supported: ChainId[]; 11 | 12 | constructor({ key, onChains, config }: { key: string; onChains?: AlchemySupportedChains; config?: HttpProviderConfig }) { 13 | super(config); 14 | this.key = key; 15 | if (onChains === undefined) { 16 | this.supported = alchemySupportedChains(); 17 | } else if (Array.isArray(onChains)) { 18 | this.supported = onChains; 19 | } else { 20 | const chains = alchemySupportedChains({ onlyFree: onChains.allInTier === 'free tier' }); 21 | this.supported = onChains.except ? chains.filter((chain) => !onChains.except!.includes(chain)) : chains; 22 | } 23 | } 24 | 25 | supportedChains(): ChainId[] { 26 | return this.supported; 27 | } 28 | 29 | protected calculateUrl(chainId: ChainId): string { 30 | return buildAlchemyRPCUrl({ chainId, apiKey: this.key, protocol: 'https' }); 31 | } 32 | } 33 | 34 | export function alchemySupportedChains(args?: { onlyFree?: boolean }): ChainId[] { 35 | return Object.entries(ALCHEMY_NETWORKS) 36 | .filter( 37 | ([ 38 | _, 39 | { 40 | rpc: { tier }, 41 | }, 42 | ]) => tier === 'free' || !args?.onlyFree 43 | ) 44 | .map(([chainId]) => Number(chainId)); 45 | } 46 | 47 | export function buildAlchemyRPCUrl({ chainId, apiKey, protocol }: { chainId: ChainId; apiKey: string; protocol: 'https' | 'wss' }) { 48 | const { key: alchemyNetwork } = ALCHEMY_NETWORKS[chainId]; 49 | return `${protocol}://${alchemyNetwork}.g.alchemy.com/v2/${apiKey}`; 50 | } 51 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/ankr-provider.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { ChainId } from '@types'; 3 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 4 | 5 | const SUPPORTED_CHAINS: Record = { 6 | [Chains.POLYGON.chainId]: 'https://rpc.ankr.com/polygon', 7 | [Chains.AVALANCHE.chainId]: 'https://rpc.ankr.com/avalanche', 8 | [Chains.ETHEREUM.chainId]: 'https://rpc.ankr.com/eth', 9 | [Chains.ETHEREUM_GOERLI.chainId]: 'https://rpc.ankr.com/eth_goerli', 10 | [Chains.ETHEREUM_SEPOLIA.chainId]: 'https://rpc.ankr.com/eth_sepolia', 11 | [Chains.BNB_CHAIN.chainId]: 'https://rpc.ankr.com/bsc', 12 | [Chains.FANTOM.chainId]: 'https://rpc.ankr.com/fantom', 13 | [Chains.ARBITRUM.chainId]: 'https://rpc.ankr.com/arbitrum', 14 | [Chains.OPTIMISM.chainId]: 'https://rpc.ankr.com/optimism', 15 | [Chains.CELO.chainId]: 'https://rpc.ankr.com/celo', 16 | [Chains.GNOSIS.chainId]: 'https://rpc.ankr.com/gnosis', 17 | [Chains.POLYGON_ZKEVM.chainId]: 'https://rpc.ankr.com/polygon_zkevm', 18 | [Chains.HARMONY_SHARD_0.chainId]: 'https://rpc.ankr.com/harmony', 19 | [Chains.MOONBEAM.chainId]: 'https://rpc.ankr.com/moonbeam', 20 | [Chains.BIT_TORRENT.chainId]: 'https://rpc.ankr.com/bttc', 21 | [Chains.BASE.chainId]: 'https://rpc.ankr.com/base', 22 | [Chains.KAIA.chainId]: 'https://rpc.ankr.com/klaytn', 23 | [Chains.SCROLL.chainId]: 'https://rpc.ankr.com/scroll', 24 | }; 25 | 26 | export class AnkrProviderSource extends BaseHttpProvider { 27 | private readonly supported: ChainId[]; 28 | private readonly key: string | undefined; 29 | constructor({ key, onChains, config }: { key?: string; onChains?: ChainId[]; config?: HttpProviderConfig }) { 30 | super(config); 31 | this.key = key; 32 | this.supported = onChains ?? ankrSupportedChains(); 33 | } 34 | 35 | supportedChains(): ChainId[] { 36 | return this.supported; 37 | } 38 | 39 | protected calculateUrl(chainId: ChainId): string { 40 | return buildAnkrRPCUrl({ chainId, apiKey: this.key }); 41 | } 42 | } 43 | 44 | export function buildAnkrRPCUrl({ chainId, apiKey }: { chainId: ChainId; apiKey?: string }) { 45 | let url = SUPPORTED_CHAINS[chainId]; 46 | if (apiKey) { 47 | url += `/${apiKey}`; 48 | } 49 | return url; 50 | } 51 | 52 | export function ankrSupportedChains(): ChainId[] { 53 | return Object.keys(SUPPORTED_CHAINS).map(Number); 54 | } 55 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/base/base-http-provider.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpTransportConfig } from 'viem'; 2 | import { ChainId } from '@types'; 3 | import { IProviderSource } from '@services/providers/types'; 4 | 5 | export type HttpProviderConfig = Pick; 6 | export abstract class BaseHttpProvider implements IProviderSource { 7 | constructor(private readonly config: HttpProviderConfig | undefined) {} 8 | 9 | getViemTransport({ chainId }: { chainId: ChainId }) { 10 | this.assertChainIsValid(chainId); 11 | const url = this.calculateUrl(chainId); 12 | return http(url, this.config); 13 | } 14 | 15 | abstract supportedChains(): ChainId[]; 16 | protected abstract calculateUrl(chainId: ChainId): string; 17 | 18 | private assertChainIsValid(chainId: ChainId) { 19 | if (!this.supportedChains().includes(chainId)) throw new Error(`Chain with id ${chainId} is not supported`); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/base/base-web-socket-provider.ts: -------------------------------------------------------------------------------- 1 | import { IProviderSource } from '@services/providers/types'; 2 | import { webSocket } from 'viem'; 3 | import { ChainId } from '@types'; 4 | 5 | export abstract class BaseWebSocketProvider implements IProviderSource { 6 | getViemTransport({ chainId }: { chainId: ChainId }) { 7 | this.assertChainIsValid(chainId); 8 | const url = this.calculateUrl(chainId); 9 | return webSocket(url); 10 | } 11 | 12 | abstract supportedChains(): ChainId[]; 13 | protected abstract calculateUrl(chainId: ChainId): string; 14 | 15 | private assertChainIsValid(chainId: ChainId) { 16 | if (!this.supportedChains().includes(chainId)) throw new Error(`Chain with id ${chainId} is not supported`); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/blast-provider.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { ChainId } from '@types'; 3 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 4 | 5 | const PLACEHOLDER = '{{ PLACEHOLDER }}'; 6 | 7 | const SUPPORTED_CHAINS: Record = { 8 | [Chains.ETHEREUM.chainId]: `https://eth-mainnet.blastapi.io${PLACEHOLDER}`, 9 | [Chains.ETHEREUM_SEPOLIA.chainId]: `https://eth-sepolia.blastapi.io${PLACEHOLDER}`, 10 | [Chains.BNB_CHAIN.chainId]: `https://bsc-mainnet.blastapi.io${PLACEHOLDER}`, 11 | [Chains.POLYGON.chainId]: `https://polygon-mainnet.blastapi.io${PLACEHOLDER}`, 12 | [Chains.POLYGON_MUMBAI.chainId]: `https://polygon-testnet.blastapi.io${PLACEHOLDER}`, 13 | [Chains.ARBITRUM.chainId]: `https://arbitrum-one.blastapi.io${PLACEHOLDER}`, 14 | [Chains.ASTAR.chainId]: `https://astar.blastapi.io${PLACEHOLDER}`, 15 | [Chains.OPTIMISM.chainId]: `https://optimism-mainnet.blastapi.io${PLACEHOLDER}`, 16 | [Chains.LINEA.chainId]: `https://linea-mainnet.blastapi.io${PLACEHOLDER}`, 17 | [Chains.BASE.chainId]: `https://base-mainnet.blastapi.io${PLACEHOLDER}`, 18 | [Chains.FANTOM.chainId]: `https://fantom-mainnet.blastapi.io${PLACEHOLDER}`, 19 | [Chains.AVALANCHE.chainId]: `https://ava-mainnet.blastapi.io${PLACEHOLDER}/ext/bc/C/rpc`, 20 | [Chains.GNOSIS.chainId]: `https://gnosis-mainnet.blastapi.io${PLACEHOLDER}`, 21 | [Chains.POLYGON_ZKEVM.chainId]: `https://polygon-zkevm-mainnet.blastapi.io${PLACEHOLDER}`, 22 | [Chains.MOONBEAM.chainId]: `https://moonbeam.blastapi.io${PLACEHOLDER}`, 23 | [Chains.MOONRIVER.chainId]: `https://moonriver.blastapi.io${PLACEHOLDER}`, 24 | [Chains.OKC.chainId]: `https://oktc-mainnet.blastapi.io${PLACEHOLDER}`, 25 | [Chains.MODE.chainId]: `https://mode-mainnet.blastapi.io${PLACEHOLDER}`, 26 | [Chains.SCROLL.chainId]: `https://scroll-mainnet.public.blastapi.io${PLACEHOLDER}`, 27 | }; 28 | 29 | export class BlastProviderSource extends BaseHttpProvider { 30 | private readonly supported: ChainId[]; 31 | private readonly key: string | undefined; 32 | 33 | constructor({ key, onChains, config }: { key?: string; onChains?: ChainId[]; config?: HttpProviderConfig }) { 34 | super(config); 35 | this.key = key; 36 | this.supported = onChains ?? blastSupportedChains(); 37 | } 38 | 39 | supportedChains(): ChainId[] { 40 | return this.supported; 41 | } 42 | 43 | protected calculateUrl(chainId: ChainId): string { 44 | return buildBlastRPCUrl({ chainId, apiKey: this.key }); 45 | } 46 | } 47 | 48 | export function buildBlastRPCUrl({ chainId, apiKey }: { chainId: ChainId; apiKey?: string }) { 49 | const url = SUPPORTED_CHAINS[chainId]; 50 | const toReplace = apiKey ? `/${apiKey}` : ''; 51 | return url.replace(PLACEHOLDER, toReplace); 52 | } 53 | 54 | export function blastSupportedChains(): ChainId[] { 55 | return Object.keys(SUPPORTED_CHAINS).map(Number); 56 | } 57 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/drpc-provider.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { ChainId } from '@types'; 3 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 4 | 5 | const SUPPORTED_CHAINS: Record = { 6 | [Chains.ETHEREUM.chainId]: 'ethereum', 7 | [Chains.ETHEREUM_SEPOLIA.chainId]: 'sepolia', 8 | [Chains.BNB_CHAIN.chainId]: 'bsc', 9 | [Chains.POLYGON.chainId]: 'polygon', 10 | [Chains.POLYGON_MUMBAI.chainId]: 'polygon-mumbai', 11 | [Chains.ARBITRUM.chainId]: 'arbitrum', 12 | [Chains.OPTIMISM.chainId]: 'optimism', 13 | [Chains.LINEA.chainId]: 'linea', 14 | [Chains.BASE.chainId]: 'base', 15 | [Chains.FANTOM.chainId]: 'fantom', 16 | [Chains.AVALANCHE.chainId]: 'avalanche', 17 | [Chains.GNOSIS.chainId]: 'gnosis', 18 | [Chains.AURORA.chainId]: 'aurora', 19 | [Chains.POLYGON_ZKEVM.chainId]: 'polygon-zkevm', 20 | [Chains.KAIA.chainId]: 'klaytn', 21 | [Chains.BOBA.chainId]: 'boba-eth', 22 | [Chains.CELO.chainId]: 'celo', 23 | [Chains.CRONOS.chainId]: 'cronos', 24 | [Chains.FUSE.chainId]: 'fuse', 25 | [Chains.HECO.chainId]: 'heco', 26 | [Chains.KAVA.chainId]: 'kava', 27 | [Chains.METIS_ANDROMEDA.chainId]: 'metis', 28 | [Chains.MOONBEAM.chainId]: 'moonbeam', 29 | [Chains.MOONRIVER.chainId]: 'moonriver', 30 | [Chains.OKC.chainId]: 'oktc', 31 | [Chains.opBNB.chainId]: 'opbnb', 32 | [Chains.MODE.chainId]: 'mode', 33 | [Chains.SCROLL.chainId]: 'scroll', 34 | [Chains.BLAST.chainId]: 'blast', 35 | }; 36 | 37 | export class dRPCProviderSource extends BaseHttpProvider { 38 | private readonly supported: ChainId[]; 39 | private readonly key: string; 40 | constructor({ key, onChains, config }: { key: string; onChains?: ChainId[]; config?: HttpProviderConfig }) { 41 | super(config); 42 | this.key = key; 43 | this.supported = onChains ?? dRPCSupportedChains(); 44 | } 45 | 46 | supportedChains(): ChainId[] { 47 | return this.supported; 48 | } 49 | 50 | protected calculateUrl(chainId: ChainId): string { 51 | return buildDRPCUrl({ chainId, apiKey: this.key }); 52 | } 53 | } 54 | 55 | export function buildDRPCUrl({ chainId, apiKey }: { chainId: ChainId; apiKey: string }) { 56 | const chainKey = SUPPORTED_CHAINS[chainId]; 57 | return `https://lb.drpc.org/ogrpc?network=${chainKey}&dkey=${apiKey}`; 58 | } 59 | 60 | export function dRPCSupportedChains(): ChainId[] { 61 | return Object.keys(SUPPORTED_CHAINS).map(Number); 62 | } 63 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/fallback-provider.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@types'; 2 | import { IProviderSource } from '../types'; 3 | import { FallbackTransportConfig, fallback } from 'viem'; 4 | import { chainsUnion } from '@chains'; 5 | 6 | export type FallbackProviderSourceConfig = Pick; 7 | export class FallbackProviderSource implements IProviderSource { 8 | constructor(private readonly sources: IProviderSource[], private readonly config: FallbackProviderSourceConfig | undefined) { 9 | if (sources.length === 0) throw new Error('Need at least one source to setup the provider source'); 10 | } 11 | 12 | supportedChains() { 13 | return chainsUnion(this.sources.map((source) => source.supportedChains())); 14 | } 15 | 16 | getViemTransport({ chainId }: { chainId: ChainId }) { 17 | const transports = this.sources 18 | .filter((source) => source.supportedChains().includes(chainId)) 19 | .map((source) => source.getViemTransport({ chainId })); 20 | if (transports.length === 0) throw new Error(`Chain with id ${chainId} not supported`); 21 | return fallback(transports, this.config); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/get-block-provider.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@types'; 2 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 3 | 4 | export class GetBlockProviderSource extends BaseHttpProvider { 5 | private readonly supported: ChainId[]; 6 | private readonly accessTokens: Record; 7 | 8 | constructor({ accessTokens, config }: { accessTokens: Record; config?: HttpProviderConfig }) { 9 | super(config); 10 | this.accessTokens = accessTokens; 11 | this.supported = Object.keys(accessTokens).map(Number); 12 | } 13 | 14 | supportedChains(): ChainId[] { 15 | return this.supported; 16 | } 17 | 18 | protected calculateUrl(chainId: ChainId): string { 19 | return buildGetBlockRPCUrl({ accessToken: this.accessTokens[chainId] }); 20 | } 21 | } 22 | 23 | export function buildGetBlockRPCUrl({ accessToken }: { accessToken: string }) { 24 | return `https://go.getblock.io/${accessToken}/`; 25 | } 26 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/http-provider.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@types'; 2 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 3 | 4 | export class HttpProviderSource extends BaseHttpProvider { 5 | private readonly url: string; 6 | private readonly chains: ChainId[]; 7 | 8 | constructor({ url, chains, config }: { url: string; chains: ChainId[]; config?: HttpProviderConfig }) { 9 | super(config); 10 | if (chains.length === 0) throw new Error('Must support at least one chain'); 11 | this.url = url; 12 | this.chains = chains; 13 | } 14 | 15 | supportedChains(): ChainId[] { 16 | return this.chains; 17 | } 18 | 19 | protected calculateUrl(chainId: number): string { 20 | return this.url; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/index.ts: -------------------------------------------------------------------------------- 1 | export { FallbackProviderSourceConfig } from './fallback-provider'; 2 | export { LoadBalanceProviderSourceConfig } from './load-balance-provider'; 3 | export { PublicRPCsProviderSourceConfig } from './public-rpcs-provider'; 4 | export { buildAlchemyRPCUrl, alchemySupportedChains } from './alchemy-provider'; 5 | export { buildAnkrRPCUrl, ankrSupportedChains } from './ankr-provider'; 6 | export { buildGetBlockRPCUrl } from './get-block-provider'; 7 | export { buildInfuraRPCUrl, infuraSupportedChains } from './infura-provider'; 8 | export { buildLlamaNodesRPCUrl, llamaNodesSupportedChains } from './llama-nodes-provider'; 9 | export { buildNodeRealRPCUrl, nodeRealSupportedChains } from './node-real-provider'; 10 | export { buildTenderlyRPCUrl, tenderlySupportedChains } from './tenderly-provider'; 11 | export { buildDRPCUrl, dRPCSupportedChains } from './drpc-provider'; 12 | export { buildBlastRPCUrl, blastSupportedChains } from './blast-provider'; 13 | export { buildOnFinalityRPCUrl, onFinalitySupportedChains } from './on-finality-provider'; 14 | export { buildOneRPCUrl, oneRPCSupportedChains } from './one-rpc-provider'; 15 | export { buildMoralisRPCUrl, moralisSupportedChains } from './moralis-provider'; 16 | export { buildThirdWebRPCUrl, thirdWebSupportedChains } from './third-web-provider'; 17 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/infura-provider.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { ChainId } from '@types'; 3 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 4 | 5 | const SUPPORTED_CHAINS: Record = { 6 | [Chains.ETHEREUM.chainId]: 'https://mainnet.infura.io/v3/', 7 | [Chains.ETHEREUM_GOERLI.chainId]: 'https://goerli.infura.io/v3/', 8 | [Chains.ETHEREUM_SEPOLIA.chainId]: 'https://sepolia.infura.io/v3/', 9 | [Chains.ARBITRUM.chainId]: 'https://arbitrum-mainnet.infura.io/v3/', 10 | [Chains.AURORA.chainId]: 'https://aurora-mainnet.infura.io/v3/', 11 | [Chains.AVALANCHE.chainId]: 'https://avalanche-mainnet.infura.io/v3/', 12 | [Chains.CELO.chainId]: 'https://celo-mainnet.infura.io/v3/', 13 | [Chains.OPTIMISM.chainId]: 'https://optimism-mainnet.infura.io/v3/', 14 | [Chains.POLYGON.chainId]: 'https://polygon-mainnet.infura.io/v3/', 15 | [Chains.POLYGON_MUMBAI.chainId]: 'https://polygon-mumbai.infura.io/v3/', 16 | }; 17 | 18 | export class InfuraProviderSource extends BaseHttpProvider { 19 | private readonly supported: ChainId[]; 20 | private readonly key: string; 21 | 22 | constructor({ key, onChains, config }: { key: string; onChains?: ChainId[]; config?: HttpProviderConfig }) { 23 | super(config); 24 | this.key = key; 25 | this.supported = onChains ?? infuraSupportedChains(); 26 | } 27 | 28 | supportedChains(): ChainId[] { 29 | return this.supported; 30 | } 31 | 32 | protected calculateUrl(chainId: number): string { 33 | return buildInfuraRPCUrl({ chainId, apiKey: this.key }); 34 | } 35 | } 36 | 37 | export function buildInfuraRPCUrl({ chainId, apiKey }: { chainId: ChainId; apiKey: string }) { 38 | return SUPPORTED_CHAINS[chainId] + apiKey; 39 | } 40 | 41 | export function infuraSupportedChains(): ChainId[] { 42 | return Object.keys(SUPPORTED_CHAINS).map(Number); 43 | } 44 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/llama-nodes-provider.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { ChainId } from '@types'; 3 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 4 | 5 | const SUPPORTED_CHAINS: Record = { 6 | [Chains.ETHEREUM.chainId]: 'https://eth.llamarpc.com', 7 | [Chains.POLYGON.chainId]: 'https://polygon.llamarpc.com', 8 | [Chains.BNB_CHAIN.chainId]: 'https://binance.llamarpc.com', 9 | [Chains.ARBITRUM.chainId]: 'https://arbitrum.llamarpc.com', 10 | [Chains.OPTIMISM.chainId]: 'https://optimism.llamarpc.com', 11 | [Chains.BASE.chainId]: 'https://base.llamarpc.com', 12 | }; 13 | 14 | export class LlamaNodesProviderSource extends BaseHttpProvider { 15 | private readonly supported: ChainId[]; 16 | private readonly key: string | undefined; 17 | 18 | constructor({ key, onChains, config }: { key?: string; onChains?: ChainId[]; config?: HttpProviderConfig }) { 19 | super(config); 20 | this.key = key; 21 | this.supported = onChains ?? llamaNodesSupportedChains(); 22 | } 23 | 24 | supportedChains(): ChainId[] { 25 | return this.supported; 26 | } 27 | 28 | protected calculateUrl(chainId: number): string { 29 | return buildLlamaNodesRPCUrl({ chainId, apiKey: this.key }); 30 | } 31 | } 32 | 33 | export function buildLlamaNodesRPCUrl({ chainId, apiKey }: { chainId: ChainId; apiKey?: string }) { 34 | let url = SUPPORTED_CHAINS[chainId]; 35 | if (apiKey) { 36 | url += `/rpc/${apiKey}`; 37 | } 38 | return url; 39 | } 40 | 41 | export function llamaNodesSupportedChains(): ChainId[] { 42 | return Object.keys(SUPPORTED_CHAINS).map(Number); 43 | } 44 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/moralis-provider.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { ChainId } from '@types'; 3 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 4 | 5 | const SUPPORTED_CHAINS: Record = { 6 | [Chains.ETHEREUM.chainId]: 'eth', 7 | [Chains.ETHEREUM_SEPOLIA.chainId]: 'sepolia', 8 | [Chains.POLYGON.chainId]: 'polygon', 9 | [Chains.BNB_CHAIN.chainId]: 'bsc', 10 | [Chains.ARBITRUM.chainId]: 'arbitrum', 11 | [Chains.BASE.chainId]: 'base', 12 | [Chains.OPTIMISM.chainId]: 'optimism', 13 | [Chains.LINEA.chainId]: 'linea', 14 | [Chains.AVALANCHE.chainId]: 'avalanche', 15 | [Chains.GNOSIS.chainId]: 'gnosis', 16 | [Chains.MOONBEAM.chainId]: 'moonbeam', 17 | [Chains.MOONRIVER.chainId]: 'moonriver', 18 | [Chains.BLAST.chainId]: 'blast', 19 | [Chains.MANTLE.chainId]: 'mantle', 20 | [Chains.POLYGON_ZKEVM.chainId]: 'polygon-zkevm', 21 | }; 22 | 23 | type MoralisConfig = ({ onChains?: ChainId[] } | { keys: Record }) & { site?: 'site1' | 'site2'; config?: HttpProviderConfig }; 24 | 25 | export class MoralisProviderSource extends BaseHttpProvider { 26 | private readonly keys: Record; 27 | private readonly supported: ChainId[]; 28 | private readonly site: 'site1' | 'site2'; 29 | 30 | constructor(config: MoralisConfig) { 31 | super(config.config); 32 | if ('keys' in config) { 33 | this.supported = Object.keys(config.keys).map(Number); 34 | this.keys = config.keys; 35 | } else { 36 | this.supported = config.onChains ?? moralisSupportedChains(); 37 | this.keys = {}; 38 | } 39 | this.site = config.site ?? 'site1'; 40 | } 41 | 42 | supportedChains(): ChainId[] { 43 | return this.supported; 44 | } 45 | 46 | protected calculateUrl(chainId: ChainId): string { 47 | return buildMoralisRPCUrl({ chainId, apiKey: this.keys[chainId], site: this.site }); 48 | } 49 | } 50 | 51 | export function buildMoralisRPCUrl({ chainId, apiKey, site = 'site1' }: { chainId: ChainId; apiKey?: string; site?: 'site1' | 'site2' }) { 52 | let url = `https://${site}.moralis-nodes.com/${SUPPORTED_CHAINS[chainId]}/`; 53 | if (apiKey) { 54 | url += `${apiKey}`; 55 | } 56 | return url; 57 | } 58 | 59 | export function moralisSupportedChains(): ChainId[] { 60 | return Object.keys(SUPPORTED_CHAINS).map(Number); 61 | } 62 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/node-real-provider.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { ChainId } from '@types'; 3 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 4 | 5 | const SUPPORTED_CHAINS: Record = { 6 | [Chains.ETHEREUM.chainId]: 'https://eth-mainnet.nodereal.io/v1/', 7 | [Chains.ETHEREUM_GOERLI.chainId]: 'https://eth-goerli.nodereal.io/v1/', 8 | [Chains.BNB_CHAIN.chainId]: 'https://bsc-mainnet.nodereal.io/v1/', 9 | [Chains.POLYGON.chainId]: 'https://polygon-mainnet.nodereal.io/v1/', 10 | [Chains.OPTIMISM.chainId]: 'https://opt-mainnet.nodereal.io/v1/', 11 | }; 12 | 13 | export class NodeRealProviderSource extends BaseHttpProvider { 14 | private readonly supported: ChainId[]; 15 | private readonly key: string; 16 | 17 | constructor({ key, onChains, config }: { key: string; onChains?: ChainId[]; config?: HttpProviderConfig }) { 18 | super(config); 19 | this.key = key; 20 | this.supported = onChains ?? nodeRealSupportedChains(); 21 | } 22 | 23 | supportedChains(): ChainId[] { 24 | return this.supported; 25 | } 26 | 27 | protected calculateUrl(chainId: number): string { 28 | return buildNodeRealRPCUrl({ chainId, apiKey: this.key }); 29 | } 30 | } 31 | 32 | export function buildNodeRealRPCUrl({ chainId, apiKey }: { chainId: ChainId; apiKey: string }) { 33 | return SUPPORTED_CHAINS[chainId] + apiKey; 34 | } 35 | export function nodeRealSupportedChains(): ChainId[] { 36 | return Object.keys(SUPPORTED_CHAINS).map(Number); 37 | } 38 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/on-finality-provider.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { ChainId } from '@types'; 3 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 4 | 5 | const SUPPORTED_CHAINS: Record = { 6 | [Chains.ETHEREUM.chainId]: 'https://eth.api.onfinality.io/public', 7 | [Chains.ETHEREUM_SEPOLIA.chainId]: 'https://eth-sepolia.api.onfinality.io/public', 8 | [Chains.BNB_CHAIN.chainId]: 'https://bnb.api.onfinality.io/public', 9 | [Chains.POLYGON.chainId]: 'https://polygon.api.onfinality.io/public', 10 | [Chains.POLYGON_MUMBAI.chainId]: 'https://polygon-mumbai.api.onfinality.io/public', 11 | [Chains.ARBITRUM.chainId]: 'https://arbitrum.api.onfinality.io/public', 12 | [Chains.OPTIMISM.chainId]: 'https://optimism.api.onfinality.io/public', 13 | [Chains.BASE.chainId]: 'https://base.api.onfinality.io/public', 14 | [Chains.FANTOM.chainId]: 'https://fantom.api.onfinality.io/public', 15 | [Chains.AVALANCHE.chainId]: 'https://avalanche.api.onfinality.io/public/ext/bc/C', 16 | [Chains.GNOSIS.chainId]: 'https://gnosis.api.onfinality.io/public', 17 | [Chains.KAIA.chainId]: 'https://klaytn.api.onfinality.io/public', 18 | [Chains.CELO.chainId]: 'https://celo.api.onfinality.io/public', 19 | [Chains.FUSE.chainId]: 'https://fuse.api.onfinality.io/public', 20 | [Chains.KAVA.chainId]: 'https://kava.api.onfinality.io/public', 21 | [Chains.METIS_ANDROMEDA.chainId]: 'https://metis.api.onfinality.io/public', 22 | [Chains.MOONBEAM.chainId]: 'https://moonbeam.api.onfinality.io/public', 23 | [Chains.MOONRIVER.chainId]: 'https://moonriver.api.onfinality.io/public', 24 | [Chains.ASTAR.chainId]: 'https://astar.api.onfinality.io/public', 25 | }; 26 | 27 | export class OnFinalityProviderSource extends BaseHttpProvider { 28 | private readonly supported: ChainId[]; 29 | private readonly key: string | undefined; 30 | constructor({ key, onChains, config }: { key?: string; onChains?: ChainId[]; config?: HttpProviderConfig }) { 31 | super(config); 32 | this.key = key; 33 | this.supported = onChains ?? onFinalitySupportedChains(); 34 | } 35 | 36 | supportedChains(): ChainId[] { 37 | return this.supported; 38 | } 39 | 40 | protected calculateUrl(chainId: ChainId): string { 41 | return buildOnFinalityRPCUrl({ chainId, apiKey: this.key }); 42 | } 43 | } 44 | 45 | export function buildOnFinalityRPCUrl({ chainId, apiKey }: { chainId: ChainId; apiKey?: string }) { 46 | const publicUrl = SUPPORTED_CHAINS[chainId]; 47 | return apiKey ? publicUrl.replace('/public', '') + `/rpc?apikey=${apiKey}` : publicUrl; 48 | } 49 | 50 | export function onFinalitySupportedChains(): ChainId[] { 51 | return Object.keys(SUPPORTED_CHAINS).map(Number); 52 | } 53 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/one-rpc-provider.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { ChainId } from '@types'; 3 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 4 | 5 | const SUPPORTED_CHAINS: Record = { 6 | [Chains.ETHEREUM.chainId]: 'eth', 7 | [Chains.ETHEREUM_SEPOLIA.chainId]: 'sepolia', 8 | [Chains.BNB_CHAIN.chainId]: 'bnb', 9 | [Chains.POLYGON.chainId]: 'matic', 10 | [Chains.POLYGON_ZKEVM.chainId]: 'polygon/zkevm', 11 | [Chains.AVALANCHE.chainId]: 'avax/c', 12 | [Chains.ARBITRUM.chainId]: 'arb', 13 | [Chains.MOONBEAM.chainId]: 'glmr', 14 | [Chains.ASTAR.chainId]: 'astr', 15 | [Chains.OPTIMISM.chainId]: 'op', 16 | [Chains.FANTOM.chainId]: 'ftm', 17 | [Chains.CELO.chainId]: 'celo', 18 | [Chains.KAIA.chainId]: 'klay', 19 | [Chains.AURORA.chainId]: 'aurora', 20 | [Chains.BASE.chainId]: 'base', 21 | [Chains.GNOSIS.chainId]: 'gnosis', 22 | [Chains.OKC.chainId]: 'oktc', 23 | [Chains.CRONOS.chainId]: 'cro', 24 | [Chains.opBNB.chainId]: 'opbnb', 25 | [Chains.BOBA.chainId]: 'boba/eth', 26 | [Chains.MODE.chainId]: 'mode', 27 | }; 28 | 29 | export class OneRPCProviderSource extends BaseHttpProvider { 30 | private readonly supported: ChainId[]; 31 | private readonly key: string | undefined; 32 | 33 | constructor({ key, onChains, config }: { key?: string; onChains?: ChainId[]; config?: HttpProviderConfig }) { 34 | super(config); 35 | this.key = key; 36 | this.supported = onChains ?? oneRPCSupportedChains(); 37 | } 38 | 39 | supportedChains(): ChainId[] { 40 | return this.supported; 41 | } 42 | 43 | protected calculateUrl(chainId: ChainId): string { 44 | return buildOneRPCUrl({ chainId, apiKey: this.key }); 45 | } 46 | } 47 | 48 | export function buildOneRPCUrl({ chainId, apiKey }: { chainId: ChainId; apiKey?: string }) { 49 | let url = 'https://1rpc.io/'; 50 | if (apiKey) { 51 | url += `${apiKey}/`; 52 | } 53 | url += SUPPORTED_CHAINS[chainId]; 54 | return url; 55 | } 56 | 57 | export function oneRPCSupportedChains(): ChainId[] { 58 | return Object.keys(SUPPORTED_CHAINS).map(Number); 59 | } 60 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/prioritized-provider-source-combinator.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@types'; 2 | import { chainsUnion } from '@chains'; 3 | import { IProviderSource } from '../types'; 4 | 5 | // This source will take a list of sources, sorted by priority, and use the first one possible 6 | // that supports the given chain 7 | export class PrioritizedProviderSourceCombinator implements IProviderSource { 8 | constructor(private readonly sources: IProviderSource[]) { 9 | if (sources.length === 0) throw new Error('Need at least one source to setup the provider source'); 10 | } 11 | 12 | supportedChains() { 13 | return chainsUnion(this.sources.map((source) => source.supportedChains())); 14 | } 15 | 16 | getViemTransport({ chainId }: { chainId: ChainId }) { 17 | const source = this.sources.find((source) => source.supportedChains().includes(chainId)); 18 | if (!source) throw new Error(`Chain with id ${chainId} not supported`); 19 | return source.getViemTransport({ chainId }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/public-rpcs-provider.ts: -------------------------------------------------------------------------------- 1 | import { Transport } from 'viem'; 2 | import { getAllChains } from '@chains'; 3 | import { ChainId, Chain } from '@types'; 4 | import { HttpProviderSource } from './http-provider'; 5 | import { LoadBalanceProviderSource, LoadBalanceProviderSourceConfig } from './load-balance-provider'; 6 | import { FallbackProviderSource, FallbackProviderSourceConfig } from './fallback-provider'; 7 | import { IProviderSource } from '../types'; 8 | 9 | export type PublicRPCsProviderSourceConfig = 10 | | ({ type: 'load-balance' } & LoadBalanceProviderSourceConfig) 11 | | ({ type: 'fallback' } & FallbackProviderSourceConfig); 12 | 13 | export class PublicRPCsProviderSource implements IProviderSource { 14 | private readonly source: IProviderSource; 15 | 16 | constructor(params?: { publicRPCs?: Record; config?: PublicRPCsProviderSourceConfig }) { 17 | const sources = buildSources(calculateRPCs(params?.publicRPCs)); 18 | this.source = 19 | params?.config?.type === 'fallback' 20 | ? new FallbackProviderSource(sources, params.config) 21 | : new LoadBalanceProviderSource(sources, params?.config); 22 | } 23 | 24 | supportedChains(): ChainId[] { 25 | return this.source.supportedChains(); 26 | } 27 | 28 | getViemTransport({ chainId }: { chainId: ChainId }): Transport { 29 | return this.source.getViemTransport({ chainId }); 30 | } 31 | } 32 | 33 | function buildSources(publicRPCs: { chainId: ChainId; publicRPC: string }[]) { 34 | return publicRPCs.map(({ chainId, publicRPC }) => new HttpProviderSource({ url: publicRPC, chains: [chainId] })); 35 | } 36 | 37 | function calculateRPCs(publicRPCs?: Record): { chainId: ChainId; publicRPC: string }[] { 38 | const rpcsByChain: [ChainId, string[]][] = publicRPCs 39 | ? Object.entries(publicRPCs).map(([chainId, rpcs]) => [Number(chainId), rpcs]) 40 | : getAllChains() 41 | .filter((chain): chain is Chain & { publicRPCs: string[] } => chain.publicRPCs.length > 0) 42 | .map(({ chainId, publicRPCs }) => [chainId, publicRPCs]); 43 | return rpcsByChain.flatMap(([chainId, publicRPCs]) => publicRPCs.map((publicRPC) => ({ publicRPC, chainId: Number(chainId) }))); 44 | } 45 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/tenderly-provider.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { ChainId } from '@types'; 3 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 4 | 5 | const SUPPORTED_CHAINS: Record = { 6 | [Chains.ETHEREUM.chainId]: 'https://mainnet.gateway.tenderly.co', 7 | [Chains.ETHEREUM_SEPOLIA.chainId]: 'https://sepolia.gateway.tenderly.co', 8 | [Chains.POLYGON.chainId]: 'https://polygon.gateway.tenderly.co', 9 | [Chains.POLYGON_MUMBAI.chainId]: 'https://polygon-mumbai.gateway.tenderly.co', 10 | [Chains.OPTIMISM.chainId]: 'https://optimism.gateway.tenderly.co', 11 | [Chains.BASE.chainId]: 'https://base.gateway.tenderly.co', 12 | [Chains.ARBITRUM.chainId]: 'https://arbitrum.gateway.tenderly.co', 13 | [Chains.BOBA.chainId]: 'https://boba-ethereum.gateway.tenderly.co', 14 | [Chains.BLAST.chainId]: 'https://blast.gateway.tenderly.co', 15 | [Chains.MODE.chainId]: 'https://mode.gateway.tenderly.co', 16 | [Chains.AVALANCHE.chainId]: 'https://avalanche.gateway.tenderly.co', 17 | [Chains.LINEA.chainId]: 'https://linea.gateway.tenderly.co', 18 | [Chains.SONIC.chainId]: 'https://sonic.gateway.tenderly.co', 19 | [Chains.MANTLE.chainId]: 'https://mantle.gateway.tenderly.co', 20 | }; 21 | 22 | export class TenderlyProviderSource extends BaseHttpProvider { 23 | private readonly supported: ChainId[]; 24 | private readonly key: string | undefined; 25 | 26 | constructor({ key, onChains, config }: { key?: string; onChains?: ChainId[]; config?: HttpProviderConfig }) { 27 | super(config); 28 | this.key = key; 29 | this.supported = onChains ?? tenderlySupportedChains(); 30 | } 31 | 32 | supportedChains(): ChainId[] { 33 | return this.supported; 34 | } 35 | 36 | protected calculateUrl(chainId: ChainId): string { 37 | return buildTenderlyRPCUrl({ chainId, apiKey: this.key }); 38 | } 39 | } 40 | 41 | export function buildTenderlyRPCUrl({ chainId, apiKey }: { chainId: ChainId; apiKey?: string }) { 42 | let url = SUPPORTED_CHAINS[chainId]; 43 | if (apiKey) { 44 | url += `/${apiKey}`; 45 | } 46 | return url; 47 | } 48 | 49 | export function tenderlySupportedChains(): ChainId[] { 50 | return Object.keys(SUPPORTED_CHAINS).map(Number); 51 | } 52 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/third-web-provider.ts: -------------------------------------------------------------------------------- 1 | import { Chain, ChainId } from '@types'; 2 | import { Chains } from '@chains'; 3 | import { BaseHttpProvider, HttpProviderConfig } from './base/base-http-provider'; 4 | 5 | const SUPPORTED_NETWORKS: Chain[] = [ 6 | Chains.ETHEREUM, 7 | Chains.OPTIMISM, 8 | Chains.BNB_CHAIN, 9 | Chains.GNOSIS, 10 | Chains.POLYGON, 11 | Chains.MANTLE, 12 | Chains.BASE, 13 | Chains.MODE, 14 | Chains.ARBITRUM, 15 | Chains.ARBITRUM_NOVA, 16 | Chains.AVALANCHE, 17 | Chains.LINEA, 18 | Chains.SCROLL, 19 | Chains.FUSE, 20 | Chains.opBNB, 21 | Chains.FANTOM, 22 | Chains.BOBA, 23 | Chains.METIS_ANDROMEDA, 24 | Chains.POLYGON_ZKEVM, 25 | Chains.MOONBEAM, 26 | Chains.CELO, 27 | Chains.BLAST, 28 | Chains.CRONOS, 29 | Chains.ROOTSTOCK, 30 | Chains.ONTOLOGY, 31 | Chains.OKC, 32 | Chains.VELAS, 33 | Chains.BIT_TORRENT, 34 | Chains.ASTAR, 35 | ]; 36 | 37 | export class ThirdWebProviderSource extends BaseHttpProvider { 38 | private readonly supported: ChainId[]; 39 | 40 | constructor({ onChains, config }: { onChains?: ChainId[]; config?: HttpProviderConfig }) { 41 | super(config); 42 | this.supported = onChains ?? thirdWebSupportedChains(); 43 | } 44 | 45 | supportedChains(): ChainId[] { 46 | return this.supported; 47 | } 48 | 49 | protected calculateUrl(chainId: ChainId): string { 50 | return buildThirdWebRPCUrl({ chainId }); 51 | } 52 | } 53 | 54 | export function thirdWebSupportedChains(): ChainId[] { 55 | return SUPPORTED_NETWORKS.map(({ chainId }) => chainId); 56 | } 57 | 58 | export function buildThirdWebRPCUrl({ chainId }: { chainId: ChainId }) { 59 | return `https://${chainId}.rpc.thirdweb.com`; 60 | } 61 | -------------------------------------------------------------------------------- /src/services/providers/provider-sources/web-sockets-provider.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@types'; 2 | import { BaseWebSocketProvider } from './base/base-web-socket-provider'; 3 | 4 | export class WebSocketProviderSource extends BaseWebSocketProvider { 5 | constructor(private readonly url: string, private readonly chains: ChainId[]) { 6 | super(); 7 | if (chains.length === 0) throw new Error('Must support at least one chain'); 8 | } 9 | 10 | supportedChains(): ChainId[] { 11 | return this.chains; 12 | } 13 | 14 | protected calculateUrl(chainId: number): string { 15 | return this.url; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/services/providers/types.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@types'; 2 | import { PublicClient, Transport } from 'viem'; 3 | 4 | export type IProviderService = { 5 | supportedChains(): ChainId[]; 6 | getViemPublicClient(_: { chainId: ChainId }): PublicClient; 7 | getViemTransport(_: { chainId: ChainId }): Transport; 8 | }; 9 | 10 | export type IProviderSource = { 11 | supportedChains(): ChainId[]; 12 | getViemTransport(_: { chainId: ChainId }): Transport; 13 | }; 14 | -------------------------------------------------------------------------------- /src/services/providers/utils.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@types'; 2 | import * as chains from 'viem/chains'; 3 | import { Contract } from '@shared/contracts'; 4 | export const MULTICALL_CONTRACT = Contract.with({ defaultAddress: '0xcA11bde05977b3631167028862bE2a173976CA11' }).build(); 5 | export function getViemChain(chainId: ChainId): any { 6 | return Object.values(chains).find((chain) => 'id' in chain && chain.id === chainId); 7 | } 8 | -------------------------------------------------------------------------------- /src/services/quotes/errors.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, TokenAddress } from '@types'; 2 | import { SourceId } from './types'; 3 | import { getChainByKey } from '@chains'; 4 | 5 | export class SourceNotFoundError extends Error { 6 | constructor(sourceId: SourceId) { 7 | super(`Could not find a source with id '${sourceId}'`); 8 | } 9 | } 10 | 11 | export class SourceNoBuyOrdersError extends Error { 12 | constructor(sourceId: SourceId) { 13 | super(`Source with id '${sourceId}' does not support buy orders`); 14 | } 15 | } 16 | 17 | export class SourceInvalidConfigOrContextError extends Error { 18 | constructor(sourceId: SourceId) { 19 | super(`The current context or config is not valid for source with id '${sourceId}'`); 20 | } 21 | } 22 | 23 | export class FailedToGenerateQuoteError extends Error { 24 | constructor(sourceName: string, chainId: ChainId, sellToken: TokenAddress, buyToken: TokenAddress, error?: any) { 25 | const context = error ? ` with error ${JSON.stringify(error)}` : ''; 26 | const chain = getChainByKey(chainId)?.name ?? `chain with id ${chainId}`; 27 | super(`${sourceName}: failed to calculate a quote between ${sellToken} and ${buyToken} on ${chain}${context}`); 28 | } 29 | } 30 | 31 | export class FailedToGenerateAnyQuotesError extends Error { 32 | constructor(chainId: ChainId, sellToken: TokenAddress, buyToken: TokenAddress) { 33 | const chain = getChainByKey(chainId)?.name ?? `chain with id ${chainId}`; 34 | super(`Failed to calculate a quote between ${sellToken} and ${buyToken} on ${chain}`); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/services/quotes/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | SourceId, 3 | SourceMetadata, 4 | IQuoteService, 5 | QuoteRequest, 6 | QuoteResponse, 7 | EstimatedQuoteRequest, 8 | EstimatedQuoteResponse, 9 | FailedResponse, 10 | GlobalQuoteSourceConfig, 11 | QuoteTransaction, 12 | QuoteResponseWithTx, 13 | QuoteResponseRelevantForTxBuild, 14 | } from './types'; 15 | export { 16 | COMPARE_BY, 17 | COMPARE_USING, 18 | CompareQuotesBy, 19 | CompareQuotesUsing, 20 | ComparableQuote, 21 | sortQuotesBy, 22 | chooseQuotesBy, 23 | compareQuotesBy, 24 | } from './quote-compare'; 25 | export { SOURCES_METADATA, SourceConfig, SourceWithConfigId } from './source-registry'; 26 | export * from './source-lists'; 27 | export * from './errors'; 28 | -------------------------------------------------------------------------------- /src/services/quotes/quote-sources/balancer-quote-source.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { QuoteParams, QuoteSourceMetadata, SourceQuoteResponse, SourceQuoteTransaction, BuildTxParams } from './types'; 3 | import { calculateAllowanceTarget, failed } from './utils'; 4 | import { AlwaysValidConfigAndContextSource } from './base/always-valid-source'; 5 | import { StringifyBigInt } from '@utility-types'; 6 | import { SourceListQuoteResponse } from '../source-lists'; 7 | 8 | const BALANCER_METADATA: QuoteSourceMetadata = { 9 | name: 'Balancer', 10 | supports: { 11 | chains: [ 12 | Chains.ARBITRUM.chainId, 13 | Chains.AVALANCHE.chainId, 14 | Chains.BASE.chainId, 15 | Chains.FANTOM.chainId, 16 | Chains.GNOSIS.chainId, 17 | Chains.ETHEREUM.chainId, 18 | Chains.POLYGON.chainId, 19 | Chains.OPTIMISM.chainId, 20 | Chains.POLYGON_ZKEVM.chainId, 21 | Chains.ETHEREUM_SEPOLIA.chainId, 22 | Chains.BNB_CHAIN.chainId, 23 | Chains.SONIC.chainId, 24 | Chains.MODE.chainId, 25 | ], 26 | swapAndTransfer: false, 27 | buyOrders: true, 28 | }, 29 | logoURI: 'ipfs://QmSb9Lr6Jgi9Y3RUuShfWcuCaa9EYxpyZWgBTe8GbvsUL7', 30 | }; 31 | type BalancerSupport = { buyOrders: true; swapAndTransfer: false }; 32 | type BalancerConfig = { url?: string }; 33 | type BalancerData = { tx: SourceQuoteTransaction }; 34 | export class BalancerQuoteSource extends AlwaysValidConfigAndContextSource { 35 | getMetadata() { 36 | return BALANCER_METADATA; 37 | } 38 | async quote({ 39 | components: { fetchService }, 40 | request: { 41 | chainId, 42 | config: { slippagePercentage, timeout, txValidFor }, 43 | accounts: { takeFrom }, 44 | order, 45 | external, 46 | ...request 47 | }, 48 | config, 49 | }: QuoteParams): Promise> { 50 | const balmyUrl = config.url ?? 'https://api.balmy.xyz'; 51 | const url = `${balmyUrl}/v1/swap/networks/${chainId}/quotes/balancer`; 52 | const body = { 53 | ...request, 54 | order, 55 | slippagePercentage, 56 | takerAddress: takeFrom, 57 | txValidFor, 58 | quoteTimeout: timeout, 59 | sourceConfig: config, 60 | }; 61 | 62 | const response = await fetchService.fetch(url, { 63 | method: 'POST', 64 | body: JSON.stringify(body), 65 | timeout, 66 | }); 67 | if (!response.ok) { 68 | failed(BALANCER_METADATA, chainId, request.sellToken, request.buyToken, await response.text()); 69 | } 70 | const { 71 | sellAmount, 72 | buyAmount, 73 | maxSellAmount, 74 | minBuyAmount, 75 | estimatedGas, 76 | source: { allowanceTarget }, 77 | customData, 78 | }: StringifyBigInt> = await response.json(); 79 | 80 | return { 81 | sellAmount: BigInt(sellAmount), 82 | maxSellAmount: BigInt(maxSellAmount), 83 | buyAmount: BigInt(buyAmount), 84 | minBuyAmount: BigInt(minBuyAmount), 85 | estimatedGas: estimatedGas ? BigInt(estimatedGas) : undefined, 86 | allowanceTarget: calculateAllowanceTarget(request.sellToken, allowanceTarget), 87 | type: order.type, 88 | customData: { 89 | tx: { 90 | ...customData.tx, 91 | value: customData.tx.value ? BigInt(customData.tx.value) : undefined, 92 | }, 93 | }, 94 | }; 95 | } 96 | 97 | async buildTx({ request }: BuildTxParams): Promise { 98 | return request.customData.tx; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/services/quotes/quote-sources/base/always-valid-source.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IQuoteSource, 3 | QuoteParams, 4 | QuoteSourceMetadata, 5 | QuoteSourceSupport, 6 | SourceQuoteResponse, 7 | SourceQuoteTransaction, 8 | BuildTxParams, 9 | } from '../types'; 10 | 11 | export abstract class AlwaysValidConfigAndContextSource< 12 | Support extends QuoteSourceSupport, 13 | CustomQuoteSourceConfig extends object = {}, 14 | CustomQuoteSourceData extends Record = Record 15 | > implements IQuoteSource 16 | { 17 | abstract getMetadata(): QuoteSourceMetadata; 18 | abstract quote(_: QuoteParams): Promise>; 19 | abstract buildTx(_: BuildTxParams): Promise; 20 | 21 | isConfigAndContextValidForQuoting(config: Partial | undefined): config is CustomQuoteSourceConfig { 22 | return true; 23 | } 24 | 25 | isConfigAndContextValidForTxBuilding(config: Partial | undefined): config is CustomQuoteSourceConfig { 26 | return true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/services/quotes/quote-sources/changelly-quote-source.ts: -------------------------------------------------------------------------------- 1 | import qs from 'qs'; 2 | import { Chains } from '@chains'; 3 | import { IQuoteSource, QuoteParams, QuoteSourceMetadata, SourceQuoteResponse, SourceQuoteTransaction, BuildTxParams } from './types'; 4 | import { addQuoteSlippage, calculateAllowanceTarget, failed } from './utils'; 5 | import { Addresses } from '@shared/constants'; 6 | import { isSameAddress } from '@shared/utils'; 7 | 8 | // Supported Networks: https://dex-api.changelly.com/v1/platforms 9 | export const CHANGELLY_METADATA: QuoteSourceMetadata = { 10 | name: 'Changelly DEX', 11 | supports: { 12 | chains: [Chains.ETHEREUM, Chains.OPTIMISM, Chains.ARBITRUM, Chains.BNB_CHAIN, Chains.POLYGON, Chains.FANTOM, Chains.AVALANCHE].map( 13 | ({ chainId }) => chainId 14 | ), 15 | swapAndTransfer: true, 16 | buyOrders: false, 17 | }, 18 | logoURI: 'ipfs://Qmbnnx5bD1wytBna4oY8DaL1cw5c5mTStwUMqLCoLt3yHR', 19 | }; 20 | type ChangellyConfig = { apiKey: string }; 21 | type ChangellySupport = { buyOrders: false; swapAndTransfer: true }; 22 | type ChangellyData = { tx: SourceQuoteTransaction }; 23 | export class ChangellyQuoteSource implements IQuoteSource { 24 | getMetadata() { 25 | return CHANGELLY_METADATA; 26 | } 27 | 28 | async quote({ 29 | components: { fetchService }, 30 | request: { 31 | chainId, 32 | sellToken, 33 | buyToken, 34 | order, 35 | accounts: { takeFrom, recipient }, 36 | config: { slippagePercentage, timeout }, 37 | }, 38 | config, 39 | }: QuoteParams): Promise> { 40 | const queryParams = { 41 | fromTokenAddress: sellToken, 42 | toTokenAddress: buyToken, 43 | amount: order.sellAmount.toString(), 44 | slippage: slippagePercentage * 10, 45 | recipientAddress: recipient && !isSameAddress(recipient, takeFrom) ? recipient : undefined, 46 | skipValidation: config.disableValidation, 47 | // We disable RFQ when validation is turned off, because it fails quite often 48 | takerAddress: config.disableValidation ? undefined : takeFrom, 49 | }; 50 | const queryString = qs.stringify(queryParams, { skipNulls: true, arrayFormat: 'comma' }); 51 | const url = `https://dex-api.changelly.com/v1/${chainId}/quote?${queryString}`; 52 | 53 | const headers = { 'X-Api-Key': config.apiKey }; 54 | const response = await fetchService.fetch(url, { timeout, headers }); 55 | if (!response.ok) { 56 | failed(CHANGELLY_METADATA, chainId, sellToken, buyToken, await response.text()); 57 | } 58 | const { amount_out_total, estimate_gas_total, calldata, to } = await response.json(); 59 | 60 | const quote = { 61 | sellAmount: order.sellAmount, 62 | buyAmount: BigInt(amount_out_total), 63 | estimatedGas: BigInt(estimate_gas_total), 64 | allowanceTarget: calculateAllowanceTarget(sellToken, to), 65 | customData: { 66 | tx: { 67 | to, 68 | calldata, 69 | value: isSameAddress(sellToken, Addresses.NATIVE_TOKEN) ? order.sellAmount : 0n, 70 | }, 71 | }, 72 | }; 73 | return addQuoteSlippage(quote, order.type, slippagePercentage); 74 | } 75 | 76 | async buildTx({ request }: BuildTxParams): Promise { 77 | return request.customData.tx; 78 | } 79 | 80 | isConfigAndContextValidForQuoting(config: Partial | undefined): config is ChangellyConfig { 81 | return !!config?.apiKey; 82 | } 83 | 84 | isConfigAndContextValidForTxBuilding(config: Partial | undefined): config is ChangellyConfig { 85 | return true; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/services/quotes/quote-sources/sovryn-quote-source.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { StringifyBigInt } from '@utility-types'; 3 | import { QuoteTransaction } from '../types'; 4 | import { QuoteParams, QuoteSourceMetadata, SourceQuoteResponse, SourceQuoteTransaction, BuildTxParams } from './types'; 5 | import { calculateAllowanceTarget, failed } from './utils'; 6 | import { AlwaysValidConfigAndContextSource } from './base/always-valid-source'; 7 | import { SourceListQuoteResponse } from '../source-lists/types'; 8 | 9 | const SOVRYN_METADATA: QuoteSourceMetadata = { 10 | name: 'Sovryn', 11 | supports: { 12 | chains: [Chains.ROOTSTOCK.chainId], 13 | buyOrders: false, 14 | swapAndTransfer: false, 15 | }, 16 | logoURI: 'ipfs://QmUpdb1zxtB2kUSjR1Qs1QMFPsSeZNkL21fMzGUfdjkXQA', 17 | }; 18 | type SovrynSupport = { buyOrders: false; swapAndTransfer: false }; 19 | type SovrynConfig = { 20 | url?: string; 21 | }; 22 | type SovrynData = { tx: SourceQuoteTransaction }; 23 | export class SovrynQuoteSource extends AlwaysValidConfigAndContextSource { 24 | getMetadata() { 25 | return SOVRYN_METADATA; 26 | } 27 | 28 | async quote({ 29 | components: { fetchService }, 30 | request: { 31 | chainId, 32 | config: { slippagePercentage, timeout, txValidFor }, 33 | accounts: { takeFrom }, 34 | order, 35 | external, 36 | ...request 37 | }, 38 | config, 39 | }: QuoteParams): Promise> { 40 | const balmyUrl = config.url ?? 'https://api.balmy.xyz'; 41 | const url = `${balmyUrl}/v1/swap/networks/${chainId}/quotes/sovryn`; 42 | const body = { 43 | ...request, 44 | order: { type: 'sell', sellAmount: order.sellAmount.toString() }, 45 | slippagePercentage, 46 | takerAddress: takeFrom, 47 | txValidFor, 48 | quoteTimeout: timeout, 49 | sourceConfig: config, 50 | }; 51 | 52 | const response = await fetchService.fetch(url, { 53 | method: 'POST', 54 | body: JSON.stringify(body), 55 | timeout, 56 | }); 57 | if (!response.ok) { 58 | failed(SOVRYN_METADATA, chainId, request.sellToken, request.buyToken, await response.text()); 59 | } 60 | const { 61 | sellAmount, 62 | buyAmount, 63 | maxSellAmount, 64 | minBuyAmount, 65 | estimatedGas, 66 | source: { allowanceTarget }, 67 | customData, 68 | }: StringifyBigInt> = await response.json(); 69 | 70 | return { 71 | sellAmount: BigInt(sellAmount), 72 | maxSellAmount: BigInt(maxSellAmount), 73 | buyAmount: BigInt(buyAmount), 74 | minBuyAmount: BigInt(minBuyAmount), 75 | estimatedGas: estimatedGas ? BigInt(estimatedGas) : undefined, 76 | allowanceTarget: calculateAllowanceTarget(request.sellToken, allowanceTarget), 77 | type: order.type, 78 | customData: { 79 | tx: { 80 | ...customData.tx, 81 | value: customData.tx.value ? BigInt(customData.tx.value) : undefined, 82 | }, 83 | }, 84 | }; 85 | } 86 | 87 | async buildTx({ request }: BuildTxParams): Promise { 88 | return request.customData.tx; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/services/quotes/quote-sources/utils.ts: -------------------------------------------------------------------------------- 1 | import { getAddress } from 'viem'; 2 | import { addPercentage, isSameAddress, subtractPercentage } from '@shared/utils'; 3 | import { Address, Chain, ChainId, TokenAddress } from '@types'; 4 | import { SourceQuoteResponse } from './types'; 5 | import { FailedToGenerateQuoteError } from '../errors'; 6 | import { SourceMetadata } from '../types'; 7 | import { Addresses } from '@shared/constants'; 8 | 9 | export function failed(metadata: SourceMetadata, chain: Chain | ChainId, sellToken: TokenAddress, buyToken: TokenAddress, error?: any): never { 10 | const chainId = typeof chain === 'number' ? chain : chain.chainId; 11 | throw new FailedToGenerateQuoteError(metadata.name, chainId, sellToken, buyToken, error); 12 | } 13 | 14 | type SlippagelessQuote> = Omit< 15 | SourceQuoteResponse, 16 | 'minBuyAmount' | 'maxSellAmount' | 'type' 17 | >; 18 | export function addQuoteSlippage>( 19 | quote: SlippagelessQuote, 20 | type: 'sell' | 'buy', 21 | slippagePercentage: number 22 | ): SourceQuoteResponse { 23 | return type === 'sell' 24 | ? { 25 | ...quote, 26 | type, 27 | minBuyAmount: subtractPercentage(quote.buyAmount, slippagePercentage, 'up'), 28 | maxSellAmount: quote.sellAmount, 29 | } 30 | : { 31 | ...quote, 32 | type, 33 | maxSellAmount: addPercentage(quote.sellAmount, slippagePercentage, 'up'), 34 | minBuyAmount: quote.buyAmount, 35 | }; 36 | } 37 | 38 | export function calculateAllowanceTarget(sellToken: TokenAddress, allowanceTarget: Address) { 39 | return isSameAddress(sellToken, Addresses.NATIVE_TOKEN) ? Addresses.ZERO_ADDRESS : allowanceTarget; 40 | } 41 | 42 | export function checksum(address: Address) { 43 | return getAddress(address); 44 | } 45 | -------------------------------------------------------------------------------- /src/services/quotes/quote-sources/wrappers/forced-timeout-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { timeoutPromise } from '@shared/timeouts'; 2 | import { IQuoteSource, QuoteSourceSupport } from '../types'; 3 | 4 | // We will pass the timeout to the quote sources, but sometime they don't have a way to enforce. So the idea will be to 5 | // add a wrapper that can enforce it 6 | export function forcedTimeoutWrapper< 7 | Support extends QuoteSourceSupport, 8 | CustomQuoteSourceConfig extends object, 9 | CustomQuoteSourceData extends Record 10 | >( 11 | source: IQuoteSource 12 | ): IQuoteSource { 13 | return { 14 | getMetadata: () => source.getMetadata(), 15 | quote: ({ components, request, config }) => { 16 | const description = `Quote ${request.sellToken} => ${request.buyToken} on chain with id ${request.chainId} for source ${ 17 | source.getMetadata().name 18 | }`; 19 | return timeoutPromise(source.quote({ components, request, config }), request.config.timeout, { description }); 20 | }, 21 | buildTx: ({ components, request, config }) => { 22 | const description = `Tx build ${request.sellToken} => ${request.buyToken} on chain with id ${request.chainId} for source ${ 23 | source.getMetadata().name 24 | }`; 25 | return timeoutPromise(source.buildTx({ components, request, config }), request.config.timeout, { description }); 26 | }, 27 | isConfigAndContextValidForQuoting: (config): config is CustomQuoteSourceConfig => { 28 | return source.isConfigAndContextValidForQuoting(config); 29 | }, 30 | isConfigAndContextValidForTxBuilding: (config): config is CustomQuoteSourceConfig => { 31 | return source.isConfigAndContextValidForTxBuilding(config); 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/services/quotes/source-lists/index.ts: -------------------------------------------------------------------------------- 1 | export { IQuoteSourceList, SourceListQuoteRequest, SourceListQuoteResponse, SourceListBuildTxRequest } from './types'; 2 | -------------------------------------------------------------------------------- /src/services/quotes/source-lists/types.ts: -------------------------------------------------------------------------------- 1 | import { GasPrice } from '@services/gas/types'; 2 | import { BaseTokenMetadata } from '@services/metadata/types'; 3 | import { ITriggerablePromise } from '@shared/triggerable-promise'; 4 | import { Address, TimeString } from '@types'; 5 | import { QuoteRequest, SourceMetadata, SourceId, QuoteTransaction, QuoteResponseRelevantForTxBuild } from '../types'; 6 | import { SourceConfig } from '../source-registry'; 7 | 8 | export type IQuoteSourceList = { 9 | supportedSources(): Record; 10 | getQuotes(request: SourceListQuoteRequest): Record>; 11 | buildTxs(request: SourceListBuildTxRequest): Record>; 12 | }; 13 | 14 | export type SourceListBuildTxRequest = { 15 | sourceConfig?: SourceConfig; 16 | quotes: Record>; 17 | quoteTimeout?: TimeString; 18 | }; 19 | 20 | export type SourceListQuoteRequest = Omit & { 21 | sources: SourceId[]; 22 | external: { 23 | tokenData: ITriggerablePromise<{ 24 | sellToken: BaseTokenMetadata; 25 | buyToken: BaseTokenMetadata; 26 | }>; 27 | gasPrice: ITriggerablePromise; 28 | }; 29 | sourceConfig?: SourceConfig; 30 | quoteTimeout?: TimeString; 31 | }; 32 | 33 | export type SourceListQuoteResponse = Record> = { 34 | sellAmount: bigint; 35 | buyAmount: bigint; 36 | maxSellAmount: bigint; 37 | minBuyAmount: bigint; 38 | estimatedGas?: bigint; 39 | type: 'sell' | 'buy'; 40 | recipient: Address; 41 | source: { id: SourceId; allowanceTarget: Address; name: string; logoURI: string }; 42 | customData: CustomQuoteSourceData; 43 | }; 44 | -------------------------------------------------------------------------------- /src/services/quotes/source-lists/utils.ts: -------------------------------------------------------------------------------- 1 | import { BigIntish } from '@types'; 2 | import { SourceListQuoteResponse } from './types'; 3 | import { QuoteTransaction } from '../types'; 4 | import { StringifyBigInt } from '@utility-types'; 5 | 6 | export function bigintifyQuote(quote: StringifyBigInt): SourceListQuoteResponse { 7 | return { 8 | ...quote, 9 | sellAmount: BigInt(quote.sellAmount), 10 | buyAmount: BigInt(quote.buyAmount), 11 | maxSellAmount: BigInt(quote.maxSellAmount), 12 | minBuyAmount: BigInt(quote.minBuyAmount), 13 | estimatedGas: toBigInt(quote.estimatedGas), 14 | }; 15 | } 16 | 17 | export function bigintifyTx(tx: StringifyBigInt): QuoteTransaction { 18 | return { 19 | ...tx, 20 | value: toBigInt(tx.value), 21 | maxPriorityFeePerGas: toBigInt(tx.maxPriorityFeePerGas), 22 | maxFeePerGas: toBigInt(tx.maxFeePerGas), 23 | gasPrice: toBigInt(tx.gasPrice), 24 | gasLimit: toBigInt(tx.gasLimit), 25 | }; 26 | } 27 | 28 | function toBigInt(value: BigIntish | undefined) { 29 | return value === undefined ? undefined : BigInt(value); 30 | } 31 | -------------------------------------------------------------------------------- /src/shared/abis/earn-delayed-withdrawal-manager.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | inputs: [ 4 | { internalType: 'uint256', name: 'positionId', type: 'uint256' }, 5 | { internalType: 'address', name: 'token', type: 'address' }, 6 | ], 7 | name: 'withdrawableFunds', 8 | outputs: [{ internalType: 'uint256', name: 'funds', type: 'uint256' }], 9 | stateMutability: 'view', 10 | type: 'function', 11 | }, 12 | { 13 | inputs: [ 14 | { internalType: 'uint256', name: 'positionId', type: 'uint256' }, 15 | { internalType: 'address', name: 'token', type: 'address' }, 16 | { internalType: 'address', name: 'recipient', type: 'address' }, 17 | ], 18 | name: 'withdraw', 19 | outputs: [ 20 | { internalType: 'uint256', name: 'withdrawn', type: 'uint256' }, 21 | { internalType: 'uint256', name: 'stillPending', type: 'uint256' }, 22 | ], 23 | stateMutability: 'nonpayable', 24 | type: 'function', 25 | }, 26 | { 27 | inputs: [{ internalType: 'bytes[]', name: 'data', type: 'bytes[]' }], 28 | name: 'multicall', 29 | outputs: [{ internalType: 'bytes[]', name: 'results', type: 'bytes[]' }], 30 | stateMutability: 'payable', 31 | type: 'function', 32 | }, 33 | ] as const; 34 | -------------------------------------------------------------------------------- /src/shared/abis/earn-strategy-router.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | inputs: [ 4 | { internalType: 'contract IEarnVault', name: 'vault', type: 'address' }, 5 | { internalType: 'uint256', name: 'positionId', type: 'uint256' }, 6 | { internalType: 'bytes', name: 'data', type: 'bytes' }, 7 | ], 8 | name: 'routeByPositionId', 9 | outputs: [{ internalType: 'bytes', name: 'result', type: 'bytes' }], 10 | stateMutability: 'payable', 11 | type: 'function', 12 | }, 13 | { 14 | inputs: [ 15 | { internalType: 'contract IEarnStrategyRegistry', name: 'registry', type: 'address' }, 16 | { internalType: 'StrategyId', name: 'strategyId', type: 'uint96' }, 17 | { internalType: 'bytes', name: 'data', type: 'bytes' }, 18 | ], 19 | name: 'routeByStrategyId', 20 | outputs: [{ internalType: 'bytes', name: 'result', type: 'bytes' }], 21 | stateMutability: 'payable', 22 | type: 'function', 23 | }, 24 | ] as const; 25 | -------------------------------------------------------------------------------- /src/shared/abis/earn-strategy.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { inputs: [], name: 'asset', outputs: [{ internalType: 'address', name: '', type: 'address' }], stateMutability: 'view', type: 'function' }, 3 | { 4 | inputs: [], 5 | name: 'supportedDepositTokens', 6 | outputs: [{ internalType: 'address[]', name: '', type: 'address[]' }], 7 | stateMutability: 'view', 8 | type: 'function', 9 | }, 10 | { 11 | inputs: [ 12 | { internalType: 'uint256', name: 'positionId', type: 'uint256' }, 13 | { internalType: 'SpecialWithdrawalCode', name: 'withdrawalCode', type: 'uint256' }, 14 | { internalType: 'uint256[]', name: 'toWithdraw', type: 'uint256[]' }, 15 | { internalType: 'bytes', name: 'withdrawalData', type: 'bytes' }, 16 | { internalType: 'address', name: 'recipient', type: 'address' }, 17 | ], 18 | name: 'specialWithdraw', 19 | outputs: [ 20 | { internalType: 'uint256[]', name: 'balanceChanges', type: 'uint256[]' }, 21 | { internalType: 'address[]', name: 'actualWithdrawnTokens', type: 'address[]' }, 22 | { internalType: 'uint256[]', name: 'actualWithdrawnAmounts', type: 'uint256[]' }, 23 | { internalType: 'bytes', name: 'result', type: 'bytes' }, 24 | ], 25 | stateMutability: 'nonpayable', 26 | type: 'function', 27 | }, 28 | ] as const; 29 | -------------------------------------------------------------------------------- /src/shared/abis/erc20.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | constant: true, 4 | inputs: [], 5 | name: 'name', 6 | outputs: [ 7 | { 8 | name: '', 9 | type: 'string', 10 | }, 11 | ], 12 | payable: false, 13 | stateMutability: 'view', 14 | type: 'function', 15 | }, 16 | { 17 | constant: false, 18 | inputs: [ 19 | { 20 | name: '_from', 21 | type: 'address', 22 | }, 23 | { 24 | name: '_to', 25 | type: 'address', 26 | }, 27 | { 28 | name: '_value', 29 | type: 'uint256', 30 | }, 31 | ], 32 | name: 'transferFrom', 33 | outputs: [ 34 | { 35 | name: '', 36 | type: 'bool', 37 | }, 38 | ], 39 | payable: false, 40 | stateMutability: 'nonpayable', 41 | type: 'function', 42 | }, 43 | { 44 | constant: true, 45 | inputs: [], 46 | name: 'decimals', 47 | outputs: [ 48 | { 49 | name: '', 50 | type: 'uint8', 51 | }, 52 | ], 53 | payable: false, 54 | stateMutability: 'view', 55 | type: 'function', 56 | }, 57 | { 58 | constant: true, 59 | inputs: [ 60 | { 61 | name: '_owner', 62 | type: 'address', 63 | }, 64 | ], 65 | name: 'balanceOf', 66 | outputs: [ 67 | { 68 | name: 'balance', 69 | type: 'uint256', 70 | }, 71 | ], 72 | payable: false, 73 | stateMutability: 'view', 74 | type: 'function', 75 | }, 76 | { 77 | constant: true, 78 | inputs: [], 79 | name: 'symbol', 80 | outputs: [ 81 | { 82 | name: '', 83 | type: 'string', 84 | }, 85 | ], 86 | payable: false, 87 | stateMutability: 'view', 88 | type: 'function', 89 | }, 90 | 91 | { 92 | constant: true, 93 | inputs: [ 94 | { 95 | name: '_owner', 96 | type: 'address', 97 | }, 98 | { 99 | name: '_spender', 100 | type: 'address', 101 | }, 102 | ], 103 | name: 'allowance', 104 | outputs: [ 105 | { 106 | name: '', 107 | type: 'uint256', 108 | }, 109 | ], 110 | payable: false, 111 | stateMutability: 'view', 112 | type: 'function', 113 | }, 114 | ] as const; 115 | -------------------------------------------------------------------------------- /src/shared/abis/erc721.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | inputs: [ 4 | { 5 | internalType: 'uint256', 6 | name: 'tokenId', 7 | type: 'uint256', 8 | }, 9 | ], 10 | name: 'ownerOf', 11 | outputs: [ 12 | { 13 | internalType: 'address', 14 | name: 'owner', 15 | type: 'address', 16 | }, 17 | ], 18 | stateMutability: 'view', 19 | type: 'function', 20 | }, 21 | ] as const; 22 | -------------------------------------------------------------------------------- /src/shared/abis/permit2.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | inputs: [ 4 | { internalType: 'address', name: '', type: 'address' }, 5 | { internalType: 'uint256', name: '', type: 'uint256' }, 6 | ], 7 | name: 'nonceBitmap', 8 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 9 | stateMutability: 'view', 10 | type: 'function', 11 | }, 12 | ] as const; 13 | -------------------------------------------------------------------------------- /src/shared/alchemy.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { ChainId } from '@types'; 3 | 4 | export const ALCHEMY_NETWORKS: Record = { 5 | [Chains.ETHEREUM.chainId]: { key: 'eth-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, 6 | [Chains.ETHEREUM_SEPOLIA.chainId]: { key: 'eth-sepolia', rpc: { tier: 'free' }, price: { supported: false } }, 7 | [Chains.OPTIMISM.chainId]: { key: 'opt-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, 8 | [Chains.ARBITRUM.chainId]: { key: 'arb-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, 9 | [Chains.POLYGON.chainId]: { key: 'polygon-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, 10 | [Chains.POLYGON_MUMBAI.chainId]: { key: 'polygon-mumbai', rpc: { tier: 'free' }, price: { supported: false } }, 11 | [Chains.ASTAR.chainId]: { key: 'astar-mainnet', rpc: { tier: 'free' }, price: { supported: false } }, 12 | [Chains.BLAST.chainId]: { key: 'blast-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, 13 | [Chains.BNB_CHAIN.chainId]: { key: 'bnb-mainnet', rpc: { tier: 'paid' }, price: { supported: true } }, 14 | [Chains.AVALANCHE.chainId]: { key: 'avax-mainnet', rpc: { tier: 'paid' }, price: { supported: true } }, 15 | [Chains.FANTOM.chainId]: { key: 'fantom-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, 16 | [Chains.METIS_ANDROMEDA.chainId]: { key: 'metis-mainnet', rpc: { tier: 'paid' }, price: { supported: true } }, 17 | [Chains.POLYGON_ZKEVM.chainId]: { key: 'polygonzkevm-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, 18 | [Chains.BASE.chainId]: { key: 'base-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, 19 | [Chains.GNOSIS.chainId]: { key: 'gnosis-mainnet', rpc: { tier: 'paid' }, price: { supported: true } }, 20 | [Chains.SCROLL.chainId]: { key: 'scroll-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, 21 | [Chains.opBNB.chainId]: { key: 'opbnb-mainnet', rpc: { tier: 'paid' }, price: { supported: true } }, 22 | [Chains.MANTLE.chainId]: { key: 'mantle-mainnet', rpc: { tier: 'free' }, price: { supported: false } }, 23 | [Chains.ROOTSTOCK.chainId]: { key: 'rootstock-mainnet', rpc: { tier: 'free' }, price: { supported: false } }, 24 | [Chains.LINEA.chainId]: { key: 'linea-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, 25 | [Chains.SONIC.chainId]: { key: 'sonic-mainnet', rpc: { tier: 'free' }, price: { supported: false } }, 26 | [Chains.ZK_SYNC_ERA.chainId]: { key: 'zksync-mainnet', rpc: { tier: 'free' }, price: { supported: true } }, 27 | [Chains.CELO.chainId]: { key: 'celo-mainnet', rpc: { tier: 'free' }, price: { supported: false } }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/shared/auto-update-cache.ts: -------------------------------------------------------------------------------- 1 | import { TimeString, Timestamp } from '@types'; 2 | import ms from 'ms'; 3 | type CacheConstructorParams = { 4 | calculate: () => Promise; 5 | config: AutoUpdateCacheConfig; 6 | }; 7 | 8 | const DEFAULT_RETRY_TIME: TimeString = '5Minutes'; 9 | 10 | export type AutoUpdateCacheConfig = { 11 | valid?: 'always' | { onlyFor: TimeString }; 12 | update: { every: TimeString; ifFailsTryAgainIn?: TimeString }; 13 | }; 14 | 15 | export class AutoUpdateCache { 16 | private readonly calculate: () => Promise; 17 | private readonly config: AutoUpdateCacheConfig; 18 | private cache: { lastUpdated: Timestamp; value: Value | undefined }; 19 | 20 | constructor({ calculate, config }: CacheConstructorParams) { 21 | this.calculate = calculate; 22 | this.config = config; 23 | this.cache = { lastUpdated: 0, value: undefined }; 24 | 25 | const isInvalid = config.valid && config.valid !== 'always' && ms(config.valid.onlyFor) <= ms(config.update.every); 26 | 27 | if (isInvalid) throw new Error(`'onlyFor' must be greater than 'every'`); 28 | 29 | this.update(); 30 | } 31 | 32 | getValue(): Value | undefined { 33 | const now = Date.now(); 34 | 35 | const isValid = ({ lastUpdated }: { lastUpdated: Timestamp }) => 36 | !this.config.valid || this.config.valid === 'always' || (this.config.valid.onlyFor && lastUpdated >= now - ms(this.config.valid.onlyFor)); 37 | 38 | return this.cache && isValid(this.cache) ? this.cache.value : undefined; 39 | } 40 | 41 | private async update() { 42 | try { 43 | const result = await this.calculate(); 44 | if (result !== undefined) { 45 | this.cache = { lastUpdated: Date.now(), value: result }; 46 | } 47 | setTimeout(() => this.update(), ms(this.config.update.every)); 48 | } catch (error) { 49 | setTimeout(() => this.update(), ms(this.config.update.ifFailsTryAgainIn ?? DEFAULT_RETRY_TIME)); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | export enum Addresses { 2 | NATIVE_TOKEN = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 3 | ZERO_ADDRESS = '0x0000000000000000000000000000000000000000', 4 | } 5 | 6 | export const Uint = { 7 | MAX_256: 2n ** 256n - 1n, 8 | }; 9 | -------------------------------------------------------------------------------- /src/shared/contracts.ts: -------------------------------------------------------------------------------- 1 | import { Address as ViemAddress } from 'viem'; 2 | import { ChainId, Address, Chain } from '@types'; 3 | import { toLower } from './utils'; 4 | 5 | export class Contract { 6 | private readonly defaultAddress: ContractAddress | undefined; 7 | private readonly overrides: Record = {}; 8 | 9 | constructor({ defaultAddress, overrides }: { defaultAddress: ContractAddress | undefined; overrides: Record }) { 10 | this.defaultAddress = defaultAddress; 11 | this.overrides = overrides; 12 | } 13 | 14 | address(chain: ChainId | Chain): ContractAddress { 15 | const chainId = typeof chain === 'number' ? chain : chain.chainId; 16 | const address = this.overrides[chainId] ?? this.defaultAddress; 17 | if (!address) { 18 | throw new Error(`Found no address on chain with id ${chainId}`); 19 | } 20 | return address; 21 | } 22 | 23 | static with({ defaultAddress }: { defaultAddress: Address }): ContractBuilder { 24 | return new ContractBuilder(defaultAddress); 25 | } 26 | 27 | static withNoDefault(): ContractBuilder { 28 | return new ContractBuilder(); 29 | } 30 | } 31 | 32 | class ContractBuilder { 33 | private readonly overrides: Record = {}; 34 | private readonly defaultAddress: ContractAddress | undefined; 35 | 36 | constructor(defaultAddress?: Address) { 37 | this.defaultAddress = defaultAddress ? (toLower(defaultAddress) as ContractAddress) : undefined; 38 | } 39 | 40 | and({ address, onChain }: { address: Address; onChain: ChainId | Chain }) { 41 | const chainId = typeof onChain === 'number' ? onChain : onChain.chainId; 42 | this.overrides[chainId] = toLower(address) as ContractAddress; 43 | return this; 44 | } 45 | 46 | build(): Contract { 47 | return new Contract({ defaultAddress: this.defaultAddress, overrides: this.overrides }); 48 | } 49 | } 50 | 51 | type ContractAddress = Lowercase
& ViemAddress; 52 | -------------------------------------------------------------------------------- /src/shared/deferred.ts: -------------------------------------------------------------------------------- 1 | export class Deferred implements Promise { 2 | private _resolveSelf: (value: T | PromiseLike) => void; 3 | private _rejectSelf: (reason?: any) => void; 4 | private promise: Promise; 5 | 6 | constructor() { 7 | // Will overwrite these below, but the compile can't tell 8 | this._resolveSelf = () => {}; 9 | this._rejectSelf = () => {}; 10 | this.promise = new Promise((resolve, reject) => { 11 | this._resolveSelf = resolve; 12 | this._rejectSelf = reject; 13 | }); 14 | } 15 | 16 | public then( 17 | onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, 18 | onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null 19 | ): Promise { 20 | return this.promise.then(onfulfilled, onrejected); 21 | } 22 | 23 | public catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise { 24 | return this.promise.catch(onrejected); 25 | } 26 | 27 | public finally(onfinally?: (() => void) | undefined | null): Promise { 28 | return this.promise.finally(onfinally); 29 | } 30 | 31 | public resolve(val: T) { 32 | this._resolveSelf(val); 33 | } 34 | public reject(reason: any) { 35 | this._rejectSelf(reason); 36 | } 37 | 38 | // @ts-ignore 39 | [Symbol.toStringTag]: 'Promise'; 40 | } 41 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deferred'; 2 | export * from './triggerable-promise'; 3 | export { ExpirationConfigOptions, ConcurrentLRUCacheWithContext, ConcurrentLRUCache } from './concurrent-lru-cache'; 4 | export { timeoutPromise, reduceTimeout, TimeoutError } from './timeouts'; 5 | export { 6 | isSameAddress, 7 | subtractPercentage, 8 | addPercentage, 9 | mulDivByNumber, 10 | calculateDeadline, 11 | filterRejectedResults, 12 | ruleOfThree, 13 | splitInChunks, 14 | timeToSeconds, 15 | toLower, 16 | amountToUSD, 17 | } from './utils'; 18 | export { AutoUpdateCache, AutoUpdateCacheConfig } from './auto-update-cache'; 19 | export { toChainId as defiLlamaToChainId } from './defi-llama'; 20 | export { Addresses, Uint } from './constants'; 21 | export { Contract } from './contracts'; 22 | export { wait, waitUntil } from './wait'; 23 | -------------------------------------------------------------------------------- /src/shared/timeouts.ts: -------------------------------------------------------------------------------- 1 | import { TimeString } from '@types'; 2 | import ms from 'ms'; 3 | 4 | export class TimeoutError extends Error { 5 | constructor(description: string, timeout: TimeString | number) { 6 | super(`${description} timeouted at ${typeof timeout === 'number' ? `${timeout}ms` : timeout}`); 7 | } 8 | } 9 | 10 | export function timeoutPromise( 11 | promise: Promise, 12 | timeout: TimeString | number | undefined, 13 | options?: { reduceBy?: TimeString; description?: string; onTimeout?: (timeout: TimeString | number) => void } 14 | ) { 15 | if (!timeout) return promise; 16 | const realTimeout = options?.reduceBy ? reduceTimeout(timeout, options.reduceBy) : timeout; 17 | const timeoutMs = typeof realTimeout === 'number' ? realTimeout : ms(realTimeout); 18 | return new Promise((resolve, reject) => { 19 | const timer = setTimeout(() => { 20 | options?.onTimeout?.(realTimeout); 21 | reject(new TimeoutError(options?.description ?? 'Promise', timeout)); 22 | }, timeoutMs); 23 | promise 24 | .then(resolve) 25 | .catch(reject) 26 | .finally(() => clearTimeout(timer)); 27 | }); 28 | } 29 | 30 | export function reduceTimeout(timeout: T, reduceBy: TimeString): T { 31 | if (!timeout) return undefined as T; 32 | const millisTimeout = typeof timeout === 'number' ? timeout : ms(timeout); 33 | const millisToTakeOut = ms(reduceBy); 34 | return millisTimeout > millisToTakeOut 35 | ? ((millisTimeout - millisToTakeOut).toString() as T) 36 | : (Math.floor((millisTimeout * 3) / 4).toString() as T); 37 | } 38 | -------------------------------------------------------------------------------- /src/shared/triggerable-promise.ts: -------------------------------------------------------------------------------- 1 | // It could happen that we don't want to trigger a promise unless it's necessary. This is specially true with some 2 | // requests (like gas prices) when we might get rate limited, and only a few of the sources need it. So the idea here 3 | // is to have a triggerable promise. It will only be executed when it's requested. At the same time, we will share the 4 | // same promise between all who request it, so that we don't make extra requests 5 | export type ITriggerablePromise = { 6 | request: () => Promise; 7 | }; 8 | 9 | export class TriggerablePromise implements ITriggerablePromise { 10 | private promise: Promise | undefined; 11 | 12 | constructor(private readonly trigger: () => Promise) {} 13 | 14 | request(): Promise { 15 | if (!this.promise) { 16 | this.promise = this.trigger(); 17 | } 18 | return this.promise; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/viem.ts: -------------------------------------------------------------------------------- 1 | import { Hex, TransactionRequest, Address as ViemAddress } from 'viem'; 2 | import { InputTransaction } from '@types'; 3 | 4 | export function mapTxToViemTx(tx: InputTransaction): TransactionRequest & { account: ViemAddress } { 5 | return { 6 | ...tx, 7 | data: tx.data as Hex | undefined, 8 | to: tx.to as ViemAddress | undefined, 9 | from: tx.from as ViemAddress, 10 | account: tx.from as ViemAddress, 11 | value: tx.value ? BigInt(tx.value) : undefined, 12 | gasPrice: tx.gasPrice ? BigInt(tx.gasPrice) : undefined, 13 | maxFeePerGas: tx.maxFeePerGas ? BigInt(tx.maxFeePerGas) : undefined, 14 | maxPriorityFeePerGas: tx.maxPriorityFeePerGas ? BigInt(tx.maxPriorityFeePerGas) : undefined, 15 | } as TransactionRequest & { account: ViemAddress }; 16 | } 17 | -------------------------------------------------------------------------------- /src/shared/wait.ts: -------------------------------------------------------------------------------- 1 | import ms from 'ms'; 2 | import { TimeString } from '@types'; 3 | 4 | export function wait(time: TimeString | number) { 5 | return new Promise((resolve) => setTimeout(resolve, ms(`${time}`))); 6 | } 7 | 8 | export function waitUntil({ check, every, maxAttempts }: { check: () => Promise | boolean; every: TimeString; maxAttempts?: number }) { 9 | return new Promise(async (resolve, reject) => { 10 | try { 11 | let attempts = 0; 12 | let result = await check(); 13 | while (!result && attempts++ < (maxAttempts ?? Infinity)) { 14 | await wait(every); 15 | result = await check(); 16 | } 17 | if (result) { 18 | resolve(); 19 | } else { 20 | reject(new Error(`Check continued to fail after ${maxAttempts} attempts`)); 21 | } 22 | } catch (e) { 23 | reject(e); 24 | } 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/utility-types.ts: -------------------------------------------------------------------------------- 1 | export type Only = { [P in keyof T]: T[P] } & { [P in keyof U]?: never }; 2 | export type Either = Only | Only; 3 | export type DistributiveOmit = T extends any ? Omit : never; 4 | export type ArrayOneOrMore = { 0: T } & Array; 5 | export type ArrayTwoOrMore = { 0: T; 1: T } & Array; 6 | export type ArrayOneOrMoreReadonly = { 0: T } & Readonly>; 7 | export type ExcludeKeysWithTypeOf = { 8 | [K in keyof T]: Exclude extends V ? never : K; 9 | }[keyof T]; 10 | export type Without = Pick>; 11 | export type WithRequired = T & { [P in K]-?: T[P] }; 12 | export type PartialOnly = Omit & Partial>; 13 | export type KeysWithValue, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T]; 14 | export type If = true extends Condition ? IfTrue : never; 15 | export type UnionMerge = { 16 | [k in CommonKeys]: PickTypeOf; 17 | } & { 18 | [k in NonCommonKeys]?: PickTypeOf; 19 | }; 20 | export type StringifyBigInt = T extends object 21 | ? { [K in keyof T]: bigint extends T[K] ? `${bigint}` : StringifyBigInt } 22 | : T; 23 | 24 | type CommonKeys = keyof T; 25 | type AllKeys = T extends any ? keyof T : never; 26 | type Subtract = A extends C ? never : A; 27 | type NonCommonKeys = Subtract, CommonKeys>; 28 | type PickType> = T extends { [k in K]?: any } ? T[K] : undefined; 29 | type PickTypeOf = K extends AllKeys ? PickType : never; 30 | -------------------------------------------------------------------------------- /test/integration/services/blocks/block-sources.spec.ts: -------------------------------------------------------------------------------- 1 | import ms from 'ms'; 2 | import chai, { expect } from 'chai'; 3 | import { Chains, getChainByKey } from '@chains'; 4 | import { ChainId, Timestamp } from '@types'; 5 | import chaiAsPromised from 'chai-as-promised'; 6 | import dotenv from 'dotenv'; 7 | import { FetchService } from '@services/fetch/fetch-service'; 8 | import { DefiLlamaBlockSource } from '@services/blocks/block-sources/defi-llama-block-source'; 9 | import { BlockResult, IBlocksSource } from '@services/blocks'; 10 | import { ProviderService } from '@services/providers/provider-service'; 11 | import { PublicRPCsProviderSource } from '@services/providers/provider-sources/public-rpcs-provider'; 12 | dotenv.config(); 13 | chai.use(chaiAsPromised); 14 | 15 | const TESTS: Record = { 16 | [Chains.OPTIMISM.chainId]: 1672531200, // Sunday, January 1, 2023 12:00:00 AM 17 | [Chains.POLYGON.chainId]: 1651363200, // Sunday, May 1, 2022 12:00:00 AM 18 | }; 19 | 20 | const PROVIDER_SERVICE = new ProviderService({ source: new PublicRPCsProviderSource() }); 21 | const FETCH_SERVICE = new FetchService(); 22 | const DEFI_LLAMA_BLOCKS_SOURCE = new DefiLlamaBlockSource(FETCH_SERVICE, PROVIDER_SERVICE); 23 | 24 | jest.retryTimes(2); 25 | jest.setTimeout(ms('1m')); 26 | 27 | describe('Blocks Sources', () => { 28 | blocksSourceTest({ title: 'Defi Llama Source', source: DEFI_LLAMA_BLOCKS_SOURCE }); 29 | 30 | function blocksSourceTest({ title, source }: { title: string; source: IBlocksSource }) { 31 | describe(title, () => { 32 | describe('getBlocksClosestToTimestamps', () => { 33 | let result: Record>; 34 | beforeAll(async () => { 35 | const timestamps = Object.entries(TESTS).map(([chainId, timestamp]) => ({ chainId: Number(chainId), timestamp })); 36 | result = await source.getBlocksClosestToTimestamps({ timestamps }); 37 | }); 38 | 39 | for (const chainId in TESTS) { 40 | const chain = getChainByKey(chainId); 41 | test(chain?.name ?? `Chain with id ${chainId}`, async () => { 42 | const timestamp = TESTS[chainId]; 43 | const blockResult = result[chainId][timestamp]; 44 | const viemClient = PROVIDER_SERVICE.getViemPublicClient({ chainId: Number(chainId) }); 45 | const [before, block, after] = await Promise.all([ 46 | viemClient.getBlock({ blockNumber: blockResult.block - 1n }), 47 | viemClient.getBlock({ blockNumber: blockResult.block }), 48 | viemClient.getBlock({ blockNumber: blockResult.block + 1n }), 49 | ]); 50 | const timestampDiffBefore = Math.abs(Number(before.timestamp) - timestamp); 51 | const timestampDiff = Math.abs(Number(block.timestamp) - timestamp); 52 | const timestampDiffAfter = Math.abs(Number(after.timestamp) - timestamp); 53 | expect(timestampDiff).to.be.lte(timestampDiffBefore); 54 | expect(timestampDiff).to.be.lte(timestampDiffAfter); 55 | expect(blockResult.timestamp).to.be.equal(Number(block.timestamp)); 56 | }); 57 | } 58 | }); 59 | }); 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /test/integration/services/metadata/metadata-sources/rpc-metadata-source.spec.ts: -------------------------------------------------------------------------------- 1 | import ms from 'ms'; 2 | import { expect } from 'chai'; 3 | import { RPCMetadataProperties, RPCMetadataSource } from '@services/metadata/metadata-sources/rpc-metadata-source'; 4 | import { PublicRPCsProviderSource } from '@services/providers/provider-sources/public-rpcs-provider'; 5 | import { Addresses } from '@shared/constants'; 6 | import { ChainId, FieldsRequirements, TokenAddress } from '@types'; 7 | import { MetadataResult } from '@services/metadata/types'; 8 | import { given, then, when } from '@test-utils/bdd'; 9 | import { ProviderService } from '@services/providers/provider-service'; 10 | 11 | const DAI = '0x6b175474e89094c44da98b954eedeac495271d0f'; 12 | 13 | const PROVIDER_SERVICE = new ProviderService({ source: new PublicRPCsProviderSource() }); 14 | const RPC_METADATA_SOURCE = new RPCMetadataSource(PROVIDER_SERVICE); 15 | 16 | jest.retryTimes(2); 17 | jest.setTimeout(ms('1m')); 18 | 19 | describe('RPC Metadata Source', () => { 20 | rpcTest({ 21 | when: 'not specifying anything', 22 | expected: ['decimals', 'symbol', 'name'], 23 | }); 24 | rpcTest({ 25 | when: 'marking properties as required', 26 | requirements: { requirements: { decimals: 'required', symbol: 'required', name: 'required' } }, 27 | expected: ['decimals', 'symbol', 'name'], 28 | }); 29 | rpcTest({ 30 | when: 'marking properties as best effort', 31 | requirements: { requirements: { decimals: 'best effort', symbol: 'best effort', name: 'best effort' } }, 32 | expected: ['decimals', 'symbol', 'name'], 33 | }); 34 | rpcTest({ 35 | when: 'marking properties as can ignore', 36 | requirements: { requirements: { decimals: 'best effort', symbol: 'can ignore' }, default: 'can ignore' }, 37 | expected: ['decimals'], 38 | }); 39 | 40 | function rpcTest>({ 41 | when: title, 42 | requirements, 43 | expected, 44 | }: { 45 | when: string; 46 | requirements?: Requirements; 47 | expected: (keyof RPCMetadataProperties)[]; 48 | }) { 49 | describe(title, () => { 50 | describe('getMetadata', () => { 51 | when(title, () => { 52 | let result: Record>>; 53 | given(async () => { 54 | result = await RPC_METADATA_SOURCE.getMetadata({ 55 | tokens: [ 56 | { chainId: 1, token: DAI }, 57 | { chainId: 1, token: Addresses.NATIVE_TOKEN }, 58 | ], 59 | config: { fields: requirements, timeout: '30s' }, 60 | }); 61 | }); 62 | then(`the returned fields are '${expected.join(', ')}'`, () => { 63 | expect(Object.keys(result)).to.have.lengthOf(1); 64 | expect(Object.keys(result[1])).to.have.lengthOf(2); 65 | expect(result[1][DAI]).to.have.all.keys(expected); 66 | expect(result[1][Addresses.NATIVE_TOKEN]).to.have.all.keys(expected); 67 | }); 68 | }); 69 | }); 70 | }); 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /test/integration/services/quotes/failing-quote.spec.ts: -------------------------------------------------------------------------------- 1 | import ms from 'ms'; 2 | import { expect } from 'chai'; 3 | import { given, then, when } from '@test-utils/bdd'; 4 | import { Chains } from '@chains'; 5 | import { QuoteResponse } from '@services/quotes/types'; 6 | import { buildSDK } from '@builder'; 7 | import { CONFIG } from './quote-tests-config'; 8 | import { FailedResponse } from '@services/quotes'; 9 | import { ChainId, DefaultRequirements, FieldsRequirements, TimeString, TokenAddress } from '@types'; 10 | import { IMetadataSource, MetadataInput, MetadataResult } from '@services/metadata'; 11 | import { parseEther } from 'viem'; 12 | 13 | jest.setTimeout(ms('1m')); 14 | 15 | type TokenMetadata = { symbol: string; decimals: number }; 16 | const MOCKED_METADATA_SOURCE: IMetadataSource = { 17 | supportedProperties: () => ({ [Chains.ETHEREUM.chainId]: { symbol: 'present', decimals: 'present' } }), 18 | getMetadata: = DefaultRequirements>({ 19 | tokens, 20 | }: { 21 | tokens: MetadataInput[]; 22 | config?: { fields?: Requirements; timeout?: TimeString }; 23 | }) => { 24 | const result: Record>> = {}; 25 | for (const { chainId, token } of tokens) { 26 | if (!(chainId in result)) result[chainId] = {}; 27 | result[chainId][token] = { symbol: 'SYM', decimals: 18 } as MetadataResult; 28 | } 29 | return Promise.resolve(result); 30 | }, 31 | }; 32 | 33 | const { quoteService } = buildSDK({ 34 | metadata: { source: { type: 'custom', instance: MOCKED_METADATA_SOURCE } }, 35 | quotes: { sourceList: { type: 'local' }, defaultConfig: CONFIG }, 36 | }); 37 | 38 | describe('Failing Quote', () => { 39 | when('executing a quote with invalid tokens', () => { 40 | let responses: (QuoteResponse | FailedResponse)[]; 41 | given(async () => { 42 | responses = await quoteService.getAllQuotes({ 43 | request: { 44 | chainId: Chains.ETHEREUM.chainId, 45 | sellToken: '0x0000000000000000000000000000000000000000', 46 | buyToken: '0x0000000000000000000000000000000000000001', 47 | order: { 48 | type: 'sell', 49 | sellAmount: parseEther('1'), 50 | }, 51 | slippagePercentage: 3, 52 | takerAddress: '0x0000000000000000000000000000000000000002', 53 | }, 54 | config: { 55 | timeout: '10s', 56 | ignoredFailed: false, 57 | }, 58 | }); 59 | }); 60 | then('all quotes are unsuccessful', () => { 61 | for (const response of responses) { 62 | expect('failed' in response, `Expected ${(response as QuoteResponse).source?.name} to fail, but it didn't`).to.be.true; 63 | } 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/integration/services/quotes/quote-tests-config.ts: -------------------------------------------------------------------------------- 1 | import { chainsUnion } from '@chains'; 2 | import { QUOTE_SOURCES, SourceConfig, SourceWithConfigId } from '@services/quotes/source-registry'; 3 | 4 | export const CONFIG: SourceConfig = { 5 | global: { 6 | referrer: { address: '0x0000000000000000000000000000000000000001', name: 'IntegrationTest' }, 7 | disableValidation: true, 8 | }, 9 | custom: { 10 | odos: { sourceDenylist: ['Hashflow'] }, 11 | }, 12 | }; 13 | if (process.env.RANGO_API_KEY) { 14 | CONFIG.custom!.rango = { apiKey: process.env.RANGO_API_KEY }; 15 | } 16 | if (process.env.CHANGELLY_API_KEY) { 17 | CONFIG.custom!.changelly = { apiKey: process.env.CHANGELLY_API_KEY }; 18 | } 19 | if (process.env.ZRX_API_KEY) { 20 | CONFIG.custom!['0x'] = { apiKey: process.env.ZRX_API_KEY }; 21 | } 22 | if (process.env.ONE_INCH_KEY) { 23 | CONFIG.custom!['1inch'] = { apiKey: process.env.ONE_INCH_KEY }; 24 | } 25 | if (process.env.PORTALS_FI_API_KEY) { 26 | CONFIG.custom!['portals-fi'] = { apiKey: process.env.PORTALS_FI_API_KEY }; 27 | } 28 | if (process.env.DODO_API_KEY) { 29 | CONFIG.custom!.dodo = { apiKey: process.env.DODO_API_KEY }; 30 | } 31 | if (process.env.BEBOP_API_KEY) { 32 | CONFIG.custom!.bebop = { apiKey: process.env.BEBOP_API_KEY }; 33 | } 34 | if (process.env.ENSO_API_KEY) { 35 | CONFIG.custom!.enso = { apiKey: process.env.ENSO_API_KEY }; 36 | } 37 | if (process.env.BARTER_AUTH_HEADER && process.env.BARTER_CUSTOM_SUBDOMAIN) { 38 | CONFIG.custom!.barter = { 39 | authHeader: process.env.BARTER_AUTH_HEADER, 40 | sourceDenylist: ['Hashflow'], 41 | }; 42 | } 43 | if (process.env.SQUID_INTEGRATOR_ID) { 44 | CONFIG.custom!.squid = { integratorId: process.env.SQUID_INTEGRATOR_ID }; 45 | } 46 | if (process.env.OKX_DEX_API_KEY && process.env.OKX_DEX_SECRET_KEY && process.env.OKX_DEX_PASSPHRASE) { 47 | CONFIG.custom!['okx-dex'] = { 48 | apiKey: process.env.OKX_DEX_API_KEY, 49 | secretKey: process.env.OKX_DEX_SECRET_KEY, 50 | passphrase: process.env.OKX_DEX_PASSPHRASE, 51 | }; 52 | } 53 | if (process.env.FLY_API_KEY) { 54 | CONFIG.custom!['fly-trade'] = { apiKey: process.env.FLY_API_KEY }; 55 | } 56 | 57 | export enum Test { 58 | SELL_RANDOM_ERC20_TO_STABLE, 59 | SELL_STABLE_TO_NATIVE, 60 | SELL_NATIVE_TO_RANDOM_ERC20, 61 | BUY_NATIVE_WITH_STABLE, 62 | BUY_RANDOM_ERC20_WITH_STABLE, 63 | WRAP_NATIVE_TOKEN, 64 | UNWRAP_WTOKEN, 65 | SELL_NATIVE_TO_STABLE_AND_TRANSFER, 66 | } 67 | 68 | export const EXCEPTIONS: Partial> = { 69 | uniswap: [Test.WRAP_NATIVE_TOKEN, Test.UNWRAP_WTOKEN], 70 | kyberswap: [Test.WRAP_NATIVE_TOKEN, Test.UNWRAP_WTOKEN], 71 | sovryn: [Test.WRAP_NATIVE_TOKEN, Test.UNWRAP_WTOKEN], 72 | oku: [Test.WRAP_NATIVE_TOKEN, Test.UNWRAP_WTOKEN], 73 | balmy: [ 74 | Test.SELL_RANDOM_ERC20_TO_STABLE, 75 | Test.SELL_STABLE_TO_NATIVE, 76 | Test.SELL_NATIVE_TO_RANDOM_ERC20, 77 | Test.SELL_NATIVE_TO_STABLE_AND_TRANSFER, 78 | Test.BUY_NATIVE_WITH_STABLE, 79 | Test.BUY_RANDOM_ERC20_WITH_STABLE, 80 | ], 81 | }; 82 | -------------------------------------------------------------------------------- /test/integration/utils/evm.ts: -------------------------------------------------------------------------------- 1 | import { alchemySupportedChains, buildAlchemyRPCUrl } from '@services/providers'; 2 | import { Chain } from '@types'; 3 | import { network } from 'hardhat'; 4 | 5 | export const fork = async ({ chain, blockNumber }: { chain: Chain; blockNumber?: number }) => { 6 | const params = [ 7 | { 8 | forking: { 9 | jsonRpcUrl: getUrl(chain), 10 | blockNumber, 11 | }, 12 | }, 13 | ]; 14 | await network.provider.request({ 15 | method: 'hardhat_reset', 16 | params, 17 | }); 18 | }; 19 | 20 | function getUrl(chain: Chain) { 21 | const apiKey = process.env.ALCHEMY_API_KEY; 22 | const paid = process.env.ALCHEMY_API_KEY_TYPE === 'paid'; 23 | const alchemyChains = alchemySupportedChains({ onlyFree: !paid }); 24 | if (apiKey && alchemyChains.includes(chain.chainId)) { 25 | return buildAlchemyRPCUrl({ apiKey, chainId: chain.chainId, protocol: 'https' }); 26 | } 27 | return chain.publicRPCs[0]; 28 | } 29 | -------------------------------------------------------------------------------- /test/integration/utils/other.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from '@chains'; 2 | import { TransactionResponse } from '@ethersproject/providers'; 3 | 4 | export async function calculateGasSpent(...txs: TransactionResponse[]): Promise { 5 | const gasSpentEach = await Promise.all( 6 | txs.map((tx) => tx.wait().then((receipt) => BigInt(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString()))) 7 | ); 8 | return gasSpentEach.reduce((accum, curr) => accum + curr, 0n); 9 | } 10 | 11 | export const CHAINS_WITH_KNOWN_ISSUES = [ 12 | Chains.AURORA, 13 | Chains.ASTAR, 14 | Chains.OASIS_EMERALD, 15 | Chains.VELAS, 16 | Chains.POLYGON_ZKEVM, 17 | Chains.ETHEREUM_SEPOLIA, 18 | Chains.ETHEREUM_GOERLI, 19 | Chains.POLYGON_MUMBAI, 20 | Chains.BASE_GOERLI, 21 | Chains.BOBA, 22 | Chains.opBNB, 23 | Chains.HECO, 24 | Chains.EVMOS, 25 | ].map(({ chainId }) => chainId); 26 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "types": ["node", "jest"], 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "resolveJsonModule": true 14 | }, 15 | "extends": "../tsconfig.json", 16 | "include": ["./integration", ".", "./test"], 17 | "files": ["../hardhat.config.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /test/unit/services/balances/balance-service.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Chains } from '@chains'; 3 | import { Address, ChainId, TimeString, TokenAddress } from '@types'; 4 | import { BalanceInput, IBalanceService, IBalanceSource } from '@services/balances/types'; 5 | import { BalanceService } from '@services/balances/balance-service'; 6 | import { given } from '@test-utils/bdd'; 7 | 8 | const OWNER = '0x1a00e1E311009E56e3b0B9Ed6F86f5Ce128a1C01'; 9 | 10 | const DAI = '0x6b175474e89094c44da98b954eedeac495271d0f'; 11 | const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; 12 | const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; 13 | 14 | describe('Balance Service', () => { 15 | let service: IBalanceService; 16 | given(() => { 17 | service = new BalanceService(SOURCE); 18 | }); 19 | 20 | test('supportedChains is calculated based on source', () => { 21 | expect(service.supportedChains()).to.eql([Chains.ETHEREUM.chainId]); 22 | }); 23 | 24 | describe('getBalancesForTokens', () => { 25 | test('Returned balances is as expected', async () => { 26 | const balances = await service.getBalancesForAccountInChain({ 27 | account: OWNER, 28 | chainId: Chains.ETHEREUM.chainId, 29 | tokens: [DAI, USDC, WETH], 30 | }); 31 | expect(balances).to.have.keys([DAI, USDC, WETH]); 32 | expect(balances[DAI]).to.equal(0n); 33 | expect(balances[USDC]).to.equal(10000n); 34 | expect(balances[WETH]).to.equal(20000n); 35 | }); 36 | }); 37 | }); 38 | 39 | const SOURCE: IBalanceSource = { 40 | supportedChains() { 41 | return [Chains.ETHEREUM.chainId]; 42 | }, 43 | getBalances({ 44 | tokens, 45 | }: { 46 | tokens: BalanceInput[]; 47 | config?: { timeout?: TimeString }; 48 | }): Promise>>> { 49 | const result: Record>> = {}; 50 | for (let i = 0; i < tokens.length; i++) { 51 | const { chainId, token, account } = tokens[i]; 52 | if (!(chainId in result)) result[chainId] = {}; 53 | if (!(account in result[chainId])) result[chainId][account] = {}; 54 | result[chainId][account][token] = BigInt(i * 10000); 55 | } 56 | return Promise.resolve(result); 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /test/unit/services/fetch/fetch-service.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { then, when } from '@test-utils/bdd'; 3 | import { FetchService } from '@services/fetch/fetch-service'; 4 | import chaiAsPromised from 'chai-as-promised'; 5 | 6 | chai.use(chaiAsPromised); 7 | 8 | describe('Fetch Service', () => { 9 | when('request timeouts', () => { 10 | then('error is clear', async () => { 11 | const service = new FetchService(); 12 | await expect(service.fetch('https://google.com', { timeout: '1' })) 13 | .to.be.rejectedWith(AggregateError) 14 | .and.eventually.satisfy((error: AggregateError) => { 15 | return error.errors.some((error) => error.message.includes('Request to https://google.com timeouted')); 16 | }); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/unit/services/providers/provider-sources/prioritized-provider-source-combinator.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { http } from 'viem'; 3 | import { Chains } from '@chains'; 4 | import { then, when } from '@test-utils/bdd'; 5 | import { PrioritizedProviderSourceCombinator } from '@services/providers/provider-sources/prioritized-provider-source-combinator'; 6 | import { IProviderSource } from '@services/providers/types'; 7 | 8 | const PROVIDER_1 = http(); 9 | const PROVIDER_2 = http(); 10 | const FULL_SUPPORT = { ethers: true, viem: true }; 11 | 12 | describe('Prioritized Provider Source Combinator', () => { 13 | const source1: IProviderSource = { 14 | supportedChains: () => [Chains.POLYGON.chainId], 15 | getViemTransport: () => PROVIDER_1, 16 | }; 17 | const source2: IProviderSource = { 18 | supportedChains: () => [Chains.POLYGON.chainId, Chains.ETHEREUM.chainId], 19 | getViemTransport: () => PROVIDER_2, 20 | }; 21 | const fallbackSource = new PrioritizedProviderSourceCombinator([source1, source2]); 22 | 23 | when('asking for supported chains', () => { 24 | then('the union of the given sources is returned', () => { 25 | const supportedChains = fallbackSource.supportedChains(); 26 | expect(supportedChains).to.eql([Chains.POLYGON.chainId, Chains.ETHEREUM.chainId]); 27 | }); 28 | }); 29 | 30 | when('asking for a chain supported by source1', () => { 31 | then('provider1 is returned', () => { 32 | expect(fallbackSource.getViemTransport({ chainId: Chains.POLYGON.chainId })).to.equal(PROVIDER_1); 33 | }); 34 | }); 35 | 36 | when('asking for a chain not supported by source1', () => { 37 | then('provider2 is returned', () => { 38 | expect(fallbackSource.getViemTransport({ chainId: Chains.ETHEREUM.chainId })).to.equal(PROVIDER_2); 39 | }); 40 | }); 41 | 42 | when('asking for a chain not supported by any source', () => { 43 | then('an error is thrown', () => { 44 | expect(() => fallbackSource.getViemTransport({ chainId: Chains.OPTIMISM.chainId })).to.throw( 45 | `Chain with id ${Chains.OPTIMISM.chainId} not supported` 46 | ); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/unit/services/quotes/source-lists/local-source-list.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { LocalSourceList } from '@services/quotes/source-lists/local-source-list'; 4 | import { SourceListQuoteRequest } from '@services/quotes/source-lists/types'; 5 | import { then, when } from '@test-utils/bdd'; 6 | import { IProviderService } from '@services/providers'; 7 | import { IFetchService } from '@services/fetch'; 8 | chai.use(chaiAsPromised); 9 | 10 | describe('Local Source List', () => { 11 | describe('Rejected Promises', () => { 12 | when('asking for unknown source', () => { 13 | then('promise is rejected', async () => { 14 | const sourceList = new LocalSourceList({ 15 | providerService: PROVIDER_SERVICE, 16 | fetchService: FETCH_SERVICE, 17 | }); 18 | const quotes = sourceList.getQuotes({ 19 | ...REQUEST, 20 | sources: ['unknown'], 21 | order: { type: 'sell', sellAmount: 100 }, 22 | }); 23 | expect(Object.keys(quotes)).to.have.lengthOf(1); 24 | await expect(quotes['unknown']).to.have.rejectedWith(`Could not find a source with id 'unknown'`); 25 | }); 26 | }); 27 | 28 | when('executing a buy order for a source that does not support it', () => { 29 | then('promise is rejected', async () => { 30 | const sourceList = new LocalSourceList({ 31 | providerService: PROVIDER_SERVICE, 32 | fetchService: FETCH_SERVICE, 33 | }); 34 | const quotes = sourceList.getQuotes({ 35 | ...REQUEST, 36 | order: { type: 'buy', buyAmount: 100 }, 37 | sources: ['odos'], 38 | }); 39 | expect(Object.keys(quotes)).to.have.lengthOf(1); 40 | await expect(quotes['odos']).to.have.rejectedWith(`Source with id 'odos' does not support buy orders`); 41 | }); 42 | }); 43 | 44 | when('context/config is invalid for a source', () => { 45 | then('promise is rejected', async () => { 46 | const sourceList = new LocalSourceList({ 47 | providerService: PROVIDER_SERVICE, 48 | fetchService: FETCH_SERVICE, 49 | }); 50 | const quotes = sourceList.getQuotes({ 51 | ...REQUEST, 52 | order: { type: 'sell', sellAmount: 100 }, 53 | sources: ['enso'], 54 | }); 55 | expect(Object.keys(quotes)).to.have.lengthOf(1); 56 | await expect(quotes['enso']).to.have.rejectedWith(`The current context or config is not valid for source with id 'enso'`); 57 | }); 58 | }); 59 | }); 60 | }); 61 | 62 | const REQUEST: Omit = { 63 | chainId: 1, 64 | sellToken: '0x0000000000000000000000000000000000000001', 65 | buyToken: '0x0000000000000000000000000000000000000002', 66 | slippagePercentage: 0.03, 67 | takerAddress: '0x0000000000000000000000000000000000000003', 68 | external: { 69 | tokenData: {} as any, 70 | gasPrice: {} as any, 71 | }, 72 | }; 73 | 74 | const PROVIDER_SERVICE: IProviderService = {} as any; 75 | const FETCH_SERVICE: IFetchService = {} as any; 76 | -------------------------------------------------------------------------------- /test/unit/services/quotes/sources/quote-sources.spec.ts: -------------------------------------------------------------------------------- 1 | import { getChainByKey } from '@chains'; 2 | import { expect } from 'chai'; 3 | import { QUOTE_SOURCES } from '@services/quotes/source-registry'; 4 | 5 | describe('Quote Sources', () => { 6 | it('all sources have known chains assigned', () => { 7 | for (const source of allSources()) { 8 | const { 9 | name, 10 | supports: { chains }, 11 | } = source.getMetadata(); 12 | for (const chain of chains) { 13 | expect(getChainByKey(chain), `Unknown chain with id ${chain} on ${name}`).to.not.be.undefined; 14 | } 15 | } 16 | }); 17 | }); 18 | 19 | function allSources() { 20 | return Object.values(QUOTE_SOURCES); 21 | } 22 | -------------------------------------------------------------------------------- /test/utils/bdd.ts: -------------------------------------------------------------------------------- 1 | export const then = it; 2 | export const given = beforeEach; 3 | export const when = (title: string, fn: () => void) => describe('when ' + title, fn); 4 | when.only = (title: string, fn?: () => void) => describe.only('when ' + title, fn!); 5 | when.skip = (title: string, fn: () => void) => describe.skip('when ' + title, fn); 6 | 7 | export const contract = describe; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "dist", 5 | "target": "es2021", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "pretty": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "noImplicitAny": true, 14 | "noImplicitThis": true, 15 | "declaration": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "esModuleInterop": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "baseUrl": ".", 21 | "paths": { 22 | "@builder": ["src/sdk/sdk-builder"], 23 | "@services/*": ["src/services/*"], 24 | "@shared/*": ["src/shared/*"], 25 | "@chains": ["src/chains"], 26 | "@types": ["src/types"], 27 | "@utility-types": ["src/utility-types"], 28 | "@test-utils/*": ["test/utils/*", "test/integration/utils/*"] 29 | } 30 | }, 31 | "include": ["src"] 32 | } 33 | --------------------------------------------------------------------------------