├── .c8rc.json ├── .env.example ├── .eslintrc.cjs ├── .github └── workflows │ ├── integration-tests.yml │ ├── publish-npmjs.yml │ └── unit-tests.yml ├── .gitignore ├── .graphclientrc.yaml ├── .mocharc.json ├── .prettierrc.cjs ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── debug-return-value.js ├── hardhat.config.js ├── package.json ├── patches ├── @railgun-community+engine+9.3.1.patch └── @railgun-community+wallet+10.3.3.patch ├── release ├── run-anvil ├── src ├── __tests__ │ └── index.test.ts ├── abi │ ├── abi.ts │ ├── adapt │ │ └── RelayAdapt.json │ ├── lido │ │ ├── LidoSTETH.json │ │ └── LidoWSTETH.json │ ├── liquidity │ │ ├── UniV2LikeFactory.json │ │ ├── UniV2LikePair.json │ │ └── UniV2LikeRouter.json │ ├── token │ │ ├── erc20.json │ │ └── erc721.json │ └── vault │ │ └── beefy │ │ └── BeefyVault-MergedV6V7.json ├── api │ ├── beefy │ │ ├── __tests__ │ │ │ └── beefy-api.test.ts │ │ ├── beefy-api.ts │ │ ├── beefy-fetch.ts │ │ └── index.ts │ ├── index.ts │ ├── uni-v2-like │ │ ├── __tests__ │ │ │ ├── uni-v2-like-pairs.test.ts │ │ │ └── uni-v2-like-sdk.test.ts │ │ ├── index.ts │ │ ├── uni-v2-like-pairs.ts │ │ └── uni-v2-like-sdk.ts │ ├── zero-x-v2 │ │ ├── __tests__ │ │ │ ├── zero-x-v2-fetch.test.ts │ │ │ └── zero-x-v2-quote.test.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ ├── zero-x-v2-fetch.ts │ │ └── zero-x-v2-quote.ts │ └── zero-x │ │ ├── IZeroEx.json │ │ ├── __tests__ │ │ ├── zero-x-fetch.test.ts │ │ └── zero-x-quote.test.ts │ │ ├── index.ts │ │ ├── zero-x-fetch.ts │ │ └── zero-x-quote.ts ├── combo-meals │ ├── combo-meal.ts │ └── liquidity-vault │ │ ├── __tests__ │ │ ├── FORK-run-uni-v2-like-liquidity-beefy-combo-meals.test.ts │ │ └── sushiswap-v2-add-liquidity-beefy-deposit-combo-meal.test.ts │ │ └── uni-v2-like-add-liquidity-beefy-deposit-combo-meal.ts ├── contract │ ├── adapt │ │ ├── index.ts │ │ └── relay-adapt-contract.ts │ ├── index.ts │ ├── lido │ │ ├── index.ts │ │ ├── lido-stETH-contract.ts │ │ └── lido-wstETH-contract.ts │ ├── liquidity │ │ ├── uni-v2-like-factory-contract.ts │ │ ├── uni-v2-like-pair-contract.ts │ │ └── uni-v2-like-router-contract.ts │ ├── token │ │ ├── erc20-contract.ts │ │ ├── erc721-contract.ts │ │ └── index.ts │ └── vault │ │ └── beefy │ │ └── beefy-vault-contract.ts ├── graph │ ├── graph-cache │ │ ├── PANCAKE-V2-BSC-PAIRS.json │ │ ├── QUICK-V2-POLYGON-PAIRS.json │ │ ├── README.md │ │ ├── SUSHI-V2-ARBITRUM-PAIRS.json │ │ ├── SUSHI-V2-BSC-PAIRS.json │ │ ├── SUSHI-V2-ETH-PAIRS.json │ │ ├── SUSHI-V2-POLYGON-PAIRS.json │ │ ├── UNI-V2-ETH-PAIRS.json │ │ └── uni-v2-like-subgraph-cache.ts │ ├── graphql │ │ ├── .graphclient │ │ │ ├── index.ts │ │ │ ├── schema.graphql │ │ │ └── sources │ │ │ │ ├── pancakeswap-v2-bsc │ │ │ │ ├── introspectionSchema.ts │ │ │ │ ├── schema.graphql │ │ │ │ └── types.ts │ │ │ │ ├── quickswap-v2-polygon │ │ │ │ ├── introspectionSchema.ts │ │ │ │ ├── schema.graphql │ │ │ │ └── types.ts │ │ │ │ ├── sushiswap-v2-arbitrum │ │ │ │ ├── introspectionSchema.ts │ │ │ │ ├── schema.graphql │ │ │ │ └── types.ts │ │ │ │ ├── sushiswap-v2-bsc │ │ │ │ ├── introspectionSchema.ts │ │ │ │ ├── schema.graphql │ │ │ │ └── types.ts │ │ │ │ ├── sushiswap-v2-ethereum │ │ │ │ ├── introspectionSchema.ts │ │ │ │ ├── schema.graphql │ │ │ │ └── types.ts │ │ │ │ ├── sushiswap-v2-polygon │ │ │ │ ├── introspectionSchema.ts │ │ │ │ ├── schema.graphql │ │ │ │ └── types.ts │ │ │ │ └── uniswap-v2-ethereum │ │ │ │ ├── introspectionSchema.ts │ │ │ │ ├── schema.graphql │ │ │ │ └── types.ts │ │ └── uni-v2-like-query.graphql │ └── uni-v2-like-subgraph.ts ├── index.ts ├── init │ ├── __tests__ │ │ └── init.test.ts │ ├── index.ts │ └── init.ts ├── models │ ├── constants.ts │ ├── export-models.ts │ ├── index.ts │ ├── min-gas-limits.ts │ ├── railgun-config.ts │ ├── uni-v2-like.ts │ └── zero-x-config.ts ├── recipes │ ├── __tests__ │ │ └── custom-recipe.test.ts │ ├── adapt │ │ ├── __tests__ │ │ │ ├── FORK-run-unwrap-transfer-base-token-recipe.test.ts │ │ │ └── unwrap-transfer-base-token-recipe.test.ts │ │ ├── index.ts │ │ └── unwrap-transfer-base-token-recipe.ts │ ├── custom-recipe.ts │ ├── empty │ │ ├── __tests__ │ │ │ ├── FORK-run-designate-shield-erc20-recipient-empty-recipe.test.ts │ │ │ ├── FORK-run-empty-recipe-erc721.test.ts │ │ │ ├── FORK-run-empty-recipe.test.ts │ │ │ ├── designate-shield-erc20-recipient-empty-recipe.test.ts │ │ │ └── empty-recipe.test.ts │ │ ├── designate-shield-erc20-recipient-empty-recipe.ts │ │ └── empty-recipe.ts │ ├── index.ts │ ├── lido │ │ ├── __tests__ │ │ │ ├── FORK-lido-stake-recipe.test.ts │ │ │ └── FORK-lido-stake-shortcut-recipe.test.ts │ │ ├── index.ts │ │ ├── lido-stake-recipe.ts │ │ └── lido-stake-shortcut-recipe.ts │ ├── liquidity │ │ ├── add-liquidity-recipe.ts │ │ ├── index.ts │ │ ├── remove-liquidity-recipe.ts │ │ └── uni-v2-like │ │ │ ├── __tests__ │ │ │ ├── FORK-run-sushiswap-v2-liquidity-recipes.test.ts │ │ │ ├── uniswap-v2-add-liquidity-recipe.test.ts │ │ │ └── uniswap-v2-remove-liquidity-recipe.test.ts │ │ │ ├── index.ts │ │ │ ├── uni-v2-like-add-liquidity-recipe.ts │ │ │ └── uni-v2-like-remove-liquidity-recipe.ts │ ├── recipe.ts │ ├── swap │ │ ├── __tests__ │ │ │ ├── FORK-run-zero-x-swap-recipe.test.ts │ │ │ ├── zero-x-swap-recipe-private-destination.test.ts │ │ │ ├── zero-x-swap-recipe-public-destination.test.ts │ │ │ ├── zero-x-swap-recipe.test.ts │ │ │ ├── zero-x-v2-swap-recipe-private-destination.test.ts │ │ │ ├── zero-x-v2-swap-recipe-public-destination.test.ts │ │ │ └── zero-x-v2-swap-recipe.test.ts │ │ ├── index.ts │ │ ├── swap-recipe.ts │ │ ├── zero-x-swap-recipe.ts │ │ └── zero-x-v2-swap-recipe.ts │ └── vault │ │ ├── beefy │ │ ├── __tests__ │ │ │ ├── FORK-run-beefy-vault-recipes.test.ts │ │ │ ├── beefy-deposit-recipe.test.ts │ │ │ └── beefy-withdraw-recipe.test.ts │ │ ├── beefy-deposit-recipe.ts │ │ └── beefy-withdraw-recipe.ts │ │ └── index.ts ├── steps │ ├── adapt │ │ ├── __tests__ │ │ │ ├── transfer-base-token-step.test.ts │ │ │ ├── unwrap-base-token-step.test.ts │ │ │ └── wrap-base-token-step.test.ts │ │ ├── empty-transfer-base-token-step.ts │ │ ├── index.ts │ │ ├── transfer-base-token-step.ts │ │ ├── unwrap-base-token-step.ts │ │ └── wrap-base-token-step.ts │ ├── index.ts │ ├── lido │ │ ├── __tests__ │ │ │ └── lido-stake-step.test.ts │ │ ├── index.ts │ │ ├── lido-stake-shortcut-step.ts │ │ ├── lido-stake-step.ts │ │ └── lido-wrap-steth-step.ts │ ├── liquidity │ │ └── uni-v2-like │ │ │ ├── __tests__ │ │ │ ├── uniswap-v2-add-liquidity-step.test.ts │ │ │ └── uniswap-v2-remove-liquidity-step.test.ts │ │ │ ├── uni-v2-like-add-liquidity-step.ts │ │ │ └── uni-v2-like-remove-liquidity-step.ts │ ├── railgun │ │ ├── __tests__ │ │ │ ├── shield-default-step.test.ts │ │ │ └── unshield-default-step.test.ts │ │ ├── designate-shield-erc20-recipient-step.ts │ │ ├── index.ts │ │ ├── shield-default-step.ts │ │ └── unshield-default-step.ts │ ├── step.ts │ ├── swap │ │ ├── index.ts │ │ └── zero-x │ │ │ ├── __tests__ │ │ │ ├── zero-x-swap-step.test.ts │ │ │ └── zero-x-v2-swap-step.test.ts │ │ │ ├── zero-x-swap-step.ts │ │ │ └── zero-x-v2-swap-step.ts │ ├── token │ │ ├── erc20 │ │ │ ├── __tests__ │ │ │ │ ├── approve-erc20-spender-step.test.ts │ │ │ │ └── transfer-erc20-step.test.ts │ │ │ ├── approve-erc20-spender-step.ts │ │ │ ├── index.ts │ │ │ └── transfer-erc20-step.ts │ │ ├── erc721 │ │ │ └── index.ts │ │ └── index.ts │ └── vault │ │ ├── beefy │ │ ├── __tests__ │ │ │ ├── beefy-deposit-step.test.ts │ │ │ ├── beefy-util.test.ts │ │ │ └── beefy-withdraw-step.test.ts │ │ ├── beefy-deposit-step.ts │ │ ├── beefy-util.ts │ │ └── beefy-withdraw-step.ts │ │ └── index.ts ├── test │ ├── common.test.ts │ ├── mocks.test.ts │ ├── railgun-setup.test.ts │ ├── rpc-setup.test.ts │ ├── setup.test.ts │ ├── shared.test.ts │ ├── test-config-overrides.test-example.ts │ └── test-config.test.ts ├── typechain │ ├── adapt │ │ ├── RelayAdapt.ts │ │ └── index.ts │ ├── common.ts │ ├── factories │ │ ├── adapt │ │ │ ├── RelayAdapt__factory.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── lido │ │ │ ├── LidoSTETH__factory.ts │ │ │ ├── LidoWSTETH__factory.ts │ │ │ └── index.ts │ │ ├── liquidity │ │ │ ├── UniV2LikeFactory__factory.ts │ │ │ ├── UniV2LikePair__factory.ts │ │ │ ├── UniV2LikeRouter__factory.ts │ │ │ └── index.ts │ │ ├── token │ │ │ ├── Erc20__factory.ts │ │ │ ├── Erc721__factory.ts │ │ │ └── index.ts │ │ └── vault │ │ │ ├── beefy │ │ │ ├── BeefyVaultMergedV6V7__factory.ts │ │ │ └── index.ts │ │ │ └── index.ts │ ├── index.ts │ ├── lido │ │ ├── LidoSTETH.ts │ │ ├── LidoWSTETH.ts │ │ └── index.ts │ ├── liquidity │ │ ├── UniV2LikeFactory.ts │ │ ├── UniV2LikePair.ts │ │ ├── UniV2LikeRouter.ts │ │ └── index.ts │ ├── token │ │ ├── Erc20.ts │ │ ├── Erc721.ts │ │ └── index.ts │ └── vault │ │ ├── beefy │ │ ├── BeefyVaultMergedV6V7.ts │ │ └── index.ts │ │ └── index.ts ├── types │ └── index.d.ts ├── utils │ ├── __tests__ │ │ ├── big-number.test.ts │ │ ├── fee.test.ts │ │ ├── pair-rate.test.ts │ │ └── token.test.ts │ ├── address.ts │ ├── basis-points.ts │ ├── big-number.ts │ ├── cookbook-debug.ts │ ├── fee.ts │ ├── filters.ts │ ├── id.ts │ ├── index.ts │ ├── lp-pair.ts │ ├── no-action-output.ts │ ├── number.ts │ ├── random.ts │ ├── token.ts │ └── wrap-util.ts └── validators │ └── step-validator.ts ├── tsconfig.json ├── tsconfig.test.json └── yarn.lock /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "include": ["src/**/*.ts"], 4 | "exclude": [ 5 | "src/**/__tests__/**", 6 | "dist/**", 7 | "node_modules/**", 8 | "**/*.d.ts", 9 | "src/models/**", 10 | "src/graph/graphql/**", 11 | "src/test/**", 12 | "src/typechain" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | THE_GRAPH_API_KEY="REPLACE_ME" 2 | ZERO_X_API_KEY="REPLACE_ME" 3 | ZERO_X_PROXY_API_DOMAIN="REPLACE_ME" 4 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | plugins: ['flowtype'], 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:import/typescript', 7 | 'plugin:@typescript-eslint/recommended', 8 | 'prettier', 9 | ], 10 | globals: { 11 | Optional: 'readonly', 12 | MapType: 'readonly', 13 | NodeJS: 'readonly', 14 | Response: 'readonly', 15 | Buffer: 'readonly', 16 | JSX: 'readonly', 17 | }, 18 | parser: '@typescript-eslint/parser', 19 | parserOptions: { 20 | project: ['./tsconfig.json', './tsconfig.test.json'], 21 | }, 22 | rules: { 23 | 'arrow-body-style': 0, 24 | 'import/extensions': 0, 25 | 'no-unused-vars': 0, 26 | 'no-use-before-define': 0, 27 | 'import/prefer-default-export': 0, 28 | 'import/no-unresolved': 0, 29 | 'no-restricted-syntax': 0, 30 | 'no-unused-expressions': 0, 31 | 'no-shadow': 0, 32 | 'no-continue': 0, 33 | 'no-console': 1, 34 | 'default-case': 0, 35 | '@typescript-eslint/switch-exhaustiveness-check': 2, 36 | '@typescript-eslint/no-explicit-any': 1, 37 | '@typescript-eslint/ban-ts-comment': 0, 38 | '@typescript-eslint/no-unused-vars': 1, 39 | '@typescript-eslint/no-unsafe-call': 1, 40 | '@typescript-eslint/no-unsafe-member-access': 1, 41 | '@typescript-eslint/no-unsafe-assignment': 1, 42 | '@typescript-eslint/no-unsafe-argument': 1, 43 | 'import/order': 0, 44 | 'consistent-return': 0, 45 | 'prefer-destructuring': 0, 46 | 'lines-between-class-members': 0, 47 | '@typescript-eslint/no-empty-function': 0, 48 | 'no-promise-executor-return': 0, 49 | '@typescript-eslint/no-floating-promises': 2, 50 | '@typescript-eslint/no-non-null-assertion': 2, 51 | 'no-warning-comments': 1, 52 | '@typescript-eslint/strict-boolean-expressions': 2, 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests (RPC Fork) 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | unit-tests: 11 | name: 🧪 Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: '16' 18 | cache: 'yarn' 19 | - name: Install yarn 20 | uses: borales/actions-yarn@v4 21 | with: 22 | cmd: install 23 | - name: Install Foundry 24 | uses: foundry-rs/foundry-toolchain@v1 25 | - name: Run Anvil and fork test 26 | uses: BerniWittmann/background-server-action@v1 27 | env: 28 | ZERO_X_PROXY_API_DOMAIN: ${{ secrets.ZERO_X_PROXY_API_DOMAIN }} 29 | ZERO_X_API_KEY: ${{ secrets.ZERO_X_API_KEY }} 30 | NETWORK_NAME: Ethereum 31 | with: 32 | command: yarn test-fork 33 | start: ./run-anvil Ethereum https://uber.us.proxy.railwayapi.xyz/rpc/alchemy/eth-mainnet --silent 34 | -------------------------------------------------------------------------------- /.github/workflows/publish-npmjs.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/heads 5 | branches: 6 | - release 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | # Setup .npmrc file to publish to npm 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: '16.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - name: Install yarn 18 | uses: borales/actions-yarn@v4 19 | with: 20 | cmd: install 21 | - run: yarn publish --access public 22 | env: 23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | unit-tests: 11 | name: 🧪 Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: '16' 18 | cache: 'yarn' 19 | - name: Install yarn 20 | uses: borales/actions-yarn@v4 21 | with: 22 | cmd: install 23 | - name: Yarn test 24 | shell: bash 25 | env: 26 | ZERO_X_PROXY_API_DOMAIN: ${{ secrets.ZERO_X_PROXY_API_DOMAIN }} 27 | ZERO_X_API_KEY: ${{ secrets.ZERO_X_API_KEY }} 28 | run: yarn test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | 4 | # Node 5 | node_modules/ 6 | npm-debug.log 7 | yarn-error.log 8 | package-lock.json 9 | dist/ 10 | .env 11 | 12 | # Test 13 | .nyc_output 14 | coverage/ 15 | src/test/test-config-overrides.test.ts 16 | 17 | # Quickstart assets 18 | test.db 19 | artifacts-* 20 | -------------------------------------------------------------------------------- /.graphclientrc.yaml: -------------------------------------------------------------------------------- 1 | sources: 2 | - name: uniswap-v2-ethereum 3 | handler: 4 | graphql: 5 | # The core Uniswap V2 subgraph is experiencing problems with indexing. 6 | # endpoint: https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2 7 | endpoint: https://gateway.thegraph.com/api/${THE_GRAPH_API_KEY}/subgraphs/id/EYCKATKGBKLWvSfwvBjzfCBmGwYNdVkduYXVivCsLRFu 8 | - name: sushiswap-v2-ethereum 9 | handler: 10 | graphql: 11 | endpoint: https://gateway.thegraph.com/api/${THE_GRAPH_API_KEY}/subgraphs/id/6NUtT5mGjZ1tSshKLf5Q3uEEJtjBZJo1TpL5MXsUBqrT 12 | - name: sushiswap-v2-polygon 13 | handler: 14 | graphql: 15 | endpoint: https://gateway.thegraph.com/api/${THE_GRAPH_API_KEY}/subgraphs/id/8NiXkxLRT3R22vpwLB4DXttpEf3X1LrKhe4T1tQ3jjbP 16 | - name: sushiswap-v2-bsc 17 | handler: 18 | graphql: 19 | endpoint: https://gateway.thegraph.com/api/${THE_GRAPH_API_KEY}/subgraphs/id/GPRigpbNuPkxkwpSbDuYXbikodNJfurc1LCENLzboWer 20 | - name: sushiswap-v2-arbitrum 21 | handler: 22 | graphql: 23 | endpoint: https://gateway.thegraph.com/api/${THE_GRAPH_API_KEY}/subgraphs/id/8nFDCAhdnJQEhQF3ZRnfWkJ6FkRsfAiiVabVn4eGoAZH 24 | - name: pancakeswap-v2-bsc 25 | handler: 26 | graphql: 27 | endpoint: https://gateway.thegraph.com/api/${THE_GRAPH_API_KEY}/subgraphs/id/Aj9TDh9SPcn7cz4DXW26ga22VnBzHhPVuKGmE4YBzDFj 28 | - name: quickswap-v2-polygon 29 | handler: 30 | graphql: 31 | endpoint: https://gateway.thegraph.com/api/${THE_GRAPH_API_KEY}/subgraphs/id/CCFSaj7uS128wazXMdxdnbGA3YQnND9yBdHjPtvH7Bc7 32 | 33 | documents: 34 | - ./src/graph/graphql/uni-v2-like-query.graphql 35 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "ts-node/register", 3 | "file": ["./src/test/setup.test.ts"], 4 | "spec": ["src/**/__tests__/*.test.ts"], 5 | "timeout": 10000, 6 | "exit": true 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: true, 3 | 'editor.formatOnSave': true, 4 | 'eslint.autoFixOnSave': true, 5 | bracketSpacing: true, 6 | singleQuote: true, 7 | trailingComma: 'all', 8 | arrowParens: 'avoid', 9 | 'eslint.validate': [ 10 | 'javascript', 11 | 'javascriptreact', 12 | { 13 | language: 'typescript', 14 | autoFix: true, 15 | }, 16 | { 17 | language: 'typescriptreact', 18 | autoFix: true, 19 | }, 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 RAILGUN Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /debug-return-value.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const { parseRelayAdaptReturnValue } = require('@railgun-community/wallet'); 4 | 5 | function main(args) { 6 | const returnValue = args.length ? args[0] : null; 7 | if (!returnValue) { 8 | throw new Error('No return value provided'); 9 | } 10 | 11 | return parseRelayAdaptReturnValue(returnValue); 12 | } 13 | 14 | const consoleArgs = process.argv.slice(2); 15 | 16 | const debugString = main(consoleArgs); 17 | console.log(`RELAY ADAPT ERROR: ${debugString}`); 18 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | /** @type import('hardhat/config').HardhatUserConfig */ 2 | export default { 3 | networks: {}, 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@railgun-community/cookbook", 3 | "version": "2.10.1", 4 | "license": "MIT", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist/**/*" 8 | ], 9 | "exports": { 10 | ".": "./dist/index.js" 11 | }, 12 | "scripts": { 13 | "clean": "rm -rf dist", 14 | "build": "npm run clean && patch-package && tsc", 15 | "prepare": "npm run build", 16 | "check-circular-deps": "madge --circular .", 17 | "eslint": "eslint src --ext .ts,.tsx --fix", 18 | "lint": "npm run check-circular-deps && npm run eslint && tsc --noEmit && tsc --noEmit -p tsconfig.test.json", 19 | "test-fork-ganache": "env USE_GANACHE=1 npm run test-fork", 20 | "test-fork-hardhat": "env USE_HARDHAT=1 npm run test-fork", 21 | "test-fork": "env DEBUG=railgun*,rpc*,cookbook* NODE_ENV=test RUN_FORK_TESTS=1 npm test", 22 | "tsc-test": "tsc -p tsconfig.test.json && tsc-alias -p tsconfig.test.json", 23 | "compile-test": "npm run clean && npm run tsc-test", 24 | "test-coverage-fork": "env RUN_FORK_TESTS=1 npm run test-coverage", 25 | "test-coverage": "npm run compile-test && c8 mocha", 26 | "test": "npm run compile-test && NODE_ENV=test mocha", 27 | "build-graphql": "graphclient build && rm -rf src/graph/graphql/.graphclient && mv .graphclient src/graph/graphql", 28 | "gen-typechain": "typechain --target ethers-v6 --out-dir ./src/typechain ./src/abi/**/*.json ./src/abi/**/**/*.json" 29 | }, 30 | "dependencies": { 31 | "@0x/contract-addresses": "^8.13.0", 32 | "@graphql-mesh/cache-localforage": "^0.7.20", 33 | "@graphql-mesh/cross-helpers": "^0.3.4", 34 | "@graphql-mesh/graphql": "^0.34.16", 35 | "@graphql-mesh/http": "^0.3.28", 36 | "@graphql-mesh/merger-stitching": "^0.18.24", 37 | "@graphql-mesh/runtime": "^0.46.23", 38 | "@graphql-mesh/store": "^0.9.20", 39 | "@graphql-mesh/types": "^0.91.14", 40 | "@graphql-mesh/utils": "^0.43.22", 41 | "@railgun-community/shared-models": "^7.5.0", 42 | "axios": "1.7.2", 43 | "custom-uniswap-v2-sdk": "^3.2.0", 44 | "ethers": "6.13.1", 45 | "graphql": "^16.6.0" 46 | }, 47 | "devDependencies": { 48 | "@graphprotocol/client-cli": "^2.2.22", 49 | "@railgun-community/wallet": "10.3.3", 50 | "@typechain/ethers-v6": "^0.3.3", 51 | "@types/chai": "^4.3.4", 52 | "@types/chai-as-promised": "^7.1.5", 53 | "@types/debug": "^4.1.7", 54 | "@types/leveldown": "^4.0.3", 55 | "@types/mocha": "^10.0.1", 56 | "@types/node": "^18.7.23", 57 | "@types/sinon": "^10.0.13", 58 | "@typescript-eslint/eslint-plugin": "^5.38.1", 59 | "@typescript-eslint/parser": "^5.38.1", 60 | "c8": "^7.12.0", 61 | "chai": "^4.3.7", 62 | "chai-as-promised": "^7.1.1", 63 | "debug": "^4.3.4", 64 | "dotenv": "^16.4.5", 65 | "eslint": "^8.24.0", 66 | "eslint-config-prettier": "^8.5.0", 67 | "eslint-plugin-flowtype": "^8.0.3", 68 | "eslint-plugin-import": "^2.26.0", 69 | "ganache": "^7.8.0", 70 | "hardhat": "^2.14.1", 71 | "leveldown": "^6.1.1", 72 | "madge": "^5.0.1", 73 | "mocha": "^10.2.0", 74 | "patch-package": "^7.0.0", 75 | "prettier": "^2.8.3", 76 | "sinon": "^15.0.1", 77 | "snarkjs": "^0.6.10", 78 | "ts-node": "^10.9.1", 79 | "tsc-alias": "^1.8.2", 80 | "typechain": "^8.2.0", 81 | "typescript": "^4.8.3" 82 | }, 83 | "packageManager": "yarn@1.22.22" 84 | } 85 | -------------------------------------------------------------------------------- /patches/@railgun-community+engine+9.3.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@railgun-community/engine/dist/contracts/railgun-smart-wallet/V2/railgun-smart-wallet.js b/node_modules/@railgun-community/engine/dist/contracts/railgun-smart-wallet/V2/railgun-smart-wallet.js 2 | index 62590aa..cbe3ba8 100644 3 | --- a/node_modules/@railgun-community/engine/dist/contracts/railgun-smart-wallet/V2/railgun-smart-wallet.js 4 | +++ b/node_modules/@railgun-community/engine/dist/contracts/railgun-smart-wallet/V2/railgun-smart-wallet.js 5 | @@ -264,6 +264,9 @@ class RailgunSmartWalletContract extends events_1.default { 6 | * @param latestBlock - block to scan to 7 | */ 8 | async getHistoricalEvents(initialStartBlock, latestBlock, getNextStartBlockFromValidMerkletree, eventsCommitmentListener, eventsNullifierListener, eventsUnshieldListener, setLastSyncedBlock) { 9 | + // Cookbook does not need event scanning. 10 | + return; 11 | + 12 | const engineV3StartBlockNumber = RailgunSmartWalletContract.getEngineV2StartBlockNumber(this.chain); 13 | const engineV3ShieldEventUpdate030923BlockNumber = RailgunSmartWalletContract.getEngineV2ShieldEventUpdate030923BlockNumber(this.chain); 14 | // TODO: Possible data integrity issue in using commitment block numbers. 15 | diff --git a/node_modules/@railgun-community/engine/dist/poi/poi.js b/node_modules/@railgun-community/engine/dist/poi/poi.js 16 | index c140949..b5932c4 100644 17 | --- a/node_modules/@railgun-community/engine/dist/poi/poi.js 18 | +++ b/node_modules/@railgun-community/engine/dist/poi/poi.js 19 | @@ -229,6 +229,7 @@ class POI { 20 | } 21 | static isActiveForChain(chain) { 22 | try { 23 | + return false; 24 | return this.nodeInterface.isActive(chain); 25 | } 26 | catch (err) { 27 | @@ -236,6 +237,7 @@ class POI { 28 | } 29 | } 30 | static isRequiredForChain(chain) { 31 | + return false; 32 | return this.nodeInterface.isRequired(chain); 33 | } 34 | static async getSpendableBalanceBuckets(chain) { 35 | -------------------------------------------------------------------------------- /patches/@railgun-community+wallet+10.3.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@railgun-community/wallet/dist/services/railgun/core/load-provider.js b/node_modules/@railgun-community/wallet/dist/services/railgun/core/load-provider.js 2 | index 9fd5143..8ae2e53 100644 3 | --- a/node_modules/@railgun-community/wallet/dist/services/railgun/core/load-provider.js 4 | +++ b/node_modules/@railgun-community/wallet/dist/services/railgun/core/load-provider.js 5 | @@ -44,9 +44,6 @@ const loadProviderForNetwork = async (chain, networkName, fallbackProviderJsonCo 6 | throw new Error(`Could not find Relay Adapt contract for network: ${publicName}`); 7 | } 8 | const engine = (0, engine_1.getEngine)(); 9 | - if (!engine.isPOINode && (0, shared_models_1.isDefined)(poi) && !wallet_poi_1.WalletPOI.started) { 10 | - throw new Error('This network requires Proof Of Innocence. Pass "poiNodeURL" to startRailgunEngine to initialize POI before loading this provider.'); 11 | - } 12 | const deploymentBlocks = { 13 | [shared_models_1.TXIDVersion.V2_PoseidonMerkle]: deploymentBlock ?? 0, 14 | [shared_models_1.TXIDVersion.V3_PoseidonMerkle]: deploymentBlockPoseidonMerkleAccumulatorV3 ?? 0, 15 | -------------------------------------------------------------------------------- /release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # patch by default. 5 | # Use `./release minor` or `./release major`. 6 | VERSION_TYPE=${1-patch} 7 | 8 | git fetch; 9 | git checkout release --; 10 | git rebase main; 11 | npm version $VERSION_TYPE; 12 | git push -f; 13 | git checkout main; 14 | git rebase origin/release; 15 | git push --tags --no-verify; 16 | git push --no-verify; 17 | echo https://github.com/Railgun-Community/cookbook/actions; 18 | -------------------------------------------------------------------------------- /run-anvil: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -lt 2 ]; then 4 | echo "ERROR: Requires 2 parameters: run-anvil " 5 | exit 1; 6 | fi 7 | 8 | case $1 in 9 | Ethereum) 10 | PORT=8600 11 | CHAIN_ID=1 12 | ;; 13 | Arbitrum) 14 | PORT=8601 15 | CHAIN_ID=42161 16 | ;; 17 | *) 18 | echo "ERROR: Unrecognized chain identifier $1" 1>&2 19 | exit 1 20 | ;; 21 | esac 22 | 23 | # $3 allows for additional options to be passed to anvil 24 | 25 | anvil \ 26 | $3 \ 27 | --fork-url $2 \ 28 | --port $PORT \ 29 | --chain-id $CHAIN_ID \ 30 | --balance '10000' \ 31 | --gas-limit 30000000 \ 32 | --block-base-fee-per-gas 100 \ 33 | ; 34 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { setRailgunFees } from '../index'; 4 | 5 | chai.use(chaiAsPromised); 6 | const { expect } = chai; 7 | 8 | describe('index', () => { 9 | it('Should load index', async () => { 10 | expect(setRailgunFees).to.be.a('function'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/abi/abi.ts: -------------------------------------------------------------------------------- 1 | import ABI_ERC20 from './token/erc20.json'; 2 | import ABI_ERC721 from './token/erc721.json'; 3 | import ABI_RELAY_ADAPT from './adapt/RelayAdapt.json'; 4 | import ABI_UNI_V2_LIKE_FACTORY from './liquidity/UniV2LikeFactory.json'; 5 | import ABI_UNI_V2_LIKE_ROUTER from './liquidity/UniV2LikeRouter.json'; 6 | import ABI_UNI_V2_LIKE_PAIR from './liquidity/UniV2LikePair.json'; 7 | import ABI_BEEFY_VAULT_MERGED_V6V7 from './vault/beefy/BeefyVault-MergedV6V7.json'; 8 | 9 | export const abi = { 10 | token: { 11 | erc20: ABI_ERC20, 12 | erc721: ABI_ERC721, 13 | }, 14 | adapt: { 15 | relay: ABI_RELAY_ADAPT, 16 | }, 17 | liquidity: { 18 | uniV2LikeFactory: ABI_UNI_V2_LIKE_FACTORY, 19 | uniV2LikeRouter: ABI_UNI_V2_LIKE_ROUTER, 20 | uniV2LikePair: ABI_UNI_V2_LIKE_PAIR, 21 | }, 22 | vault: { 23 | beefy: ABI_BEEFY_VAULT_MERGED_V6V7, 24 | }, 25 | } as const; 26 | -------------------------------------------------------------------------------- /src/abi/vault/beefy/BeefyVault-MergedV6V7.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint256", 6 | "name": "_amount", 7 | "type": "uint256" 8 | } 9 | ], 10 | "name": "deposit", 11 | "outputs": [], 12 | "stateMutability": "nonpayable", 13 | "type": "function" 14 | }, 15 | { 16 | "inputs": [], 17 | "name": "depositAll", 18 | "outputs": [], 19 | "stateMutability": "nonpayable", 20 | "type": "function" 21 | }, 22 | { 23 | "inputs": [ 24 | { 25 | "internalType": "uint256", 26 | "name": "_shares", 27 | "type": "uint256" 28 | } 29 | ], 30 | "name": "withdraw", 31 | "outputs": [], 32 | "stateMutability": "nonpayable", 33 | "type": "function" 34 | }, 35 | { 36 | "inputs": [], 37 | "name": "withdrawAll", 38 | "outputs": [], 39 | "stateMutability": "nonpayable", 40 | "type": "function" 41 | }, 42 | { 43 | "inputs": [], 44 | "name": "want", 45 | "outputs": [ 46 | { 47 | "internalType": "address", 48 | "name": "", 49 | "type": "address" 50 | } 51 | ], 52 | "stateMutability": "view", 53 | "type": "function" 54 | }, 55 | { 56 | "inputs": [], 57 | "name": "getPricePerFullShare", 58 | "outputs": [ 59 | { 60 | "internalType": "uint256", 61 | "name": "", 62 | "type": "uint256" 63 | } 64 | ], 65 | "stateMutability": "view", 66 | "type": "function" 67 | }, 68 | { 69 | "inputs": [], 70 | "name": "earn", 71 | "outputs": [], 72 | "stateMutability": "nonpayable", 73 | "type": "function" 74 | } 75 | ] 76 | -------------------------------------------------------------------------------- /src/api/beefy/__tests__/beefy-api.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | 4 | import { BeefyAPI } from '../beefy-api'; 5 | import { NetworkName } from '@railgun-community/shared-models'; 6 | import { testConfig } from '../../../test/test-config.test'; 7 | 8 | chai.use(chaiAsPromised); 9 | const { expect } = chai; 10 | 11 | describe('beefy-api', () => { 12 | const beefyVaultId = 'bifi-vault'; // https://app.beefy.finance/vault/bifi-vault 13 | 14 | before(() => {}); 15 | 16 | // @@ TODO: Update vaults with accurate info, its fetching a different one ?. 17 | it.skip('Should get Beefy vaults data for each network', async () => { 18 | const supportedNetworks = [ 19 | NetworkName.Ethereum, 20 | NetworkName.Polygon, 21 | NetworkName.BNBChain, 22 | NetworkName.Arbitrum, 23 | ]; 24 | 25 | await Promise.all( 26 | supportedNetworks.map(async networkName => { 27 | const chainVaults = await BeefyAPI.getFilteredBeefyVaults( 28 | networkName, 29 | false, // skipCache 30 | false, // includeInactiveVaults 31 | ); 32 | expect(chainVaults.length).to.be.greaterThan(10); 33 | }), 34 | ); 35 | 36 | const vaultsForEthereumToken = await BeefyAPI.getFilteredBeefyVaults( 37 | NetworkName.Ethereum, 38 | false, // skipCache 39 | false, // includeInactiveVaults 40 | testConfig.contractsEthereum.conicEthLPToken.toUpperCase(), 41 | ); 42 | expect(vaultsForEthereumToken.length).to.equal(1); 43 | expect({ 44 | ...vaultsForEthereumToken[0], 45 | apy: 0, 46 | vaultRate: 1n, 47 | }).to.deep.equal({ 48 | // Set to exact values to skip comparison: 49 | apy: 0, 50 | vaultRate: 1n, 51 | 52 | // Compare the rest of the values: 53 | chain: 'ethereum', 54 | depositERC20Symbol: 'cncETH', 55 | depositERC20Address: '0x58649ec8add732ea710731b5cb37c99529a394d3', 56 | depositERC20Decimals: 18n, 57 | depositFeeBasisPoints: 0n, 58 | network: 'ethereum', 59 | vaultContractAddress: '0xaf5bf2d152e6a16095588d3438b55edc2bb28343', 60 | vaultID: 'conic-eth', 61 | vaultName: 'ETH LP', 62 | vaultERC20Symbol: 'mooConicETH', 63 | vaultERC20Address: '0xaf5bf2d152e6a16095588d3438b55edc2bb28343', 64 | withdrawFeeBasisPoints: 0n, 65 | isActive: true, 66 | }); 67 | expect(vaultsForEthereumToken[0].apy).to.be.greaterThan(0.01); 68 | expect(vaultsForEthereumToken[0].apy).to.be.lessThan(0.5); 69 | 70 | const vaultsForPolygonToken = await BeefyAPI.getFilteredBeefyVaults( 71 | NetworkName.Polygon, 72 | false, // skipCache 73 | false, // includeInactiveVaults 74 | '0xEcd5e75AFb02eFa118AF914515D6521aaBd189F1'.toUpperCase(), 75 | ); 76 | expect(vaultsForPolygonToken.length).to.equal(0); 77 | }).timeout(17500); 78 | 79 | it('Should get specific Beefy vault data', async () => { 80 | const vaultID = beefyVaultId; 81 | const vault = await BeefyAPI.getBeefyVaultForID( 82 | vaultID, 83 | NetworkName.Ethereum, 84 | ); 85 | expect(vault.vaultID).to.equal(vaultID); 86 | }).timeout(7500); 87 | 88 | it('Should get specific Beefy vault APY', async () => { 89 | const vaultID = beefyVaultId; 90 | const apy = await BeefyAPI.getBeefyVaultAPY(vaultID); 91 | expect(apy).to.be.greaterThan(0.001); 92 | }).timeout(7500); 93 | 94 | it('Should error for inactive Beefy Vault', async () => { 95 | const vaultID = 'convex-crveth'; 96 | await expect( 97 | BeefyAPI.getBeefyVaultForID(vaultID, NetworkName.Ethereum), 98 | ).to.be.rejectedWith(`Beefy vault is not active for ID: ${vaultID}.`); 99 | }).timeout(7500); 100 | }); 101 | -------------------------------------------------------------------------------- /src/api/beefy/beefy-fetch.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export enum BeefyApiEndpoint { 4 | GetVaults = 'vaults', 5 | GetFees = 'fees', 6 | GetAPYs = 'apy', 7 | GetTVLs = 'tvl', 8 | } 9 | 10 | const BEEFY_API_URL = 'https://api.beefy.finance'; 11 | 12 | export const getBeefyAPIData = async ( 13 | endpoint: BeefyApiEndpoint, 14 | ): Promise => { 15 | const url = `${BEEFY_API_URL}/${endpoint}`; 16 | const rsp = await axios.get(url); 17 | return rsp.data; 18 | }; 19 | -------------------------------------------------------------------------------- /src/api/beefy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './beefy-api'; 2 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './uni-v2-like'; 2 | export * from './zero-x'; 3 | export * from './zero-x-v2'; 4 | export * from './beefy'; 5 | -------------------------------------------------------------------------------- /src/api/uni-v2-like/__tests__/uni-v2-like-sdk.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { NetworkName } from '@railgun-community/shared-models'; 4 | import { UniV2LikeSDK } from '../uni-v2-like-sdk'; 5 | import { RecipeERC20Info, UniswapV2Fork } from '../../../models/export-models'; 6 | import { JsonRpcProvider } from 'ethers'; 7 | 8 | chai.use(chaiAsPromised); 9 | const { expect } = chai; 10 | 11 | const networkName = NetworkName.Ethereum; 12 | 13 | const USDC_TOKEN: RecipeERC20Info = { 14 | tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 15 | decimals: 6n, 16 | }; 17 | const WETH_TOKEN: RecipeERC20Info = { 18 | tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 19 | decimals: 18n, 20 | }; 21 | 22 | let provider: JsonRpcProvider; 23 | 24 | describe('uni-v2-like-pairs', () => { 25 | before(() => { 26 | provider = new JsonRpcProvider('https://rpc.ankr.com/eth'); 27 | }); 28 | 29 | it('Should get Uniswap LP address for USDC-WETH pair', async () => { 30 | const lpAddress = UniV2LikeSDK.getPairLPAddress( 31 | UniswapV2Fork.Uniswap, 32 | networkName, 33 | USDC_TOKEN, 34 | WETH_TOKEN, 35 | ); 36 | expect(lpAddress).to.equal('0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc'); 37 | }); 38 | 39 | it('Should get Uniswap LP rate for USDC-WETH pair', async () => { 40 | const rate = await UniV2LikeSDK.getPairRateWith18Decimals( 41 | UniswapV2Fork.Uniswap, 42 | networkName, 43 | provider, 44 | USDC_TOKEN, 45 | WETH_TOKEN, 46 | ); 47 | 48 | const oneWithDecimals = 10n ** 18n; 49 | 50 | expect(rate > oneWithDecimals * 500n).to.equal( 51 | true, 52 | 'Expected USDC-WETH LP rate to be greater than 500', 53 | ); 54 | expect(rate < oneWithDecimals * 5000n).to.equal( 55 | true, 56 | 'Expected USDC-WETH LP rate to be less than 5000', 57 | ); 58 | }).timeout(5000); 59 | }); 60 | -------------------------------------------------------------------------------- /src/api/uni-v2-like/index.ts: -------------------------------------------------------------------------------- 1 | export * from './uni-v2-like-pairs'; 2 | export * from './uni-v2-like-sdk'; 3 | -------------------------------------------------------------------------------- /src/api/uni-v2-like/uni-v2-like-pairs.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName } from '@railgun-community/shared-models'; 2 | import { UniswapV2Fork } from '../../models/export-models'; 3 | import { LiquidityV2Pool } from '../../models/uni-v2-like'; 4 | import { UniV2LikeSubgraph } from '../../graph/uni-v2-like-subgraph'; 5 | import { CookbookDebug } from '../../utils/cookbook-debug'; 6 | import { Provider } from 'ethers'; 7 | import { UniV2LikeSubgraphCache } from '../../graph/graph-cache/uni-v2-like-subgraph-cache'; 8 | 9 | export const queryAllLPPairsForTokenAddressesPerFork = async ( 10 | uniswapV2Fork: UniswapV2Fork, 11 | networkName: NetworkName, 12 | tokenAddresses: string[], 13 | ): Promise => { 14 | try { 15 | return await UniV2LikeSubgraph.queryPairsForTokenAddresses( 16 | uniswapV2Fork, 17 | networkName, 18 | tokenAddresses, 19 | ); 20 | } catch (cause) { 21 | if (!(cause instanceof Error)) { 22 | throw new Error('Unexpected non-error thrown', { cause }); 23 | } 24 | CookbookDebug.error(cause); 25 | throw new Error('Failed to query LP pairs for token addresses.', { cause }); 26 | } 27 | }; 28 | 29 | export const queryAllLPPairsForTokenAddresses = async ( 30 | networkName: NetworkName, 31 | tokenAddresses: string[], 32 | ): Promise => { 33 | const allLPPairs = await Promise.all( 34 | Object.values(UniswapV2Fork).map(fork => { 35 | return queryAllLPPairsForTokenAddressesPerFork( 36 | fork, 37 | networkName, 38 | tokenAddresses, 39 | ); 40 | }), 41 | ); 42 | return allLPPairs.flat(); 43 | }; 44 | 45 | export const getCachedLPPairsForTokenAddresses = async ( 46 | provider: Provider, 47 | networkName: NetworkName, 48 | tokenAddresses: string[], 49 | ): Promise => { 50 | try { 51 | return await UniV2LikeSubgraphCache.getCachedPairsForTokenAddresses( 52 | provider, 53 | networkName, 54 | tokenAddresses, 55 | ); 56 | } catch (cause) { 57 | if (!(cause instanceof Error)) { 58 | throw new Error('Unexpected non-error thrown', { cause }); 59 | } 60 | CookbookDebug.error(cause); 61 | throw new Error('Failed to get cached LP pairs for token addresses.', { 62 | cause, 63 | }); 64 | } 65 | }; 66 | 67 | export const getLPPairsForTokenAddresses = async ( 68 | provider: Provider, 69 | networkName: NetworkName, 70 | tokenAddresses: string[], 71 | ): Promise => { 72 | try { 73 | return await getCachedLPPairsForTokenAddresses( 74 | provider, 75 | networkName, 76 | tokenAddresses, 77 | ); 78 | } catch (cause) { 79 | if (!(cause instanceof Error)) { 80 | throw new Error('Unexpected non-error thrown', { cause }); 81 | } 82 | CookbookDebug.error(cause); 83 | return queryAllLPPairsForTokenAddresses(networkName, tokenAddresses); 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /src/api/zero-x-v2/__tests__/zero-x-v2-fetch.test.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName } from '@railgun-community/shared-models'; 2 | import { 3 | createZeroXV2UrlAndHeaders, 4 | ZeroXV2ApiEndpoint, 5 | } from '../zero-x-v2-fetch'; 6 | import chai from 'chai'; 7 | import chaiAsPromised from 'chai-as-promised'; 8 | import { ZeroXConfig } from '../../../models/zero-x-config'; 9 | 10 | chai.use(chaiAsPromised); 11 | const { expect } = chai; 12 | const NULL_SPENDER_ADDRESS = '0x0000000000000000000000000000000000000000'; 13 | 14 | describe('zero-x-v2-fetch', () => { 15 | before(() => { 16 | ZeroXConfig.API_KEY = 'test-api-key'; 17 | }); 18 | after(() => { 19 | ZeroXConfig.PROXY_API_DOMAIN = undefined; 20 | ZeroXConfig.API_KEY = undefined; 21 | }); 22 | 23 | it('Should create correct ZeroXV2 URLs', async () => { 24 | ZeroXConfig.PROXY_API_DOMAIN = undefined; 25 | expect( 26 | createZeroXV2UrlAndHeaders(ZeroXV2ApiEndpoint.GetSwapQuote, true, { 27 | chainId: '1', 28 | sellAmount: '100000000000000000000', 29 | buyToken: '0x6b175474e89094c44da98b954eedeac495271d0f', 30 | sellToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 31 | }), 32 | ).to.deep.equal({ 33 | url: 'https://api.0x.org/swap/allowance-holder/quote?chainId=1&sellAmount=100000000000000000000&buyToken=0x6b175474e89094c44da98b954eedeac495271d0f&sellToken=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 34 | headers: { '0x-api-key': 'test-api-key', '0x-version': 'v2' }, 35 | }); 36 | expect( 37 | createZeroXV2UrlAndHeaders(ZeroXV2ApiEndpoint.GetSwapQuote, true), 38 | ).to.deep.equal({ 39 | url: 'https://api.0x.org/swap/allowance-holder/quote', 40 | headers: { '0x-api-key': 'test-api-key', '0x-version': 'v2' }, 41 | }); 42 | 43 | ZeroXConfig.PROXY_API_DOMAIN = 'testapi.com'; 44 | expect( 45 | createZeroXV2UrlAndHeaders(ZeroXV2ApiEndpoint.GetSwapQuote, true, { 46 | chainId: '1', 47 | sellAmount: '100000000000000000000', 48 | buyToken: '0x6b175474e89094c44da98b954eedeac495271d0f', 49 | sellToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 50 | taker: NULL_SPENDER_ADDRESS, // this needs to be added in by network, it will be the 'relay adapt contract' ie the contract sending the tx 51 | }), 52 | ).to.deep.equal({ 53 | url: 54 | 'testapi.com/0x/railgun/api/swap/allowance-holder/quote?chainId=1&sellAmount=100000000000000000000&buyToken=0x6b175474e89094c44da98b954eedeac495271d0f&sellToken=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2&taker=' + 55 | NULL_SPENDER_ADDRESS, 56 | headers: {}, 57 | }); 58 | expect( 59 | createZeroXV2UrlAndHeaders(ZeroXV2ApiEndpoint.GetSwapQuote, false), 60 | ).to.deep.equal({ 61 | url: 'testapi.com/0x/public/api/swap/allowance-holder/quote', 62 | headers: {}, 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/api/zero-x-v2/__tests__/zero-x-v2-quote.test.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName } from '@railgun-community/shared-models'; 2 | import { ZeroXV2Quote } from '../zero-x-v2-quote'; 3 | import * as zeroXV2Api from '../zero-x-v2-fetch'; 4 | import chai from 'chai'; 5 | import sinon from 'sinon'; 6 | import chaiAsPromised from 'chai-as-promised'; 7 | import { 8 | RecipeERC20Amount, 9 | RecipeERC20Info, 10 | } from '../../../models/export-models'; 11 | import { ZeroXConfig } from '../../../models/zero-x-config'; 12 | import { testConfig } from '../../../test/test-config.test'; 13 | import { NoLiquidityError } from '../errors'; 14 | 15 | chai.use(chaiAsPromised); 16 | const { expect } = chai; 17 | 18 | const networkName = NetworkName.Ethereum; 19 | const runV2QuoteTest = async (amount = '1000000000000000000') => { 20 | const sellERC20Amount: RecipeERC20Amount = { 21 | tokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f', 22 | decimals: 18n, 23 | isBaseToken: false, 24 | amount: BigInt(amount), 25 | }; 26 | const buyERC20Info: RecipeERC20Info = { 27 | tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 28 | decimals: 18n, 29 | isBaseToken: false, 30 | }; 31 | 32 | const quote = await ZeroXV2Quote.getSwapQuote({ 33 | networkName, 34 | sellERC20Amount, 35 | buyERC20Info, 36 | slippageBasisPoints: 100, 37 | isRailgun: true, 38 | }); 39 | 40 | expect(typeof quote === 'object').to.be.true; 41 | expect(quote).to.haveOwnProperty('price'); 42 | expect(quote).to.haveOwnProperty('spender'); 43 | expect(quote).to.haveOwnProperty('sellTokenValue'); 44 | }; 45 | 46 | describe('zero-x-v2-quote', () => { 47 | let getZeroXV2DataStub: sinon.SinonStub; 48 | before(() => {}); 49 | 50 | beforeEach(() => { 51 | ZeroXConfig.PROXY_API_DOMAIN = undefined; 52 | ZeroXConfig.API_KEY = testConfig.zeroXApiKey; 53 | }); 54 | 55 | it('Should fetch quotes from ZeroXV2 proxy', async () => { 56 | ZeroXConfig.PROXY_API_DOMAIN = testConfig.zeroXProxyApiDomain; 57 | ZeroXConfig.API_KEY = undefined; 58 | await runV2QuoteTest(); 59 | }).timeout(10000); 60 | 61 | it('Should fetch quotes from ZeroXV2 API Key', async () => { 62 | await runV2QuoteTest(); 63 | }).timeout(10000); 64 | 65 | it('Should fetch quotes from ZeroXV2 API Key with large volume request', async () => { 66 | await runV2QuoteTest('0x1000000000000000000000'); 67 | }).timeout(10000); 68 | 69 | it('Should fetch quotes from ZeroXV2 API Key with large volume request and fail', async () => { 70 | 71 | await expect(runV2QuoteTest('0x10000000000000000000000000000000000000000')) 72 | .to.be.rejected; 73 | }).timeout(10000); 74 | 75 | it('Should error with no liquidity error when theres no liquidity for trade on ZeroXV2 API', async () => { 76 | 77 | const getZeroXV2DataStub = sinon.stub(zeroXV2Api, 'getZeroXV2Data').resolves({ 78 | liquidityAvailable: false, 79 | // Add other necessary properties to match the expected response structure 80 | }); 81 | 82 | const noLiquidityErrMessage = new NoLiquidityError().getCause(); 83 | 84 | const sellERC20Amount: RecipeERC20Amount = { 85 | tokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f', 86 | decimals: 18n, 87 | isBaseToken: false, 88 | amount: BigInt('1000000000000000000'), 89 | }; 90 | 91 | const buyERC20Info: RecipeERC20Info = { 92 | tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 93 | decimals: 18n, 94 | isBaseToken: false, 95 | }; 96 | 97 | try { 98 | await ZeroXV2Quote.getSwapQuote({ 99 | networkName, 100 | sellERC20Amount, 101 | buyERC20Info, 102 | slippageBasisPoints: 100, 103 | isRailgun: true, 104 | }); 105 | } catch(err) { 106 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 107 | expect(err.cause).to.contain(noLiquidityErrMessage); 108 | } finally { 109 | getZeroXV2DataStub.restore(); 110 | } 111 | }); 112 | }) -------------------------------------------------------------------------------- /src/api/zero-x-v2/errors.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios'; 2 | import type { ZeroXAPIErrorData } from '../../models'; 3 | 4 | export abstract class ZeroXAPIError extends Error { 5 | override readonly name: string; 6 | readonly cause: string; 7 | 8 | constructor({ name, cause }: { name: string; cause: string }) { 9 | super(cause); 10 | this.name = name; 11 | this.cause = cause; 12 | Object.setPrototypeOf(this, ZeroXAPIError.prototype); 13 | } 14 | 15 | public getCause(): string { 16 | return this.cause; 17 | } 18 | } 19 | 20 | export class MissingHeadersError extends ZeroXAPIError { 21 | constructor() { 22 | super({ 23 | name: 'MissingHeadersError', 24 | cause: 'No 0x API Key is configured. Set ZeroXConfig.API_KEY. For tests, modify test-config-overrides.test.ts.', 25 | }); 26 | } 27 | } 28 | 29 | export class QuoteParamsError extends ZeroXAPIError { 30 | constructor(description: string) { 31 | super({ 32 | name: 'QuoteParamsError', 33 | cause: description, 34 | }); 35 | } 36 | } 37 | 38 | export class InvalidExchangeContractError extends ZeroXAPIError { 39 | constructor(to: string, exchangeAllowanceHolderAddress: string) { 40 | super({ 41 | name: 'InvalidExchangeContractError', 42 | cause: `Invalid 0x V2 Exchange contract address: ${to} vs ${exchangeAllowanceHolderAddress}`, 43 | }); 44 | } 45 | } 46 | 47 | export class InvalidProxyContractChainError extends ZeroXAPIError { 48 | constructor(chain: string) { 49 | super({ 50 | name: 'InvalidProxyContractChainError', 51 | cause: `No 0x V2 Exchange Proxy contract address for chain ${chain}`, 52 | }); 53 | } 54 | } 55 | 56 | export class NoLiquidityError extends ZeroXAPIError { 57 | constructor() { 58 | super({ 59 | name: 'NoLiquidityError', 60 | cause: 'No liquidity available for this trade', 61 | }); 62 | } 63 | } 64 | 65 | export class SwapQuoteError extends ZeroXAPIError { 66 | constructor(cause: string) { 67 | super({ 68 | name: 'SwapQuoteError', 69 | cause: `Error fetching 0x V2 swap quote: ${cause}`, 70 | }); 71 | } 72 | 73 | private static fromAxiosError(error: AxiosError): SwapQuoteError { 74 | const formattedError = SwapQuoteError.formatV2ApiError(error); 75 | return new SwapQuoteError(formattedError); 76 | } 77 | 78 | private static formatV2ApiError(error: AxiosError): string { 79 | if (!error.response) { 80 | return `0x V2 API request failed: ${error.message}.`; 81 | } 82 | 83 | const { data } = error.response; 84 | 85 | if (data.name === 'TOKEN_NOT_SUPPORTED') { 86 | return 'One of the selected tokens is not supported by the 0x Exchange.'; 87 | } 88 | 89 | if (data.data?.details && data.data.details.length > 0) { 90 | const firstDetailsError = data.data.details[0]; 91 | return `0x V2 Exchange: ${firstDetailsError.field}:${firstDetailsError.reason}. ${data.name}.`; 92 | } 93 | 94 | return `0x V2 API request failed: ${data.name}`; 95 | } 96 | 97 | static from(error: unknown): SwapQuoteError { 98 | if (error instanceof AxiosError) { 99 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 100 | return SwapQuoteError.fromAxiosError(error); 101 | } 102 | if (error instanceof ZeroXAPIError) { 103 | return new SwapQuoteError(error.cause); 104 | } 105 | return new SwapQuoteError(error instanceof Error ? error.message : String(error)); 106 | } 107 | } -------------------------------------------------------------------------------- /src/api/zero-x-v2/index.ts: -------------------------------------------------------------------------------- 1 | export * from './zero-x-v2-quote'; 2 | export * from './errors'; -------------------------------------------------------------------------------- /src/api/zero-x-v2/zero-x-v2-fetch.ts: -------------------------------------------------------------------------------- 1 | import { isDefined } from '@railgun-community/shared-models'; 2 | import { ZeroXConfig } from '../../models'; 3 | import { MissingHeadersError } from './errors'; 4 | import axios from 'axios'; 5 | import { ZERO_X_V2_BASE_URL } from '../../models/constants'; 6 | 7 | type SearchParams = Record; 8 | 9 | export enum ZeroXV2ApiEndpoint { 10 | GetSwapQuote = 'swap/allowance-holder/quote', 11 | GetSwapPrice = 'swap/allowance-holder/price', 12 | } 13 | 14 | const getSearchV2Params = (params?: SearchParams) => { 15 | const searchParams = new URLSearchParams(params); 16 | return searchParams.toString() ? `?${searchParams.toString()}` : ''; 17 | }; 18 | 19 | const createZeroXV2Url = ( 20 | endpoint: ZeroXV2ApiEndpoint, 21 | params?: SearchParams, 22 | ) => { 23 | return `${ZERO_X_V2_BASE_URL}${endpoint}${getSearchV2Params(params)}`; 24 | }; 25 | 26 | const createV2Headers = () => { 27 | const apiKey = ZeroXConfig.API_KEY; 28 | if (isDefined(apiKey) && apiKey.length > 0) { 29 | return { 30 | '0x-api-key': apiKey, 31 | '0x-version': 'v2', 32 | }; 33 | } 34 | throw new MissingHeadersError(); 35 | }; 36 | 37 | export const createZeroXV2UrlAndHeaders = ( 38 | endpoint: ZeroXV2ApiEndpoint, 39 | isRailgun: boolean, 40 | params?: SearchParams, 41 | ) => { 42 | const proxyDomain = ZeroXConfig.PROXY_API_DOMAIN; 43 | if (isDefined(proxyDomain) && proxyDomain.length > 0) { 44 | return { 45 | url: createZeroXV2ProxyAPIUrl(proxyDomain, endpoint, isRailgun, params), 46 | headers: {}, 47 | }; 48 | } 49 | const url = createZeroXV2Url(endpoint, params); 50 | const headers = createV2Headers(); 51 | return { 52 | url, 53 | headers, 54 | }; 55 | }; 56 | 57 | export const createZeroXV2ProxyAPIUrl = ( 58 | proxyDomain: string, 59 | endpoint: ZeroXV2ApiEndpoint, 60 | isRailgun: boolean, 61 | params?: SearchParams, 62 | ) => { 63 | // this is unused for now, need to check 0x proxy code. 64 | const route = isRailgun ? 'railgun' : 'public'; 65 | const url = `${proxyDomain}/0x/${route}/api/${endpoint}${getSearchV2Params( 66 | params, 67 | )}`; 68 | return url; 69 | }; 70 | 71 | export const getZeroXV2Data = async ( 72 | endpoint: ZeroXV2ApiEndpoint, 73 | isRailgun: boolean, 74 | params?: SearchParams, 75 | ): Promise => { 76 | const { url, headers } = createZeroXV2UrlAndHeaders( 77 | endpoint, 78 | isRailgun, 79 | params, 80 | ); 81 | const response = await axios.get(url, { headers }); 82 | return response?.data; 83 | }; 84 | -------------------------------------------------------------------------------- /src/api/zero-x/__tests__/zero-x-fetch.test.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName } from '@railgun-community/shared-models'; 2 | import { createZeroXUrlAndHeaders, ZeroXApiEndpoint } from '../zero-x-fetch'; 3 | import chai from 'chai'; 4 | import chaiAsPromised from 'chai-as-promised'; 5 | import { ZeroXConfig } from '../../../models/zero-x-config'; 6 | 7 | chai.use(chaiAsPromised); 8 | const { expect } = chai; 9 | 10 | describe('zero-x-fetch', () => { 11 | before(() => { 12 | ZeroXConfig.API_KEY = 'test-api-key'; 13 | }); 14 | after(() => { 15 | ZeroXConfig.PROXY_API_DOMAIN = undefined; 16 | ZeroXConfig.API_KEY = undefined; 17 | }); 18 | 19 | it('Should create correct ZeroX URLs', async () => { 20 | ZeroXConfig.PROXY_API_DOMAIN = undefined; 21 | expect( 22 | createZeroXUrlAndHeaders( 23 | ZeroXApiEndpoint.GetSwapQuote, 24 | NetworkName.Ethereum, 25 | true, 26 | { 27 | something: 'new', 28 | another: 'thing', 29 | }, 30 | ), 31 | ).to.deep.equal({ 32 | url: 'https://api.0x.org/swap/v1/quote?something=new&another=thing', 33 | headers: { '0x-api-key': 'test-api-key' }, 34 | }); 35 | expect( 36 | createZeroXUrlAndHeaders( 37 | ZeroXApiEndpoint.GetSwapQuote, 38 | NetworkName.Polygon, 39 | true, 40 | ), 41 | ).to.deep.equal({ 42 | url: 'https://polygon.api.0x.org/swap/v1/quote', 43 | headers: { '0x-api-key': 'test-api-key' }, 44 | }); 45 | 46 | ZeroXConfig.PROXY_API_DOMAIN = 'testapi.com'; 47 | expect( 48 | createZeroXUrlAndHeaders( 49 | ZeroXApiEndpoint.GetSwapQuote, 50 | NetworkName.Ethereum, 51 | true, 52 | { 53 | something: 'new', 54 | another: 'thing', 55 | }, 56 | ), 57 | ).to.deep.equal({ 58 | url: 'testapi.com/0x/railgun/api/swap/v1/quote?something=new&another=thing', 59 | headers: {}, 60 | }); 61 | expect( 62 | createZeroXUrlAndHeaders( 63 | ZeroXApiEndpoint.GetSwapQuote, 64 | NetworkName.Polygon, 65 | false, 66 | ), 67 | ).to.deep.equal({ 68 | url: 'testapi.com/0x/public/polygon.api/swap/v1/quote', 69 | headers: {}, 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/api/zero-x/__tests__/zero-x-quote.test.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName } from '@railgun-community/shared-models'; 2 | import { ZeroXQuote } from '../zero-x-quote'; 3 | import chai from 'chai'; 4 | import chaiAsPromised from 'chai-as-promised'; 5 | import { 6 | RecipeERC20Amount, 7 | RecipeERC20Info, 8 | } from '../../../models/export-models'; 9 | import { ZeroXConfig } from '../../../models/zero-x-config'; 10 | import { testConfig } from '../../../test/test-config.test'; 11 | 12 | chai.use(chaiAsPromised); 13 | const { expect } = chai; 14 | 15 | const networkName = NetworkName.Ethereum; 16 | 17 | const runQuoteTest = async () => { 18 | const sellERC20Amount: RecipeERC20Amount = { 19 | tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 20 | decimals: 18n, 21 | isBaseToken: false, 22 | amount: BigInt('0x1000000000000000000'), 23 | }; 24 | const buyERC20Info: RecipeERC20Info = { 25 | tokenAddress: 'DAI', 26 | decimals: 18n, 27 | isBaseToken: false, 28 | }; 29 | 30 | const quote = await ZeroXQuote.getSwapQuote({ 31 | networkName, 32 | sellERC20Amount, 33 | buyERC20Info, 34 | slippageBasisPoints: BigInt(100), 35 | isRailgun: true, 36 | }); 37 | 38 | expect(typeof quote === 'object').to.be.true; 39 | expect(quote).to.haveOwnProperty('price'); 40 | expect(quote).to.haveOwnProperty('spender'); 41 | expect(quote).to.haveOwnProperty('sellTokenValue'); 42 | }; 43 | 44 | describe('zero-x-quote', () => { 45 | before(() => {}); 46 | 47 | it('Should fetch quotes from ZeroX proxy', async () => { 48 | ZeroXConfig.PROXY_API_DOMAIN = testConfig.zeroXProxyApiDomain; 49 | ZeroXConfig.API_KEY = undefined; 50 | await runQuoteTest(); 51 | }).timeout(10000); 52 | 53 | it('Should fetch quotes from ZeroX API Key', async () => { 54 | ZeroXConfig.PROXY_API_DOMAIN = undefined; 55 | ZeroXConfig.API_KEY = testConfig.zeroXApiKey; 56 | await runQuoteTest(); 57 | }).timeout(10000); 58 | }); 59 | -------------------------------------------------------------------------------- /src/api/zero-x/index.ts: -------------------------------------------------------------------------------- 1 | export * from './zero-x-quote'; 2 | -------------------------------------------------------------------------------- /src/api/zero-x/zero-x-fetch.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName, isDefined } from '@railgun-community/shared-models'; 2 | import axios from 'axios'; 3 | import { ZeroXConfig } from '../../models/zero-x-config'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | type APIParams = Record; 7 | 8 | export enum ZeroXApiEndpoint { 9 | GetSwapQuote = 'swap/v1/quote', 10 | } 11 | 12 | export const zeroXApiSubdomain = (networkName: NetworkName): string => { 13 | switch (networkName) { 14 | case NetworkName.Ethereum: 15 | return 'api'; 16 | case NetworkName.BNBChain: 17 | return 'bsc.api'; 18 | case NetworkName.Polygon: 19 | return 'polygon.api'; 20 | case NetworkName.Arbitrum: 21 | return 'arbitrum.api'; 22 | case NetworkName.EthereumSepolia: 23 | return 'sepolia.api'; 24 | case NetworkName.Hardhat: 25 | case NetworkName.PolygonAmoy: 26 | case NetworkName.EthereumRopsten_DEPRECATED: 27 | case NetworkName.EthereumGoerli_DEPRECATED: 28 | case NetworkName.ArbitrumGoerli_DEPRECATED: 29 | case NetworkName.PolygonMumbai_DEPRECATED: 30 | throw new Error(`No 0x API URL for chain ${networkName}`); 31 | } 32 | }; 33 | 34 | const zeroXApiUrl = (networkName: NetworkName): string => { 35 | return `https://${zeroXApiSubdomain(networkName)}.0x.org`; 36 | }; 37 | 38 | const paramString = (params?: APIParams) => { 39 | if (!params) { 40 | return ''; 41 | } 42 | const searchParams = new URLSearchParams(params); 43 | return searchParams.toString() ? `?${searchParams.toString()}` : ''; 44 | }; 45 | 46 | export const createZeroXUrlAndHeaders = ( 47 | endpoint: ZeroXApiEndpoint, 48 | networkName: NetworkName, 49 | isRailgun: boolean, 50 | params?: APIParams, 51 | ) => { 52 | const proxyDomain = ZeroXConfig.PROXY_API_DOMAIN; 53 | if (isDefined(proxyDomain) && proxyDomain.length > 0) { 54 | return { 55 | url: createZeroXProxyAPIUrl( 56 | proxyDomain, 57 | endpoint, 58 | networkName, 59 | isRailgun, 60 | params, 61 | ), 62 | headers: {}, 63 | }; 64 | } 65 | const apiKey = ZeroXConfig.API_KEY; 66 | if (isDefined(apiKey) && apiKey.length > 0) { 67 | const url = `${zeroXApiUrl(networkName)}/${endpoint}${paramString(params)}`; 68 | return { url, headers: { '0x-api-key': apiKey } }; 69 | } 70 | throw new Error( 71 | 'No 0x proxy domain or API Key configured. Set ZeroXConfig.PROXY_API_DOMAIN or ZeroXConfig.API_KEY. For tests, modify test-config-overrides.test.ts.', 72 | ); 73 | }; 74 | 75 | const createZeroXProxyAPIUrl = ( 76 | proxyDomain: string, 77 | endpoint: ZeroXApiEndpoint, 78 | networkName: NetworkName, 79 | isRailgun: boolean, 80 | params?: APIParams, 81 | ) => { 82 | const route = isRailgun ? 'railgun' : 'public'; 83 | const url = `${proxyDomain}/0x/${route}/${zeroXApiSubdomain( 84 | networkName, 85 | )}/${endpoint}${paramString(params)}`; 86 | return url; 87 | }; 88 | 89 | export const getZeroXData = async ( 90 | endpoint: ZeroXApiEndpoint, 91 | networkName: NetworkName, 92 | isRailgun: boolean, 93 | params?: APIParams, 94 | ): Promise => { 95 | const { url, headers } = createZeroXUrlAndHeaders( 96 | endpoint, 97 | networkName, 98 | isRailgun, 99 | params, 100 | ); 101 | const rsp = await axios.get(url, { headers }); 102 | return rsp.data; 103 | }; 104 | -------------------------------------------------------------------------------- /src/combo-meals/combo-meal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComboMealConfig, 3 | RecipeInput, 4 | RecipeOutput, 5 | } from '../models/export-models'; 6 | import { Recipe } from '../recipes'; 7 | import { compareTokenAddress } from '../utils'; 8 | 9 | export abstract class ComboMeal { 10 | abstract readonly config: ComboMealConfig; 11 | 12 | protected abstract getRecipes(): Promise; 13 | 14 | private createNextRecipeInput( 15 | input: RecipeInput, 16 | output: RecipeOutput, 17 | ): RecipeInput { 18 | return { 19 | railgunAddress: input.railgunAddress, 20 | networkName: input.networkName, 21 | // TODO: Minimum balance is lost for combos. (amount is expectedBalance). 22 | erc20Amounts: output.erc20AmountRecipients.filter( 23 | ({ amount }) => amount > 0n, 24 | ), 25 | nfts: output.nftRecipients, 26 | }; 27 | } 28 | 29 | async getComboMealOutput(input: RecipeInput): Promise { 30 | const recipes = await this.getRecipes(); 31 | 32 | let nextInput = input; 33 | 34 | const aggregatedRecipeOutput: RecipeOutput = { 35 | name: this.config.name, 36 | stepOutputs: [], 37 | crossContractCalls: [], 38 | erc20AmountRecipients: [], 39 | nftRecipients: [], 40 | feeERC20AmountRecipients: [], 41 | minGasLimit: this.config.minGasLimit, 42 | }; 43 | 44 | for (let i = 0; i < recipes.length; i++) { 45 | const isFirst = i === 0; 46 | const isLast = i === recipes.length - 1; 47 | const recipe = recipes[i]; 48 | 49 | const recipeOutput = await recipe.getRecipeOutput( 50 | nextInput, 51 | !isFirst, // skipUnshield 52 | !isLast, // skipShield 53 | ); 54 | nextInput = this.createNextRecipeInput(nextInput, recipeOutput); 55 | 56 | aggregatedRecipeOutput.stepOutputs.push(...recipeOutput.stepOutputs); 57 | aggregatedRecipeOutput.crossContractCalls.push( 58 | ...recipeOutput.crossContractCalls, 59 | ); 60 | aggregatedRecipeOutput.feeERC20AmountRecipients.push( 61 | ...recipeOutput.feeERC20AmountRecipients, 62 | ); 63 | 64 | // Add amounts to remove any duplicates. 65 | recipeOutput.erc20AmountRecipients.forEach(erc20AmountRecipient => { 66 | const found = aggregatedRecipeOutput.erc20AmountRecipients.find( 67 | existingERC20Amount => { 68 | return ( 69 | compareTokenAddress( 70 | existingERC20Amount.tokenAddress, 71 | erc20AmountRecipient.tokenAddress, 72 | ) && 73 | existingERC20Amount.recipient === erc20AmountRecipient.recipient 74 | ); 75 | }, 76 | ); 77 | if (found) { 78 | found.amount = found.amount + erc20AmountRecipient.amount; 79 | return; 80 | } 81 | aggregatedRecipeOutput.erc20AmountRecipients.push(erc20AmountRecipient); 82 | }); 83 | 84 | // Add amounts to remove any duplicates. 85 | recipeOutput.nftRecipients.forEach(nftRecipient => { 86 | const found = aggregatedRecipeOutput.nftRecipients.find( 87 | existingNFTRecipient => { 88 | return ( 89 | compareTokenAddress( 90 | existingNFTRecipient.nftAddress, 91 | nftRecipient.nftAddress, 92 | ) && 93 | nftRecipient.tokenSubID === existingNFTRecipient.tokenSubID && 94 | nftRecipient.nftTokenType === existingNFTRecipient.nftTokenType && 95 | existingNFTRecipient.recipient === nftRecipient.recipient 96 | ); 97 | }, 98 | ); 99 | if (found) { 100 | found.amount = found.amount + nftRecipient.amount; 101 | return; 102 | } 103 | aggregatedRecipeOutput.nftRecipients.push(nftRecipient); 104 | }); 105 | } 106 | 107 | return aggregatedRecipeOutput; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/combo-meals/liquidity-vault/uni-v2-like-add-liquidity-beefy-deposit-combo-meal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComboMealConfig, 3 | RecipeERC20Amount, 4 | RecipeERC20Info, 5 | UniswapV2Fork, 6 | } from '../../models/export-models'; 7 | import { Recipe } from '../../recipes'; 8 | import { UniV2LikeAddLiquidityRecipe } from '../../recipes/liquidity/uni-v2-like/uni-v2-like-add-liquidity-recipe'; 9 | import { BeefyDepositRecipe } from '../../recipes/vault/beefy/beefy-deposit-recipe'; 10 | import { ComboMeal } from '../combo-meal'; 11 | import { UniV2LikeSDK } from '../../api/uni-v2-like/uni-v2-like-sdk'; 12 | import { NetworkName } from '@railgun-community/shared-models'; 13 | import { Provider } from 'ethers'; 14 | import { MIN_GAS_LIMIT_COMBO_MEAL } from '../../models/min-gas-limits'; 15 | 16 | export class UniV2LikeAddLiquidity_BeefyDeposit_ComboMeal extends ComboMeal { 17 | readonly config: ComboMealConfig = { 18 | name: '[NAME] Add Liquidity + Beefy Vault Deposit Combo Meal', 19 | description: 20 | 'Adds liquidity to a [NAME] Pool and deposits the LP tokens into a Beefy Vault.', 21 | minGasLimit: MIN_GAS_LIMIT_COMBO_MEAL, 22 | }; 23 | 24 | private readonly uniV2LikeAddLiquidityRecipe: UniV2LikeAddLiquidityRecipe; 25 | 26 | private readonly beefyDepositRecipe: BeefyDepositRecipe; 27 | 28 | constructor( 29 | uniswapV2Fork: UniswapV2Fork, 30 | erc20InfoA: RecipeERC20Info, 31 | erc20InfoB: RecipeERC20Info, 32 | slippageBasisPoints: bigint, 33 | vaultID: string, 34 | provider: Provider, 35 | ) { 36 | super(); 37 | 38 | this.uniV2LikeAddLiquidityRecipe = new UniV2LikeAddLiquidityRecipe( 39 | uniswapV2Fork, 40 | erc20InfoA, 41 | erc20InfoB, 42 | slippageBasisPoints, 43 | provider, 44 | ); 45 | this.beefyDepositRecipe = new BeefyDepositRecipe(vaultID); 46 | 47 | const forkName = UniV2LikeSDK.getForkName(uniswapV2Fork); 48 | this.config.name = `${forkName} Add Liquidity + Beefy Vault Deposit Combo Meal`; 49 | this.config.description = `Adds liquidity to a ${forkName} Pool and deposits the LP tokens into a Beefy Vault.`; 50 | } 51 | 52 | getAddLiquidityAmountBForUnshield( 53 | networkName: NetworkName, 54 | targetUnshieldERC20AmountA: RecipeERC20Amount, 55 | ) { 56 | return this.uniV2LikeAddLiquidityRecipe.getAddLiquidityAmountBForUnshield( 57 | networkName, 58 | targetUnshieldERC20AmountA, 59 | ); 60 | } 61 | 62 | protected async getRecipes(): Promise { 63 | return [this.uniV2LikeAddLiquidityRecipe, this.beefyDepositRecipe]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/contract/adapt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './relay-adapt-contract'; 2 | -------------------------------------------------------------------------------- /src/contract/adapt/relay-adapt-contract.ts: -------------------------------------------------------------------------------- 1 | import { abi } from '../../abi/abi'; 2 | import { 3 | NETWORK_CONFIG, 4 | NetworkName, 5 | isDefined, 6 | } from '@railgun-community/shared-models'; 7 | import { ZERO_ADDRESS } from '../../models/constants'; 8 | import { validateContractAddress } from '../../utils/address'; 9 | import { RelayAdapt } from '../../typechain'; 10 | import { TokenDataStruct } from '../../typechain/adapt/RelayAdapt'; 11 | import { Contract, ContractTransaction } from 'ethers'; 12 | 13 | export class RelayAdaptContract { 14 | private readonly contract: RelayAdapt; 15 | 16 | constructor(networkName: NetworkName) { 17 | const network = NETWORK_CONFIG[networkName]; 18 | if (!isDefined(network)) { 19 | throw new Error(`Network not found: ${networkName}`); 20 | } 21 | if (!validateContractAddress(network.relayAdaptContract)) { 22 | throw new Error('Invalid address for Relay Adapt contract.'); 23 | } 24 | this.contract = new Contract( 25 | network.relayAdaptContract, 26 | abi.adapt.relay, 27 | ) as unknown as RelayAdapt; 28 | } 29 | 30 | private createERC20TokenData(tokenAddress: string): TokenDataStruct { 31 | return { 32 | tokenAddress, 33 | tokenType: 0, // ERC20 34 | tokenSubID: ZERO_ADDRESS, 35 | }; 36 | } 37 | 38 | createBaseTokenWrap(amount?: bigint): Promise { 39 | return this.contract.wrapBase.populateTransaction( 40 | // 0 will automatically wrap full balance. 41 | amount ?? 0n, 42 | ); 43 | } 44 | 45 | createBaseTokenUnwrap(amount?: bigint): Promise { 46 | return this.contract.unwrapBase.populateTransaction( 47 | // 0 will automatically unwrap full balance. 48 | amount ?? 0n, 49 | ); 50 | } 51 | 52 | createBaseTokenTransfer( 53 | toAddress: string, 54 | amount?: bigint, 55 | ): Promise { 56 | const baseTokenTransfer: RelayAdapt.TokenTransferStruct = { 57 | token: this.createERC20TokenData(ZERO_ADDRESS), 58 | to: toAddress, 59 | // 0 will automatically transfer full balance. 60 | value: amount ?? 0n, 61 | }; 62 | return this.contract.transfer.populateTransaction([baseTokenTransfer]); 63 | } 64 | 65 | createERC20Transfer( 66 | toAddress: string, 67 | tokenAddress: string, 68 | amount?: bigint, 69 | ) { 70 | const erc20Transfer: RelayAdapt.TokenTransferStruct = { 71 | token: this.createERC20TokenData(tokenAddress), 72 | to: toAddress, 73 | // 0 will automatically transfer full balance. 74 | value: amount ?? 0n, 75 | }; 76 | return this.contract.transfer.populateTransaction([erc20Transfer]); 77 | } 78 | 79 | multicall( 80 | requireSuccess: boolean, 81 | calls: Array<{ to: string; data: string; value: bigint }>, 82 | ): Promise { 83 | return this.contract.multicall.populateTransaction(requireSuccess, calls); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/contract/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapt'; 2 | export * from './token'; 3 | -------------------------------------------------------------------------------- /src/contract/lido/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lido-stETH-contract'; 2 | export * from './lido-wstETH-contract'; 3 | -------------------------------------------------------------------------------- /src/contract/lido/lido-stETH-contract.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractRunner } from 'ethers'; 2 | 3 | import { LidoSTETH } from 'typechain'; 4 | import lidoSTETHAbi from '../../abi/lido/LidoSTETH.json'; 5 | 6 | export class LidoSTETHContract { 7 | private readonly contract: LidoSTETH; 8 | constructor(contractAddress: string, provider?: ContractRunner) { 9 | this.contract = new Contract( 10 | contractAddress, 11 | lidoSTETHAbi, 12 | provider, 13 | ) as unknown as LidoSTETH; 14 | } 15 | 16 | async submit(amount: bigint, referralAddress: string) { 17 | const tx = await this.contract.submit.populateTransaction(referralAddress); 18 | tx.value = amount; 19 | return tx; 20 | } 21 | 22 | balanceOf(address: string): Promise { 23 | return this.contract.balanceOf(address); 24 | } 25 | 26 | sharesOf(address: string): Promise { 27 | return this.contract.sharesOf(address); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/contract/lido/lido-wstETH-contract.ts: -------------------------------------------------------------------------------- 1 | import { Addressable, Contract, ContractRunner } from 'ethers'; 2 | import { LidoWSTETH } from 'typechain'; 3 | import lidoWSTETHAbi from '../../abi/lido/LidoWSTETH.json'; 4 | 5 | export class LidoWSTETHContract { 6 | private readonly contract: LidoWSTETH; 7 | constructor(contractAddress: string, provider?: ContractRunner) { 8 | this.contract = new Contract( 9 | contractAddress, 10 | lidoWSTETHAbi, 11 | provider, 12 | ) as unknown as LidoWSTETH; 13 | } 14 | 15 | wrap(amount: bigint) { 16 | return this.contract.wrap.populateTransaction(amount); 17 | } 18 | 19 | getWstETHByStETH(amount: bigint): Promise { 20 | return this.contract.getWstETHByStETH(amount); 21 | } 22 | 23 | balanceOf(address: Addressable | string): Promise { 24 | return this.contract.balanceOf(address); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/contract/liquidity/uni-v2-like-factory-contract.ts: -------------------------------------------------------------------------------- 1 | import { Contract, Provider } from 'ethers'; 2 | import { abi } from '../../abi/abi'; 3 | import { UniV2LikeFactory } from '../../typechain'; 4 | import { validateContractAddress } from '../../utils/address'; 5 | 6 | export class UniV2LikeFactoryContract { 7 | private readonly contract: UniV2LikeFactory; 8 | 9 | constructor(address: string, provider: Provider) { 10 | if (!validateContractAddress(address)) { 11 | throw new Error('Invalid factory address for LP factory contract'); 12 | } 13 | this.contract = new Contract( 14 | address, 15 | abi.liquidity.uniV2LikeFactory, 16 | provider, 17 | ) as unknown as UniV2LikeFactory; 18 | } 19 | 20 | async feeTo(): Promise { 21 | return this.contract.feeTo(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/contract/liquidity/uni-v2-like-pair-contract.ts: -------------------------------------------------------------------------------- 1 | import { Contract, Provider } from 'ethers'; 2 | import { abi } from '../../abi/abi'; 3 | import { UniV2LikePair } from '../../typechain'; 4 | import { validateContractAddress } from '../../utils/address'; 5 | 6 | export class UniV2LikePairContract { 7 | private readonly contract: UniV2LikePair; 8 | 9 | constructor(address: string, provider: Provider) { 10 | if (!validateContractAddress(address)) { 11 | throw new Error('Invalid pair address for LP router contract'); 12 | } 13 | this.contract = new Contract( 14 | address, 15 | abi.liquidity.uniV2LikePair, 16 | provider, 17 | ) as unknown as UniV2LikePair; 18 | } 19 | 20 | async getReserves() { 21 | const { _reserve0, _reserve1 } = await this.contract.getReserves(); 22 | return { 23 | reserveA: _reserve0, 24 | reserveB: _reserve1, 25 | }; 26 | } 27 | 28 | async totalSupply(): Promise { 29 | const totalSupply = await this.contract.totalSupply(); 30 | return totalSupply; 31 | } 32 | 33 | async kLast(): Promise { 34 | return this.contract.kLast(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/contract/liquidity/uni-v2-like-router-contract.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractTransaction } from 'ethers'; 2 | import { abi } from '../../abi/abi'; 3 | import { UniV2LikeRouter } from '../../typechain'; 4 | import { validateContractAddress } from '../../utils/address'; 5 | 6 | export class UniV2LikeRouterContract { 7 | private readonly contract: UniV2LikeRouter; 8 | 9 | constructor(address: string) { 10 | if (!validateContractAddress(address)) { 11 | throw new Error('Invalid address for LP router contract'); 12 | } 13 | this.contract = new Contract( 14 | address, 15 | abi.liquidity.uniV2LikeRouter, 16 | ) as unknown as UniV2LikeRouter; 17 | } 18 | 19 | createAddLiquidity( 20 | tokenA: string, 21 | tokenB: string, 22 | amountADesired: bigint, 23 | amountBDesired: bigint, 24 | amountAMin: bigint, 25 | amountBMin: bigint, 26 | to: string, 27 | deadline: number, 28 | ): Promise { 29 | return this.contract.addLiquidity.populateTransaction( 30 | tokenA, 31 | tokenB, 32 | amountADesired, 33 | amountBDesired, 34 | amountAMin, 35 | amountBMin, 36 | to, 37 | deadline, 38 | ); 39 | } 40 | 41 | createRemoveLiquidity( 42 | tokenA: string, 43 | tokenB: string, 44 | liquidity: bigint, 45 | amountAMin: bigint, 46 | amountBMin: bigint, 47 | to: string, 48 | deadline: number, 49 | ): Promise { 50 | return this.contract.removeLiquidity.populateTransaction( 51 | tokenA, 52 | tokenB, 53 | liquidity, 54 | amountAMin, 55 | amountBMin, 56 | to, 57 | deadline, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/contract/token/erc20-contract.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractTransaction, Provider } from 'ethers'; 2 | import { abi } from '../../abi/abi'; 3 | import { validateContractAddress } from '../../utils/address'; 4 | import { Erc20 } from '../../typechain'; 5 | 6 | export class ERC20Contract { 7 | private readonly contract: Erc20; 8 | 9 | constructor(address: string, provider?: Provider) { 10 | if (!validateContractAddress(address)) { 11 | throw new Error('Invalid ERC20 address for contract'); 12 | } 13 | this.contract = new Contract( 14 | address, 15 | abi.token.erc20, 16 | provider, 17 | ) as unknown as Erc20; 18 | } 19 | 20 | createSpenderApproval( 21 | spender: string, 22 | amount: bigint, 23 | ): Promise { 24 | return this.contract.approve.populateTransaction(spender, amount); 25 | } 26 | 27 | createTransfer( 28 | toAddress: string, 29 | amount: bigint, 30 | ): Promise { 31 | return this.contract.transfer.populateTransaction(toAddress, amount); 32 | } 33 | 34 | balanceOf(account: string): Promise { 35 | return this.contract.balanceOf(account); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/contract/token/erc721-contract.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractTransaction } from 'ethers'; 2 | import { abi } from '../../abi/abi'; 3 | import { Erc721 } from '../../typechain'; 4 | import { validateContractAddress } from '../../utils/address'; 5 | 6 | export class ERC721Contract { 7 | private readonly contract: Erc721; 8 | 9 | constructor(address: string) { 10 | if (!validateContractAddress(address)) { 11 | throw new Error('Invalid ERC20 address for contract'); 12 | } 13 | this.contract = new Contract( 14 | address, 15 | abi.token.erc721, 16 | ) as unknown as Erc721; 17 | } 18 | 19 | createSpenderApprovalForAll(spender: string): Promise { 20 | return this.contract.setApprovalForAll.populateTransaction(spender, true); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/contract/token/index.ts: -------------------------------------------------------------------------------- 1 | export * from './erc20-contract'; 2 | export * from './erc721-contract'; 3 | -------------------------------------------------------------------------------- /src/contract/vault/beefy/beefy-vault-contract.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractTransaction, Provider } from 'ethers'; 2 | import { abi } from '../../../abi/abi'; 3 | import { BeefyVaultMergedV6V7 } from '../../../typechain'; 4 | import { validateContractAddress } from '../../../utils/address'; 5 | 6 | export class BeefyVaultContract { 7 | private readonly contract: BeefyVaultMergedV6V7; 8 | 9 | constructor(address: string, provider?: Provider) { 10 | if (!validateContractAddress(address)) { 11 | throw new Error('Invalid Vault address for Beefy contract'); 12 | } 13 | this.contract = new Contract( 14 | address, 15 | abi.vault.beefy, 16 | provider, 17 | ) as unknown as BeefyVaultMergedV6V7; 18 | } 19 | 20 | createDepositAll(): Promise { 21 | return this.contract.depositAll.populateTransaction(); 22 | } 23 | 24 | createWithdrawAll(): Promise { 25 | return this.contract.withdrawAll.populateTransaction(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/graph/graph-cache/README.md: -------------------------------------------------------------------------------- 1 | ## Query 2 | 3 | 4 | 5 | curl -L -H "Content-Type: application/json" -X POST -d '{ "query": "query Pairs { pairs(first: 1000, orderBy:reserveUSD, orderDirection:desc, where: {reserveUSD_gt: 100}) { id token0 { id symbol decimals } token1 { id symbol decimals } }}"}' [URL] 6 | 7 | ## URLs 8 | 9 | 10 | 11 | https://gateway.thegraph.com/api/{THE_GRAPH_API_KEY}/subgraphs/id/EYCKATKGBKLWvSfwvBjzfCBmGwYNdVkduYXVivCsLRFu 12 | 13 | 14 | 15 | https://gateway.thegraph.com/api/{THE_GRAPH_API_KEY}/subgraphs/id/6NUtT5mGjZ1tSshKLf5Q3uEEJtjBZJo1TpL5MXsUBqrT 16 | 17 | 18 | 19 | https://gateway.thegraph.com/api/{THE_GRAPH_API_KEY}/subgraphs/id/8NiXkxLRT3R22vpwLB4DXttpEf3X1LrKhe4T1tQ3jjbP 20 | 21 | 22 | 23 | https://gateway.thegraph.com/api/{THE_GRAPH_API_KEY}/subgraphs/id/GPRigpbNuPkxkwpSbDuYXbikodNJfurc1LCENLzboWer 24 | 25 | 26 | 27 | https://gateway.thegraph.com/api/{THE_GRAPH_API_KEY}/subgraphs/id/8nFDCAhdnJQEhQF3ZRnfWkJ6FkRsfAiiVabVn4eGoAZH 28 | 29 | 30 | 31 | https://gateway.thegraph.com/api/${THE_GRAPH_API_KEY}/subgraphs/id/Aj9TDh9SPcn7cz4DXW26ga22VnBzHhPVuKGmE4YBzDFj 32 | 33 | 34 | 35 | https://gateway.thegraph.com/api/${THE_GRAPH_API_KEY}/subgraphs/id/CCFSaj7uS128wazXMdxdnbGA3YQnND9yBdHjPtvH7Bc7 36 | 37 | ## How to update lists: 38 | 39 | 1. Run full query against proper URL. 40 | 2. Remove "data" and "pairs" fields to get final array 41 | -------------------------------------------------------------------------------- /src/graph/graphql/uni-v2-like-query.graphql: -------------------------------------------------------------------------------- 1 | query PairsByTokensAB($tokens: [String!]) { 2 | pairs(where: { token0_in: $tokens, token1_in: $tokens }) { 3 | id 4 | reserve0 5 | reserve1 6 | token0 { 7 | id 8 | symbol 9 | decimals 10 | } 11 | token1 { 12 | id 13 | symbol 14 | decimals 15 | } 16 | } 17 | } 18 | 19 | query PairsByLPToken($tokens: [ID!]) { 20 | pairs(where: { id_in: $tokens }) { 21 | id 22 | reserve0 23 | reserve1 24 | token0 { 25 | id 26 | symbol 27 | decimals 28 | } 29 | token1 { 30 | id 31 | symbol 32 | decimals 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './init'; 3 | export * from './contract'; 4 | export * from './models'; 5 | export * from './recipes'; 6 | export * from './steps'; 7 | export * from './utils'; 8 | -------------------------------------------------------------------------------- /src/init/__tests__/init.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { setRailgunFees } from '../init'; 4 | import { RailgunConfig } from '../../models/railgun-config'; 5 | import { NetworkName } from '@railgun-community/shared-models'; 6 | 7 | chai.use(chaiAsPromised); 8 | const { expect } = chai; 9 | 10 | describe('init', () => { 11 | it('Should run init script', async () => { 12 | setRailgunFees(NetworkName.Arbitrum, 35n, 40n); 13 | expect( 14 | RailgunConfig.SHIELD_FEE_BASIS_POINTS_FOR_NETWORK[NetworkName.Arbitrum], 15 | ).to.equal(35n); 16 | expect( 17 | RailgunConfig.UNSHIELD_FEE_BASIS_POINTS_FOR_NETWORK[NetworkName.Arbitrum], 18 | ).to.equal(40n); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/init/index.ts: -------------------------------------------------------------------------------- 1 | export * from './init'; 2 | -------------------------------------------------------------------------------- /src/init/init.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName } from '@railgun-community/shared-models'; 2 | import { CookbookDebugger } from '../models/export-models'; 3 | import { RailgunConfig } from '../models/railgun-config'; 4 | import { CookbookDebug } from '../utils/cookbook-debug'; 5 | 6 | export const setRailgunFees = ( 7 | networkName: NetworkName, 8 | shieldFeeBasisPoints: bigint, 9 | unshieldFeeBasisPoints: bigint, 10 | ) => { 11 | RailgunConfig.SHIELD_FEE_BASIS_POINTS_FOR_NETWORK[networkName] = 12 | shieldFeeBasisPoints; 13 | RailgunConfig.UNSHIELD_FEE_BASIS_POINTS_FOR_NETWORK[networkName] = 14 | unshieldFeeBasisPoints; 15 | }; 16 | 17 | export const setCookbookDebugger = (cookbookDebugger: CookbookDebugger) => { 18 | CookbookDebug.setDebugger(cookbookDebugger); 19 | }; 20 | -------------------------------------------------------------------------------- /src/models/constants.ts: -------------------------------------------------------------------------------- 1 | export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; 2 | // ZeroX Constants v2 3 | export const ZERO_X_V2_BASE_URL = 'https://api.0x.org/'; 4 | export const ZERO_X_EXCHANGE_ALLOWANCE_HOLDER_ADDRESS = '0x0000000000001fF3684f28c67538d4D072C22734'; 5 | export const ZERO_X_PROXY_BASE_TOKEN_ADDRESS = 6 | '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './export-models'; 2 | export * from './uni-v2-like'; 3 | export * from './zero-x-config'; 4 | -------------------------------------------------------------------------------- /src/models/min-gas-limits.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-inferrable-types */ 2 | 3 | // Combo Meals 4 | export const MIN_GAS_LIMIT_COMBO_MEAL: bigint = 2_900_000n; // 2.741 5 | 6 | // Recipes 7 | export const MIN_GAS_LIMIT_ANY_RECIPE: bigint = 2_800_000n; 8 | export const MIN_GAS_LIMIT_EMPTY: bigint = 1_800_000n; // 1.783 9 | export const MIN_GAS_LIMIT_EMPTY_SHIELD: bigint = 2_600_000n; 10 | export const MIN_GAS_LIMIT_0X_SWAP: bigint = 2_400_000n; // 1.916 11 | export const MIN_GAS_LIMIT_0X_SWAP_TRANSFER: bigint = 2_500_000n; 12 | export const MIN_GAS_LIMIT_0X_SWAP_SHIELD: bigint = 2_700_000n; 13 | export const MIN_GAS_LIMIT_BEEFY_VAULT_DEPOSIT: bigint = 3_100_000n; // 2.667 (3.113 for a different vault) 14 | export const MIN_GAS_LIMIT_BEEFY_VAULT_WITHDRAW: bigint = 3_000_000n; // 2.603 (3.012 for a different vault...) 15 | export const MIN_GAS_LIMIT_LP_V2_ADD_LIQUIDITY: bigint = 2_700_000n; // 2.588 16 | export const MIN_GAS_LIMIT_LP_V2_REMOVE_LIQUIDITY: bigint = 2_300_000n; // 2.194 17 | export const MIN_GAS_LIMIT_TRANSFER: bigint = 1_900_000n; // 1.817 18 | -------------------------------------------------------------------------------- /src/models/railgun-config.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName } from '@railgun-community/shared-models'; 2 | 3 | export class RailgunConfig { 4 | static SHIELD_FEE_BASIS_POINTS_FOR_NETWORK: Record = {}; 5 | static UNSHIELD_FEE_BASIS_POINTS_FOR_NETWORK: Record = {}; 6 | 7 | static getShieldFeeBasisPoints = (networkName: NetworkName): bigint => { 8 | const shieldFee = this.SHIELD_FEE_BASIS_POINTS_FOR_NETWORK[networkName]; 9 | if (!shieldFee) { 10 | throw new Error(`No shield fee defined for network ${networkName}.`); 11 | } 12 | return shieldFee; 13 | }; 14 | 15 | static getUnshieldFeeBasisPoints = (networkName: NetworkName): bigint => { 16 | const unshieldFee = this.UNSHIELD_FEE_BASIS_POINTS_FOR_NETWORK[networkName]; 17 | if (!unshieldFee) { 18 | throw new Error(`No unshield fee defined for network ${networkName}.`); 19 | } 20 | return unshieldFee; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/models/uni-v2-like.ts: -------------------------------------------------------------------------------- 1 | import { UniswapV2Fork } from './export-models'; 2 | 3 | export type LiquidityV2Pool = { 4 | name: string; 5 | uniswapV2Fork: UniswapV2Fork; 6 | tokenAddressA: string; 7 | tokenSymbolA: string; 8 | tokenDecimalsA: bigint; 9 | tokenAddressB: string; 10 | tokenSymbolB: string; 11 | tokenDecimalsB: bigint; 12 | pairAddress: string; 13 | pairTokenName: string; 14 | pairTokenSymbol: string; 15 | pairTokenDecimals: bigint; 16 | rateWith18Decimals: bigint; 17 | }; 18 | -------------------------------------------------------------------------------- /src/models/zero-x-config.ts: -------------------------------------------------------------------------------- 1 | export class ZeroXConfig { 2 | static PROXY_API_DOMAIN: Optional; 3 | static API_KEY: Optional; 4 | } 5 | -------------------------------------------------------------------------------- /src/recipes/__tests__/custom-recipe.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { CustomRecipe } from '../custom-recipe'; 4 | import { UnwrapBaseTokenStep } from '../../steps/adapt/unwrap-base-token-step'; 5 | import { StepInput } from '../../models/export-models'; 6 | import { NetworkName } from '@railgun-community/shared-models'; 7 | import { setRailgunFees } from '../../init'; 8 | import { MIN_GAS_LIMIT_ANY_RECIPE } from '../../models/min-gas-limits'; 9 | 10 | chai.use(chaiAsPromised); 11 | const { expect } = chai; 12 | 13 | const networkName = NetworkName.Ethereum; 14 | 15 | describe('custom-recipe', () => { 16 | before(() => { 17 | setRailgunFees(networkName, 25n, 25n); 18 | }); 19 | 20 | it('Should add custom recipe steps', async () => { 21 | const supportedNetworks = [networkName]; 22 | 23 | const recipe = new CustomRecipe( 24 | { 25 | name: 'custom', 26 | description: 'this is a custom recipe', 27 | minGasLimit: MIN_GAS_LIMIT_ANY_RECIPE, 28 | }, 29 | supportedNetworks, 30 | ); 31 | 32 | recipe.addStep(new UnwrapBaseTokenStep()); 33 | 34 | const firstStepInput: StepInput = { 35 | networkName, 36 | erc20Amounts: [], 37 | nfts: [], 38 | }; 39 | 40 | // @ts-expect-error 41 | const steps = await recipe.getFullSteps(firstStepInput); 42 | 43 | expect(steps.map(step => step.config.name)).to.deep.equal([ 44 | 'Unshield (Default)', 45 | 'Unwrap Base Token', 46 | 'Shield (Default)', 47 | ]); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/recipes/adapt/index.ts: -------------------------------------------------------------------------------- 1 | // Exported individual recipes 2 | export * from './unwrap-transfer-base-token-recipe'; 3 | -------------------------------------------------------------------------------- /src/recipes/adapt/unwrap-transfer-base-token-recipe.ts: -------------------------------------------------------------------------------- 1 | import { UnwrapBaseTokenStep } from '../../steps/adapt/unwrap-base-token-step'; 2 | import { Recipe } from '../recipe'; 3 | import { TransferBaseTokenStep } from '../../steps/adapt/transfer-base-token-step'; 4 | 5 | import { Step } from '../../steps'; 6 | import { RecipeConfig } from '../../models/export-models'; 7 | import { MIN_GAS_LIMIT_TRANSFER } from '../../models/min-gas-limits'; 8 | 9 | export class UnwrapTransferBaseTokenRecipe extends Recipe { 10 | readonly config: RecipeConfig = { 11 | name: 'Unwrap Base Token and Transfer', 12 | description: 13 | 'Unwraps wrapped token into base token, and transfers base token to an external public address.', 14 | minGasLimit: MIN_GAS_LIMIT_TRANSFER, 15 | }; 16 | 17 | private readonly toAddress: string; 18 | private readonly amount: Optional; 19 | 20 | constructor(toAddress: string, amount?: bigint) { 21 | super(); 22 | this.toAddress = toAddress; 23 | this.amount = amount; 24 | } 25 | 26 | protected supportsNetwork(): boolean { 27 | return true; 28 | } 29 | 30 | protected async getInternalSteps(): Promise { 31 | return [ 32 | new UnwrapBaseTokenStep(this.amount), 33 | new TransferBaseTokenStep(this.toAddress), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/recipes/custom-recipe.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName } from '@railgun-community/shared-models'; 2 | import { RecipeConfig } from '../models/export-models'; 3 | import { Step } from '../steps/step'; 4 | import { Recipe } from './recipe'; 5 | 6 | export class CustomRecipe extends Recipe { 7 | readonly config: RecipeConfig; 8 | 9 | private readonly supportedNetworks: NetworkName[] = []; 10 | 11 | private internalSteps: Step[] = []; 12 | 13 | constructor(config: RecipeConfig, supportedNetworks: NetworkName[]) { 14 | super(); 15 | this.config = config; 16 | this.supportedNetworks = supportedNetworks; 17 | } 18 | 19 | protected async getInternalSteps(): Promise { 20 | return this.internalSteps; 21 | } 22 | 23 | protected supportsNetwork(networkName: NetworkName): boolean { 24 | return this.supportedNetworks.includes(networkName); 25 | } 26 | 27 | addStep(step: Step): void { 28 | if (!step.canAddStep) { 29 | throw new Error(`Cannot add Recipe Step: ${step.config.name}`); 30 | } 31 | 32 | this.internalSteps.push(step); 33 | } 34 | 35 | addSteps(steps: Step[]): void { 36 | steps.forEach(this.addStep); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/recipes/empty/__tests__/FORK-run-designate-shield-erc20-recipient-empty-recipe.test.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { RecipeERC20Info, RecipeInput } from '../../../models/export-models'; 4 | import { 5 | NETWORK_CONFIG, 6 | NetworkName, 7 | TXIDVersion, 8 | } from '@railgun-community/shared-models'; 9 | import { setRailgunFees } from '../../../init'; 10 | import { 11 | MOCK_RAILGUN_WALLET_ADDRESS, 12 | MOCK_SHIELD_FEE_BASIS_POINTS, 13 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 14 | } from '../../../test/mocks.test'; 15 | import { 16 | executeRecipeStepsAndAssertUnshieldBalances, 17 | shouldSkipForkTest, 18 | } from '../../../test/common.test'; 19 | import { DesignateShieldERC20RecipientEmptyRecipe } from '../designate-shield-erc20-recipient-empty-recipe'; 20 | import { testRailgunWallet2 } from '../../../test/shared.test'; 21 | import { balanceForERC20Token } from '@railgun-community/wallet'; 22 | 23 | chai.use(chaiAsPromised); 24 | 25 | const networkName = NetworkName.Ethereum; 26 | const txidVersion = TXIDVersion.V2_PoseidonMerkle; 27 | const tokenAddress = NETWORK_CONFIG[networkName].baseToken.wrappedAddress; 28 | 29 | const erc20Info: RecipeERC20Info = { 30 | tokenAddress, 31 | decimals: 18n, 32 | isBaseToken: false, 33 | }; 34 | 35 | describe('FORK-designate-shield-erc20-recipient-empty-recipe', function run() { 36 | this.timeout(60000); 37 | 38 | before(async function run() { 39 | setRailgunFees( 40 | networkName, 41 | MOCK_SHIELD_FEE_BASIS_POINTS, 42 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 43 | ); 44 | }); 45 | 46 | it('[FORK] Should run designate-shield-erc20-recipient-empty-recipe', async function run() { 47 | if (shouldSkipForkTest(networkName)) { 48 | this.skip(); 49 | return; 50 | } 51 | 52 | const wallet2 = testRailgunWallet2; 53 | const privateWalletAddress = wallet2.getAddress(); 54 | 55 | const recipe = new DesignateShieldERC20RecipientEmptyRecipe( 56 | privateWalletAddress, 57 | [erc20Info], 58 | ); 59 | 60 | const recipeInput: RecipeInput = { 61 | railgunAddress: MOCK_RAILGUN_WALLET_ADDRESS, 62 | networkName, 63 | erc20Amounts: [ 64 | { 65 | tokenAddress, 66 | decimals: 18n, 67 | isBaseToken: false, 68 | amount: 12000n, 69 | }, 70 | ], 71 | nfts: [], 72 | }; 73 | 74 | const initialPrivateRAILBalance2 = await balanceForERC20Token( 75 | txidVersion, 76 | wallet2, 77 | networkName, 78 | tokenAddress, 79 | false, // onlySpendable - not required for tests 80 | ); 81 | 82 | const recipeOutput = await recipe.getRecipeOutput(recipeInput); 83 | await executeRecipeStepsAndAssertUnshieldBalances( 84 | recipe.config.name, 85 | recipeInput, 86 | recipeOutput, 87 | ); 88 | 89 | // REQUIRED TESTS: 90 | 91 | // 1. Add New Private Balance expectations. 92 | const privateRAILBalance2 = await balanceForERC20Token( 93 | txidVersion, 94 | wallet2, 95 | networkName, 96 | tokenAddress, 97 | false, // onlySpendable - not required for tests 98 | ); 99 | const originalAmount = 12000n; 100 | const unshieldFee = 101 | (originalAmount * MOCK_UNSHIELD_FEE_BASIS_POINTS) / 10000n; 102 | const unshieldedAmount = originalAmount - unshieldFee; 103 | const shieldFee = 104 | (unshieldedAmount * MOCK_SHIELD_FEE_BASIS_POINTS) / 10000n; 105 | const expectedPrivateRAILBalance2 = 106 | initialPrivateRAILBalance2 + 107 | unshieldedAmount - // Amount to re-shield 108 | shieldFee; // Shield fee 109 | expect(privateRAILBalance2).to.equal( 110 | expectedPrivateRAILBalance2, 111 | `Private RAIL balance (wallet 2) incorrect after shield`, 112 | ); 113 | 114 | // 2. Add External Balance expectations. 115 | // N/A 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /src/recipes/empty/__tests__/FORK-run-empty-recipe-erc721.test.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { RecipeInput } from '../../../models/export-models'; 4 | import { 5 | NFTTokenType, 6 | NetworkName, 7 | TXIDVersion, 8 | } from '@railgun-community/shared-models'; 9 | import { setRailgunFees } from '../../../init'; 10 | import { 11 | MOCK_RAILGUN_WALLET_ADDRESS, 12 | MOCK_SHIELD_FEE_BASIS_POINTS, 13 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 14 | } from '../../../test/mocks.test'; 15 | import { EmptyRecipe } from '../empty-recipe'; 16 | import { 17 | executeRecipeStepsAndAssertUnshieldBalances, 18 | shouldSkipForkTest, 19 | } from '../../../test/common.test'; 20 | import { 21 | TokenType, 22 | balanceForNFT, 23 | getTokenDataNFT, 24 | } from '@railgun-community/wallet'; 25 | import { testRailgunWallet } from '../../../test/shared.test'; 26 | 27 | chai.use(chaiAsPromised); 28 | 29 | const networkName = NetworkName.Ethereum; 30 | const txidVersion = TXIDVersion.V2_PoseidonMerkle; 31 | const nftAddress = '0x1234567890'; 32 | const tokenSubID = '0x0000'; 33 | 34 | describe.skip('FORK-run-empty-recipe-erc721', function run() { 35 | this.timeout(45000); 36 | 37 | before(async function run() { 38 | setRailgunFees( 39 | networkName, 40 | MOCK_SHIELD_FEE_BASIS_POINTS, 41 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 42 | ); 43 | }); 44 | 45 | it('[FORK] Should run empty-recipe with ERC721 inputs', async function run() { 46 | if (shouldSkipForkTest(networkName)) { 47 | this.skip(); 48 | return; 49 | } 50 | 51 | const recipe = new EmptyRecipe(); 52 | 53 | const recipeInput: RecipeInput = { 54 | railgunAddress: MOCK_RAILGUN_WALLET_ADDRESS, 55 | networkName, 56 | erc20Amounts: [], 57 | nfts: [ 58 | { 59 | nftAddress, 60 | tokenSubID, 61 | amount: 1n, 62 | nftTokenType: NFTTokenType.ERC721, 63 | }, 64 | ], 65 | }; 66 | 67 | const recipeOutput = await recipe.getRecipeOutput(recipeInput); 68 | await executeRecipeStepsAndAssertUnshieldBalances( 69 | recipe.config.name, 70 | recipeInput, 71 | recipeOutput, 72 | ); 73 | 74 | // REQUIRED TESTS: 75 | 76 | const nftTokenData = getTokenDataNFT( 77 | nftAddress, 78 | TokenType.ERC721, 79 | tokenSubID, 80 | ); 81 | expect( 82 | balanceForNFT( 83 | txidVersion, 84 | testRailgunWallet, 85 | networkName, 86 | nftTokenData, 87 | false, // onlySpendable - not required for tests 88 | ), 89 | ).to.equal(1n); 90 | 91 | // 2. Add External Balance expectations. 92 | // N/A 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/recipes/empty/__tests__/FORK-run-empty-recipe.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { RecipeInput } from '../../../models/export-models'; 4 | import { NETWORK_CONFIG, NetworkName } from '@railgun-community/shared-models'; 5 | import { setRailgunFees } from '../../../init'; 6 | import { 7 | MOCK_RAILGUN_WALLET_ADDRESS, 8 | MOCK_SHIELD_FEE_BASIS_POINTS, 9 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 10 | } from '../../../test/mocks.test'; 11 | import { EmptyRecipe } from '../empty-recipe'; 12 | import { 13 | executeRecipeStepsAndAssertUnshieldBalances, 14 | shouldSkipForkTest, 15 | } from '../../../test/common.test'; 16 | 17 | chai.use(chaiAsPromised); 18 | 19 | const networkName = NetworkName.Ethereum; 20 | const tokenAddress = NETWORK_CONFIG[networkName].baseToken.wrappedAddress; 21 | 22 | describe('FORK-run-empty-recipe with ERC20 inputs', function run() { 23 | this.timeout(45000); 24 | 25 | before(async function run() { 26 | setRailgunFees( 27 | networkName, 28 | MOCK_SHIELD_FEE_BASIS_POINTS, 29 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 30 | ); 31 | }); 32 | 33 | it('[FORK] Should run empty-recipe', async function run() { 34 | if (shouldSkipForkTest(networkName)) { 35 | this.skip(); 36 | return; 37 | } 38 | 39 | const recipe = new EmptyRecipe(); 40 | 41 | const recipeInput: RecipeInput = { 42 | railgunAddress: MOCK_RAILGUN_WALLET_ADDRESS, 43 | networkName, 44 | erc20Amounts: [ 45 | { 46 | tokenAddress, 47 | decimals: 18n, 48 | isBaseToken: false, 49 | amount: 12000n, 50 | }, 51 | ], 52 | nfts: [], 53 | }; 54 | 55 | const recipeOutput = await recipe.getRecipeOutput(recipeInput); 56 | await executeRecipeStepsAndAssertUnshieldBalances( 57 | recipe.config.name, 58 | recipeInput, 59 | recipeOutput, 60 | ); 61 | 62 | // REQUIRED TESTS: 63 | 64 | // 1. Add New Private Balance expectations. 65 | // N/A 66 | 67 | // 2. Add External Balance expectations. 68 | // N/A 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/recipes/empty/designate-shield-erc20-recipient-empty-recipe.ts: -------------------------------------------------------------------------------- 1 | import { Recipe } from '../recipe'; 2 | import { EmptyTransferBaseTokenStep, Step } from '../../steps'; 3 | import { RecipeConfig, RecipeERC20Info } from '../../models/export-models'; 4 | import { MIN_GAS_LIMIT_EMPTY_SHIELD } from '../../models/min-gas-limits'; 5 | import { DesignateShieldERC20RecipientStep } from '../../steps/railgun/designate-shield-erc20-recipient-step'; 6 | 7 | export class DesignateShieldERC20RecipientEmptyRecipe extends Recipe { 8 | readonly config: RecipeConfig = { 9 | name: 'Designate Shield ERC20 Recipient Empty Recipe', 10 | description: 11 | 'Shield empty recipe for testing. Designates erc20 tokens to be shielded to an address.', 12 | minGasLimit: MIN_GAS_LIMIT_EMPTY_SHIELD, 13 | }; 14 | 15 | toAddress: string; 16 | erc20Infos: RecipeERC20Info[]; 17 | 18 | constructor(toAddress: string, erc20Infos: RecipeERC20Info[]) { 19 | super(); 20 | this.toAddress = toAddress; 21 | this.erc20Infos = erc20Infos; 22 | } 23 | 24 | protected supportsNetwork(): boolean { 25 | return true; 26 | } 27 | 28 | protected async getInternalSteps(): Promise { 29 | return [ 30 | new EmptyTransferBaseTokenStep(), 31 | new DesignateShieldERC20RecipientStep(this.toAddress, this.erc20Infos), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/recipes/empty/empty-recipe.ts: -------------------------------------------------------------------------------- 1 | import { Recipe } from '../recipe'; 2 | import { Step } from '../../steps'; 3 | import { EmptyTransferBaseTokenStep } from '../../steps/adapt/empty-transfer-base-token-step'; 4 | import { RecipeConfig } from '../../models/export-models'; 5 | import { MIN_GAS_LIMIT_EMPTY } from '../../models/min-gas-limits'; 6 | 7 | export class EmptyRecipe extends Recipe { 8 | readonly config: RecipeConfig = { 9 | name: 'Empty Recipe', 10 | description: 'Empty recipe for testing. Sends 0 tokens to null address.', 11 | minGasLimit: MIN_GAS_LIMIT_EMPTY, 12 | }; 13 | 14 | constructor() { 15 | super(); 16 | } 17 | 18 | protected supportsNetwork(): boolean { 19 | return true; 20 | } 21 | 22 | protected async getInternalSteps(): Promise { 23 | return [new EmptyTransferBaseTokenStep()]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/recipes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './recipe'; 2 | export * from './adapt'; 3 | export * from './swap'; 4 | export * from './liquidity'; 5 | export * from './vault'; 6 | export * from './lido'; 7 | -------------------------------------------------------------------------------- /src/recipes/lido/__tests__/FORK-lido-stake-recipe.test.ts: -------------------------------------------------------------------------------- 1 | import { LidoStakeRecipe } from '../lido-stake-recipe'; 2 | import { RecipeERC20Info, RecipeInput } from '../../../models'; 3 | import { 4 | MOCK_RAILGUN_WALLET_ADDRESS, 5 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 6 | MOCK_SHIELD_FEE_BASIS_POINTS, 7 | } from '../../../test/mocks.test'; 8 | import { NETWORK_CONFIG, NetworkName } from '@railgun-community/shared-models'; 9 | import { expect } from 'chai'; 10 | import { setRailgunFees } from '../../../init'; 11 | import { 12 | executeRecipeStepsAndAssertUnshieldBalances, 13 | shouldSkipForkTest, 14 | } from '../../../test/common.test'; 15 | import { 16 | getTestProvider, 17 | getTestRailgunWallet, 18 | } from '../../../test/shared.test'; 19 | import { refreshBalances } from '@railgun-community/wallet'; 20 | import { LidoWSTETHContract, LidoSTETHContract } from '../../../contract/lido'; 21 | 22 | const networkName = NetworkName.Ethereum; 23 | const STETH_TOKEN_INFO: RecipeERC20Info = { 24 | tokenAddress: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', 25 | decimals: 18n, 26 | }; 27 | 28 | const WSTETH_TOKEN_INFO: RecipeERC20Info = { 29 | tokenAddress: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', 30 | decimals: 18n, 31 | isBaseToken: false, 32 | }; 33 | 34 | const baseTokenAddress = NETWORK_CONFIG[networkName].baseToken.wrappedAddress; 35 | 36 | describe('Stake ETH to get stETH and wrap it to wstETH', () => { 37 | before(async function () { 38 | this.timeout(100_000); 39 | setRailgunFees( 40 | networkName, 41 | MOCK_SHIELD_FEE_BASIS_POINTS, 42 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 43 | ); 44 | 45 | // @TODO syncing balance at the start of test doesn't reflect the balance, we have to resync it again 46 | const railgunId = getTestRailgunWallet().id; 47 | await refreshBalances(NETWORK_CONFIG[networkName].chain, [railgunId]); 48 | }); 49 | 50 | it('[FORK] Should stake ETH to get stETH', async function () { 51 | if (shouldSkipForkTest(networkName)) { 52 | this.skip(); 53 | return; 54 | } 55 | 56 | const unshieldAmount = 10000n; 57 | const recipeInput: RecipeInput = { 58 | railgunAddress: MOCK_RAILGUN_WALLET_ADDRESS, 59 | networkName: networkName, 60 | erc20Amounts: [ 61 | { 62 | tokenAddress: baseTokenAddress, 63 | decimals: 18n, 64 | amount: unshieldAmount, 65 | }, 66 | ], 67 | nfts: [], 68 | }; 69 | 70 | const provider = getTestProvider(); 71 | 72 | const recipe = new LidoStakeRecipe( 73 | STETH_TOKEN_INFO, 74 | WSTETH_TOKEN_INFO, 75 | provider, 76 | ); 77 | const recipeOutput = await recipe.getRecipeOutput(recipeInput); 78 | 79 | const { proxyContract: railgun, relayAdaptContract } = 80 | NETWORK_CONFIG[networkName]; 81 | const wstETHContract = new LidoWSTETHContract( 82 | WSTETH_TOKEN_INFO.tokenAddress, 83 | provider, 84 | ); 85 | const railgunPrevBalance = await wstETHContract.balanceOf(railgun); 86 | 87 | await executeRecipeStepsAndAssertUnshieldBalances( 88 | recipe.config.name, 89 | recipeInput, 90 | recipeOutput, 91 | true, 92 | ); 93 | 94 | const stETHContract = new LidoSTETHContract( 95 | STETH_TOKEN_INFO.tokenAddress, 96 | provider, 97 | ); 98 | 99 | const stETHBalance = await stETHContract.balanceOf(relayAdaptContract); 100 | expect(stETHBalance).equal(0n); 101 | 102 | const stETHShares = await stETHContract.sharesOf(relayAdaptContract); 103 | expect(stETHShares).equal(0n); 104 | 105 | const railgunPostBalance = await wstETHContract.balanceOf(railgun); 106 | const railgunBalance = railgunPostBalance - railgunPrevBalance; 107 | 108 | const unshieldFee = 25n; 109 | const amountMinusUnshieldFee = unshieldAmount - unshieldFee; // Unshield fee 110 | const shieldFee = 20n; 111 | 112 | const expectedBalance = await wstETHContract.getWstETHByStETH( 113 | amountMinusUnshieldFee, 114 | ); 115 | expect(expectedBalance - shieldFee).equals(railgunBalance); 116 | }).timeout(100_000); 117 | }); 118 | -------------------------------------------------------------------------------- /src/recipes/lido/__tests__/FORK-lido-stake-shortcut-recipe.test.ts: -------------------------------------------------------------------------------- 1 | import { RecipeERC20Info, RecipeInput } from '../../../models'; 2 | import { 3 | MOCK_RAILGUN_WALLET_ADDRESS, 4 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 5 | MOCK_SHIELD_FEE_BASIS_POINTS, 6 | } from '../../../test/mocks.test'; 7 | import { NETWORK_CONFIG, NetworkName } from '@railgun-community/shared-models'; 8 | import { setRailgunFees } from '../../../init'; 9 | import { 10 | executeRecipeStepsAndAssertUnshieldBalances, 11 | shouldSkipForkTest, 12 | } from '../../../test/common.test'; 13 | import { 14 | getTestProvider, 15 | getTestRailgunWallet, 16 | } from '../../../test/shared.test'; 17 | import { refreshBalances } from '@railgun-community/wallet'; 18 | import { LidoStakeShortcutRecipe } from '../lido-stake-shortcut-recipe'; 19 | import { LidoWSTETHContract } from '../../../contract/lido'; 20 | import { expect } from 'chai'; 21 | 22 | const networkName = NetworkName.Ethereum; 23 | const WSTETH_TOKEN_INFO: RecipeERC20Info = { 24 | tokenAddress: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', 25 | decimals: 18n, 26 | isBaseToken: false, 27 | }; 28 | 29 | const tokenAddress = NETWORK_CONFIG[networkName].baseToken.wrappedAddress; 30 | 31 | describe('Lido Liquid Staking', () => { 32 | before(async function () { 33 | this.timeout(1_000_000); 34 | 35 | setRailgunFees( 36 | networkName, 37 | MOCK_SHIELD_FEE_BASIS_POINTS, 38 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 39 | ); 40 | const railgunId = getTestRailgunWallet().id; 41 | await refreshBalances(NETWORK_CONFIG[networkName].chain, [railgunId]); 42 | }); 43 | 44 | it('Should directly stake ETH to get wstETH', async function () { 45 | if (shouldSkipForkTest(networkName)) { 46 | this.skip(); 47 | return; 48 | } 49 | 50 | const unshieldAmount = 10000n; 51 | const recipeInput: RecipeInput = { 52 | railgunAddress: MOCK_RAILGUN_WALLET_ADDRESS, 53 | networkName: networkName, 54 | erc20Amounts: [ 55 | { 56 | tokenAddress, 57 | decimals: 18n, 58 | amount: unshieldAmount, 59 | isBaseToken: false, 60 | }, 61 | ], 62 | nfts: [], 63 | }; 64 | 65 | const provider = getTestProvider(); 66 | const recipe = new LidoStakeShortcutRecipe(WSTETH_TOKEN_INFO, provider); 67 | 68 | const { proxyContract: railgun } = NETWORK_CONFIG[networkName]; 69 | const wstETHContract = new LidoWSTETHContract( 70 | WSTETH_TOKEN_INFO.tokenAddress, 71 | provider, 72 | ); 73 | const railgunPrevBalance = await wstETHContract.balanceOf(railgun); 74 | 75 | const recipeOutput = await recipe.getRecipeOutput(recipeInput); 76 | await executeRecipeStepsAndAssertUnshieldBalances( 77 | recipe.config.name, 78 | recipeInput, 79 | recipeOutput, 80 | true, 81 | ); 82 | 83 | const railgunPostBalance = await wstETHContract.balanceOf(railgun); 84 | const railgunBalance = railgunPostBalance - railgunPrevBalance; 85 | 86 | const unshieldFee = 25n; 87 | const amountMinusFee = unshieldAmount - unshieldFee; 88 | const expectedBalance = await wstETHContract.getWstETHByStETH( 89 | amountMinusFee, 90 | ); 91 | const shieldFee = 20n; 92 | expect(expectedBalance - shieldFee).equals(railgunBalance); 93 | }).timeout(1_000_000); 94 | }); 95 | -------------------------------------------------------------------------------- /src/recipes/lido/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lido-stake-recipe'; 2 | export * from './lido-stake-shortcut-recipe'; 3 | -------------------------------------------------------------------------------- /src/recipes/lido/lido-stake-recipe.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName } from '@railgun-community/shared-models'; 2 | import { RecipeConfig, RecipeERC20Info, StepInput } from 'models'; 3 | import { Recipe } from '../recipe'; 4 | import { 5 | ApproveERC20SpenderStep, 6 | Step, 7 | UnwrapBaseTokenStep, 8 | } from '../../steps'; 9 | import { LidoStakeStep, LidoWrapSTETHStep } from '../../steps/lido'; 10 | import { Provider } from 'ethers'; 11 | 12 | const MIN_GAS_LIMIT_LIDO_STAKING = 2_400_000n; 13 | 14 | export class LidoStakeRecipe extends Recipe { 15 | readonly config: RecipeConfig = { 16 | name: 'Lido Staking Recipe', 17 | description: 'Stake Eth to Get stETH and wrap it to wstETH', 18 | minGasLimit: MIN_GAS_LIMIT_LIDO_STAKING, 19 | }; 20 | 21 | private stETHTokenInfo: RecipeERC20Info; 22 | private wstETHTokenInfo: RecipeERC20Info; 23 | private provider: Provider; 24 | 25 | constructor( 26 | stETHTokenInfo: RecipeERC20Info, 27 | wstETHTokenInfo: RecipeERC20Info, 28 | provider: Provider, 29 | ) { 30 | super(); 31 | this.stETHTokenInfo = stETHTokenInfo; 32 | this.wstETHTokenInfo = wstETHTokenInfo; 33 | this.provider = provider; 34 | } 35 | 36 | protected supportsNetwork(networkName: NetworkName): boolean { 37 | switch (networkName) { 38 | case NetworkName.Ethereum: 39 | case NetworkName.EthereumSepolia: 40 | return true; 41 | default: 42 | return false; 43 | } 44 | } 45 | 46 | protected async getInternalSteps( 47 | firstInternalStepInput: StepInput, 48 | ): Promise { 49 | const steps: Step[] = [ 50 | new UnwrapBaseTokenStep(), // WETH => ETH 51 | new LidoStakeStep(this.stETHTokenInfo), // ETH => stETH 52 | new ApproveERC20SpenderStep( 53 | this.wstETHTokenInfo.tokenAddress, 54 | this.stETHTokenInfo, 55 | ), // approve wstETH to wrap stETH 56 | new LidoWrapSTETHStep( 57 | this.wstETHTokenInfo, 58 | this.stETHTokenInfo, 59 | this.provider, 60 | ), // wrap stETH to wstETH 61 | ]; 62 | 63 | return steps; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/recipes/lido/lido-stake-shortcut-recipe.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName } from '@railgun-community/shared-models'; 2 | import { RecipeConfig, RecipeERC20Info, StepInput } from 'models'; 3 | import { Recipe } from '../recipe'; 4 | import { Step, UnwrapBaseTokenStep } from '../../steps'; 5 | import { LidoStakeShortcutStep } from '../../steps/lido'; 6 | import { Provider } from 'ethers'; 7 | 8 | const MIN_GAS_LIMIT_LIDO_STAKING = 2_400_000n; 9 | 10 | export class LidoStakeShortcutRecipe extends Recipe { 11 | readonly config: RecipeConfig = { 12 | name: 'Lido Staking Shortcut Recipe', 13 | description: 'Stake Eth to Get wstETH', 14 | minGasLimit: MIN_GAS_LIMIT_LIDO_STAKING, 15 | }; 16 | 17 | private wstETHTokenInfo: RecipeERC20Info; 18 | private provider: Provider; 19 | constructor(wstETHTokenInfo: RecipeERC20Info, provider: Provider) { 20 | super(); 21 | this.wstETHTokenInfo = wstETHTokenInfo; 22 | this.provider = provider; 23 | } 24 | 25 | protected supportsNetwork(networkName: NetworkName): boolean { 26 | switch (networkName) { 27 | case NetworkName.Ethereum: 28 | case NetworkName.EthereumSepolia: 29 | return true; 30 | default: 31 | return false; 32 | } 33 | } 34 | 35 | protected async getInternalSteps( 36 | firstInternalStepInput: StepInput, 37 | ): Promise { 38 | const steps: Step[] = [ 39 | new UnwrapBaseTokenStep(), // WETH => ETH 40 | new LidoStakeShortcutStep(this.wstETHTokenInfo, this.provider), // ETH => wstETH 41 | ]; 42 | 43 | return steps; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/recipes/liquidity/add-liquidity-recipe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeAddLiquidityData, 3 | RecipeERC20Amount, 4 | RecipeOutput, 5 | } from '../../models/export-models'; 6 | import { compareERC20Info } from '../../utils'; 7 | import { Recipe } from '../recipe'; 8 | import { CookbookDebug } from '../../utils/cookbook-debug'; 9 | 10 | import { NetworkName } from '@railgun-community/shared-models'; 11 | 12 | export abstract class AddLiquidityRecipe extends Recipe { 13 | addLiquidityData: Optional; 14 | 15 | /** 16 | * This will return the amount of ERC20 B that is proportional to the amounts in the LP Pool, 17 | * adjusting for unshield fees on either end. 18 | */ 19 | protected abstract getAddLiquidityAmountBForUnshield( 20 | networkName: NetworkName, 21 | targetUnshieldERC20AmountA: RecipeERC20Amount, 22 | ): Promise<{ 23 | erc20UnshieldAmountB: RecipeERC20Amount; 24 | addLiquidityData: RecipeAddLiquidityData; 25 | }>; 26 | 27 | getExpectedLPAmountFromRecipeOutput( 28 | recipeOutput: Optional, 29 | ): Optional<{ 30 | aUnshieldFee: bigint; 31 | bUnshieldFee: bigint; 32 | lpAmount: bigint; 33 | lpMinimum: bigint; 34 | lpShieldFee: bigint; 35 | }> { 36 | try { 37 | if (!recipeOutput) { 38 | return undefined; 39 | } 40 | if (!this.addLiquidityData) { 41 | return undefined; 42 | } 43 | 44 | const { erc20AmountA, erc20AmountB, expectedLPAmount } = 45 | this.addLiquidityData; 46 | 47 | const unshieldStepOutput = recipeOutput.stepOutputs[0]; 48 | const unshieldFeeA = unshieldStepOutput.feeERC20AmountRecipients?.find( 49 | fee => { 50 | return compareERC20Info(fee, erc20AmountA); 51 | }, 52 | ); 53 | if (!unshieldFeeA) { 54 | throw new Error('Expected one unshield fee to match token A.'); 55 | } 56 | const unshieldFeeB = unshieldStepOutput.feeERC20AmountRecipients?.find( 57 | fee => { 58 | return compareERC20Info(fee, erc20AmountB); 59 | }, 60 | ); 61 | if (!unshieldFeeB) { 62 | throw new Error('Expected one unshield fee to match token B.'); 63 | } 64 | 65 | const shieldStepOutput = 66 | recipeOutput.stepOutputs[recipeOutput.stepOutputs.length - 1]; 67 | const shieldFee = shieldStepOutput.feeERC20AmountRecipients?.find(fee => { 68 | return compareERC20Info(fee, expectedLPAmount); 69 | }); 70 | if (!shieldFee) { 71 | throw new Error('Expected shield fee to match LP token.'); 72 | } 73 | 74 | const output = shieldStepOutput.outputERC20Amounts.find(outputAmount => { 75 | return compareERC20Info(outputAmount, expectedLPAmount); 76 | }); 77 | if (!output) { 78 | throw new Error('Expected output to match LP token.'); 79 | } 80 | 81 | return { 82 | aUnshieldFee: unshieldFeeA.amount, 83 | bUnshieldFee: unshieldFeeB.amount, 84 | lpAmount: output.expectedBalance, 85 | lpMinimum: output.minBalance, 86 | lpShieldFee: shieldFee.amount, 87 | }; 88 | } catch (cause) { 89 | if (!(cause instanceof Error)) { 90 | throw new Error('Unexpected non-error thrown', { cause }); 91 | } 92 | CookbookDebug.error(cause); 93 | return undefined; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/recipes/liquidity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './uni-v2-like'; 2 | export * from './add-liquidity-recipe'; 3 | export * from './remove-liquidity-recipe'; 4 | -------------------------------------------------------------------------------- /src/recipes/liquidity/remove-liquidity-recipe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20Amount, 3 | RecipeOutput, 4 | RecipeRemoveLiquidityData, 5 | } from '../../models/export-models'; 6 | import { compareERC20Info } from '../../utils'; 7 | import { Recipe } from '../recipe'; 8 | import { CookbookDebug } from '../../utils/cookbook-debug'; 9 | 10 | import { NetworkName } from '@railgun-community/shared-models'; 11 | 12 | export abstract class RemoveLiquidityRecipe extends Recipe { 13 | removeLiquidityData: Optional; 14 | 15 | protected abstract getRemoveLiquidityData( 16 | networkName: NetworkName, 17 | lpERC20Amount: RecipeERC20Amount, 18 | ): Promise; 19 | 20 | getExpectedABAmountsFromRecipeOutput( 21 | recipeOutput: Optional, 22 | ): Optional<{ 23 | lpUnshieldFee: bigint; 24 | aAmount: bigint; 25 | aMinimum: bigint; 26 | bAmount: bigint; 27 | bMinimum: bigint; 28 | aShieldFee: bigint; 29 | bShieldFee: bigint; 30 | }> { 31 | try { 32 | if (!recipeOutput) { 33 | return undefined; 34 | } 35 | if (!this.removeLiquidityData) { 36 | return undefined; 37 | } 38 | 39 | const { lpERC20Amount, expectedERC20AmountA, expectedERC20AmountB } = 40 | this.removeLiquidityData; 41 | 42 | const unshieldStepOutput = recipeOutput.stepOutputs[0]; 43 | const unshieldFee = unshieldStepOutput.feeERC20AmountRecipients?.find( 44 | fee => { 45 | return compareERC20Info(fee, lpERC20Amount); 46 | }, 47 | ); 48 | if (!unshieldFee) { 49 | throw new Error('Expected unshield fee to match LP token.'); 50 | } 51 | 52 | const shieldStepOutput = 53 | recipeOutput.stepOutputs[recipeOutput.stepOutputs.length - 1]; 54 | const shieldFeeA = shieldStepOutput.feeERC20AmountRecipients?.find( 55 | fee => { 56 | return compareERC20Info(fee, expectedERC20AmountA); 57 | }, 58 | ); 59 | if (!shieldFeeA) { 60 | throw new Error('Expected one shield fee to match token A.'); 61 | } 62 | const shieldFeeB = shieldStepOutput.feeERC20AmountRecipients?.find( 63 | fee => { 64 | return compareERC20Info(fee, expectedERC20AmountB); 65 | }, 66 | ); 67 | if (!shieldFeeB) { 68 | throw new Error('Expected one shield fee to match token B.'); 69 | } 70 | 71 | const outputA = shieldStepOutput.outputERC20Amounts.find(outputAmount => { 72 | return compareERC20Info(outputAmount, expectedERC20AmountA); 73 | }); 74 | if (!outputA) { 75 | throw new Error('Expected one output to match token A.'); 76 | } 77 | const outputB = shieldStepOutput.outputERC20Amounts.find(outputAmount => { 78 | return compareERC20Info(outputAmount, expectedERC20AmountB); 79 | }); 80 | if (!outputB) { 81 | throw new Error('Expected one output to match token B.'); 82 | } 83 | 84 | return { 85 | lpUnshieldFee: unshieldFee.amount, 86 | aAmount: outputA.expectedBalance, 87 | aMinimum: outputA.minBalance, 88 | bAmount: outputB.expectedBalance, 89 | bMinimum: outputB.minBalance, 90 | aShieldFee: shieldFeeA.amount, 91 | bShieldFee: shieldFeeB.amount, 92 | }; 93 | } catch (cause) { 94 | if (!(cause instanceof Error)) { 95 | throw new Error('Unexpected non-error thrown', { cause }); 96 | } 97 | CookbookDebug.error(cause); 98 | return undefined; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/recipes/liquidity/uni-v2-like/index.ts: -------------------------------------------------------------------------------- 1 | export * from './uni-v2-like-add-liquidity-recipe'; 2 | export * from './uni-v2-like-remove-liquidity-recipe'; 3 | -------------------------------------------------------------------------------- /src/recipes/liquidity/uni-v2-like/uni-v2-like-remove-liquidity-recipe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeConfig, 3 | RecipeERC20Info, 4 | RecipeRemoveLiquidityData, 5 | StepInput, 6 | UniswapV2Fork, 7 | } from '../../../models/export-models'; 8 | import { UniV2LikeSDK } from '../../../api/uni-v2-like/uni-v2-like-sdk'; 9 | import { NetworkName } from '@railgun-community/shared-models'; 10 | import { RecipeERC20Amount } from '../../../models'; 11 | import { ApproveERC20SpenderStep } from '../../../steps/token/erc20/approve-erc20-spender-step'; 12 | import { UniV2LikeRemoveLiquidityStep } from '../../../steps/liquidity/uni-v2-like/uni-v2-like-remove-liquidity-step'; 13 | 14 | import { Step } from '../../../steps/step'; 15 | import { RemoveLiquidityRecipe } from '../remove-liquidity-recipe'; 16 | import { findFirstInputERC20Amount } from '../../../utils/filters'; 17 | import { Provider } from 'ethers'; 18 | import { MIN_GAS_LIMIT_LP_V2_REMOVE_LIQUIDITY } from '../../../models/min-gas-limits'; 19 | 20 | export class UniV2LikeRemoveLiquidityRecipe extends RemoveLiquidityRecipe { 21 | readonly config: RecipeConfig = { 22 | name: '[Name] Remove Liquidity', 23 | description: 'Removes liquidity from a [NAME] Pool.', 24 | minGasLimit: MIN_GAS_LIMIT_LP_V2_REMOVE_LIQUIDITY, 25 | }; 26 | 27 | private readonly uniswapV2Fork: UniswapV2Fork; 28 | 29 | private readonly lpERC20Info: RecipeERC20Info; 30 | private readonly erc20InfoA: RecipeERC20Info; 31 | private readonly erc20InfoB: RecipeERC20Info; 32 | 33 | private readonly slippageBasisPoints: bigint; 34 | private readonly provider: Provider; 35 | 36 | constructor( 37 | uniswapV2Fork: UniswapV2Fork, 38 | lpERC20Info: RecipeERC20Info, 39 | erc20InfoA: RecipeERC20Info, 40 | erc20InfoB: RecipeERC20Info, 41 | slippageBasisPoints: bigint, 42 | provider: Provider, 43 | ) { 44 | super(); 45 | this.uniswapV2Fork = uniswapV2Fork; 46 | 47 | this.lpERC20Info = lpERC20Info; 48 | this.erc20InfoA = erc20InfoA; 49 | this.erc20InfoB = erc20InfoB; 50 | 51 | this.slippageBasisPoints = slippageBasisPoints; 52 | this.provider = provider; 53 | 54 | const forkName = UniV2LikeSDK.getForkName(uniswapV2Fork); 55 | this.config.name = `${forkName} Remove Liquidity`; 56 | this.config.description = `Removes liquidity from a ${forkName} Pool.`; 57 | } 58 | 59 | protected supportsNetwork(networkName: NetworkName): boolean { 60 | return UniV2LikeSDK.supportsForkAndNetwork(this.uniswapV2Fork, networkName); 61 | } 62 | 63 | async getRemoveLiquidityData( 64 | networkName: NetworkName, 65 | lpERC20Amount: RecipeERC20Amount, 66 | ): Promise { 67 | this.removeLiquidityData = await UniV2LikeSDK.getRemoveLiquidityData( 68 | this.uniswapV2Fork, 69 | networkName, 70 | lpERC20Amount, 71 | this.erc20InfoA, 72 | this.erc20InfoB, 73 | this.slippageBasisPoints, 74 | this.provider, 75 | ); 76 | return this.removeLiquidityData; 77 | } 78 | 79 | protected async getInternalSteps( 80 | firstInternalStepInput: StepInput, 81 | ): Promise { 82 | const { networkName, erc20Amounts } = firstInternalStepInput; 83 | 84 | const lpERC20Amount = findFirstInputERC20Amount( 85 | erc20Amounts, 86 | this.lpERC20Info, 87 | ); 88 | const removeLiquidityData = await this.getRemoveLiquidityData( 89 | networkName, 90 | lpERC20Amount, 91 | ); 92 | 93 | return [ 94 | new ApproveERC20SpenderStep( 95 | removeLiquidityData.routerContractAddress, 96 | this.lpERC20Info, 97 | ), 98 | new UniV2LikeRemoveLiquidityStep(this.uniswapV2Fork, removeLiquidityData), 99 | ]; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/recipes/swap/index.ts: -------------------------------------------------------------------------------- 1 | export * from './swap-recipe'; 2 | 3 | // Exported individual recipes 4 | export * from './zero-x-swap-recipe'; 5 | export * from './zero-x-v2-swap-recipe'; 6 | -------------------------------------------------------------------------------- /src/recipes/swap/swap-recipe.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName, isDefined } from '@railgun-community/shared-models'; 2 | import { 3 | RecipeERC20Amount, 4 | RecipeERC20AmountRecipient, 5 | RecipeERC20Info, 6 | RecipeOutput, 7 | StepOutputERC20Amount, 8 | SwapQuoteData, 9 | } from '../../models/export-models'; 10 | import { compareERC20Info, isPrefixedRailgunAddress } from '../../utils'; 11 | import { Recipe } from '../recipe'; 12 | import { CookbookDebug } from '../../utils/cookbook-debug'; 13 | 14 | export abstract class SwapRecipe extends Recipe { 15 | protected quote: Optional; 16 | 17 | protected abstract readonly sellERC20Info: RecipeERC20Info; 18 | protected abstract readonly buyERC20Info: RecipeERC20Info; 19 | 20 | protected readonly destinationAddress: Optional; 21 | 22 | constructor() { 23 | super(); 24 | } 25 | 26 | getLatestQuote(): Optional { 27 | return this.quote; 28 | } 29 | 30 | protected abstract getSwapQuote( 31 | networkName: NetworkName, 32 | sellERC20Amount: RecipeERC20Amount, 33 | ): Promise; 34 | 35 | getBuySellAmountsFromRecipeOutput( 36 | recipeOutput: Optional, 37 | ): Optional<{ 38 | sellUnshieldFee: bigint; 39 | buyAmount: bigint; 40 | buyMinimum: bigint; 41 | buyShieldFee: bigint; 42 | }> { 43 | try { 44 | if (!recipeOutput) { 45 | return undefined; 46 | } 47 | 48 | const firstOutputIndex = 0; 49 | const unshieldStepOutput = recipeOutput.stepOutputs[firstOutputIndex]; 50 | const unshieldFee = unshieldStepOutput.feeERC20AmountRecipients?.find( 51 | fee => { 52 | return compareERC20Info(fee, this.sellERC20Info); 53 | }, 54 | ); 55 | if (!unshieldFee) { 56 | throw new Error('Expected unshield fee to match sell token.'); 57 | } 58 | 59 | const swapStepOutput = recipeOutput.stepOutputs[2]; 60 | if ( 61 | swapStepOutput.name !== '0x Exchange Swap' && 62 | swapStepOutput.name !== '0x V2 Exchange Swap' 63 | ) { 64 | throw new Error('Expected step output 3 to be 0x Exchange Swap.'); 65 | } 66 | 67 | let buyOutput: Optional; 68 | let buyShieldFee: Optional; 69 | 70 | if ( 71 | isDefined(this.destinationAddress) && 72 | !isPrefixedRailgunAddress(this.destinationAddress) 73 | ) { 74 | // If there's a public destination address: 75 | // Buy output is from swap value, which is transferred out before it's shielded. 76 | buyOutput = swapStepOutput.outputERC20Amounts.find(outputAmount => { 77 | return compareERC20Info(outputAmount, this.buyERC20Info); 78 | }); 79 | if (!buyOutput) { 80 | throw new Error('Expected swap output to match buy token.'); 81 | } 82 | } else { 83 | // If there's no destination address, or a private destination: 84 | // Buy output is from final shield value. 85 | const lastOutputIndex = recipeOutput.stepOutputs.length - 1; 86 | const shieldStepOutput = recipeOutput.stepOutputs[lastOutputIndex]; 87 | buyOutput = shieldStepOutput.outputERC20Amounts.find(outputAmount => { 88 | return compareERC20Info(outputAmount, this.buyERC20Info); 89 | }); 90 | if (!buyOutput) { 91 | throw new Error('Expected swap output to match buy token.'); 92 | } 93 | buyShieldFee = shieldStepOutput.feeERC20AmountRecipients?.find(fee => { 94 | return compareERC20Info(fee, this.buyERC20Info); 95 | }); 96 | if (!buyShieldFee) { 97 | throw new Error('Expected shield fee to match buy token.'); 98 | } 99 | } 100 | 101 | return { 102 | sellUnshieldFee: unshieldFee.amount, 103 | buyAmount: buyOutput.expectedBalance, 104 | buyMinimum: buyOutput.minBalance, 105 | buyShieldFee: buyShieldFee?.amount ?? 0n, 106 | }; 107 | } catch (cause) { 108 | if (!(cause instanceof Error)) { 109 | throw new Error('Unexpected non-error thrown', { cause }); 110 | } 111 | CookbookDebug.error(cause); 112 | return undefined; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/recipes/swap/zero-x-v2-swap-recipe.ts: -------------------------------------------------------------------------------- 1 | import { ApproveERC20SpenderStep } from '../../steps/token/erc20/approve-erc20-spender-step'; 2 | import { Step } from '../../steps/step'; 3 | import { 4 | RecipeConfig, 5 | RecipeERC20Amount, 6 | RecipeERC20Info, 7 | StepInput, 8 | type SwapQuoteDataV2, 9 | type SwapQuoteParamsV2, 10 | } from '../../models/export-models'; 11 | import { SwapRecipe } from './swap-recipe'; 12 | import { NetworkName, isDefined } from '@railgun-community/shared-models'; 13 | import { 14 | findFirstInputERC20Amount, 15 | isPrefixedRailgunAddress, 16 | } from '../../utils'; 17 | import { 18 | MIN_GAS_LIMIT_0X_SWAP, 19 | MIN_GAS_LIMIT_0X_SWAP_SHIELD, 20 | MIN_GAS_LIMIT_0X_SWAP_TRANSFER, 21 | } from '../../models/min-gas-limits'; 22 | import { 23 | TransferBaseTokenStep, 24 | TransferERC20Step, 25 | ZeroXV2SwapStep, 26 | } from '../../steps'; 27 | import { DesignateShieldERC20RecipientStep } from '../../steps/railgun/designate-shield-erc20-recipient-step'; 28 | import { 29 | ZeroXV2Quote } from '../../api/zero-x-v2'; 30 | 31 | export class ZeroXV2SwapRecipe extends SwapRecipe { 32 | readonly config: RecipeConfig = { 33 | name: '0x V2 Exchange Swap', 34 | description: 'Swaps two ERC20 tokens using 0x V2 Exchange DEX Aggregator.', 35 | minGasLimit: MIN_GAS_LIMIT_0X_SWAP, 36 | }; 37 | 38 | protected readonly sellERC20Info: RecipeERC20Info; 39 | protected readonly buyERC20Info: RecipeERC20Info; 40 | 41 | private readonly slippageBasisPoints: number; 42 | 43 | protected readonly destinationAddress: Optional; 44 | protected readonly isRailgunDestinationAddress: Optional; 45 | 46 | constructor( 47 | sellERC20Info: RecipeERC20Info, 48 | buyERC20Info: RecipeERC20Info, 49 | slippageBasisPoints: number, 50 | destinationAddress?: string, 51 | ) { 52 | super(); 53 | this.sellERC20Info = sellERC20Info; 54 | this.buyERC20Info = buyERC20Info; 55 | 56 | this.slippageBasisPoints = slippageBasisPoints; 57 | 58 | this.destinationAddress = destinationAddress; 59 | if (isDefined(destinationAddress)) { 60 | this.isRailgunDestinationAddress = 61 | isPrefixedRailgunAddress(destinationAddress); 62 | if (this.isRailgunDestinationAddress) { 63 | this.config.name += ' and Shield'; 64 | this.config.minGasLimit = MIN_GAS_LIMIT_0X_SWAP_SHIELD; 65 | } else { 66 | this.config.name += ' and Transfer'; 67 | this.config.minGasLimit = MIN_GAS_LIMIT_0X_SWAP_TRANSFER; 68 | } 69 | } 70 | } 71 | 72 | protected supportsNetwork(networkName: NetworkName): boolean { 73 | return ZeroXV2Quote.supportsNetwork(networkName); 74 | } 75 | 76 | async getSwapQuote( 77 | networkName: NetworkName, 78 | sellERC20Amount: RecipeERC20Amount, 79 | ): Promise { 80 | const quoteParams: SwapQuoteParamsV2 = { 81 | networkName, 82 | sellERC20Amount, 83 | buyERC20Info: this.buyERC20Info, 84 | slippageBasisPoints: this.slippageBasisPoints, 85 | isRailgun: true, 86 | }; 87 | return ZeroXV2Quote.getSwapQuote(quoteParams); 88 | } 89 | 90 | protected async getInternalSteps( 91 | firstInternalStepInput: StepInput, 92 | ): Promise { 93 | const { networkName } = firstInternalStepInput; 94 | const sellERC20Amount = findFirstInputERC20Amount( 95 | firstInternalStepInput.erc20Amounts, 96 | this.sellERC20Info, 97 | ); 98 | this.quote = await this.getSwapQuote(networkName, sellERC20Amount); 99 | 100 | const steps: Step[] = [ 101 | new ApproveERC20SpenderStep( 102 | this.quote.spender, 103 | sellERC20Amount, 104 | ), 105 | new ZeroXV2SwapStep(this.quote, this.sellERC20Info), 106 | ]; 107 | if (isDefined(this.destinationAddress)) { 108 | steps.push( 109 | this.isRailgunDestinationAddress === true 110 | ? new DesignateShieldERC20RecipientStep(this.destinationAddress, [ 111 | this.buyERC20Info, 112 | ]) 113 | : this.buyERC20Info.isBaseToken ?? false 114 | ? new TransferBaseTokenStep(this.destinationAddress) 115 | : new TransferERC20Step(this.destinationAddress, this.buyERC20Info), 116 | ); 117 | } 118 | return steps; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/recipes/vault/beefy/beefy-deposit-recipe.ts: -------------------------------------------------------------------------------- 1 | import { Recipe } from '../../recipe'; 2 | import { ApproveERC20SpenderStep, Step } from '../../../steps'; 3 | import { BeefyAPI } from '../../../api/beefy'; 4 | import { NetworkName } from '@railgun-community/shared-models'; 5 | import { 6 | RecipeConfig, 7 | RecipeERC20Info, 8 | StepInput, 9 | } from '../../../models/export-models'; 10 | import { BeefyDepositStep } from '../../../steps/vault/beefy/beefy-deposit-step'; 11 | import { MIN_GAS_LIMIT_BEEFY_VAULT_DEPOSIT } from '../../../models/min-gas-limits'; 12 | 13 | export class BeefyDepositRecipe extends Recipe { 14 | readonly config: RecipeConfig = { 15 | name: 'Beefy Vault Deposit', 16 | description: 17 | 'Auto-approves and deposits tokens into a yield-bearing Beefy Vault.', 18 | minGasLimit: MIN_GAS_LIMIT_BEEFY_VAULT_DEPOSIT, 19 | }; 20 | 21 | protected readonly vaultID: string; 22 | 23 | constructor(vaultID: string) { 24 | super(); 25 | this.vaultID = vaultID; 26 | } 27 | 28 | protected supportsNetwork(networkName: NetworkName): boolean { 29 | return BeefyAPI.supportsNetwork(networkName); 30 | } 31 | 32 | protected async getInternalSteps( 33 | firstInternalStepInput: StepInput, 34 | ): Promise { 35 | const { networkName } = firstInternalStepInput; 36 | const vault = await BeefyAPI.getBeefyVaultForID(this.vaultID, networkName); 37 | const spender = vault.vaultContractAddress; 38 | const depositERC20Info: RecipeERC20Info = { 39 | tokenAddress: vault.depositERC20Address, 40 | decimals: vault.depositERC20Decimals, 41 | }; 42 | return [ 43 | new ApproveERC20SpenderStep(spender, depositERC20Info), 44 | new BeefyDepositStep(vault), 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/recipes/vault/beefy/beefy-withdraw-recipe.ts: -------------------------------------------------------------------------------- 1 | import { Recipe } from '../../recipe'; 2 | import { Step } from '../../../steps'; 3 | import { BeefyAPI } from '../../../api/beefy'; 4 | import { NetworkName } from '@railgun-community/shared-models'; 5 | import { StepInput } from '../../../models/export-models'; 6 | import { BeefyWithdrawStep } from '../../../steps/vault/beefy/beefy-withdraw-step'; 7 | import { RecipeConfig } from '../../../models/export-models'; 8 | import { MIN_GAS_LIMIT_BEEFY_VAULT_WITHDRAW } from '../../../models/min-gas-limits'; 9 | 10 | export class BeefyWithdrawRecipe extends Recipe { 11 | readonly config: RecipeConfig = { 12 | name: 'Beefy Vault Withdraw', 13 | description: 'Withdraws ERC20 tokens from yield-bearing Beefy Vault.', 14 | minGasLimit: MIN_GAS_LIMIT_BEEFY_VAULT_WITHDRAW, 15 | }; 16 | 17 | protected readonly vaultID: string; 18 | 19 | constructor(vaultID: string) { 20 | super(); 21 | this.vaultID = vaultID; 22 | } 23 | 24 | protected supportsNetwork(networkName: NetworkName): boolean { 25 | return BeefyAPI.supportsNetwork(networkName); 26 | } 27 | 28 | protected async getInternalSteps( 29 | firstInternalStepInput: StepInput, 30 | ): Promise { 31 | const { networkName } = firstInternalStepInput; 32 | const vault = await BeefyAPI.getBeefyVaultForID(this.vaultID, networkName); 33 | return [new BeefyWithdrawStep(vault)]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/recipes/vault/index.ts: -------------------------------------------------------------------------------- 1 | // Exported individual recipes 2 | export * from './beefy/beefy-deposit-recipe'; 3 | export * from './beefy/beefy-withdraw-recipe'; 4 | -------------------------------------------------------------------------------- /src/steps/adapt/empty-transfer-base-token-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StepConfig, 3 | StepInput, 4 | UnvalidatedStepOutput, 5 | } from '../../models/export-models'; 6 | import { Step } from '../step'; 7 | import { ZERO_ADDRESS } from '../../models/constants'; 8 | import { ContractTransaction } from 'ethers'; 9 | 10 | export class EmptyTransferBaseTokenStep extends Step { 11 | readonly config: StepConfig = { 12 | name: 'Empty Transfer Base Token', 13 | description: 14 | 'Used for testing. Sends a 0-token transfer to a null address.', 15 | }; 16 | 17 | private readonly toAddress = ZERO_ADDRESS; 18 | private readonly amount = 0n; 19 | 20 | constructor() { 21 | super(); 22 | } 23 | 24 | protected async getStepOutput( 25 | input: StepInput, 26 | ): Promise { 27 | const { erc20Amounts } = input; 28 | const unusedERC20Amounts = erc20Amounts; 29 | 30 | const crossContractCalls: ContractTransaction[] = [ 31 | { 32 | data: '0x', 33 | to: this.toAddress, 34 | value: this.amount, 35 | }, 36 | ]; 37 | 38 | return { 39 | crossContractCalls, 40 | outputERC20Amounts: unusedERC20Amounts, 41 | outputNFTs: input.nfts, 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/steps/adapt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './empty-transfer-base-token-step'; 2 | export * from './transfer-base-token-step'; 3 | export * from './unwrap-base-token-step'; 4 | export * from './wrap-base-token-step'; 5 | -------------------------------------------------------------------------------- /src/steps/adapt/transfer-base-token-step.ts: -------------------------------------------------------------------------------- 1 | import { RelayAdaptContract } from '../../contract/adapt/relay-adapt-contract'; 2 | import { 3 | RecipeERC20AmountRecipient, 4 | StepConfig, 5 | StepInput, 6 | UnvalidatedStepOutput, 7 | } from '../../models/export-models'; 8 | import { compareERC20Info } from '../../utils/token'; 9 | import { Step } from '../step'; 10 | import { getBaseToken } from '../../utils/wrap-util'; 11 | import { ContractTransaction } from 'ethers'; 12 | 13 | export class TransferBaseTokenStep extends Step { 14 | readonly config: StepConfig = { 15 | name: 'Transfer Base Token', 16 | description: 'Transfers base token to an external public address.', 17 | }; 18 | 19 | private readonly toAddress: string; 20 | private readonly amount: Optional; 21 | 22 | constructor(toAddress: string, amount?: bigint) { 23 | super(); 24 | this.toAddress = toAddress; 25 | this.amount = amount; 26 | } 27 | 28 | protected async getStepOutput( 29 | input: StepInput, 30 | ): Promise { 31 | const { networkName, erc20Amounts } = input; 32 | 33 | const baseToken = getBaseToken(networkName); 34 | const { erc20AmountForStep, unusedERC20Amounts } = 35 | this.getValidInputERC20Amount( 36 | erc20Amounts, 37 | erc20Amount => compareERC20Info(erc20Amount, baseToken), 38 | this.amount, 39 | ); 40 | 41 | const contract = new RelayAdaptContract(input.networkName); 42 | const crossContractCalls: ContractTransaction[] = [ 43 | await contract.createBaseTokenTransfer(this.toAddress, this.amount), 44 | ]; 45 | 46 | const transferredBaseToken: RecipeERC20AmountRecipient = { 47 | ...baseToken, 48 | amount: this.amount ?? erc20AmountForStep.expectedBalance, 49 | recipient: this.toAddress, 50 | }; 51 | 52 | return { 53 | crossContractCalls, 54 | spentERC20Amounts: [transferredBaseToken], 55 | outputERC20Amounts: unusedERC20Amounts, 56 | outputNFTs: input.nfts, 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/steps/adapt/unwrap-base-token-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20AmountRecipient, 3 | StepConfig, 4 | StepInput, 5 | StepOutputERC20Amount, 6 | UnvalidatedStepOutput, 7 | } from '../../models/export-models'; 8 | import { Step } from '../step'; 9 | import { getWrappedBaseToken } from '../../utils/wrap-util'; 10 | import { RelayAdaptContract } from '../../contract/adapt/relay-adapt-contract'; 11 | import { compareERC20Info } from '../../utils/token'; 12 | import { ContractTransaction } from 'ethers'; 13 | 14 | export class UnwrapBaseTokenStep extends Step { 15 | readonly config: StepConfig = { 16 | name: 'Unwrap Base Token', 17 | description: 'Unwraps wrapped token into base token, ie WETH to ETH.', 18 | }; 19 | 20 | private readonly amount: Optional; 21 | 22 | constructor(amount?: bigint) { 23 | super(); 24 | this.amount = amount; 25 | } 26 | 27 | protected async getStepOutput( 28 | input: StepInput, 29 | ): Promise { 30 | const { networkName, erc20Amounts } = input; 31 | 32 | const wrappedBaseToken = getWrappedBaseToken(networkName); 33 | const { erc20AmountForStep, unusedERC20Amounts } = 34 | this.getValidInputERC20Amount( 35 | erc20Amounts, 36 | erc20Amount => compareERC20Info(erc20Amount, wrappedBaseToken), 37 | this.amount, 38 | ); 39 | 40 | const contract = new RelayAdaptContract(input.networkName); 41 | const crossContractCalls: ContractTransaction[] = [ 42 | await contract.createBaseTokenUnwrap(this.amount), 43 | ]; 44 | 45 | const unwrappedBaseERC20Amount: StepOutputERC20Amount = { 46 | ...erc20AmountForStep, 47 | isBaseToken: true, 48 | expectedBalance: this.amount ?? erc20AmountForStep.expectedBalance, 49 | minBalance: this.amount ?? erc20AmountForStep.minBalance, 50 | }; 51 | const spentWrappedERC20Amount: RecipeERC20AmountRecipient = { 52 | ...wrappedBaseToken, 53 | amount: this.amount ?? erc20AmountForStep.expectedBalance, 54 | recipient: 'Wrapped Token Contract', 55 | }; 56 | 57 | return { 58 | crossContractCalls, 59 | spentERC20Amounts: [spentWrappedERC20Amount], 60 | outputERC20Amounts: [unwrappedBaseERC20Amount, ...unusedERC20Amounts], 61 | outputNFTs: input.nfts, 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/steps/adapt/wrap-base-token-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20AmountRecipient, 3 | StepConfig, 4 | StepInput, 5 | StepOutputERC20Amount, 6 | UnvalidatedStepOutput, 7 | } from '../../models/export-models'; 8 | import { Step } from '../step'; 9 | import { RelayAdaptContract } from '../../contract/adapt/relay-adapt-contract'; 10 | import { getBaseToken } from '../../utils/wrap-util'; 11 | import { compareERC20Info } from '../../utils/token'; 12 | import { ContractTransaction } from 'ethers'; 13 | 14 | export class WrapBaseTokenStep extends Step { 15 | readonly config: StepConfig = { 16 | name: 'Wrap Base Token', 17 | description: 'Wraps base token into wrapped token, ie ETH to WETH.', 18 | }; 19 | 20 | private readonly amount: Optional; 21 | 22 | constructor(amount?: bigint) { 23 | super(); 24 | this.amount = amount; 25 | } 26 | 27 | protected async getStepOutput( 28 | input: StepInput, 29 | ): Promise { 30 | const { networkName, erc20Amounts } = input; 31 | 32 | const baseToken = getBaseToken(networkName); 33 | const { erc20AmountForStep, unusedERC20Amounts } = 34 | this.getValidInputERC20Amount( 35 | erc20Amounts, 36 | erc20Amount => compareERC20Info(erc20Amount, baseToken), 37 | this.amount, 38 | ); 39 | 40 | const contract = new RelayAdaptContract(input.networkName); 41 | const crossContractCalls: ContractTransaction[] = [ 42 | await contract.createBaseTokenWrap(this.amount), 43 | ]; 44 | 45 | const wrappedBaseERC20Amount: StepOutputERC20Amount = { 46 | ...erc20AmountForStep, 47 | isBaseToken: false, 48 | expectedBalance: this.amount ?? erc20AmountForStep.expectedBalance, 49 | minBalance: this.amount ?? erc20AmountForStep.minBalance, 50 | }; 51 | const spentBaseERC20Amount: RecipeERC20AmountRecipient = { 52 | ...baseToken, 53 | amount: this.amount ?? erc20AmountForStep.expectedBalance, 54 | recipient: 'Wrapped Token Contract', 55 | }; 56 | 57 | return { 58 | crossContractCalls, 59 | spentERC20Amounts: [spentBaseERC20Amount], 60 | outputERC20Amounts: [wrappedBaseERC20Amount, ...unusedERC20Amounts], 61 | outputNFTs: input.nfts, 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/steps/index.ts: -------------------------------------------------------------------------------- 1 | // Base Step 2 | export * from './step'; 3 | 4 | // Step exports 5 | export * from './adapt'; 6 | export * from './token'; 7 | export * from './railgun'; 8 | export * from './swap'; 9 | -------------------------------------------------------------------------------- /src/steps/lido/__tests__/lido-stake-step.test.ts: -------------------------------------------------------------------------------- 1 | import { NetworkName } from '@railgun-community/shared-models'; 2 | import { LidoStakeStep } from '../lido-stake-step'; 3 | import { RecipeERC20Info, StepInput } from '../../../models'; 4 | import { expect } from 'chai'; 5 | import { NETWORK_CONFIG } from '@railgun-community/shared-models'; 6 | 7 | const STETH_TOKEN: RecipeERC20Info = { 8 | tokenAddress: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', 9 | decimals: 18n, 10 | }; 11 | const networkName = NetworkName.Ethereum; 12 | const tokenAddress = 13 | NETWORK_CONFIG[NetworkName.Ethereum].baseToken.wrappedAddress; 14 | 15 | const amount = 10000n; 16 | 17 | const stepInput: StepInput = { 18 | networkName, 19 | erc20Amounts: [ 20 | { 21 | tokenAddress, 22 | decimals: 18n, 23 | isBaseToken: true, 24 | expectedBalance: 10000n, 25 | minBalance: 10000n, 26 | approvedSpender: undefined, 27 | }, 28 | ], 29 | nfts: [], 30 | }; 31 | 32 | describe('lido staking step', () => { 33 | it('Should stake ETH and get stETH', async () => { 34 | const step = new LidoStakeStep(STETH_TOKEN); 35 | const output = await step.getValidStepOutput(stepInput); 36 | 37 | expect(output.spentERC20Amounts).to.deep.equals([ 38 | { 39 | amount, 40 | isBaseToken: true, 41 | recipient: 'Lido', 42 | tokenAddress, 43 | decimals: 18n, 44 | }, 45 | ]); 46 | 47 | expect(output.outputERC20Amounts[0]).to.deep.equal({ 48 | approvedSpender: undefined, 49 | expectedBalance: amount, 50 | minBalance: amount, 51 | tokenAddress: STETH_TOKEN.tokenAddress, 52 | decimals: 18n, 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/steps/lido/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lido-stake-step'; 2 | export * from './lido-wrap-steth-step'; 3 | export * from './lido-stake-shortcut-step'; 4 | -------------------------------------------------------------------------------- /src/steps/lido/lido-stake-shortcut-step.ts: -------------------------------------------------------------------------------- 1 | import { RelayAdaptContract } from '../../contract/adapt/relay-adapt-contract'; 2 | import { 3 | RecipeERC20AmountRecipient, 4 | RecipeERC20Info, 5 | StepConfig, 6 | StepInput, 7 | StepOutputERC20Amount, 8 | UnvalidatedStepOutput, 9 | } from '../../models/export-models'; 10 | import { compareERC20Info } from '../../utils/token'; 11 | import { Step } from '../step'; 12 | import { getBaseToken } from '../../utils/wrap-util'; 13 | import { ContractTransaction } from 'ethers'; 14 | import { LidoWSTETHContract } from '../../contract/lido'; 15 | import { Provider } from 'ethers'; 16 | 17 | export class LidoStakeShortcutStep extends Step { 18 | readonly config: StepConfig = { 19 | name: 'Lido Staking [wstETH]', 20 | description: 'Stake ETH to get wstETH', 21 | }; 22 | 23 | private provider: Provider; 24 | 25 | constructor(private wstETHTokenInfo: RecipeERC20Info, provider: Provider) { 26 | super(); 27 | this.provider = provider; 28 | } 29 | 30 | private getWrappedAmount(stakeAmount: bigint): Promise { 31 | const wstETHContract = new LidoWSTETHContract( 32 | this.wstETHTokenInfo.tokenAddress, 33 | this.provider, 34 | ); 35 | return wstETHContract.getWstETHByStETH(stakeAmount); 36 | } 37 | 38 | protected async getStepOutput( 39 | input: StepInput, 40 | ): Promise { 41 | const { networkName, erc20Amounts } = input; 42 | 43 | const baseToken = getBaseToken(networkName); 44 | const { erc20AmountForStep, unusedERC20Amounts } = 45 | this.getValidInputERC20Amount( 46 | erc20Amounts, 47 | erc20Amount => compareERC20Info(erc20Amount, baseToken), 48 | undefined, 49 | ); 50 | 51 | const amount = erc20AmountForStep.expectedBalance; 52 | const contract = new RelayAdaptContract(input.networkName); 53 | const crossContractCalls: ContractTransaction[] = [ 54 | await contract.multicall(false, [ 55 | { 56 | to: this.wstETHTokenInfo.tokenAddress, 57 | data: '0x', 58 | value: amount, 59 | }, 60 | ]), 61 | ]; 62 | 63 | const transferredBaseToken: RecipeERC20AmountRecipient = { 64 | ...baseToken, 65 | amount, 66 | recipient: this.wstETHTokenInfo.tokenAddress, 67 | }; 68 | 69 | const wrappedAmount = await this.getWrappedAmount(amount); 70 | const outputWSTETHToken: StepOutputERC20Amount = { 71 | ...this.wstETHTokenInfo, 72 | expectedBalance: wrappedAmount, 73 | minBalance: wrappedAmount, 74 | approvedSpender: undefined, 75 | }; 76 | 77 | return { 78 | crossContractCalls, 79 | spentERC20Amounts: [transferredBaseToken], 80 | outputERC20Amounts: [outputWSTETHToken, ...unusedERC20Amounts], 81 | outputNFTs: input.nfts, 82 | }; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/steps/lido/lido-stake-step.ts: -------------------------------------------------------------------------------- 1 | import { ZeroAddress } from 'ethers'; 2 | import { 3 | RecipeERC20AmountRecipient, 4 | RecipeERC20Info, 5 | StepConfig, 6 | StepInput, 7 | StepOutputERC20Amount, 8 | UnvalidatedStepOutput, 9 | } from 'models'; 10 | import { Step } from '../../steps/step'; 11 | import { compareERC20Info, getBaseToken } from '../../utils'; 12 | import { LidoSTETHContract } from '../../contract/lido'; 13 | 14 | export class LidoStakeStep extends Step { 15 | readonly config: StepConfig = { 16 | name: 'Lido Staking [stETH]', 17 | description: 'Stake ETH to get stETH', 18 | hasNonDeterministicOutput: false, 19 | }; 20 | 21 | readonly stETHTokenInfo: RecipeERC20Info; 22 | 23 | constructor(stETHTokenInfo: RecipeERC20Info) { 24 | super(); 25 | this.stETHTokenInfo = stETHTokenInfo; 26 | } 27 | 28 | protected async getStepOutput( 29 | input: StepInput, 30 | ): Promise { 31 | const { erc20Amounts, networkName } = input; 32 | const baseToken = getBaseToken(networkName); 33 | const { erc20AmountForStep, unusedERC20Amounts } = 34 | this.getValidInputERC20Amount( 35 | erc20Amounts, 36 | erc20Amount => compareERC20Info(erc20Amount, baseToken), 37 | undefined, 38 | ); 39 | 40 | const amount = erc20AmountForStep.minBalance; 41 | const stakedBaseToken: RecipeERC20AmountRecipient = { 42 | ...baseToken, 43 | amount, 44 | recipient: 'Lido', 45 | }; 46 | 47 | const stETHTokenAmount: StepOutputERC20Amount = { 48 | ...this.stETHTokenInfo, 49 | expectedBalance: amount, 50 | minBalance: amount, 51 | approvedSpender: undefined, 52 | }; 53 | 54 | const stEthContract = new LidoSTETHContract(stETHTokenAmount.tokenAddress); 55 | const crossContractCall = await stEthContract.submit(amount, ZeroAddress); 56 | 57 | return { 58 | crossContractCalls: [crossContractCall], 59 | spentERC20Amounts: [stakedBaseToken], 60 | outputERC20Amounts: [stETHTokenAmount, ...unusedERC20Amounts], 61 | outputNFTs: input.nfts, 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/steps/lido/lido-wrap-steth-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20AmountRecipient, 3 | StepConfig, 4 | StepInput, 5 | StepOutputERC20Amount, 6 | UnvalidatedStepOutput, 7 | RecipeERC20Info, 8 | } from '../../models'; 9 | import { Step } from '../../steps/step'; 10 | import { compareERC20Info } from '../../utils'; 11 | import { LidoWSTETHContract } from '../../contract/lido'; 12 | import { Provider } from 'ethers'; 13 | 14 | export class LidoWrapSTETHStep extends Step { 15 | readonly config: StepConfig = { 16 | name: 'Lido Wrap stETH', 17 | description: 'Wrap stETH to wstETH', 18 | hasNonDeterministicOutput: false, 19 | }; 20 | 21 | readonly wstETHTokenInfo: RecipeERC20Info; 22 | readonly stETHTokenInfo: RecipeERC20Info; 23 | private provider: Provider; 24 | 25 | constructor( 26 | wstETHTokenInfo: RecipeERC20Info, 27 | stETHTokenInfo: RecipeERC20Info, 28 | provider: Provider, 29 | ) { 30 | super(); 31 | this.wstETHTokenInfo = wstETHTokenInfo; 32 | this.stETHTokenInfo = stETHTokenInfo; 33 | this.provider = provider; 34 | } 35 | 36 | private getWrapAmount(stakeAmount: bigint): Promise { 37 | const wstETHContract = new LidoWSTETHContract( 38 | this.wstETHTokenInfo.tokenAddress, 39 | this.provider, 40 | ); 41 | return wstETHContract.getWstETHByStETH(stakeAmount); 42 | } 43 | 44 | protected async getStepOutput( 45 | input: StepInput, 46 | ): Promise { 47 | const { erc20Amounts } = input; 48 | const { erc20AmountForStep, unusedERC20Amounts } = 49 | this.getValidInputERC20Amount( 50 | erc20Amounts, 51 | erc20Amount => compareERC20Info(erc20Amount, this.stETHTokenInfo), 52 | undefined, 53 | ); 54 | 55 | const wrapAmount = erc20AmountForStep.minBalance; 56 | 57 | const spentTokenAmount: RecipeERC20AmountRecipient = { 58 | ...this.stETHTokenInfo, 59 | amount: wrapAmount, 60 | recipient: this.wstETHTokenInfo.tokenAddress, 61 | }; 62 | 63 | const wrappedAmount = await this.getWrapAmount(wrapAmount); 64 | const wrappedTokenAmount: StepOutputERC20Amount = { 65 | ...this.wstETHTokenInfo, 66 | expectedBalance: wrappedAmount, 67 | minBalance: wrappedAmount, 68 | approvedSpender: undefined, 69 | }; 70 | 71 | const wstethContract = new LidoWSTETHContract( 72 | this.wstETHTokenInfo.tokenAddress, 73 | ); 74 | const crossContractCall = await wstethContract.wrap(wrapAmount); 75 | 76 | return { 77 | crossContractCalls: [crossContractCall], 78 | spentERC20Amounts: [spentTokenAmount], 79 | outputERC20Amounts: [wrappedTokenAmount, ...unusedERC20Amounts], 80 | outputNFTs: input.nfts, 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/steps/railgun/__tests__/shield-default-step.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { ShieldDefaultStep } from '../shield-default-step'; 4 | import { StepInput } from '../../../models/export-models'; 5 | import { NETWORK_CONFIG, NetworkName } from '@railgun-community/shared-models'; 6 | import { setRailgunFees } from '../../../init'; 7 | import { 8 | MOCK_SHIELD_FEE_BASIS_POINTS, 9 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 10 | } from '../../../test/mocks.test'; 11 | 12 | chai.use(chaiAsPromised); 13 | const { expect } = chai; 14 | 15 | const networkName = NetworkName.Ethereum; 16 | const tokenAddress = NETWORK_CONFIG[networkName].baseToken.wrappedAddress; 17 | 18 | describe('shield-default-step', () => { 19 | before(() => { 20 | setRailgunFees( 21 | networkName, 22 | MOCK_SHIELD_FEE_BASIS_POINTS, 23 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 24 | ); 25 | }); 26 | 27 | it('Should create shield default step', async () => { 28 | const step = new ShieldDefaultStep(); 29 | 30 | const stepInput: StepInput = { 31 | networkName: networkName, 32 | erc20Amounts: [ 33 | { 34 | tokenAddress, 35 | decimals: 18n, 36 | isBaseToken: false, 37 | expectedBalance: 12000n, 38 | minBalance: 12000n, 39 | approvedSpender: undefined, 40 | }, 41 | ], 42 | nfts: [], 43 | }; 44 | const output = await step.getValidStepOutput(stepInput); 45 | 46 | expect(output.name).to.equal('Shield (Default)'); 47 | expect(output.description).to.equal( 48 | 'Shield ERC20s and NFTs into private RAILGUN balance.', 49 | ); 50 | 51 | expect(output.spentERC20Amounts).to.equal(undefined); 52 | 53 | expect(output.outputERC20Amounts).to.deep.equal([ 54 | { 55 | tokenAddress, 56 | expectedBalance: 11970n, 57 | minBalance: 11970n, 58 | approvedSpender: undefined, 59 | isBaseToken: false, 60 | decimals: 18n, 61 | recipient: undefined, 62 | }, 63 | ]); 64 | 65 | expect(output.spentNFTs).to.equal(undefined); 66 | expect(output.outputNFTs).to.deep.equal([]); 67 | 68 | expect(output.feeERC20AmountRecipients).to.deep.equal([ 69 | { 70 | tokenAddress, 71 | decimals: 18n, 72 | amount: 30n, 73 | recipient: 'RAILGUN Shield Fee', 74 | }, 75 | ]); 76 | 77 | expect(output.crossContractCalls).to.deep.equal([]); 78 | }); 79 | 80 | it('Should test shield default step error cases', async () => { 81 | const step = new ShieldDefaultStep(); 82 | 83 | // No matching erc20 inputs 84 | const stepInputNoERC20s: StepInput = { 85 | networkName: networkName, 86 | erc20Amounts: [ 87 | { 88 | tokenAddress, 89 | decimals: 18n, 90 | isBaseToken: true, 91 | expectedBalance: 12000n, 92 | minBalance: 12000n, 93 | approvedSpender: undefined, 94 | }, 95 | ], 96 | nfts: [], 97 | }; 98 | await expect(step.getValidStepOutput(stepInputNoERC20s)).to.be.rejectedWith( 99 | 'Shield (Default) step is invalid.', 100 | ); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/steps/railgun/__tests__/unshield-default-step.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { UnshieldDefaultStep } from '../unshield-default-step'; 4 | import { StepInput } from '../../../models/export-models'; 5 | import { NETWORK_CONFIG, NetworkName } from '@railgun-community/shared-models'; 6 | import { setRailgunFees } from '../../../init'; 7 | import { 8 | MOCK_SHIELD_FEE_BASIS_POINTS, 9 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 10 | } from '../../../test/mocks.test'; 11 | 12 | chai.use(chaiAsPromised); 13 | const { expect } = chai; 14 | 15 | const networkName = NetworkName.Ethereum; 16 | const tokenAddress = NETWORK_CONFIG[networkName].baseToken.wrappedAddress; 17 | 18 | describe('unshield-default-step', () => { 19 | before(() => { 20 | setRailgunFees( 21 | networkName, 22 | MOCK_SHIELD_FEE_BASIS_POINTS, 23 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 24 | ); 25 | }); 26 | 27 | it('Should create unshield default step', async () => { 28 | const step = new UnshieldDefaultStep(); 29 | 30 | const stepInput: StepInput = { 31 | networkName: networkName, 32 | erc20Amounts: [ 33 | { 34 | tokenAddress, 35 | decimals: 18n, 36 | isBaseToken: false, 37 | expectedBalance: 12000n, 38 | minBalance: 12000n, 39 | approvedSpender: undefined, 40 | }, 41 | ], 42 | nfts: [], 43 | }; 44 | const output = await step.getValidStepOutput(stepInput); 45 | 46 | expect(output.name).to.equal('Unshield (Default)'); 47 | expect(output.description).to.equal( 48 | 'Unshield ERC20s and NFTs from private RAILGUN balance.', 49 | ); 50 | 51 | expect(output.spentERC20Amounts).to.equal(undefined); 52 | 53 | expect(output.outputERC20Amounts).to.deep.equal([ 54 | { 55 | tokenAddress, 56 | expectedBalance: 11970n, 57 | minBalance: 11970n, 58 | approvedSpender: undefined, 59 | isBaseToken: false, 60 | decimals: 18n, 61 | }, 62 | ]); 63 | 64 | expect(output.spentNFTs).to.equal(undefined); 65 | expect(output.outputNFTs).to.deep.equal([]); 66 | 67 | expect(output.feeERC20AmountRecipients).to.deep.equal([ 68 | { 69 | decimals: 18n, 70 | tokenAddress, 71 | amount: 30n, 72 | recipient: 'RAILGUN Unshield Fee', 73 | }, 74 | ]); 75 | 76 | expect(output.crossContractCalls).to.deep.equal([]); 77 | }); 78 | 79 | it('Should test unshield default step error cases', async () => { 80 | const step = new UnshieldDefaultStep(); 81 | 82 | // No matching erc20 inputs 83 | const stepInputNoERC20s: StepInput = { 84 | networkName: networkName, 85 | erc20Amounts: [ 86 | { 87 | tokenAddress, 88 | decimals: 18n, 89 | isBaseToken: true, 90 | expectedBalance: 12000n, 91 | minBalance: 12000n, 92 | approvedSpender: undefined, 93 | }, 94 | ], 95 | nfts: [], 96 | }; 97 | await expect(step.getValidStepOutput(stepInputNoERC20s)).to.be.rejectedWith( 98 | 'Unshield (Default) step is invalid.', 99 | ); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/steps/railgun/designate-shield-erc20-recipient-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20Info, 3 | StepConfig, 4 | StepInput, 5 | UnvalidatedStepOutput, 6 | } from '../../models/export-models'; 7 | import { Step } from '../step'; 8 | import { compareERC20Info } from '../../utils/token'; 9 | 10 | export class DesignateShieldERC20RecipientStep extends Step { 11 | readonly config: StepConfig = { 12 | name: 'Designate Shield ERC20s Recipient', 13 | description: 'Designates ERC20s to shield into a private RAILGUN balance.', 14 | }; 15 | 16 | private readonly toAddress: string; 17 | 18 | private readonly erc20Infos: RecipeERC20Info[]; 19 | 20 | constructor(toAddress: string, erc20Infos: RecipeERC20Info[]) { 21 | super(); 22 | this.toAddress = toAddress; 23 | this.erc20Infos = erc20Infos; 24 | } 25 | 26 | async getStepOutput(input: StepInput): Promise { 27 | const { erc20AmountsForStep, unusedERC20Amounts } = 28 | this.getValidInputERC20Amounts( 29 | input.erc20Amounts, 30 | [ 31 | // Filter by comparing inputs with erc20Infos list. 32 | erc20Amount => 33 | this.erc20Infos.find(erc20Info => 34 | compareERC20Info(erc20Amount, erc20Info), 35 | ) != null, 36 | ], 37 | {}, 38 | ); 39 | 40 | // Designate recipient as toAddress. 41 | // Fees will be calculated in the ShieldDefaultStep. 42 | const outputERC20Amounts = erc20AmountsForStep.map(erc20Amount => ({ 43 | ...erc20Amount, 44 | recipient: this.toAddress, 45 | })); 46 | 47 | return { 48 | crossContractCalls: [], 49 | outputERC20Amounts: [...outputERC20Amounts, ...unusedERC20Amounts], 50 | outputNFTs: input.nfts, 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/steps/railgun/index.ts: -------------------------------------------------------------------------------- 1 | export * from './shield-default-step'; 2 | export * from './unshield-default-step'; 3 | -------------------------------------------------------------------------------- /src/steps/railgun/shield-default-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20AmountRecipient, 3 | StepConfig, 4 | StepInput, 5 | StepOutputERC20Amount, 6 | UnvalidatedStepOutput, 7 | } from '../../models/export-models'; 8 | import { Step } from '../step'; 9 | import { NetworkName } from '@railgun-community/shared-models'; 10 | import { getShieldFee, getShieldedAmountAfterFee } from '../../utils/fee'; 11 | 12 | export class ShieldDefaultStep extends Step { 13 | readonly config: StepConfig = { 14 | name: 'Shield (Default)', 15 | description: 'Shield ERC20s and NFTs into private RAILGUN balance.', 16 | }; 17 | 18 | readonly canAddStep = false; 19 | 20 | async getStepOutput(input: StepInput): Promise { 21 | const { outputERC20Amounts, feeERC20AmountRecipients } = 22 | ShieldDefaultStep.getOutputERC20AmountsAndFees( 23 | input.networkName, 24 | input.erc20Amounts, 25 | ); 26 | if ( 27 | !outputERC20Amounts.every( 28 | erc20Amount => !(erc20Amount.isBaseToken ?? false), 29 | ) 30 | ) { 31 | throw new Error('Cannot shield base token.'); 32 | } 33 | 34 | return { 35 | crossContractCalls: [], 36 | outputERC20Amounts, 37 | outputNFTs: input.nfts, 38 | feeERC20AmountRecipients, 39 | }; 40 | } 41 | 42 | static getOutputERC20AmountsAndFees( 43 | networkName: NetworkName, 44 | inputERC20Amounts: StepOutputERC20Amount[], 45 | ) { 46 | const outputERC20Amounts: StepOutputERC20Amount[] = []; 47 | const feeERC20AmountRecipients: RecipeERC20AmountRecipient[] = []; 48 | 49 | inputERC20Amounts.forEach(erc20Amount => { 50 | const shieldFeeAmount = getShieldFee( 51 | networkName, 52 | erc20Amount.expectedBalance, 53 | ); 54 | const shieldedAmount = getShieldedAmountAfterFee( 55 | networkName, 56 | erc20Amount.expectedBalance, 57 | ); 58 | const shieldedAmountMinimum = getShieldedAmountAfterFee( 59 | networkName, 60 | erc20Amount.minBalance, 61 | ); 62 | 63 | outputERC20Amounts.push({ 64 | tokenAddress: erc20Amount.tokenAddress, 65 | decimals: erc20Amount.decimals, 66 | isBaseToken: erc20Amount.isBaseToken, 67 | approvedSpender: erc20Amount.approvedSpender, 68 | expectedBalance: shieldedAmount, 69 | minBalance: shieldedAmountMinimum, 70 | recipient: erc20Amount.recipient, 71 | }); 72 | 73 | feeERC20AmountRecipients.push({ 74 | tokenAddress: erc20Amount.tokenAddress, 75 | decimals: erc20Amount.decimals, 76 | amount: shieldFeeAmount, 77 | recipient: 'RAILGUN Shield Fee', 78 | }); 79 | }); 80 | 81 | if ( 82 | !outputERC20Amounts.every( 83 | erc20Amount => !(erc20Amount.isBaseToken ?? false), 84 | ) 85 | ) { 86 | throw new Error('Cannot shield base token.'); 87 | } 88 | 89 | return { outputERC20Amounts, feeERC20AmountRecipients }; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/steps/railgun/unshield-default-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20AmountRecipient, 3 | StepConfig, 4 | StepInput, 5 | StepOutputERC20Amount, 6 | UnvalidatedStepOutput, 7 | } from '../../models/export-models'; 8 | import { Step } from '../step'; 9 | import { NetworkName } from '@railgun-community/shared-models'; 10 | import { getUnshieldFee, getUnshieldedAmountAfterFee } from '../../utils/fee'; 11 | 12 | export class UnshieldDefaultStep extends Step { 13 | readonly config: StepConfig = { 14 | name: 'Unshield (Default)', 15 | description: 'Unshield ERC20s and NFTs from private RAILGUN balance.', 16 | }; 17 | 18 | readonly canAddStep = false; 19 | 20 | protected async getStepOutput( 21 | input: StepInput, 22 | ): Promise { 23 | const { outputERC20Amounts, feeERC20AmountRecipients } = 24 | this.getOutputERC20AmountsAndFees(input.networkName, input.erc20Amounts); 25 | if ( 26 | !outputERC20Amounts.every( 27 | erc20Amount => !(erc20Amount.isBaseToken ?? false), 28 | ) 29 | ) { 30 | throw new Error('Cannot unshield base token.'); 31 | } 32 | 33 | return { 34 | crossContractCalls: [], 35 | outputERC20Amounts, 36 | outputNFTs: input.nfts, 37 | feeERC20AmountRecipients, 38 | }; 39 | } 40 | 41 | private getOutputERC20AmountsAndFees( 42 | networkName: NetworkName, 43 | inputERC20Amounts: StepOutputERC20Amount[], 44 | ) { 45 | const outputERC20Amounts: StepOutputERC20Amount[] = []; 46 | const feeERC20AmountRecipients: RecipeERC20AmountRecipient[] = []; 47 | 48 | inputERC20Amounts.forEach(erc20Amount => { 49 | const unshieldFeeAmount = getUnshieldFee( 50 | networkName, 51 | erc20Amount.expectedBalance, 52 | ); 53 | const unshieldedAmount = getUnshieldedAmountAfterFee( 54 | networkName, 55 | erc20Amount.expectedBalance, 56 | ); 57 | 58 | outputERC20Amounts.push({ 59 | tokenAddress: erc20Amount.tokenAddress, 60 | decimals: erc20Amount.decimals, 61 | isBaseToken: erc20Amount.isBaseToken, 62 | expectedBalance: unshieldedAmount, 63 | minBalance: unshieldedAmount, 64 | approvedSpender: undefined, 65 | }); 66 | 67 | feeERC20AmountRecipients.push({ 68 | tokenAddress: erc20Amount.tokenAddress, 69 | decimals: erc20Amount.decimals, 70 | amount: unshieldFeeAmount, 71 | recipient: 'RAILGUN Unshield Fee', 72 | }); 73 | }); 74 | return { outputERC20Amounts, feeERC20AmountRecipients }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/steps/swap/index.ts: -------------------------------------------------------------------------------- 1 | export * from './zero-x/zero-x-swap-step'; 2 | export * from './zero-x/zero-x-v2-swap-step'; 3 | -------------------------------------------------------------------------------- /src/steps/swap/zero-x/zero-x-swap-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20AmountRecipient, 3 | RecipeERC20Info, 4 | StepConfig, 5 | StepInput, 6 | StepOutputERC20Amount, 7 | SwapQuoteData, 8 | UnvalidatedStepOutput, 9 | } from '../../../models/export-models'; 10 | import { compareERC20Info, isApprovedForSpender } from '../../../utils/token'; 11 | import { Step } from '../../step'; 12 | 13 | export class ZeroXSwapStep extends Step { 14 | readonly config: StepConfig = { 15 | name: '0x Exchange Swap', 16 | description: 'Swaps two ERC20 tokens using 0x Exchange DEX Aggregator.', 17 | hasNonDeterministicOutput: true, 18 | }; 19 | 20 | private readonly quote: SwapQuoteData; 21 | private readonly sellERC20Info: RecipeERC20Info; 22 | 23 | constructor(quote: SwapQuoteData, sellERC20Info: RecipeERC20Info) { 24 | super(); 25 | this.quote = quote; 26 | this.sellERC20Info = sellERC20Info; 27 | } 28 | 29 | protected async getStepOutput( 30 | input: StepInput, 31 | ): Promise { 32 | const { 33 | buyERC20Amount, 34 | minimumBuyAmount, 35 | crossContractCall, 36 | sellTokenValue, 37 | spender, 38 | } = this.quote; 39 | const { erc20Amounts } = input; 40 | 41 | const sellERC20Amount = BigInt(sellTokenValue); 42 | const { erc20AmountForStep, unusedERC20Amounts } = 43 | this.getValidInputERC20Amount( 44 | erc20Amounts, 45 | erc20Amount => 46 | compareERC20Info(erc20Amount, this.sellERC20Info) && 47 | isApprovedForSpender(erc20Amount, spender), 48 | sellERC20Amount, 49 | ); 50 | 51 | const sellERC20AmountRecipient: RecipeERC20AmountRecipient = { 52 | ...this.sellERC20Info, 53 | amount: erc20AmountForStep.expectedBalance, 54 | recipient: '0x Exchange', 55 | }; 56 | const outputBuyERC20Amount: StepOutputERC20Amount = { 57 | tokenAddress: buyERC20Amount.tokenAddress, 58 | decimals: buyERC20Amount.decimals, 59 | isBaseToken: buyERC20Amount.isBaseToken, 60 | expectedBalance: buyERC20Amount.amount, 61 | minBalance: minimumBuyAmount, 62 | approvedSpender: undefined, 63 | }; 64 | 65 | return { 66 | crossContractCalls: [crossContractCall], 67 | spentERC20Amounts: [sellERC20AmountRecipient], 68 | outputERC20Amounts: [outputBuyERC20Amount, ...unusedERC20Amounts], 69 | outputNFTs: input.nfts, 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/steps/swap/zero-x/zero-x-v2-swap-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20AmountRecipient, 3 | RecipeERC20Info, 4 | StepConfig, 5 | StepInput, 6 | StepOutputERC20Amount, 7 | SwapQuoteData, 8 | UnvalidatedStepOutput, 9 | } from '../../../models/export-models'; 10 | import { compareERC20Info, isApprovedForSpender } from '../../../utils/token'; 11 | import { Step } from '../../step'; 12 | 13 | export class ZeroXV2SwapStep extends Step { 14 | readonly config: StepConfig = { 15 | name: '0x V2 Exchange Swap', 16 | description: 'Swaps two ERC20 tokens using 0x V2 Exchange DEX Aggregator.', 17 | hasNonDeterministicOutput: true, 18 | }; 19 | 20 | private readonly quote: SwapQuoteData; 21 | private readonly sellERC20Info: RecipeERC20Info; 22 | 23 | constructor(quote: SwapQuoteData, sellERC20Info: RecipeERC20Info) { 24 | super(); 25 | this.quote = quote; 26 | this.sellERC20Info = sellERC20Info; 27 | } 28 | 29 | protected async getStepOutput( 30 | input: StepInput, 31 | ): Promise { 32 | const { 33 | buyERC20Amount, 34 | minimumBuyAmount, 35 | crossContractCall, 36 | sellTokenValue, 37 | spender, 38 | } = this.quote; 39 | const { erc20Amounts } = input; 40 | 41 | const sellERC20Amount = BigInt(sellTokenValue); 42 | const { erc20AmountForStep, unusedERC20Amounts } = 43 | this.getValidInputERC20Amount( 44 | erc20Amounts, 45 | erc20Amount => 46 | compareERC20Info(erc20Amount, this.sellERC20Info) && 47 | isApprovedForSpender(erc20Amount, spender), 48 | sellERC20Amount, 49 | ); 50 | 51 | const sellERC20AmountRecipient: RecipeERC20AmountRecipient = { 52 | ...this.sellERC20Info, 53 | amount: erc20AmountForStep.expectedBalance, 54 | recipient: '0x V2 Exchange', 55 | }; 56 | const outputBuyERC20Amount: StepOutputERC20Amount = { 57 | tokenAddress: buyERC20Amount.tokenAddress, 58 | decimals: buyERC20Amount.decimals, 59 | isBaseToken: buyERC20Amount.isBaseToken, 60 | expectedBalance: buyERC20Amount.amount, 61 | minBalance: minimumBuyAmount, 62 | approvedSpender: undefined, 63 | }; 64 | 65 | return { 66 | crossContractCalls: [crossContractCall], 67 | spentERC20Amounts: [sellERC20AmountRecipient], 68 | outputERC20Amounts: [outputBuyERC20Amount, ...unusedERC20Amounts], 69 | outputNFTs: input.nfts, 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/steps/token/erc20/index.ts: -------------------------------------------------------------------------------- 1 | export * from './approve-erc20-spender-step'; 2 | export * from './transfer-erc20-step'; 3 | -------------------------------------------------------------------------------- /src/steps/token/erc20/transfer-erc20-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20AmountRecipient, 3 | RecipeERC20Info, 4 | StepConfig, 5 | StepInput, 6 | UnvalidatedStepOutput, 7 | } from '../../../models/export-models'; 8 | import { compareERC20Info } from '../../../utils/token'; 9 | import { Step } from '../../step'; 10 | import { ContractTransaction } from 'ethers'; 11 | import { RelayAdaptContract } from '../../../contract'; 12 | 13 | export class TransferERC20Step extends Step { 14 | readonly config: StepConfig = { 15 | name: 'Transfer ERC20', 16 | description: 'Transfers ERC20 token to an external public address.', 17 | }; 18 | 19 | private readonly toAddress: string; 20 | 21 | private readonly erc20Info: RecipeERC20Info; 22 | 23 | private readonly amount: Optional; 24 | 25 | constructor(toAddress: string, erc20Info: RecipeERC20Info, amount?: bigint) { 26 | super(); 27 | this.toAddress = toAddress; 28 | this.erc20Info = erc20Info; 29 | this.amount = amount; 30 | } 31 | 32 | protected async getStepOutput( 33 | input: StepInput, 34 | ): Promise { 35 | const { erc20Amounts } = input; 36 | 37 | const { erc20AmountForStep, unusedERC20Amounts } = 38 | this.getValidInputERC20Amount( 39 | erc20Amounts, 40 | erc20Amount => compareERC20Info(erc20Amount, this.erc20Info), 41 | this.amount, 42 | ); 43 | 44 | const contract = new RelayAdaptContract(input.networkName); 45 | const crossContractCalls: ContractTransaction[] = [ 46 | await contract.createERC20Transfer( 47 | this.toAddress, 48 | this.erc20Info.tokenAddress, 49 | this.amount, 50 | ), 51 | ]; 52 | 53 | const transferredERC20: RecipeERC20AmountRecipient = { 54 | tokenAddress: this.erc20Info.tokenAddress, 55 | decimals: this.erc20Info.decimals, 56 | amount: this.amount ?? erc20AmountForStep.expectedBalance, 57 | recipient: this.toAddress, 58 | }; 59 | 60 | return { 61 | crossContractCalls, 62 | spentERC20Amounts: [transferredERC20], 63 | outputERC20Amounts: unusedERC20Amounts, 64 | outputNFTs: input.nfts, 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/steps/token/erc721/index.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /src/steps/token/index.ts: -------------------------------------------------------------------------------- 1 | export * from './erc20'; 2 | export * from './erc721'; 3 | -------------------------------------------------------------------------------- /src/steps/vault/beefy/__tests__/beefy-util.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { BeefyVaultData } from '../../../../api/beefy/beefy-api'; 4 | import { 5 | calculateOutputsForBeefyDeposit, 6 | calculateOutputsForBeefyWithdraw, 7 | } from '../beefy-util'; 8 | 9 | chai.use(chaiAsPromised); 10 | const { expect } = chai; 11 | 12 | const vault: BeefyVaultData = { 13 | apy: 0.02050812831779769, 14 | chain: 'ethereum', 15 | depositERC20Address: '0x06325440d014e39736583c165c2963ba99faf14e', 16 | depositERC20Decimals: 18n, 17 | depositERC20Symbol: 'crvUSDCWBTCWETH', 18 | depositFeeBasisPoints: 0n, 19 | isActive: true, 20 | network: 'ethereum', 21 | vaultContractAddress: '0xa7739fd3d12ac7f16d8329af3ee407e19de10d8d', 22 | vaultERC20Address: '0xa7739fd3d12ac7f16d8329af3ee407e19de10d8d', 23 | vaultERC20Symbol: 'mooConvexTriCryptoUSDC', 24 | vaultID: 'convex-steth', 25 | vaultName: 'stETH/ETH', 26 | vaultRate: 1011005831134112183n, 27 | withdrawFeeBasisPoints: 1n, 28 | }; 29 | 30 | describe('beefy-util', () => { 31 | it('Should calculate outputs for beefy deposit', async () => { 32 | expect( 33 | calculateOutputsForBeefyDeposit(997500000000000000n, vault), 34 | ).to.deep.equal({ 35 | depositAmountAfterFee: 997500000000000000n, 36 | depositFeeAmount: 0n, 37 | 38 | // TODO: This +1n should not occur. It's because the vaultRate is rounded. We could fix with the totalSupply and pool amounts. 39 | // https://github.com/beefyfinance/beefy-contracts/blob/6ec0105986aaf6cf4a0b64127829001d51f3955a/contracts/BIFI/vaults/BeefyVaultV7.sol#L112 40 | receivedVaultTokenAmount: 986641193632917231n + 1n, 41 | }); 42 | }); 43 | 44 | it('Should calculate outputs for beefy withdraw', async () => { 45 | expect( 46 | calculateOutputsForBeefyWithdraw(997500000000000000n, vault), 47 | ).to.deep.equal({ 48 | receivedWithdrawAmount: 1008478316556276902n, 49 | withdrawFeeAmount: 100847831655627n, 50 | 51 | // TODO: This -1n should not occur. It's because the vaultRate is rounded. We could fix with the totalSupply and pool amounts. 52 | // https://github.com/beefyfinance/beefy-contracts/blob/6ec0105986aaf6cf4a0b64127829001d51f3955a/contracts/BIFI/vaults/BeefyVaultV7.sol#L112 53 | withdrawAmountAfterFee: 1008377468724621276n - 1n, 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/steps/vault/beefy/__tests__/beefy-withdraw-step.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { NetworkName } from '@railgun-community/shared-models'; 4 | import { StepInput } from '../../../../models/export-models'; 5 | import { BeefyVaultData } from '../../../../api/beefy/beefy-api'; 6 | import { BeefyWithdrawStep } from '../beefy-withdraw-step'; 7 | 8 | chai.use(chaiAsPromised); 9 | const { expect } = chai; 10 | 11 | const networkName = NetworkName.Ethereum; 12 | 13 | const tokenAddress = '0xe76C6c83af64e4C60245D8C7dE953DF673a7A33D'; 14 | 15 | const vault: BeefyVaultData = { 16 | vaultID: 'id', 17 | vaultName: 'VAULT_NAME', 18 | apy: 5.0, 19 | chain: 'ethereum', 20 | network: 'ethereum', 21 | depositERC20Symbol: 'RAIL', 22 | depositERC20Address: tokenAddress, 23 | depositERC20Decimals: 18n, 24 | vaultERC20Symbol: 'mooHermesMETIS-m.USDC', 25 | vaultERC20Address: '0x40324434a0b53dd1ED167Ba30dcB6B4bd7a9536d', 26 | vaultContractAddress: '0x40324434a0b53dd1ED167Ba30dcB6B4bd7a9536d', 27 | vaultRate: BigInt('2000000000000000000'), // 2x 28 | depositFeeBasisPoints: 0n, 29 | withdrawFeeBasisPoints: 1000n, 30 | isActive: true, 31 | }; 32 | 33 | describe('beefy-withdraw-step', () => { 34 | it('Should create beefy-withdraw step', async () => { 35 | const step = new BeefyWithdrawStep(vault); 36 | 37 | const stepInput: StepInput = { 38 | networkName, 39 | erc20Amounts: [ 40 | { 41 | tokenAddress: vault.vaultERC20Address, 42 | decimals: 18n, 43 | expectedBalance: 10000n, 44 | minBalance: 10000n, 45 | approvedSpender: undefined, 46 | }, 47 | ], 48 | nfts: [], 49 | }; 50 | const output = await step.getValidStepOutput(stepInput); 51 | 52 | expect(output.name).to.equal('Beefy Vault Withdraw'); 53 | expect(output.description).to.equal( 54 | 'Withdraws from a yield-bearing Beefy Vault.', 55 | ); 56 | 57 | // Withdrawn 58 | expect(output.spentERC20Amounts).to.deep.equal([ 59 | { 60 | amount: 10000n, 61 | recipient: 'VAULT_NAME Vault', 62 | tokenAddress: vault.vaultERC20Address, 63 | decimals: 18n, 64 | }, 65 | ]); 66 | 67 | // Received 68 | expect(output.outputERC20Amounts).to.deep.equal([ 69 | { 70 | approvedSpender: undefined, 71 | expectedBalance: 18000n, 72 | minBalance: 18000n, 73 | tokenAddress, 74 | decimals: 18n, 75 | }, 76 | ]); 77 | 78 | expect(output.spentNFTs).to.equal(undefined); 79 | expect(output.outputNFTs).to.deep.equal([]); 80 | 81 | expect(output.feeERC20AmountRecipients).to.deep.equal([ 82 | { 83 | decimals: 18n, 84 | tokenAddress, 85 | amount: 2000n, 86 | recipient: 'VAULT_NAME Vault Withdraw Fee', 87 | }, 88 | ]); 89 | 90 | expect(output.crossContractCalls).to.deep.equal([ 91 | { 92 | data: '0x853828b6', 93 | to: '0x40324434a0b53dd1ED167Ba30dcB6B4bd7a9536d', 94 | }, 95 | ]); 96 | }); 97 | 98 | it('Should test beefy-withdraw step error cases', async () => { 99 | const step = new BeefyWithdrawStep(vault); 100 | 101 | // No matching erc20 inputs 102 | const stepInputNoERC20s: StepInput = { 103 | networkName, 104 | erc20Amounts: [ 105 | { 106 | tokenAddress, 107 | decimals: 18n, 108 | expectedBalance: 12000n, 109 | minBalance: 12000n, 110 | approvedSpender: vault.vaultContractAddress, 111 | }, 112 | ], 113 | nfts: [], 114 | }; 115 | await expect(step.getValidStepOutput(stepInputNoERC20s)).to.be.rejectedWith( 116 | 'Beefy Vault Withdraw step is invalid.', 117 | ); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /src/steps/vault/beefy/beefy-deposit-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20AmountRecipient, 3 | RecipeERC20Info, 4 | StepConfig, 5 | StepInput, 6 | StepOutputERC20Amount, 7 | UnvalidatedStepOutput, 8 | } from '../../../models/export-models'; 9 | import { compareERC20Info, isApprovedForSpender } from '../../../utils/token'; 10 | import { Step } from '../../step'; 11 | import { BEEFY_VAULT_ERC20_DECIMALS, BeefyVaultData } from '../../../api/beefy'; 12 | import { BeefyVaultContract } from '../../../contract/vault/beefy/beefy-vault-contract'; 13 | import { calculateOutputsForBeefyDeposit } from './beefy-util'; 14 | 15 | export class BeefyDepositStep extends Step { 16 | readonly config: StepConfig = { 17 | name: 'Beefy Vault Deposit', 18 | description: 'Deposits into a yield-bearing Beefy Vault.', 19 | }; 20 | 21 | private readonly vault: BeefyVaultData; 22 | 23 | constructor(vault: BeefyVaultData) { 24 | super(); 25 | this.vault = vault; 26 | } 27 | 28 | protected async getStepOutput( 29 | input: StepInput, 30 | ): Promise { 31 | const { 32 | vaultName, 33 | depositERC20Address, 34 | depositERC20Decimals, 35 | vaultContractAddress, 36 | vaultERC20Address, 37 | } = this.vault; 38 | const { erc20Amounts } = input; 39 | 40 | const depositERC20Info: RecipeERC20Info = { 41 | tokenAddress: depositERC20Address, 42 | decimals: depositERC20Decimals, 43 | }; 44 | const { erc20AmountForStep, unusedERC20Amounts } = 45 | this.getValidInputERC20Amount( 46 | erc20Amounts, 47 | erc20Amount => 48 | compareERC20Info(erc20Amount, depositERC20Info) && 49 | isApprovedForSpender(erc20Amount, vaultContractAddress), 50 | undefined, // amount 51 | ); 52 | 53 | const contract = new BeefyVaultContract(vaultContractAddress); 54 | const crossContractCall = await contract.createDepositAll(); 55 | 56 | const { 57 | depositFeeAmount, 58 | depositAmountAfterFee, 59 | receivedVaultTokenAmount, 60 | } = calculateOutputsForBeefyDeposit( 61 | erc20AmountForStep.expectedBalance, 62 | this.vault, 63 | ); 64 | 65 | const spentERC20AmountRecipient: RecipeERC20AmountRecipient = { 66 | ...depositERC20Info, 67 | amount: depositAmountAfterFee, 68 | recipient: `${vaultName} Vault`, 69 | }; 70 | const outputERC20Amount: StepOutputERC20Amount = { 71 | tokenAddress: vaultERC20Address, 72 | decimals: BEEFY_VAULT_ERC20_DECIMALS, 73 | expectedBalance: receivedVaultTokenAmount, 74 | minBalance: receivedVaultTokenAmount, 75 | approvedSpender: undefined, 76 | }; 77 | const feeERC20AmountRecipients: RecipeERC20AmountRecipient[] = 78 | depositFeeAmount > 0n 79 | ? [ 80 | { 81 | tokenAddress: depositERC20Address, 82 | decimals: depositERC20Decimals, 83 | amount: depositFeeAmount, 84 | recipient: `${vaultName} Vault Deposit Fee`, 85 | }, 86 | ] 87 | : []; 88 | 89 | return { 90 | crossContractCalls: [crossContractCall], 91 | spentERC20Amounts: [spentERC20AmountRecipient], 92 | outputERC20Amounts: [outputERC20Amount, ...unusedERC20Amounts], 93 | outputNFTs: input.nfts, 94 | feeERC20AmountRecipients, 95 | }; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/steps/vault/beefy/beefy-util.ts: -------------------------------------------------------------------------------- 1 | import { BEEFY_VAULT_ERC20_DECIMALS, BeefyVaultData } from '../../../api'; 2 | 3 | export const calculateOutputsForBeefyDeposit = ( 4 | stepInitialBalance: bigint, 5 | vault: BeefyVaultData, 6 | ) => { 7 | const { vaultRate, depositFeeBasisPoints } = vault; 8 | 9 | const depositFeeAmount = 10 | (stepInitialBalance * depositFeeBasisPoints) / 10000n; 11 | const depositAmountAfterFee = stepInitialBalance - depositFeeAmount; 12 | 13 | const vaultERC20DecimalsAdjustment = 14 | 10n ** BigInt(BEEFY_VAULT_ERC20_DECIMALS); 15 | 16 | const receivedVaultTokenAmount = 17 | (depositAmountAfterFee * vaultERC20DecimalsAdjustment) / vaultRate; 18 | 19 | return { 20 | depositFeeAmount, 21 | depositAmountAfterFee, 22 | receivedVaultTokenAmount, 23 | }; 24 | }; 25 | 26 | export const calculateOutputsForBeefyWithdraw = ( 27 | stepInitialBalance: bigint, 28 | vault: BeefyVaultData, 29 | ) => { 30 | const { vaultRate, withdrawFeeBasisPoints } = vault; 31 | 32 | const vaultERC20DecimalsAdjustment = 33 | 10n ** BigInt(BEEFY_VAULT_ERC20_DECIMALS); 34 | 35 | const receivedWithdrawAmount = 36 | (stepInitialBalance * vaultRate) / vaultERC20DecimalsAdjustment; 37 | 38 | const withdrawFeeAmount = 39 | (receivedWithdrawAmount * withdrawFeeBasisPoints) / 10000n; 40 | const withdrawAmountAfterFee = receivedWithdrawAmount - withdrawFeeAmount; 41 | 42 | return { 43 | receivedWithdrawAmount, 44 | withdrawFeeAmount, 45 | withdrawAmountAfterFee, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/steps/vault/beefy/beefy-withdraw-step.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20AmountRecipient, 3 | RecipeERC20Info, 4 | StepConfig, 5 | StepInput, 6 | StepOutputERC20Amount, 7 | UnvalidatedStepOutput, 8 | } from '../../../models/export-models'; 9 | import { compareERC20Info } from '../../../utils/token'; 10 | import { Step } from '../../step'; 11 | import { BEEFY_VAULT_ERC20_DECIMALS, BeefyVaultData } from '../../../api/beefy'; 12 | import { BeefyVaultContract } from '../../../contract/vault/beefy/beefy-vault-contract'; 13 | import { calculateOutputsForBeefyWithdraw } from './beefy-util'; 14 | 15 | export class BeefyWithdrawStep extends Step { 16 | readonly config: StepConfig = { 17 | name: 'Beefy Vault Withdraw', 18 | description: 'Withdraws from a yield-bearing Beefy Vault.', 19 | }; 20 | 21 | private readonly vault: BeefyVaultData; 22 | 23 | constructor(vault: BeefyVaultData) { 24 | super(); 25 | this.vault = vault; 26 | } 27 | 28 | protected async getStepOutput( 29 | input: StepInput, 30 | ): Promise { 31 | const { 32 | vaultName, 33 | depositERC20Address, 34 | depositERC20Decimals, 35 | vaultContractAddress, 36 | vaultERC20Address, 37 | } = this.vault; 38 | const { erc20Amounts } = input; 39 | 40 | const withdrawERC20Info: RecipeERC20Info = { 41 | tokenAddress: vaultERC20Address, 42 | decimals: BEEFY_VAULT_ERC20_DECIMALS, 43 | }; 44 | const { erc20AmountForStep, unusedERC20Amounts } = 45 | this.getValidInputERC20Amount( 46 | erc20Amounts, 47 | erc20Amount => compareERC20Info(erc20Amount, withdrawERC20Info), 48 | undefined, // amount 49 | ); 50 | 51 | const contract = new BeefyVaultContract(vaultContractAddress); 52 | const crossContractCall = await contract.createWithdrawAll(); 53 | 54 | const { withdrawFeeAmount, withdrawAmountAfterFee } = 55 | calculateOutputsForBeefyWithdraw( 56 | erc20AmountForStep.expectedBalance, 57 | this.vault, 58 | ); 59 | 60 | const spentERC20AmountRecipient: RecipeERC20AmountRecipient = { 61 | ...withdrawERC20Info, 62 | amount: erc20AmountForStep.expectedBalance, 63 | recipient: `${vaultName} Vault`, 64 | }; 65 | const outputERC20Amount: StepOutputERC20Amount = { 66 | tokenAddress: depositERC20Address, 67 | decimals: depositERC20Decimals, 68 | expectedBalance: withdrawAmountAfterFee, 69 | minBalance: withdrawAmountAfterFee, 70 | approvedSpender: undefined, 71 | }; 72 | const feeERC20AmountRecipients: RecipeERC20AmountRecipient[] = 73 | withdrawFeeAmount > 0n 74 | ? [ 75 | { 76 | tokenAddress: depositERC20Address, 77 | decimals: depositERC20Decimals, 78 | amount: withdrawFeeAmount, 79 | recipient: `${vaultName} Vault Withdraw Fee`, 80 | }, 81 | ] 82 | : []; 83 | 84 | return { 85 | crossContractCalls: [crossContractCall], 86 | spentERC20Amounts: [spentERC20AmountRecipient], 87 | outputERC20Amounts: [outputERC20Amount, ...unusedERC20Amounts], 88 | outputNFTs: input.nfts, 89 | feeERC20AmountRecipients, 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/steps/vault/index.ts: -------------------------------------------------------------------------------- 1 | export * from './beefy/beefy-deposit-step'; 2 | -------------------------------------------------------------------------------- /src/test/mocks.test.ts: -------------------------------------------------------------------------------- 1 | export const MOCK_SHIELD_FEE_BASIS_POINTS = 25n; 2 | export const MOCK_UNSHIELD_FEE_BASIS_POINTS = 25n; 3 | 4 | // testRailgunWallet address 5 | export const MOCK_RAILGUN_WALLET_ADDRESS = 6 | '0zk1qyk9nn28x0u3rwn5pknglda68wrn7gw6anjw8gg94mcj6eq5u48tlrv7j6fe3z53lama02nutwtcqc979wnce0qwly4y7w4rls5cq040g7z8eagshxrw5ajy990'; 7 | 8 | // testRailgunWallet2 address 9 | export const MOCK_RAILGUN_WALLET_ADDRESS_2 = 10 | '0zk1qygxma3y6ljx3205z7lxreftpx47zr25z0nmt3a32zzygcyz5mg5mrv7j6fe3z53l74h5eldal4evywm297acap2thxn7c9pp8vhvmwzvz32m36a70y06gejrsw'; 11 | -------------------------------------------------------------------------------- /src/test/setup.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Chain, 3 | NetworkName, 4 | TXIDVersion, 5 | isDefined, 6 | } from '@railgun-community/shared-models'; 7 | import { refreshBalances } from '@railgun-community/wallet'; 8 | import { 9 | createRailgunWallet2ForTests, 10 | createRailgunWalletForTests, 11 | loadLocalhostFallbackProviderForTests, 12 | pollUntilUTXOMerkletreeScanned, 13 | removeTestDB, 14 | shieldAllTokensForTests, 15 | startRailgunForTests, 16 | waitForShieldedTokenBalances, 17 | } from './railgun-setup.test'; 18 | import { ForkRPCType, setupTestRPCAndWallets } from './rpc-setup.test'; 19 | import { testConfig } from './test-config.test'; 20 | import { getForkTestNetworkName } from './common.test'; 21 | 22 | before(async function run() { 23 | if (isDefined(process.env.RUN_FORK_TESTS)) { 24 | this.timeout(5 * 60 * 1000); // 10 min timeout for setup after adding refresh balances 25 | removeTestDB(); 26 | await setupForkTests(); 27 | } 28 | }); 29 | 30 | after(() => { 31 | if (isDefined(process.env.RUN_FORK_TESTS)) { 32 | removeTestDB(); 33 | } 34 | }); 35 | 36 | const getTestERC20Addresses = (networkName: NetworkName): string[] => { 37 | switch (networkName) { 38 | case NetworkName.Ethereum: 39 | return [ 40 | testConfig.contractsEthereum.weth9, 41 | testConfig.contractsEthereum.rail, 42 | testConfig.contractsEthereum.usdc, 43 | testConfig.contractsEthereum.conicEthLPToken, 44 | testConfig.contractsEthereum.crvUSDCWBTCWETH, 45 | testConfig.contractsEthereum.mooConvexTriCryptoUSDC, 46 | ]; 47 | case NetworkName.Arbitrum: 48 | return [testConfig.contractsArbitrum.dai]; 49 | case NetworkName.BNBChain: 50 | case NetworkName.Polygon: 51 | case NetworkName.EthereumSepolia: 52 | case NetworkName.Hardhat: 53 | case NetworkName.PolygonAmoy: 54 | case NetworkName.EthereumRopsten_DEPRECATED: 55 | case NetworkName.EthereumGoerli_DEPRECATED: 56 | case NetworkName.ArbitrumGoerli_DEPRECATED: 57 | case NetworkName.PolygonMumbai_DEPRECATED: 58 | return []; 59 | } 60 | }; 61 | 62 | const getSupportedNetworkNamesForTest = (): NetworkName[] => { 63 | return [NetworkName.Ethereum, NetworkName.Arbitrum]; 64 | }; 65 | 66 | export const setupForkTests = async () => { 67 | try { 68 | const networkName = getForkTestNetworkName(); 69 | const txidVersion = TXIDVersion.V2_PoseidonMerkle; 70 | 71 | if (!Object.keys(NetworkName).includes(networkName)) { 72 | throw new Error( 73 | `Unrecognized network name, expected one of list: ${getSupportedNetworkNamesForTest().join( 74 | ', ', 75 | )}`, 76 | ); 77 | } 78 | 79 | const tokenAddresses: string[] = getTestERC20Addresses(networkName); 80 | const testChain: Chain = { id: 1, type: 0 }; 81 | 82 | const forkRPCType = isDefined(process.env.USE_GANACHE) 83 | ? ForkRPCType.Ganache 84 | : isDefined(process.env.USE_HARDHAT) 85 | ? ForkRPCType.Hardhat 86 | : ForkRPCType.Anvil; 87 | 88 | // Ganache forked Ethereum RPC setup 89 | await setupTestRPCAndWallets(forkRPCType, networkName, tokenAddresses); 90 | // Quickstart setup 91 | await startRailgunForTests(); 92 | 93 | await loadLocalhostFallbackProviderForTests(networkName); 94 | 95 | void refreshBalances(testChain, undefined); 96 | 97 | await pollUntilUTXOMerkletreeScanned(); 98 | // Set up primary wallet 99 | await createRailgunWalletForTests(); 100 | // Set up secondary wallets 101 | await createRailgunWallet2ForTests(); 102 | // Shield tokens for tests 103 | await shieldAllTokensForTests(networkName, tokenAddresses); 104 | 105 | // Make sure shielded balances are updated 106 | await waitForShieldedTokenBalances(txidVersion, networkName, tokenAddresses); 107 | } catch(error) { 108 | console.error("Setup Fork tests error: ", error); 109 | throw error; 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /src/test/test-config-overrides.test-example.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // Copy this file to `test-config-overrides.test.ts` to add git-ignored local configs. 3 | // Add test-config overrides here. 4 | }; 5 | -------------------------------------------------------------------------------- /src/test/test-config.test.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | 3 | export let testConfig = { 4 | // Set env ETHEREUM_RPC to change default fork RPC. 5 | ethereumForkRPC: process.env.ETHEREUM_RPC ?? 'https://cloudflare-eth.com', 6 | 7 | showVerboseLogs: false, 8 | 9 | // Mock wallets for tests 10 | signerMnemonic: 'test test test test test test test test test test test junk', 11 | railgunMnemonic: 12 | 'test test test test test test test test test test test junk', 13 | railgunMnemonic2: 14 | 'nation page hawk lawn rescue slim cup tired clutch brand holiday genuine', 15 | encryptionKey: 16 | '0101010101010101010101010101010101010101010101010101010101010101', 17 | 18 | contractsEthereum: { 19 | proxy: '0xfa7093cdd9ee6932b4eb2c9e1cde7ce00b1fa4b9', 20 | treasuryProxy: '0xE8A8B458BcD1Ececc6b6b58F80929b29cCecFF40', 21 | weth9: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 22 | relayAdapt: '0x0355B7B8cb128fA5692729Ab3AAa199C1753f726', 23 | 24 | // WARNING: Be careful adding tokens to this list. 25 | // Each new token will increase the setup time for tests. 26 | // Standard tokens 27 | rail: '0xe76C6c83af64e4C60245D8C7dE953DF673a7A33D', 28 | usdc: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 29 | // LP tokens 30 | conicEthLPToken: '0x58649Ec8adD732ea710731b5Cb37c99529A394d3', 31 | // Vault tokens 32 | crvUSDCWBTCWETH: '0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B', 33 | mooConvexTriCryptoUSDC: '0xD1BeaD7CadcCC6b6a715A6272c39F1EC54F6EC99', 34 | }, 35 | 36 | contractsArbitrum: { 37 | proxy: '0xfa7093cdd9ee6932b4eb2c9e1cde7ce00b1fa4b9', 38 | treasuryProxy: '0xE8A8B458BcD1Ececc6b6b58F80929b29cCecFF40', 39 | weth9: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 40 | relayAdapt: '0x0355B7B8cb128fA5692729Ab3AAa199C1753f726', 41 | 42 | // Standard tokens 43 | dai: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', 44 | }, 45 | 46 | // OVERRIDES - override using test-config-overrides.ts 47 | 48 | // API Domain for a proxy server equipped with 0x nginx config that includes private API key. 49 | zeroXProxyApiDomain: process.env.ZERO_X_PROXY_API_DOMAIN ?? '', 50 | // API Key for 0x API. 51 | zeroXApiKey: process.env.ZERO_X_API_KEY ?? '', 52 | // API Key for The Graph API. 53 | theGraphApiKey: process.env.THE_GRAPH_API_KEY ?? '', 54 | }; 55 | 56 | try { 57 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, global-require, @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access 58 | const overrides = require('./test-config-overrides.test').default; 59 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 60 | testConfig = { ...testConfig, ...overrides }; 61 | // eslint-disable-next-line no-empty 62 | } catch { 63 | // eslint-disable-next-line no-console 64 | console.error('Could not load test-config-overrides.'); 65 | } 66 | -------------------------------------------------------------------------------- /src/typechain/adapt/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export type { RelayAdapt } from "./RelayAdapt"; 5 | -------------------------------------------------------------------------------- /src/typechain/factories/adapt/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export { RelayAdapt__factory } from "./RelayAdapt__factory"; 5 | -------------------------------------------------------------------------------- /src/typechain/factories/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export * as adapt from "./adapt"; 5 | export * as lido from "./lido"; 6 | export * as liquidity from "./liquidity"; 7 | export * as token from "./token"; 8 | export * as vault from "./vault"; 9 | -------------------------------------------------------------------------------- /src/typechain/factories/lido/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export { LidoSTETH__factory } from "./LidoSTETH__factory"; 5 | export { LidoWSTETH__factory } from "./LidoWSTETH__factory"; 6 | -------------------------------------------------------------------------------- /src/typechain/factories/liquidity/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export { UniV2LikeFactory__factory } from "./UniV2LikeFactory__factory"; 5 | export { UniV2LikePair__factory } from "./UniV2LikePair__factory"; 6 | export { UniV2LikeRouter__factory } from "./UniV2LikeRouter__factory"; 7 | -------------------------------------------------------------------------------- /src/typechain/factories/token/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export { Erc20__factory } from "./Erc20__factory"; 5 | export { Erc721__factory } from "./Erc721__factory"; 6 | -------------------------------------------------------------------------------- /src/typechain/factories/vault/beefy/BeefyVaultMergedV6V7__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | 5 | import { Contract, Interface, type ContractRunner } from "ethers"; 6 | import type { 7 | BeefyVaultMergedV6V7, 8 | BeefyVaultMergedV6V7Interface, 9 | } from "../../../vault/beefy/BeefyVaultMergedV6V7"; 10 | 11 | const _abi = [ 12 | { 13 | inputs: [ 14 | { 15 | internalType: "uint256", 16 | name: "_amount", 17 | type: "uint256", 18 | }, 19 | ], 20 | name: "deposit", 21 | outputs: [], 22 | stateMutability: "nonpayable", 23 | type: "function", 24 | }, 25 | { 26 | inputs: [], 27 | name: "depositAll", 28 | outputs: [], 29 | stateMutability: "nonpayable", 30 | type: "function", 31 | }, 32 | { 33 | inputs: [ 34 | { 35 | internalType: "uint256", 36 | name: "_shares", 37 | type: "uint256", 38 | }, 39 | ], 40 | name: "withdraw", 41 | outputs: [], 42 | stateMutability: "nonpayable", 43 | type: "function", 44 | }, 45 | { 46 | inputs: [], 47 | name: "withdrawAll", 48 | outputs: [], 49 | stateMutability: "nonpayable", 50 | type: "function", 51 | }, 52 | { 53 | inputs: [], 54 | name: "want", 55 | outputs: [ 56 | { 57 | internalType: "address", 58 | name: "", 59 | type: "address", 60 | }, 61 | ], 62 | stateMutability: "view", 63 | type: "function", 64 | }, 65 | { 66 | inputs: [], 67 | name: "getPricePerFullShare", 68 | outputs: [ 69 | { 70 | internalType: "uint256", 71 | name: "", 72 | type: "uint256", 73 | }, 74 | ], 75 | stateMutability: "view", 76 | type: "function", 77 | }, 78 | { 79 | inputs: [], 80 | name: "earn", 81 | outputs: [], 82 | stateMutability: "nonpayable", 83 | type: "function", 84 | }, 85 | ] as const; 86 | 87 | export class BeefyVaultMergedV6V7__factory { 88 | static readonly abi = _abi; 89 | static createInterface(): BeefyVaultMergedV6V7Interface { 90 | return new Interface(_abi) as BeefyVaultMergedV6V7Interface; 91 | } 92 | static connect( 93 | address: string, 94 | runner?: ContractRunner | null 95 | ): BeefyVaultMergedV6V7 { 96 | return new Contract( 97 | address, 98 | _abi, 99 | runner 100 | ) as unknown as BeefyVaultMergedV6V7; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/typechain/factories/vault/beefy/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export { BeefyVaultMergedV6V7__factory } from "./BeefyVaultMergedV6V7__factory"; 5 | -------------------------------------------------------------------------------- /src/typechain/factories/vault/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export * as beefy from "./beefy"; 5 | -------------------------------------------------------------------------------- /src/typechain/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type * as adapt from "./adapt"; 5 | export type { adapt }; 6 | import type * as lido from "./lido"; 7 | export type { lido }; 8 | import type * as liquidity from "./liquidity"; 9 | export type { liquidity }; 10 | import type * as token from "./token"; 11 | export type { token }; 12 | import type * as vault from "./vault"; 13 | export type { vault }; 14 | export * as factories from "./factories"; 15 | export type { RelayAdapt } from "./adapt/RelayAdapt"; 16 | export { RelayAdapt__factory } from "./factories/adapt/RelayAdapt__factory"; 17 | export type { LidoSTETH } from "./lido/LidoSTETH"; 18 | export { LidoSTETH__factory } from "./factories/lido/LidoSTETH__factory"; 19 | export type { LidoWSTETH } from "./lido/LidoWSTETH"; 20 | export { LidoWSTETH__factory } from "./factories/lido/LidoWSTETH__factory"; 21 | export type { UniV2LikeFactory } from "./liquidity/UniV2LikeFactory"; 22 | export { UniV2LikeFactory__factory } from "./factories/liquidity/UniV2LikeFactory__factory"; 23 | export type { UniV2LikePair } from "./liquidity/UniV2LikePair"; 24 | export { UniV2LikePair__factory } from "./factories/liquidity/UniV2LikePair__factory"; 25 | export type { UniV2LikeRouter } from "./liquidity/UniV2LikeRouter"; 26 | export { UniV2LikeRouter__factory } from "./factories/liquidity/UniV2LikeRouter__factory"; 27 | export type { Erc20 } from "./token/Erc20"; 28 | export { Erc20__factory } from "./factories/token/Erc20__factory"; 29 | export type { Erc721 } from "./token/Erc721"; 30 | export { Erc721__factory } from "./factories/token/Erc721__factory"; 31 | export type { BeefyVaultMergedV6V7 } from "./vault/beefy/BeefyVaultMergedV6V7"; 32 | export { BeefyVaultMergedV6V7__factory } from "./factories/vault/beefy/BeefyVaultMergedV6V7__factory"; 33 | -------------------------------------------------------------------------------- /src/typechain/lido/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export type { LidoSTETH } from "./LidoSTETH"; 5 | export type { LidoWSTETH } from "./LidoWSTETH"; 6 | -------------------------------------------------------------------------------- /src/typechain/liquidity/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export type { UniV2LikeFactory } from "./UniV2LikeFactory"; 5 | export type { UniV2LikePair } from "./UniV2LikePair"; 6 | export type { UniV2LikeRouter } from "./UniV2LikeRouter"; 7 | -------------------------------------------------------------------------------- /src/typechain/token/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export type { Erc20 } from "./Erc20"; 5 | export type { Erc721 } from "./Erc721"; 6 | -------------------------------------------------------------------------------- /src/typechain/vault/beefy/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export type { BeefyVaultMergedV6V7 } from "./BeefyVaultMergedV6V7"; 5 | -------------------------------------------------------------------------------- /src/typechain/vault/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type * as beefy from "./beefy"; 5 | export type { beefy }; 6 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare type Optional = T | undefined; 2 | 3 | declare module 'snarkjs'; 4 | -------------------------------------------------------------------------------- /src/utils/__tests__/big-number.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { babylonianSqrt } from '../big-number'; 4 | 5 | chai.use(chaiAsPromised); 6 | const { expect } = chai; 7 | 8 | describe('big-number', () => { 9 | it('Should get babylonian square root', async () => { 10 | expect(babylonianSqrt(0n)).to.equal(0n); 11 | expect(babylonianSqrt(1n)).to.equal(1n); 12 | expect(babylonianSqrt(4n)).to.equal(2n); 13 | expect(babylonianSqrt(9n)).to.equal(3n); 14 | expect(babylonianSqrt(100n)).to.equal(10n); 15 | 16 | expect(babylonianSqrt(48293757902093n)).to.equal(6949371n); 17 | expect(babylonianSqrt(7388840002838n)).to.equal(2718242n); 18 | expect(babylonianSqrt(8327636263626362639238923n)).to.equal(2885764415822n); 19 | expect(babylonianSqrt(99999999999999999999999999n)).to.equal( 20 | 9999999999999n, 21 | ); 22 | expect(babylonianSqrt(822614601784652458980823543363n)).to.equal( 23 | 906981037169274n, 24 | ); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/utils/__tests__/fee.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | 4 | import { NetworkName } from '@railgun-community/shared-models'; 5 | import { 6 | getAmountToUnshieldForTarget, 7 | getUnshieldedAmountAfterFee, 8 | } from '../fee'; 9 | import { setRailgunFees } from '../../init'; 10 | import { 11 | MOCK_SHIELD_FEE_BASIS_POINTS, 12 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 13 | } from '../../test/mocks.test'; 14 | 15 | chai.use(chaiAsPromised); 16 | const { expect } = chai; 17 | 18 | const networkName = NetworkName.Ethereum; 19 | 20 | describe('fee', () => { 21 | before(() => { 22 | setRailgunFees( 23 | networkName, 24 | MOCK_SHIELD_FEE_BASIS_POINTS, 25 | MOCK_UNSHIELD_FEE_BASIS_POINTS, 26 | ); 27 | }); 28 | 29 | it('Should get target unshield amount after reverse fee calc', async () => { 30 | const postUnshieldAmount = BigInt('19949999999999999'); 31 | 32 | const targetUnshieldAmount = getAmountToUnshieldForTarget( 33 | networkName, 34 | postUnshieldAmount, 35 | ); 36 | expect(targetUnshieldAmount).to.equal(19999999999999998n); 37 | 38 | const unshieldedAmount = getUnshieldedAmountAfterFee( 39 | NetworkName.Ethereum, 40 | targetUnshieldAmount, 41 | ); 42 | expect(unshieldedAmount).to.equal(19949999999999999n); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/utils/__tests__/pair-rate.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | 4 | import { calculatePairRateWith18Decimals } from '../lp-pair'; 5 | 6 | chai.use(chaiAsPromised); 7 | const { expect } = chai; 8 | 9 | const USDC_DECIMALS = 6n; 10 | const WETH_DECIMALS = 18n; 11 | 12 | const oneInDecimals6 = 10n ** 6n; 13 | const oneInDecimals18 = 10n ** 18n; 14 | 15 | const reserveA = oneInDecimals6 * 2000n; 16 | const reserveB = oneInDecimals18 * 1n; 17 | 18 | describe('pair-rate', () => { 19 | it('Should get pair-rate for LP token pair', async () => { 20 | const pairRate = calculatePairRateWith18Decimals( 21 | reserveA, 22 | USDC_DECIMALS, 23 | reserveB, 24 | WETH_DECIMALS, 25 | ); 26 | expect(pairRate).to.equal(oneInDecimals18 * 2000n); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/utils/__tests__/token.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { getRandomNFTID } from '../token'; 4 | 5 | chai.use(chaiAsPromised); 6 | const { expect } = chai; 7 | 8 | describe('token', () => { 9 | it('Should get random NFT ID', async () => { 10 | const randomNFTID = getRandomNFTID(); 11 | expect(randomNFTID).to.be.a('bigint'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/utils/address.ts: -------------------------------------------------------------------------------- 1 | import { isAddress } from '@ethersproject/address'; 2 | 3 | export const validateContractAddress = (address: string) => { 4 | return isAddress(address); 5 | }; 6 | 7 | export const isPrefixedRailgunAddress = (address: string): boolean => { 8 | if (address.startsWith('0zk')) { 9 | return true; 10 | } 11 | if (address.startsWith('0x')) { 12 | return false; 13 | } 14 | throw new Error(`Invalid address: ${address}`); 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/basis-points.ts: -------------------------------------------------------------------------------- 1 | export const numToBasisPoints = (num: Optional): bigint => { 2 | return BigInt((num ?? 0) * 10000); 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/big-number.ts: -------------------------------------------------------------------------------- 1 | export const babylonianSqrt = (y: bigint): bigint => { 2 | let z = 0n; 3 | if (y > 3n) { 4 | z = y; 5 | let x = y / 2n + 1n; 6 | while (x < z) { 7 | z = x; 8 | x = (y / x + x) / 2n; 9 | } 10 | } else if (y > 0) { 11 | z = 1n; 12 | } 13 | return z; 14 | }; 15 | 16 | export const maxBigNumber = (b1: bigint, b2: bigint) => { 17 | return b1 > b2 ? b1 : b2; 18 | }; 19 | 20 | export const minBigNumber = (b1: bigint, b2: bigint) => { 21 | return b1 < b2 ? b1 : b2; 22 | }; 23 | 24 | export const maxBigNumberForTransaction = (): bigint => { 25 | return 2n ** 256n - 1n; 26 | }; 27 | -------------------------------------------------------------------------------- /src/utils/cookbook-debug.ts: -------------------------------------------------------------------------------- 1 | import { CookbookDebugger } from '../models/export-models'; 2 | 3 | export class CookbookDebug { 4 | private static debug: Optional; 5 | 6 | static setDebugger(debug: CookbookDebugger) { 7 | this.debug = debug; 8 | } 9 | 10 | static log(msg: string) { 11 | if (this.debug) { 12 | this.debug.log(msg); 13 | } 14 | } 15 | 16 | static error(err: Error, ignoreInTests = false) { 17 | if (this.debug) { 18 | this.debug.error(err); 19 | } 20 | if (process.env.NODE_ENV === 'test' && !ignoreInTests) { 21 | // eslint-disable-next-line no-console 22 | console.error(err); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/fee.ts: -------------------------------------------------------------------------------- 1 | import { RailgunConfig } from '../models/railgun-config'; 2 | import { NetworkName } from '@railgun-community/shared-models'; 3 | 4 | export const getUnshieldFee = ( 5 | networkName: NetworkName, 6 | preUnshieldAmount: bigint, 7 | ): bigint => { 8 | const unshieldFeeBasisPoints = 9 | RailgunConfig.getUnshieldFeeBasisPoints(networkName); 10 | return (preUnshieldAmount * unshieldFeeBasisPoints) / 10000n; 11 | }; 12 | 13 | export const getUnshieldedAmountAfterFee = ( 14 | networkName: NetworkName, 15 | preUnshieldAmount: bigint, 16 | ): bigint => { 17 | const fee = getUnshieldFee(networkName, preUnshieldAmount); 18 | return preUnshieldAmount - fee; 19 | }; 20 | 21 | export const getAmountToUnshieldForTarget = ( 22 | networkName: NetworkName, 23 | postUnshieldAmount: bigint, 24 | ) => { 25 | const unshieldFeeBasisPoints = 26 | RailgunConfig.getUnshieldFeeBasisPoints(networkName); 27 | return (postUnshieldAmount * 10000n) / (10000n - unshieldFeeBasisPoints); 28 | }; 29 | 30 | export const getShieldFee = ( 31 | networkName: NetworkName, 32 | preShieldAmount: bigint, 33 | ): bigint => { 34 | const shieldFeeBasisPoints = 35 | RailgunConfig.getShieldFeeBasisPoints(networkName); 36 | return (preShieldAmount * shieldFeeBasisPoints) / 10000n; 37 | }; 38 | 39 | export const getShieldedAmountAfterFee = ( 40 | networkName: NetworkName, 41 | preShieldAmount: bigint, 42 | ): bigint => { 43 | const fee = getShieldFee(networkName, preShieldAmount); 44 | return preShieldAmount - fee; 45 | }; 46 | -------------------------------------------------------------------------------- /src/utils/filters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecipeERC20Info, 3 | RecipeNFTInfo, 4 | StepOutputERC20Amount, 5 | } from '../models/export-models'; 6 | import { compareERC20Info } from './token'; 7 | 8 | export type ERC20AmountFilter = (erc20Amount: StepOutputERC20Amount) => boolean; 9 | 10 | export type NFTAmountFilter = (nftAmount: RecipeNFTInfo) => boolean; 11 | 12 | export const filterERC20AmountInputs = ( 13 | inputERC20Amounts: StepOutputERC20Amount[], 14 | filter: ERC20AmountFilter, 15 | ): { 16 | erc20AmountsForStep: StepOutputERC20Amount[]; 17 | unusedERC20Amounts: StepOutputERC20Amount[]; 18 | } => { 19 | const erc20AmountsForStep = inputERC20Amounts.filter(filter); 20 | const unusedERC20Amounts = inputERC20Amounts.filter( 21 | erc20Amount => !filter(erc20Amount), 22 | ); 23 | return { erc20AmountsForStep, unusedERC20Amounts }; 24 | }; 25 | 26 | export const filterNFTAmountInputs = ( 27 | inputNFTAmounts: RecipeNFTInfo[], 28 | filter: NFTAmountFilter, 29 | ): { 30 | nftAmountsForStep: RecipeNFTInfo[]; 31 | unusedNFTAmounts: RecipeNFTInfo[]; 32 | } => { 33 | const nftAmountsForStep = inputNFTAmounts.filter(filter); 34 | const unusedNFTAmounts = inputNFTAmounts.filter( 35 | erc20Amount => !filter(erc20Amount), 36 | ); 37 | return { nftAmountsForStep, unusedNFTAmounts }; 38 | }; 39 | 40 | export const findFirstInputERC20Amount = ( 41 | inputERC20Amounts: StepOutputERC20Amount[], 42 | erc20Info: RecipeERC20Info, 43 | ) => { 44 | const inputERC20Amount = inputERC20Amounts.find(erc20Amount => 45 | compareERC20Info(erc20Amount, erc20Info), 46 | ); 47 | if (!inputERC20Amount) { 48 | throw new Error( 49 | `First input for this recipe must contain ERC20 Amount: ${erc20Info.tokenAddress}.`, 50 | ); 51 | } 52 | return { 53 | tokenAddress: inputERC20Amount.tokenAddress, 54 | decimals: inputERC20Amount.decimals, 55 | amount: inputERC20Amount.expectedBalance, 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /src/utils/id.ts: -------------------------------------------------------------------------------- 1 | export const generateID = (length = 16) => { 2 | const CHARSET = 'abcdefghijklnopqrstuvwxyz0123456789'; 3 | let retVal = ''; 4 | for (let i = 0; i < length; i += 1) { 5 | retVal += CHARSET.charAt(Math.floor(Math.random() * CHARSET.length)); 6 | } 7 | return retVal; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './address'; 2 | export * from './filters'; 3 | export * from './no-action-output'; 4 | export * from './token'; 5 | export * from './wrap-util'; 6 | -------------------------------------------------------------------------------- /src/utils/lp-pair.ts: -------------------------------------------------------------------------------- 1 | import { UniswapV2Fork } from '../models'; 2 | 3 | const DECIMALS_18 = 10n ** 18n; 4 | 5 | export const getPairTokenDecimals = () => { 6 | return 18n; 7 | }; 8 | 9 | export const calculatePairRateWith18Decimals = ( 10 | reserveA: bigint, 11 | tokenDecimalsA: bigint, 12 | reserveB: bigint, 13 | tokenDecimalsB: bigint, 14 | ) => { 15 | const decimalsA = 10n ** tokenDecimalsA; 16 | const decimalsB = 10n ** tokenDecimalsB; 17 | 18 | const rateWith18Decimals = 19 | (reserveA * DECIMALS_18 * decimalsB) / reserveB / decimalsA; 20 | return rateWith18Decimals; 21 | }; 22 | 23 | const lpSymbols = (tokenSymbolA: string, tokenSymbolB: string) => { 24 | return `${tokenSymbolA}-${tokenSymbolB}`; 25 | }; 26 | 27 | export const getLPPoolName = ( 28 | uniswapV2Fork: UniswapV2Fork, 29 | tokenSymbolA: string, 30 | tokenSymbolB: string, 31 | ) => { 32 | return `${uniswapV2Fork} V2 ${lpSymbols(tokenSymbolA, tokenSymbolB)}`; 33 | }; 34 | 35 | export const getLPPairTokenName = ( 36 | uniswapV2Fork: UniswapV2Fork, 37 | tokenSymbolA: string, 38 | tokenSymbolB: string, 39 | ) => { 40 | return `${uniswapV2Fork} ${lpSymbols(tokenSymbolA, tokenSymbolB)} LP`; 41 | }; 42 | 43 | export const getLPPairTokenSymbol = ( 44 | tokenSymbolA: string, 45 | tokenSymbolB: string, 46 | ) => { 47 | return `${lpSymbols(tokenSymbolA, tokenSymbolB)} LP`; 48 | }; 49 | -------------------------------------------------------------------------------- /src/utils/no-action-output.ts: -------------------------------------------------------------------------------- 1 | import { StepInput, UnvalidatedStepOutput } from '../models/export-models'; 2 | 3 | export const createNoActionStepOutput = ( 4 | input: StepInput, 5 | ): UnvalidatedStepOutput => { 6 | return { 7 | crossContractCalls: [], 8 | 9 | outputERC20Amounts: input.erc20Amounts, 10 | 11 | outputNFTs: input.nfts, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/number.ts: -------------------------------------------------------------------------------- 1 | export const minBalanceAfterSlippage = ( 2 | balance: bigint, 3 | slippageBasisPoints: bigint, 4 | ): bigint => { 5 | const slippageMax = (balance * slippageBasisPoints) / 10000n; 6 | return balance - slippageMax; 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/random.ts: -------------------------------------------------------------------------------- 1 | import { getRandomBytes } from '@railgun-community/wallet'; 2 | 3 | export const getRandomShieldPrivateKey = () => { 4 | return getRandomBytes(32); 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/token.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'ethers'; 2 | import { 3 | RecipeERC20Info, 4 | StepOutputERC20Amount, 5 | } from '../models/export-models'; 6 | import { RailgunNFTAmount, isDefined } from '@railgun-community/shared-models'; 7 | 8 | export const getRandomNFTID = (): bigint => { 9 | const randomHex = Buffer.from(randomBytes(32)).toString('hex'); 10 | return BigInt(`0x${randomHex}`); 11 | }; 12 | 13 | export const compareNFTs = ( 14 | a: RailgunNFTAmount, 15 | b: RailgunNFTAmount, 16 | ): boolean => { 17 | return ( 18 | compareTokenAddress(a.nftAddress, b.nftAddress) && 19 | a.nftTokenType === b.nftTokenType && 20 | BigInt(a.tokenSubID) === BigInt(b.tokenSubID) && 21 | a.amount === b.amount 22 | ); 23 | }; 24 | 25 | export const compareTokenAddress = (a: string, b: string): boolean => { 26 | if (!a || !b) { 27 | return false; 28 | } 29 | return a.toLowerCase() === b.toLowerCase(); 30 | }; 31 | 32 | export const compareTokenAddresses = (list: string[], b: string): boolean => { 33 | if (!list.length || !b) { 34 | return false; 35 | } 36 | return list.find(a => compareTokenAddress(a, b)) != null; 37 | }; 38 | 39 | export const compareERC20Info = ( 40 | tokenA: RecipeERC20Info, 41 | tokenB: RecipeERC20Info, 42 | ): boolean => { 43 | return ( 44 | compareTokenAddress(tokenA.tokenAddress, tokenB.tokenAddress) && 45 | (tokenA.isBaseToken ?? false) === (tokenB.isBaseToken ?? false) 46 | ); 47 | }; 48 | 49 | export const isApprovedForSpender = ( 50 | erc20Amount: StepOutputERC20Amount, 51 | spender: Optional, 52 | ) => { 53 | return !isDefined(spender) || erc20Amount.approvedSpender === spender; 54 | }; 55 | -------------------------------------------------------------------------------- /src/utils/wrap-util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NETWORK_CONFIG, 3 | NetworkName, 4 | isDefined, 5 | } from '@railgun-community/shared-models'; 6 | import { RecipeERC20Info } from '../models/export-models'; 7 | 8 | export const getWrappedBaseToken = ( 9 | networkName: NetworkName, 10 | ): RecipeERC20Info => { 11 | const network = NETWORK_CONFIG[networkName]; 12 | if (!isDefined(network)) { 13 | throw new Error(`Unknown network: ${networkName}`); 14 | } 15 | return { 16 | tokenAddress: network.baseToken.wrappedAddress.toLowerCase(), 17 | decimals: BigInt(network.baseToken.decimals), 18 | isBaseToken: false, 19 | }; 20 | }; 21 | 22 | export const getBaseToken = (networkName: NetworkName): RecipeERC20Info => { 23 | return { 24 | ...getWrappedBaseToken(networkName), 25 | isBaseToken: true, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "inlineSources": true, 8 | "declaration": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "outDir": "dist", 13 | "strict": true, 14 | "baseUrl": "./src", 15 | "types": ["node", "mocha"], 16 | "typeRoots": ["./node_modules/@types", "./src/types"], 17 | "noImplicitAny": true, 18 | "strictNullChecks": true, 19 | "strictFunctionTypes": true, 20 | "strictPropertyInitialization": true, 21 | "noImplicitThis": true, 22 | "alwaysStrict": true, 23 | "removeComments": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "isolatedModules": true, 27 | "noImplicitReturns": true, 28 | "useUnknownInCatchVariables": false, 29 | "skipLibCheck": true 30 | }, 31 | "exclude": ["node_modules", "src/**/__tests__", "src/tests", "dist"], 32 | "ts-node": { 33 | "files": true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | /* Remove test code from 'exclude' array */ 5 | "exclude": ["node_modules", "dist"] 6 | } 7 | --------------------------------------------------------------------------------