├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── next-env.d.ts ├── next.config.js ├── package.json ├── post-build.sh ├── public ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── coingecko.svg ├── defillama-light-neutral.png ├── evil.png ├── evilr.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── gib.png ├── gibr.png ├── llamanote.png ├── loader.png ├── manifest.json ├── notfound.png ├── placeholder.png ├── robots.txt └── script.js ├── server ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── resources │ └── api-gateway-errors.yml ├── serverless.yml ├── src │ ├── Aggregator │ │ ├── adapters │ │ ├── constants.ts │ │ ├── list.ts │ │ ├── nativeTokens.ts │ │ ├── rpcs.ts │ │ ├── types.ts │ │ └── utils │ ├── WalletProvider │ ├── fallback.ts │ ├── generateTokenlist.ts │ ├── getDexAggregatorQuote.ts │ ├── submitSwap.ts │ └── tokenlists │ │ ├── constants.ts │ │ ├── getTokenList.ts │ │ ├── getTokensData.ts │ │ ├── multichain │ │ ├── 250.json │ │ └── anyswap.json │ │ ├── nativeTokens.ts │ │ ├── ownTokenlist.ts │ │ ├── s3.ts │ │ ├── types.ts │ │ └── utils.ts ├── tsconfig.json └── webpack.config.js ├── src ├── Theme │ ├── globals.css │ └── index.js ├── components │ ├── Aggregator │ │ ├── ConnectButton.tsx │ │ ├── Header.tsx │ │ ├── Loader.tsx │ │ ├── RoutesPreview.tsx │ │ ├── Settings.tsx │ │ ├── SwapConfirmation.tsx │ │ ├── adapters │ │ │ ├── 0x.ts │ │ │ ├── 0xGasless.ts │ │ │ ├── 0xV2.ts │ │ │ ├── 1inch.test.ts │ │ │ ├── 1inch.ts │ │ │ ├── airswap.ts │ │ │ ├── cowswap │ │ │ │ ├── abi.ts │ │ │ │ └── index.ts │ │ │ ├── firebird.ts │ │ │ ├── hashflow │ │ │ │ ├── abi.ts │ │ │ │ └── index.ts │ │ │ ├── krystal.ts │ │ │ ├── kyberswap.ts │ │ │ ├── lifi.ts │ │ │ ├── llamazip │ │ │ │ ├── encode.ts │ │ │ │ ├── index.ts │ │ │ │ └── pairs.ts │ │ │ ├── odos │ │ │ │ └── index.ts │ │ │ ├── openocean.ts │ │ │ ├── paraswap.ts │ │ │ ├── rango.ts │ │ │ ├── unidex.ts │ │ │ ├── utils.ts │ │ │ └── yieldyak │ │ │ │ ├── abi.ts │ │ │ │ └── index.ts │ │ ├── chainToCoingeckoId.ts │ │ ├── claimAbi.ts │ │ ├── constants.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useEstimateGas.ts │ │ │ ├── useToken.tsx │ │ │ └── useTokenApprove.ts │ │ ├── index.tsx │ │ ├── list.ts │ │ ├── nativeTokens.ts │ │ ├── router.ts │ │ ├── rpcs.ts │ │ ├── testAdapters.test.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── arbitrumFees.ts │ │ │ ├── getAllowance.ts │ │ │ ├── optimismFees.ts │ │ │ └── sendTx.ts │ ├── CloseBtn │ │ └── index.tsx │ ├── FAQs │ │ └── index.tsx │ ├── HistoryModal │ │ └── index.tsx │ ├── Icons │ │ └── index.tsx │ ├── InputAmountAndTokenSelect │ │ ├── TokenSelect.tsx │ │ └── index.tsx │ ├── Lending │ │ ├── NotFound.tsx │ │ ├── TokenInput.tsx │ │ ├── index.tsx │ │ ├── llama_wif_binoculars.png │ │ └── llamas_wif_coins.png │ ├── LiquidityByToken │ │ └── index.tsx │ ├── MultiSelect │ │ └── index.tsx │ ├── PriceImpact │ │ └── index.tsx │ ├── RefreshIcon │ │ └── index.tsx │ ├── Slippage │ │ └── index.tsx │ ├── SlippageChart │ │ └── index.tsx │ ├── SmolRefuel │ │ └── index.tsx │ ├── SwapRoute │ │ └── index.tsx │ ├── Tabs │ │ └── index.tsx │ ├── Tooltip │ │ └── index.tsx │ ├── TransactionModal │ │ └── index.tsx │ ├── WalletProvider │ │ ├── chains.ts │ │ └── index.ts │ └── Yields │ │ ├── Filters.tsx │ │ ├── List.tsx │ │ ├── MenuList.tsx │ │ ├── Panel.tsx │ │ └── index.tsx ├── constants │ └── breakpoints.ts ├── hooks │ ├── index.ts │ ├── useCountdown.ts │ ├── useDebounce.tsx │ ├── useLocalStorage.tsx │ ├── useQueryParams.tsx │ └── useSelectedChainAndTokens.tsx ├── layout │ ├── Phishing.tsx │ └── index.tsx ├── pages │ ├── _app.js │ ├── _document.js │ ├── index.tsx │ ├── testAdapters.tsx │ └── token-liquidity.tsx ├── props │ ├── getLendingProps.ts │ ├── getSandwichList.ts │ ├── getTokenList.ts │ ├── getTokensMaps.ts │ └── getYieldsProps.ts ├── queries │ ├── useBalance.tsx │ ├── useGetMCap.tsx │ ├── useGetPrice.tsx │ ├── useGetRoutes.tsx │ ├── useGetSavedTokens.tsx │ ├── useGetTokenLiquidity.tsx │ ├── useGetTokenList.tsx │ ├── useLendingProps.tsx │ ├── useSwapsHistory.tsx │ ├── useTokenBalances.tsx │ └── useYieldProps.tsx ├── types.ts └── utils │ ├── formatAddress.tsx │ ├── formatAmount.ts │ ├── formatToast.ts │ ├── getChartData.tsx │ ├── getTopRoute.tsx │ └── index.tsx ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "prettier"], 3 | "rules": { 4 | "no-undef": "warn", 5 | "no-unused-vars": "warn", 6 | "react/no-unescaped-entities": "off", 7 | "@next/next/no-img-element": "off" 8 | }, 9 | "env": { 10 | "es6": true 11 | } 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .next 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | /.vscode -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | .next 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | 13 | # misc 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | /.vscode -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "semi": true, 6 | "printWidth": 120 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Join the community & report bugs 2 | 3 | If you wish to report an issue, please join our [Discord](https://discord.swap.defillama.com/) 4 | 5 | If you want to learn about LlamaSwap, read the [Twitter Thread](https://twitter.com/DefiLlama/status/1609989799653285888) 6 | 7 | ### Integration 8 | 9 | The best way to integrate is through an iframe of our page, like this: 10 | 11 | ```html 12 | 25 | ``` 26 | 27 | The widget is responsive, so you can change the width and height in any way you want and the widget will adjust to fit the space. On top of that, you can customize the widget by adding the following params to the query url: 28 | 29 | - chain: default chain (eg `chain=ethereum`). This parameter is required 30 | - from: token to sell, to use the gas token for the chain use 0x0000000000000000000000000000000000000000 (eg `from=0x0000000000000000000000000000000000000000`) 31 | - to: token to buy, to use the gas token for the chain use 0x0000000000000000000000000000000000000000 (eg `to=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48`) 32 | - background: color of the background (eg `background=rgb(10,20,30)`) 33 | 34 | Note: only tokens that are part of our token lists are accepted in `from` and `to`, this is to prevent scammers linking to llamaswap with fake tokens loaded (eg a fake USDC) 35 | 36 | #### API integration 37 | 38 | Widget integrations are preferred cause: 39 | 40 | - Our widget handles all different dex integrations, which are quite different (cowswap requires signing a message while most others send a tx onchain) 41 | - Our widget shows warnings for price impact and other things that could impact negatively your users 42 | - In case there's any issue we can push a fix to everybody by just updating the site behind the iframe 43 | 44 | But if you'd prefer to instead integrate through our API please contact @0xngmi on discord through defillama's discord and ask for an api key. We are forced to use api keys because many of the underlying aggregators have rate limits, so we have to control the volume of requests we send to them. 45 | 46 | ### Running the app locally 47 | 48 | ``` 49 | yarn install 50 | yarn dev 51 | ``` 52 | 53 | Visit: http://localhost:3000/ 54 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: process.env.ANALYZE === 'true' 3 | }); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | reactStrictMode: true, 8 | staticPageGenerationTimeout: 1000, 9 | images: { 10 | unoptimized: true, // for cloudflare pages 11 | domains: ['icons.llama.fi', 'assets.coingecko.com', 'icons.llamao.fi'] 12 | }, 13 | compiler: { 14 | styledComponents: true 15 | }, 16 | async headers() { 17 | return [ 18 | { 19 | source: '/:path*', 20 | headers: [ 21 | { 22 | key: 'Access-Control-Allow-Origin', 23 | value: '*' 24 | }, 25 | { 26 | key: 'Access-Control-Allow-Methods', 27 | value: 'GET' 28 | }, 29 | { 30 | key: 'Access-Control-Allow-Headers', 31 | value: 'X-Requested-With, content-type, Authorization' 32 | } 33 | ] 34 | } 35 | ]; 36 | }, 37 | experimental: {} 38 | }; 39 | 40 | module.exports = withBundleAnalyzer(nextConfig); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "defillama", 3 | "sideEffects": false, 4 | "version": "1.0.0", 5 | "private": true, 6 | "homepage": "https://swap.defillama.com", 7 | "license": "GPL-3.0-or-later", 8 | "scripts": { 9 | "analyze": "ANALYZE=true yarn build", 10 | "dev": "next dev", 11 | "format": "prettier --write .", 12 | "build": "next build && ./post-build.sh", 13 | "start": "next start", 14 | "lint": "next lint" 15 | }, 16 | "dependencies": { 17 | "@chakra-ui/icons": "^2.0.12", 18 | "@chakra-ui/react": "2.8.2", 19 | "@defillama/sdk": "^3.0.25", 20 | "@emotion/react": "^11", 21 | "@emotion/styled": "^11", 22 | "@rainbow-me/rainbowkit": "2.1.6", 23 | "@tanstack/react-query": "5.55.4", 24 | "@tanstack/react-virtual": "^3.0.0-beta.36", 25 | "ariakit": "^2.0.0-next.41", 26 | "bignumber.js": "^9.0.2", 27 | "echarts": "^5.4.1", 28 | "framer-motion": "^6", 29 | "lodash": "^4.17.21", 30 | "next": "12.3.1", 31 | "react": "^18.2.0", 32 | "react-dom": "^18.2.0", 33 | "react-feather": "^2.0.10", 34 | "react-select": "^5.3.2", 35 | "styled-components": "^5.3.5", 36 | "viem": "2.28.0", 37 | "wagmi": "2.15.0" 38 | }, 39 | "devDependencies": { 40 | "@next/bundle-analyzer": "^12.1.6", 41 | "@types/node": "17.0.8", 42 | "@types/react": "17.0.38", 43 | "@types/styled-components": "^5.1.25", 44 | "eslint": "9.3.0", 45 | "eslint-config-next": "^12.3.1", 46 | "eslint-config-prettier": "9.1.0", 47 | "prettier": "3.2.5", 48 | "typescript": "5.4.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /post-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm ./.next/server/pages/404.html 4 | touch ./.next/server/pages/404.html 5 | rm ./.next/server/pages/500.html 6 | touch ./.next/server/pages/500.html 7 | -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/coingecko.svg: -------------------------------------------------------------------------------- 1 | CoinGecko -------------------------------------------------------------------------------- /public/defillama-light-neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/defillama-light-neutral.png -------------------------------------------------------------------------------- /public/evil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/evil.png -------------------------------------------------------------------------------- /public/evilr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/evilr.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/favicon.ico -------------------------------------------------------------------------------- /public/gib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/gib.png -------------------------------------------------------------------------------- /public/gibr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/gibr.png -------------------------------------------------------------------------------- /public/llamanote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/llamanote.png -------------------------------------------------------------------------------- /public/loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/loader.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Llama Swap", 3 | "name": "Llama Swap", 4 | "description": "Meta DEX aggregator", 5 | "icons": [ 6 | { 7 | "src": "favicon.ico", 8 | "sizes": "64x64 32x32 24x24 16x16", 9 | "type": "image/x-icon" 10 | } 11 | ], 12 | "start_url": ".", 13 | "display": "standalone", 14 | "theme_color": "#000000", 15 | "background_color": "#ffffff" 16 | } 17 | -------------------------------------------------------------------------------- /public/notfound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/notfound.png -------------------------------------------------------------------------------- /public/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/public/placeholder.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .webpack 3 | .env 4 | .serverless 5 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | Backend 2 | 3 | ``` 4 | nvm use 14 5 | npm run deploy 6 | ``` -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "llamaswap-backend", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "deploy": "export NODE_OPTIONS=--max-old-space-size=6144 && sls deploy --stage prod", 7 | "format": "prettier --write \"src/**/*.ts\"", 8 | "serve": "node --max-old-space-size=8192 node_modules/serverless/bin/serverless offline start", 9 | "test:watch": "jest --watch", 10 | "build": "sls webpack" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.13.10", 14 | "@babel/preset-env": "^7.13.12", 15 | "@babel/preset-typescript": "^7.13.0", 16 | "@types/aws-lambda": "^8.10.72", 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^8.2.2", 19 | "serverless": "^2.31.0", 20 | "serverless-prune-plugin": "^1.4.4", 21 | "serverless-webpack-fixed": "^5.3.3", 22 | "ts-loader": "^9.5.1", 23 | "typescript": "5.4.5", 24 | "webpack": "^5.27.2" 25 | }, 26 | "dependencies": { 27 | "@aws-sdk/client-s3": "^3.504.0", 28 | "@rainbow-me/rainbowkit": "^2.1.6", 29 | "bignumber.js": "^9.1.1", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "viem": "^2.21.10", 33 | "wagmi": "^2.12.12" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/resources/api-gateway-errors.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | GatewayResponseDefault4XX: 3 | Type: 'AWS::ApiGateway::GatewayResponse' 4 | Properties: 5 | ResponseParameters: 6 | gatewayresponse.header.Access-Control-Allow-Origin: "'*'" 7 | gatewayresponse.header.Access-Control-Allow-Headers: "'*'" 8 | ResponseType: DEFAULT_4XX 9 | RestApiId: 10 | Ref: 'ApiGatewayRestApi' 11 | GatewayResponseDefault5XX: 12 | Type: 'AWS::ApiGateway::GatewayResponse' 13 | Properties: 14 | ResponseParameters: 15 | gatewayresponse.header.Access-Control-Allow-Origin: "'*'" 16 | gatewayresponse.header.Access-Control-Allow-Headers: "'*'" 17 | ResponseType: DEFAULT_5XX 18 | RestApiId: 19 | Ref: 'ApiGatewayRestApi' 20 | -------------------------------------------------------------------------------- /server/serverless.yml: -------------------------------------------------------------------------------- 1 | service: llamaswap-backend 2 | 3 | package: 4 | individually: true 5 | useDotenv: true 6 | 7 | provider: 8 | name: aws 9 | runtime: nodejs18.x 10 | memorySize: 250 11 | region: eu-central-1 12 | endpointType: REGIONAL # Set to regional because the api gateway will be behind a cloudfront distribution 13 | stage: prod 14 | tracing: # Enable X-Ray tracing (debugging) 15 | apiGateway: true 16 | lambda: true 17 | iamRoleStatements: 18 | - Effect: Allow # X-Ray permissions 19 | Action: 20 | - xray:PutTraceSegments 21 | - xray:PutTelemetryRecords 22 | Resource: '*' 23 | - Effect: Allow # Lambda logs on cloudwatch 24 | Action: 25 | - logs:CreateLogGroup 26 | - logs:CreateLogStream 27 | - logs:PutLogEvents 28 | Resource: 29 | - 'Fn::Join': 30 | - ':' 31 | - - 'arn:aws:logs' 32 | - Ref: 'AWS::Region' 33 | - Ref: 'AWS::AccountId' 34 | - 'log-group:/aws/lambda/*:*:*' 35 | - Effect: 'Allow' 36 | Action: 37 | - 's3:ListBucket' 38 | - 's3:*' 39 | Resource: 'arn:aws:s3:::llama-tokenlists/*' 40 | environment: 41 | stage: ${self:custom.stage} 42 | 43 | custom: 44 | stage: ${opt:stage, self:provider.stage} 45 | webpack: 46 | webpackConfig: ./webpack.config.js 47 | packager: 'npm' 48 | excludeFiles: src/**/*.test.ts 49 | prune: 50 | automatic: true 51 | number: 5 # Number of versions to keep 52 | 53 | functions: 54 | fallback: 55 | handler: src/fallback.default 56 | events: 57 | - http: 58 | path: /{params+} 59 | method: any 60 | dexAggregatorQuote: 61 | handler: src/getDexAggregatorQuote.default 62 | timeout: 30 63 | memorySize: 512 64 | events: 65 | - http: 66 | path: dexAggregatorQuote 67 | method: post 68 | environment: 69 | EIGEN_API_KEY: ${env:EIGEN_API_KEY} 70 | HASHFLOW_API_KEY: ${env:HASHFLOW_API_KEY} 71 | OX_API_KEY: ${env:OX_API_KEY} 72 | INCH_API_KEY: ${env:INCH_API_KEY} 73 | submitSwap: 74 | handler: src/submitSwap.default 75 | timeout: 240 76 | memorySize: 512 77 | events: 78 | - http: 79 | path: submitSwap 80 | method: post 81 | environment: 82 | EIGEN_API_KEY: ${env:EIGEN_API_KEY} 83 | HASHFLOW_API_KEY: ${env:HASHFLOW_API_KEY} 84 | OX_API_KEY: ${env:OX_API_KEY} 85 | INCH_API_KEY: ${env:INCH_API_KEY} 86 | generateTokenlist: 87 | handler: src/generateTokenlist.default 88 | timeout: 900 89 | memorySize: 512 90 | events: 91 | - schedule: cron(0 * * * ? *) 92 | environment: 93 | CG_API_KEY: ${env:CG_API_KEY} 94 | 95 | resources: 96 | # CORS for api gateway errors 97 | - ${file(resources/api-gateway-errors.yml)} 98 | 99 | plugins: 100 | - serverless-webpack-fixed 101 | - serverless-prune-plugin 102 | -------------------------------------------------------------------------------- /server/src/Aggregator/adapters: -------------------------------------------------------------------------------- 1 | ../../../src/components/Aggregator/adapters/ -------------------------------------------------------------------------------- /server/src/Aggregator/constants.ts: -------------------------------------------------------------------------------- 1 | ../../../src/components/Aggregator/constants.ts -------------------------------------------------------------------------------- /server/src/Aggregator/list.ts: -------------------------------------------------------------------------------- 1 | ../../../src/components/Aggregator/list.ts -------------------------------------------------------------------------------- /server/src/Aggregator/nativeTokens.ts: -------------------------------------------------------------------------------- 1 | ../../../src/components/Aggregator/nativeTokens.ts -------------------------------------------------------------------------------- /server/src/Aggregator/rpcs.ts: -------------------------------------------------------------------------------- 1 | ../../../src/components/Aggregator/rpcs.ts -------------------------------------------------------------------------------- /server/src/Aggregator/types.ts: -------------------------------------------------------------------------------- 1 | ../../../src/components/Aggregator/types.ts -------------------------------------------------------------------------------- /server/src/Aggregator/utils: -------------------------------------------------------------------------------- 1 | ../../../src/components/Aggregator/utils/ -------------------------------------------------------------------------------- /server/src/WalletProvider: -------------------------------------------------------------------------------- 1 | ../../src/components/WalletProvider -------------------------------------------------------------------------------- /server/src/fallback.ts: -------------------------------------------------------------------------------- 1 | const handler = async ( 2 | event: AWSLambda.APIGatewayEvent 3 | ): Promise => { 4 | if(event.httpMethod === "OPTIONS"){ 5 | return { 6 | statusCode: 200, 7 | body: "", 8 | headers: { 9 | "cache-control": "max-age=3600, s-maxage=3600", // Caches preflight req on browser and proxy for 1 hour 10 | "access-control-allow-methods": "OPTIONS,GET", 11 | "access-control-allow-headers": 12 | "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent", 13 | "Access-Control-Allow-Origin": "*", 14 | }, 15 | }; 16 | } 17 | const response = { 18 | statusCode: 404, 19 | body: "This endpoint doesn't exist", 20 | headers: { 21 | "Cache-Control": `max-age=${3600}`, 22 | "Access-Control-Allow-Origin": "*", 23 | } 24 | } 25 | 26 | return response; 27 | }; 28 | 29 | export default handler; 30 | -------------------------------------------------------------------------------- /server/src/generateTokenlist.ts: -------------------------------------------------------------------------------- 1 | import { getTokenList } from './tokenlists/getTokenList'; 2 | import { storeJSONString } from './tokenlists/s3'; 3 | 4 | const handler = async () => { 5 | try { 6 | const tokenlists = await getTokenList(); 7 | await storeJSONString('tokenlists.json', JSON.stringify(tokenlists), 3600); 8 | // store token list by chain 9 | for (const chain in tokenlists) { 10 | const list = {} 11 | for (const token of tokenlists[chain]) { 12 | list[token.address] = token; 13 | } 14 | await storeJSONString(`tokenlists-${chain}.json`, JSON.stringify(list), 3600); 15 | } 16 | } catch (e) { 17 | console.log(e); 18 | throw e; 19 | } 20 | }; 21 | 22 | export default handler; -------------------------------------------------------------------------------- /server/src/getDexAggregatorQuote.ts: -------------------------------------------------------------------------------- 1 | import { adapters } from './Aggregator/list'; 2 | 3 | const handler = async (event: AWSLambda.APIGatewayEvent): Promise => { 4 | const { protocol, chain, from, to, amount } = event.queryStringParameters!; 5 | const body = JSON.parse(event.body!); 6 | const agg = adapters.find((ag) => ag.name === protocol); 7 | if (agg === undefined) { 8 | return { 9 | statusCode: 404, 10 | body: JSON.stringify({ message: 'No DEX Aggregator with that name' }), 11 | headers: { 12 | 'Cache-Control': `max-age=${3600}`, 13 | 'Access-Control-Allow-Origin': '*' 14 | } 15 | }; 16 | } 17 | const quote = await agg.getQuote(chain!, from!, to!, amount!, body!); 18 | return { 19 | statusCode: 200, 20 | body: JSON.stringify(quote), 21 | headers: { 22 | 'Cache-Control': `max-age=${10}`, 23 | 'Access-Control-Allow-Origin': '*' 24 | } 25 | }; 26 | }; 27 | 28 | export default handler; 29 | -------------------------------------------------------------------------------- /server/src/submitSwap.ts: -------------------------------------------------------------------------------- 1 | import { adapters } from './Aggregator/list'; 2 | 3 | const handler = async (event: AWSLambda.APIGatewayEvent): Promise => { 4 | const { protocol, chain } = event.queryStringParameters!; 5 | const body = JSON.parse(event.body!); 6 | const agg = adapters.find((ag) => ag.name === protocol); 7 | if (agg === undefined) { 8 | return { 9 | statusCode: 404, 10 | body: JSON.stringify({ message: 'No DEX Aggregator with that name' }), 11 | headers: { 12 | 'Cache-Control': `max-age=${3600}`, 13 | 'Access-Control-Allow-Origin': '*' 14 | } 15 | }; 16 | } 17 | if (!(agg as any).submitSwap) { 18 | return { 19 | statusCode: 400, 20 | body: JSON.stringify({ message: "Aggregator doesn't support submitting swap" }), 21 | headers: { 22 | 'Cache-Control': `max-age=${3600}`, 23 | 'Access-Control-Allow-Origin': '*' 24 | } 25 | }; 26 | } 27 | const res = await (agg as any).submitSwap({ chain, body }); 28 | return { 29 | statusCode: 200, 30 | body: JSON.stringify(res), 31 | headers: { 32 | 'Access-Control-Allow-Origin': '*' 33 | } 34 | }; 35 | }; 36 | 37 | export default handler; 38 | -------------------------------------------------------------------------------- /server/src/tokenlists/constants.ts: -------------------------------------------------------------------------------- 1 | ../../../src/components/Aggregator/constants.ts -------------------------------------------------------------------------------- /server/src/tokenlists/getTokensData.ts: -------------------------------------------------------------------------------- 1 | import { IToken } from './types'; 2 | import { getS3, storeJSONString } from './s3'; 3 | import { createPublicClient, erc20Abi } from 'viem'; 4 | import { allChains } from '../WalletProvider/chains'; 5 | import { rpcsTransports } from '../Aggregator/rpcs'; 6 | 7 | export const getTokensData = async ([chainId, tokens]: [string, Array]): Promise<[string, Array]> => { 8 | const filename = `erc20/${chainId}`; 9 | let storedTokenMetadata; 10 | try { 11 | storedTokenMetadata = JSON.parse((await getS3(filename)).body!); 12 | } catch (e) { 13 | storedTokenMetadata = {}; 14 | } 15 | 16 | const missingTokens = tokens.filter( 17 | (token) => token !== '' && storedTokenMetadata[token.toLowerCase()] === undefined && token.length === 42 18 | ); 19 | 20 | try { 21 | const chain = allChains.find((c) => c.id === +chainId); 22 | 23 | if (!chain) { 24 | throw new Error(`Chain ${chainId} not found`); 25 | } 26 | 27 | const publicClient = createPublicClient({ 28 | chain, 29 | transport: rpcsTransports[chainId], 30 | batch: { 31 | multicall: { 32 | wait: 5_000 33 | } 34 | } 35 | }); 36 | 37 | const names = await publicClient.multicall({ 38 | contracts: missingTokens.map((token) => ({ 39 | address: token as `0x${string}`, 40 | abi: erc20Abi, 41 | functionName: 'name' 42 | })), 43 | allowFailure: true 44 | }); 45 | 46 | const symbols = await publicClient.multicall({ 47 | contracts: missingTokens.map((token) => ({ 48 | address: token as `0x${string}`, 49 | abi: erc20Abi, 50 | functionName: 'symbol' 51 | })), 52 | allowFailure: true 53 | }); 54 | 55 | const decimals = await publicClient.multicall({ 56 | contracts: missingTokens.map((token) => ({ 57 | address: token as `0x${string}`, 58 | abi: erc20Abi, 59 | functionName: 'decimals' 60 | })), 61 | allowFailure: true 62 | }); 63 | 64 | const data: any[] = []; 65 | let changed = false; 66 | missingTokens.forEach((token, i) => { 67 | const name = names[i]; 68 | const symbol = symbols[i]; 69 | const decimal = decimals[i]; 70 | 71 | if (name.status === 'success' && symbol.status === 'success' && decimal.status === 'success') { 72 | changed = true; 73 | storedTokenMetadata[token.toLowerCase()] = { 74 | name: name.result, 75 | symbol: symbol.result, 76 | decimals: decimal.result 77 | }; 78 | } 79 | }); 80 | 81 | tokens.forEach((address) => { 82 | const info = storedTokenMetadata[address.toLowerCase()]; 83 | if (info) { 84 | data.push({ 85 | name: info.name, 86 | symbol: info.symbol, 87 | decimals: info.decimals, 88 | address: address, 89 | chainId, 90 | geckoId: null, 91 | logoURI: null, 92 | isGeckoToken: true 93 | }); 94 | } else { 95 | console.log(`[ERROR] [TokensMetaData] ${chainId} ${address} not found`); 96 | } 97 | }); 98 | 99 | if (changed) { 100 | await storeJSONString(filename, JSON.stringify(storedTokenMetadata)); 101 | } 102 | 103 | return [chainId, data]; 104 | } catch (error) { 105 | console.log(`[ERROR] [GetTokensData] ${chainId} ${error}`); 106 | return [chainId, []]; 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /server/src/tokenlists/nativeTokens.ts: -------------------------------------------------------------------------------- 1 | ../../../src/components/Aggregator/nativeTokens.ts -------------------------------------------------------------------------------- /server/src/tokenlists/ownTokenlist.ts: -------------------------------------------------------------------------------- 1 | export const ownTokenList = [ 2 | { 3 | address: '0x3082CC23568eA640225c2467653dB90e9250AaA0', 4 | chainId: 42161, 5 | decimals: 18, 6 | logoURI: 7 | 'https://raw.githubusercontent.com/sushiswap/list/master/logos/token-logos/network/arbitrum/0x0C4681e6C0235179ec3D4F4fc4DF3d14FDD96017.jpg', 8 | name: 'Radiant v2', 9 | symbol: 'RDNT' 10 | }, 11 | { 12 | address: '0x0c4681e6c0235179ec3d4f4fc4df3d14fdd96017', 13 | chainId: 42161, 14 | decimals: 18, 15 | logoURI: 16 | 'https://raw.githubusercontent.com/traderjoe-xyz/joe-tokenlists/main/logos/0x0C4681e6C0235179ec3D4F4fc4DF3d14FDD96017/logo.png', 17 | ownLogoURI: 18 | 'https://raw.githubusercontent.com/traderjoe-xyz/joe-tokenlists/main/logos/0x0C4681e6C0235179ec3D4F4fc4DF3d14FDD96017/logo.png', 19 | name: 'Radiant OLD', 20 | symbol: 'RDNT' 21 | }, 22 | { 23 | symbol: 'USDC.e', 24 | chainId: 42161, 25 | name: 'USD Coin (Bridged)', 26 | decimals: 6, 27 | address: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', 28 | logoURI: 'https://tokens.1inch.io/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png' 29 | }, 30 | { 31 | symbol: 'USDC', 32 | chainId: 42161, 33 | name: 'USD Coin', 34 | decimals: 6, 35 | address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', 36 | logoURI: 'https://tokens.1inch.io/0xaf88d065e77c8cc2239327c5edb3a432268e5831.png' 37 | }, 38 | { 39 | symbol: 'WWETH', 40 | chainId: 137, 41 | name: 'Wormhole WETH', 42 | decimals: 18, 43 | address: '0x11cd37bb86f65419713f30673a480ea33c826872', 44 | logoURI: 'https://token-icons.llamao.fi/icons/tokens/137/0x11cd37bb86f65419713f30673a480ea33c826872?h=20&w=20' 45 | } 46 | ]; 47 | -------------------------------------------------------------------------------- /server/src/tokenlists/s3.ts: -------------------------------------------------------------------------------- 1 | import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectsCommand } from '@aws-sdk/client-s3'; 2 | 3 | const datasetBucket = 'llama-tokenlists'; 4 | const client = new S3Client(); 5 | 6 | export function storeJSONString(filename: string, body: string, cache?: number) { 7 | const command = new PutObjectCommand({ 8 | Bucket: datasetBucket, 9 | Key: filename, 10 | Body: body, 11 | ContentType: 'application/json', 12 | ACL: 'public-read', 13 | ...(!!cache 14 | ? { 15 | CacheControl: `max-age=${cache}` 16 | } 17 | : {}) 18 | }); 19 | return client.send(command); 20 | } 21 | 22 | export async function getS3(filename: string) { 23 | const command = new GetObjectCommand({ 24 | Bucket: datasetBucket, 25 | Key: filename 26 | }); 27 | const data = await client.send(command); 28 | return { 29 | body: await data.Body?.transformToString(), 30 | lastModified: data.LastModified 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /server/src/tokenlists/types.ts: -------------------------------------------------------------------------------- 1 | export interface IToken { 2 | address: string; 3 | label: string; 4 | value: string; 5 | logoURI: string; 6 | logoURI2?: string | null; 7 | symbol: string; 8 | decimals: number; 9 | name: string; 10 | chainId: number; 11 | amount?: string | number; 12 | balanceUSD?: number; 13 | geckoId: string | null; 14 | isGeckoToken?: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /server/src/tokenlists/utils.ts: -------------------------------------------------------------------------------- 1 | export const normalizeTokens = (t0 = '0', t1 = '0') => { 2 | if (!t0 || !t1) return null; 3 | 4 | return Number(t0) < Number(t1) ? [t0.toLowerCase(), t1.toLowerCase()] : [t1.toLowerCase(), t0.toLowerCase()]; 5 | }; 6 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | }, 7 | "compilerOptions": { 8 | "target": "ES2022", 9 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": false, 15 | "esModuleInterop": true, 16 | "module": "ESNext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "incremental": true, 22 | "downlevelIteration": true, 23 | "allowSyntheticDefaultImports": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noImplicitAny": false, 26 | "strictNullChecks": true, 27 | "noImplicitReturns": false, 28 | "noImplicitThis": false, 29 | "noUnusedLocals": false 30 | }, 31 | "include": ["src"], 32 | "exclude": ["src/setupTestEnv.js"] 33 | } 34 | -------------------------------------------------------------------------------- /server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const slsw = require('serverless-webpack-fixed'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: slsw.lib.entries, 6 | target: 'node18', 7 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.ts$/, 12 | use: 'ts-loader', 13 | include: [ 14 | path.resolve(__dirname, 'src'), 15 | path.resolve(__dirname, '../src/components/Aggregator'), 16 | path.resolve(__dirname, '../src/components/WalletProvider') 17 | ], 18 | exclude: /node_modules/ 19 | }, 20 | { 21 | test: /\.js$/, 22 | include: __dirname, 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'babel-loader' 26 | } 27 | }, 28 | { 29 | test: /\.mjs$/, 30 | resolve: { mainFields: ['default'] } 31 | }, 32 | { 33 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 34 | type: 'asset/resource' 35 | } 36 | ] 37 | }, 38 | externals: { 39 | 'node:crypto': 'commonjs crypto', 40 | }, 41 | resolve: { 42 | extensions: ['.ts', '.js', '.json'], 43 | alias: { 44 | 'bignumber.js$': 'bignumber.js/bignumber.js', 45 | 'node-fetch$': 'node-fetch/lib/index.js' 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/Theme/globals.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | 3 | :root { 4 | --font-inter: 'Inter var', sans-serif; 5 | } 6 | 7 | @supports (font-variation-settings: normal) { 8 | :root { 9 | --font-inter: 'Inter var', sans-serif; 10 | } 11 | } 12 | 13 | html, 14 | body { 15 | margin: 0; 16 | padding: 0; 17 | width: 100%; 18 | height: 100%; 19 | } 20 | 21 | html { 22 | font-family: var(--font-inter); 23 | } 24 | 25 | a { 26 | text-decoration: none; 27 | color: inherit; 28 | cursor: pointer; 29 | } 30 | 31 | html { 32 | font-size: 1rem; 33 | font-variant: none; 34 | color: 'black'; 35 | -webkit-font-smoothing: antialiased; 36 | -moz-osx-font-smoothing: grayscale; 37 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 38 | -webkit-box-sizing: border-box; 39 | -moz-box-sizing: border-box; 40 | box-sizing: border-box; 41 | } 42 | 43 | * { 44 | margin: 0; 45 | } 46 | 47 | *, 48 | *:before, 49 | *:after { 50 | -webkit-box-sizing: inherit; 51 | -moz-box-sizing: inherit; 52 | box-sizing: inherit; 53 | } 54 | 55 | body { 56 | font-size: 0.875rem; 57 | } 58 | 59 | input, 60 | button, 61 | textarea, 62 | select { 63 | font: inherit; 64 | color: inherit; 65 | } 66 | 67 | button { 68 | background: none; 69 | border: none; 70 | cursor: pointer; 71 | } 72 | 73 | img, 74 | picture, 75 | video, 76 | canvas, 77 | svg { 78 | display: block; 79 | max-width: 100%; 80 | } 81 | 82 | p, 83 | h1, 84 | h2, 85 | h3, 86 | h4, 87 | h5, 88 | h6 { 89 | overflow-wrap: break-word; 90 | } 91 | 92 | 93 | .dialog-backdrop { 94 | background: rgba(0, 0, 0, 0.7); 95 | } -------------------------------------------------------------------------------- /src/Theme/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle } from 'styled-components'; 3 | import { sm, med, lg, xl, twoXl } from '~/constants/breakpoints'; 4 | 5 | export default function ThemeProvider({ children }) { 6 | return {children}; 7 | } 8 | 9 | export const getStyle = (name, defaultValue) => { 10 | const queryParams = new URLSearchParams(window.location.search); 11 | 12 | return queryParams.get(name) || defaultValue; 13 | }; 14 | 15 | const theme = (mode = 'dark') => { 16 | const colorMode = getStyle('mode', mode) 17 | const isDark = colorMode === 'dark' 18 | 19 | return { 20 | mode: isDark ? 'dark' : 'light', 21 | 22 | text1: '#FAFAFA', 23 | text2: '#C3C5CB', 24 | text3: '#6C7284', 25 | text4: '#565A69', 26 | text5: '#2C2F36', 27 | 28 | // special case text types 29 | white: '#FFFFFF', 30 | 31 | // backgrounds / greys 32 | bg1: '#212429', 33 | bg2: '#2C2F36', 34 | bg3: '#40444F', 35 | bg4: '#565A69', 36 | bg5: '#565A69', 37 | bg6: 'rgb(20 22 25)', 38 | bg7: 'rgba(7,14,15,0.7)', 39 | 40 | //specialty colors 41 | background: '#22242A', 42 | advancedBG: 'rgba(0,0,0,0.1)', 43 | divider: 'rgba(43, 43, 43, 0.435)', 44 | 45 | //primary colors 46 | primary1: '#2172E5', 47 | 48 | // other 49 | red1: '#FF6871', 50 | green1: '#27AE60', 51 | link: '#2172E5', 52 | blue: '#2f80ed', 53 | 54 | //shadow 55 | shadowSm: '0 1px 2px 0 rgb(0 0 0 / 0.05)', 56 | shadow: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', 57 | shadowMd: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', 58 | shadowLg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', 59 | 60 | // breakpoints 61 | bpSm: `${sm}rem`, 62 | bpMed: `${med}rem`, 63 | bpLg: `${lg}rem`, 64 | bpXl: `${xl}rem`, 65 | bp2Xl: `${twoXl}rem`, 66 | 67 | maxSm: `@media screen and (max-width: ${sm}rem)`, 68 | maxMed: `@media screen and (max-width: ${med}rem)`, 69 | maxLg: `@media screen and (max-width: ${lg}rem)`, 70 | maxXl: `@media screen and (max-width: ${xl}rem)`, 71 | 72 | minSm: `@media screen and (min-width: ${sm}rem)`, 73 | minMed: `@media screen and (min-width: ${med}rem)`, 74 | minLg: `@media screen and (min-width: ${lg}rem)`, 75 | minXl: `@media screen and (min-width: ${xl}rem)`, 76 | min2Xl: `@media screen and (min-width: ${twoXl}rem)`, 77 | 78 | breakpoints: [`${sm}rem`, `${med}rem`, `${lg}rem`, `${xl}rem`] 79 | }; 80 | } 81 | 82 | export const GlobalStyle = createGlobalStyle` 83 | body, #__next { 84 | background-color: ${({ theme }) => theme.background}; 85 | } 86 | 87 | #__next { 88 | display: flex; 89 | flex-direction: column; 90 | width: 100%; 91 | min-height: 100%; 92 | position: relative; 93 | color: ${({ theme }) => theme.text1}; 94 | isolation: isolate; 95 | 96 | ${({ theme: { minLg } }) => minLg} { 97 | flex-direction: row; 98 | } 99 | } 100 | 101 | #__next, 102 | .chakra-modal__overlay, 103 | .chakra-modal__content-container { 104 | filter: ${({ theme }) => (theme.mode === 'light' ? 'invert(1) hue-rotate(180deg)' : undefined)}; 105 | } 106 | 107 | #__next img, 108 | .chakra-modal__content-container img, 109 | button[data-testid=rk-connect-button] { 110 | filter: ${({ theme }) => (theme.mode === 'light' ? 'invert(1) hue-rotate(180deg)' : undefined)}; 111 | } 112 | 113 | a, input, button, textarea, select { 114 | &:focus-visible { 115 | outline: 1px solid ${({ theme }) => theme.text1}; 116 | } 117 | } 118 | 119 | .visually-hidden { 120 | position: absolute; 121 | width: 1px; 122 | height: 1px; 123 | padding: 0; 124 | margin: -1px; 125 | overflow: hidden; 126 | clip: rect(0, 0, 0, 0); 127 | white-space: nowrap; 128 | border-width: 0; 129 | } 130 | 131 | .tooltip-trigger { 132 | color: ${({ theme }) => theme.text1}; 133 | display: flex; 134 | align-items: center; 135 | padding: 0; 136 | 137 | :focus-visible { 138 | outline-offset: 2px; 139 | } 140 | } 141 | 142 | .tooltip-trigger a { 143 | display: flex; 144 | } 145 | `; 146 | -------------------------------------------------------------------------------- /src/components/Aggregator/ConnectButton.tsx: -------------------------------------------------------------------------------- 1 | import { ConnectButton } from '@rainbow-me/rainbowkit'; 2 | import styled from 'styled-components'; 3 | import { HistoryModal } from '../HistoryModal'; 4 | 5 | const Wrapper = styled.div` 6 | position: absolute; 7 | right: 0px; 8 | z-index: 100; 9 | display: flex; 10 | gap: 8px; 11 | `; 12 | 13 | const Connect = ({ tokenList = null, tokensUrlMap = {}, tokensSymbolsMap = {} }) => { 14 | return ( 15 | 16 | 17 | {tokenList ? : null} 18 | 19 | ); 20 | }; 21 | 22 | export default Connect; 23 | -------------------------------------------------------------------------------- /src/components/Aggregator/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Heading, Image } from '@chakra-ui/react'; 2 | import styled from 'styled-components'; 3 | import loaderImg from '~/public/loader.png'; 4 | 5 | const Wrapper = styled.div` 6 | position: absolute; 7 | z-index: 100; 8 | display: flex; 9 | justify-content: space-between; 10 | width: calc(100% - 32px); 11 | `; 12 | 13 | const Name = styled(Heading)` 14 | font-size: 26px; 15 | `; 16 | 17 | const Header = ({ children }) => { 18 | return ( 19 | 20 | window.open('https://swap.defillama.com/')} 25 | cursor="pointer" 26 | > 27 | logo 34 | LlamaSwap 35 | 36 | {children} 37 | 38 | ); 39 | }; 40 | 41 | export default Header; 42 | -------------------------------------------------------------------------------- /src/components/Aggregator/Loader.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import loaderImg from '~/public/loader.png'; 3 | 4 | const LoaderWrapper = styled.div` 5 | margin: 0 auto; 6 | margin-top: 72px; 7 | `; 8 | 9 | const LoaderText = styled.div` 10 | margin-top: 8px; 11 | font-size: 20px; 12 | font-weight: 500; 13 | text-align: center; 14 | padding-left: 8px; 15 | `; 16 | 17 | const LoaderBody = styled.img` 18 | width: 120px; 19 | height: 120px; 20 | -webkit-animation: spin 3s linear infinite; 21 | -moz-animation: spin 3s linear infinite; 22 | animation: spin 3s linear infinite; 23 | @-moz-keyframes spin { 24 | 100% { 25 | -moz-transform: rotate(360deg); 26 | } 27 | } 28 | @-webkit-keyframes spin { 29 | 100% { 30 | -webkit-transform: rotate(360deg); 31 | } 32 | } 33 | @keyframes spin { 34 | 100% { 35 | -webkit-transform: rotate(360deg); 36 | transform: rotate(360deg); 37 | } 38 | } 39 | `; 40 | 41 | const Loader = (props) => { 42 | return ( 43 | 44 | 45 | Loading... 46 | 47 | ); 48 | }; 49 | 50 | export default Loader; 51 | -------------------------------------------------------------------------------- /src/components/Aggregator/RoutesPreview.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLinkIcon } from '@chakra-ui/icons'; 2 | import { Box, Flex, Heading, Link, Text } from '@chakra-ui/react'; 3 | import styled from 'styled-components'; 4 | import { AggIcons, LlamaIcon, SmolCheck } from '../Icons'; 5 | 6 | const IconsBody = styled.div` 7 | display: flex; 8 | width: fit-content; 9 | overflow: hidden; 10 | margin-top: 16px; 11 | padding-left: 48px; 12 | padding-right: 48px; 13 | padding-bottom: 16px; 14 | 15 | animation: slide infinite linear 10s; 16 | 17 | @keyframes slide { 18 | 0% { 19 | transform: translate3d(-60px, 0, 0); 20 | } 21 | 100% { 22 | transform: translate3d(-540px, 0, 0); 23 | } 24 | } 25 | `; 26 | 27 | const MainIcon = styled.div` 28 | z-index: 1; 29 | position: absolute; 30 | left: 50%; 31 | top: 50%; 32 | transform: translate(-50%, -50%); 33 | box-shadow: 0px 16.4384px 92.0548px 13.1507px #121315; 34 | `; 35 | 36 | const IconElem = styled.div` 37 | box-shadow: 0px 2.63014px 15.7808px rgba(0, 0, 0, 0.45); 38 | width: 48px; 39 | height: 48px; 40 | margin-right: 48px; 41 | `; 42 | 43 | const Header = styled.div` 44 | position: relative; 45 | `; 46 | 47 | const CheckBody = styled.div` 48 | color: rgb(112, 160, 247); 49 | display: flex; 50 | justify-content: space-around; 51 | margin-top: 16px; 52 | `; 53 | 54 | const CheckWithText = ({ text }: { text: string }) => { 55 | return ( 56 |
57 | {SmolCheck}
{text}
58 |
59 | ); 60 | }; 61 | 62 | const RoutesPreview = () => { 63 | return ( 64 | 65 |
66 | {LlamaIcon} 67 | 68 | 69 | {[...AggIcons, ...AggIcons].map((Icon, i) => ( 70 | {Icon} 71 | ))} 72 | 73 |
74 | 75 | 76 | The Aggregator of Aggregators 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | LlamaSwap looks for the best route for your trade
among a variety of Dex Aggregators, guaranteeing you{' '} 86 |
the best execution prices in DeFi. 87 |

Try it now or{' '} 88 | 89 | learn more 90 | 91 | 92 |
93 |
94 |
95 | ); 96 | }; 97 | 98 | export default RoutesPreview; 99 | -------------------------------------------------------------------------------- /src/components/Aggregator/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { InfoOutlineIcon } from '@chakra-ui/icons'; 2 | import { 3 | Button, 4 | Checkbox, 5 | Heading, 6 | HStack, 7 | List, 8 | ListItem, 9 | Modal, 10 | ModalBody, 11 | ModalCloseButton, 12 | ModalContent, 13 | ModalFooter, 14 | ModalHeader, 15 | ModalOverlay, 16 | Switch, 17 | Tooltip, 18 | useDisclosure 19 | } from '@chakra-ui/react'; 20 | import { chunk } from 'lodash'; 21 | import { useLocalStorage } from '~/hooks/useLocalStorage'; 22 | 23 | export const Settings = ({ adapters, disabledAdapters, setDisabledAdapters, onClose: onExternalClose }) => { 24 | const [isDegenModeEnabled, setIsDegenModeEnabled] = useLocalStorage('llamaswap-degenmode', false); 25 | const { isOpen, onClose } = useDisclosure({ defaultIsOpen: true }); 26 | const onCloseClick = () => { 27 | onExternalClose(); 28 | onClose(); 29 | }; 30 | const onClick = (name) => (e) => { 31 | const isChecked = e.target.checked; 32 | 33 | setDisabledAdapters((adaptersState) => 34 | isChecked ? adaptersState.filter((adapterName) => adapterName !== name) : adaptersState.concat(name) 35 | ); 36 | }; 37 | const aggregatorChunks = chunk(adapters as Array, 5); 38 | return ( 39 | <> 40 | 41 | 42 | 43 | Settings 44 | 45 | 46 | 47 | Degen Mode{' '} 48 | 49 | 50 | 51 | setIsDegenModeEnabled((mode) => !mode)} isChecked={isDegenModeEnabled} /> 52 | 53 | Enabled Aggregators 54 | 55 | 56 | {aggregatorChunks.map((aggs) => ( 57 | 58 | {aggs.map((name) => ( 59 | 60 | 61 | {name} 62 | 63 | ))} 64 | 65 | ))} 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/Aggregator/SwapConfirmation.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Input, 4 | Popover, 5 | PopoverArrow, 6 | PopoverBody, 7 | PopoverCloseButton, 8 | PopoverContent, 9 | PopoverHeader, 10 | PopoverTrigger, 11 | useDisclosure 12 | } from '@chakra-ui/react'; 13 | import * as React from 'react'; 14 | 15 | const SwapConfiramtion = ({ 16 | handleSwap, 17 | isUnknownPrice = false, 18 | isMaxPriceImpact = false, 19 | isDegenModeEnabled = false 20 | }) => { 21 | const { isOpen, onToggle, onClose } = useDisclosure(); 22 | const requiredText = isMaxPriceImpact ? 'trade' : 'confirm'; 23 | const [value, setValue] = React.useState(''); 24 | const isSwapDisabled = value?.toLowerCase() !== requiredText; 25 | 26 | const onPopoverClose = () => { 27 | setValue(''); 28 | onClose(); 29 | }; 30 | 31 | return ( 32 | <> 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | Swap Confirmation. 43 | 44 | {isUnknownPrice ? ( 45 | 46 | We can't get price for one of your tokens.
47 | Check output amount of the selected route carefully. 48 | 51 |
52 | ) : ( 53 | 54 | Price impact is too high. 55 |
56 | You'll likely lose money. 57 |
58 | {!isDegenModeEnabled ? ( 59 | <> 60 | Type "{requiredText}" to make a swap. 61 | setValue(e.target.value)} 65 | value={value} 66 | > 67 | 68 | ) : null} 69 | 77 |
78 | )} 79 |
80 |
81 | 82 | ); 83 | }; 84 | 85 | export default SwapConfiramtion; 86 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/0x.ts: -------------------------------------------------------------------------------- 1 | import { defillamaReferrerAddress } from '../constants'; 2 | import { sendTx } from '../utils/sendTx'; 3 | import { zeroAddress } from 'viem'; 4 | 5 | export const chainToId = { 6 | ethereum: 'https://api.0x.org/', 7 | bsc: 'https://bsc.api.0x.org/', 8 | polygon: 'https://polygon.api.0x.org/', 9 | optimism: 'https://optimism.api.0x.org/', 10 | arbitrum: 'https://arbitrum.api.0x.org/', 11 | avax: 'https://avalanche.api.0x.org/', 12 | fantom: 'https://fantom.api.0x.org/', 13 | celo: 'https://celo.api.0x.org/', 14 | base: 'http://base.api.0x.org/' 15 | }; 16 | 17 | export const name = 'Matcha/0x'; 18 | export const token = 'ZRX'; 19 | export const isOutputAvailable = true; 20 | 21 | export function approvalAddress() { 22 | // https://docs.0x.org/0x-api-swap/guides/swap-tokens-with-0x-api 23 | return '0xdef1c0ded9bec7f1a1670819833240f027b25eff'; 24 | } 25 | 26 | const nativeToken = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; 27 | const feeCollectorAddress = '0x9Ab6164976514F1178E2BB4219DA8700c9D96E9A'; 28 | 29 | export async function getQuote(chain: string, from: string, to: string, amount: string, extra) { 30 | // amount should include decimals 31 | 32 | const tokenFrom = from === zeroAddress ? nativeToken : from; 33 | const tokenTo = to === zeroAddress ? nativeToken : to; 34 | const amountParam = 35 | extra.amountOut && extra.amountOut !== '0' ? `buyAmount=${extra.amountOut}` : `sellAmount=${amount}`; 36 | 37 | const data = await fetch( 38 | `${chainToId[chain]}swap/v1/quote?buyToken=${tokenTo}&${amountParam}&sellToken=${tokenFrom}&slippagePercentage=${ 39 | extra.slippage / 100 40 | }&affiliateAddress=${defillamaReferrerAddress}&enableSlippageProtection=false&intentOnFilling=true&takerAddress=${ 41 | extra.userAddress 42 | }&skipValidation=true&feeRecipientTradeSurplus=${feeCollectorAddress}`, 43 | { 44 | headers: { 45 | '0x-api-key': process.env.OX_API_KEY as string 46 | } 47 | } 48 | ).then((r) => r.json()); 49 | 50 | return { 51 | amountReturned: data?.buyAmount || 0, 52 | amountIn: data?.sellAmount || 0, 53 | estimatedGas: data.gas, 54 | tokenApprovalAddress: data.to, 55 | rawQuote: data, 56 | logo: 'https://www.gitbook.com/cdn-cgi/image/width=40,height=40,fit=contain,dpr=2,format=auto/https%3A%2F%2F1690203644-files.gitbook.io%2F~%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252FKX9pG8rH3DbKDOvV7di7%252Ficon%252F1nKfBhLbPxd2KuXchHET%252F0x%2520logo.png%3Falt%3Dmedia%26token%3D25a85a3e-7f72-47ea-a8b2-e28c0d24074b' 57 | }; 58 | } 59 | 60 | export async function swap({ fromAddress, rawQuote, chain }) { 61 | const tx = await sendTx({ 62 | from: fromAddress, 63 | to: rawQuote.to, 64 | data: rawQuote.data, 65 | value: rawQuote.value, 66 | }); 67 | 68 | return tx; 69 | } 70 | 71 | export const getTxData = ({ rawQuote }) => rawQuote?.data; 72 | 73 | export const getTx = ({ rawQuote }) => ({ 74 | to: rawQuote.to, 75 | data: rawQuote.data, 76 | value: rawQuote.value 77 | }); 78 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/0xV2.ts: -------------------------------------------------------------------------------- 1 | import { numberToHex, size, zeroAddress, concat} from 'viem'; 2 | import { sendTx } from '../utils/sendTx'; 3 | 4 | export const name = 'Matcha/0x v2'; 5 | export const token = 'ZRX'; 6 | export const isOutputAvailable = false; 7 | 8 | export const chainToId = { 9 | ethereum: '1', 10 | bsc: '56', 11 | polygon: '137', 12 | optimism: '10', 13 | arbitrum: '42161', 14 | avax: '43114', 15 | base: '8453', 16 | linea: '59144', 17 | scroll: '534352', 18 | blast: '81457', 19 | mantle: '5000', 20 | mode: '34443' 21 | // missing unichain 22 | }; 23 | 24 | const nativeToken = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; 25 | const feeCollectorAddress = '0x9Ab6164976514F1178E2BB4219DA8700c9D96E9A'; 26 | const permit2Address = '0x000000000022d473030f116ddee9f6b43ac78ba3'; 27 | 28 | export async function getQuote(chain: string, from: string, to: string, amount: string, extra) { 29 | // amount should include decimals 30 | 31 | const tokenFrom = from === zeroAddress ? nativeToken : from; 32 | const tokenTo = to === zeroAddress ? nativeToken : to; 33 | 34 | if (extra.amountOut && extra.amountOut !== '0') { 35 | throw new Error('Invalid query params'); 36 | } 37 | 38 | const amountParam = `sellAmount=${amount}`; 39 | 40 | const taker = extra.userAddress === zeroAddress ? '0x1000000000000000000000000000000000000000' : extra.userAddress; 41 | 42 | // only expects integer 43 | const slippage = (extra.slippage * 100) | 0; 44 | 45 | const data = await fetch( 46 | `https://api.0x.org/swap/permit2/quote?chainId=${chainToId[chain]}&buyToken=${tokenTo}&${amountParam}&sellToken=${tokenFrom}&slippageBps=${slippage}&taker=${taker}&tradeSurplusRecipient=${feeCollectorAddress}`, 47 | { 48 | headers: { 49 | '0x-api-key': process.env.OX_API_KEY as string, 50 | '0x-version': 'v2' 51 | } 52 | } 53 | ).then(async (r) => { 54 | if (r.status !== 200) { 55 | throw new Error('Failed to fetch'); 56 | } 57 | 58 | const data = await r.json(); 59 | 60 | return data; 61 | }); 62 | 63 | if ( 64 | data.permit2 !== null && 65 | data.permit2.eip712.domain.verifyingContract.toLowerCase() !== permit2Address.toLowerCase() 66 | ) { 67 | throw new Error(`Approval address does not match`); 68 | } 69 | 70 | return { 71 | amountReturned: data?.buyAmount || 0, 72 | amountIn: data?.sellAmount || 0, 73 | tokenApprovalAddress: permit2Address, 74 | estimatedGas: data.transaction.gas, 75 | rawQuote: { ...data, gasLimit: data.transaction.gas }, 76 | isSignatureNeededForSwap: true, 77 | logo: 'https://www.gitbook.com/cdn-cgi/image/width=40,height=40,fit=contain,dpr=2,format=auto/https%3A%2F%2F1690203644-files.gitbook.io%2F~%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252FKX9pG8rH3DbKDOvV7di7%252Ficon%252F1nKfBhLbPxd2KuXchHET%252F0x%2520logo.png%3Falt%3Dmedia%26token%3D25a85a3e-7f72-47ea-a8b2-e28c0d24074b' 78 | }; 79 | } 80 | 81 | export async function signatureForSwap({ rawQuote, signTypedDataAsync }) { 82 | const signature = await signTypedDataAsync(rawQuote.permit2.eip712).catch((err) => { 83 | console.log(err) 84 | }); 85 | return signature; 86 | } 87 | 88 | export async function swap({ fromAddress, rawQuote, signature }) { 89 | // signature not needed if using allowance holder api 90 | const signatureLengthInHex = signature 91 | ? numberToHex(size(signature), { 92 | signed: false, 93 | size: 32 94 | }) 95 | : null; 96 | const data = signature 97 | ? concat([rawQuote.transaction.data, signatureLengthInHex, signature]) 98 | : rawQuote.transaction.data; 99 | const tx = await sendTx({ 100 | from: fromAddress, 101 | to: rawQuote.transaction.to, 102 | data, 103 | value: rawQuote.transaction.value 104 | }); 105 | 106 | return tx; 107 | } 108 | 109 | export const getTxData = ({ rawQuote }) => rawQuote?.transaction?.data; 110 | 111 | export const getTx = ({ rawQuote }) => ({ 112 | to: rawQuote.transaction.to, 113 | data: rawQuote.transaction.data, 114 | value: rawQuote.transaction.value 115 | }); 116 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/1inch.test.ts: -------------------------------------------------------------------------------- 1 | import { approvalAddress, chainToId } from './1inch'; 2 | import fetch from 'node-fetch'; 3 | 4 | export async function testApprovalAddresses() { 5 | await Promise.all( 6 | Object.keys(chainToId).map(async (chain) => { 7 | const { address: tokenApprovalAddress } = await fetch( 8 | `https://api.1inch.dev/swap/v6.0/${chainToId[chain]}/approve/spender`, 9 | { 10 | headers: { 'Authorization': "Bearer " + process.env.INCH_API_KEY! } 11 | } 12 | ).then((r) => r.json()); 13 | if (tokenApprovalAddress !== approvalAddress(chain)) { 14 | console.log(`Address for ${chain} is wrong`); 15 | } 16 | }) 17 | ); 18 | } 19 | testApprovalAddresses(); 20 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/1inch.ts: -------------------------------------------------------------------------------- 1 | // Source https://portal.1inch.dev/documentation/apis/swap/classic-swap/introduction 2 | 3 | import { altReferralAddress } from '../constants'; 4 | import { sendTx } from '../utils/sendTx'; 5 | import { estimateGas } from 'wagmi/actions'; 6 | import { config } from '../../WalletProvider'; 7 | import { zeroAddress } from 'viem'; 8 | 9 | export const chainToId = { 10 | ethereum: 1, 11 | bsc: 56, 12 | polygon: 137, 13 | optimism: 10, 14 | arbitrum: 42161, 15 | gnosis: 100, 16 | avax: 43114, 17 | fantom: 250, 18 | klaytn: 8217, 19 | aurora: 1313161554, 20 | zksync: 324, 21 | base: 8453 22 | }; 23 | 24 | const spenders = { 25 | ethereum: '0x111111125421ca6dc452d289314280a0f8842a65', 26 | bsc: '0x111111125421ca6dc452d289314280a0f8842a65', 27 | polygon: '0x111111125421ca6dc452d289314280a0f8842a65', 28 | optimism: '0x111111125421ca6dc452d289314280a0f8842a65', 29 | arbitrum: '0x111111125421ca6dc452d289314280a0f8842a65', 30 | gnosis: '0x111111125421ca6dc452d289314280a0f8842a65', 31 | avax: '0x111111125421ca6dc452d289314280a0f8842a65', 32 | fantom: '0x111111125421ca6dc452d289314280a0f8842a65', 33 | klaytn: '0x111111125421ca6dc452d289314280a0f8842a65', 34 | aurora: '0x111111125421ca6dc452d289314280a0f8842a65', 35 | zksync: '0x6fd4383cb451173d5f9304f041c7bcbf27d561ff', 36 | base: '0x111111125421ca6dc452d289314280a0f8842a65' 37 | }; 38 | 39 | export const name = '1inch'; 40 | export const token = '1INCH'; 41 | export const referral = true; 42 | 43 | export function approvalAddress(chain: string) { 44 | // https://api.1inch.io/v6.0/1/approve/spender 45 | return spenders[chain]; 46 | } 47 | const nativeToken = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; 48 | 49 | const apiEndpoint = 'https://api.1inch.dev/swap/v6.0/'; 50 | 51 | export async function getQuote(chain: string, from: string, to: string, amount: string, extra) { 52 | // ethereum = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE 53 | // amount should include decimals 54 | 55 | const tokenFrom = from === zeroAddress ? nativeToken : from; 56 | const tokenTo = to === zeroAddress ? nativeToken : to; 57 | const authHeader = process.env.INCH_API_KEY ? { 'Authorization': `Bearer ${process.env.INCH_API_KEY as string}` } : {}; 58 | const tokenApprovalAddress = spenders[chain]; 59 | 60 | const [data, swapData] = await Promise.all([ 61 | fetch( 62 | `${apiEndpoint}${chainToId[chain]}/quote?src=${tokenFrom}&dst=${tokenTo}&amount=${amount}&includeGas=true`, 63 | { headers: authHeader as any } 64 | ).then((r) => r.json()), 65 | extra.userAddress !== zeroAddress 66 | ? fetch( 67 | `${apiEndpoint}${chainToId[chain]}/swap?src=${tokenFrom}&dst=${tokenTo}&amount=${amount}&from=${extra.userAddress}&origin=${extra.userAddress}&slippage=${extra.slippage}&referrer=${altReferralAddress}&disableEstimate=true`, 68 | { headers: authHeader as any } 69 | ).then((r) => r.json()) 70 | : null 71 | ]); 72 | 73 | if(swapData && swapData.tx.to.toLowerCase() !== tokenApprovalAddress.toLowerCase()){ 74 | throw new Error("approval address doesn't match") 75 | } 76 | 77 | const estimatedGas = data.gas || 0; 78 | 79 | let gas = estimatedGas; 80 | 81 | return { 82 | amountReturned: swapData?.dstAmount ?? data.dstAmount, 83 | estimatedGas: gas, 84 | tokenApprovalAddress, 85 | rawQuote: swapData === null ? null : { ...swapData, tx: swapData.tx }, 86 | logo: 'https://icons.llamao.fi/icons/protocols/1inch-network?w=48&q=75' 87 | }; 88 | } 89 | 90 | export async function swap({ rawQuote }) { 91 | const txObject = { 92 | from: rawQuote.tx.from, 93 | to: rawQuote.tx.to, 94 | data: rawQuote.tx.data, 95 | value: rawQuote.tx.value 96 | }; 97 | 98 | const gasPrediction = await estimateGas(config, txObject).catch(() => null); 99 | 100 | const tx = await sendTx({ 101 | ...txObject, 102 | // Increase gas +20% + 2 erc20 txs 103 | ...(gasPrediction ? { gas: (gasPrediction * 12n) / 10n + 86000n } : {}) 104 | }); 105 | return tx; 106 | } 107 | 108 | export const getTxData = ({ rawQuote }) => rawQuote?.tx?.data; 109 | 110 | export const getTx = ({ rawQuote }) => { 111 | if (rawQuote === null) { 112 | return {}; 113 | } 114 | return { 115 | from: rawQuote.tx.from, 116 | to: rawQuote.tx.to, 117 | data: rawQuote.tx.data, 118 | value: rawQuote.tx.value 119 | }; 120 | }; 121 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/airswap.ts: -------------------------------------------------------------------------------- 1 | import { readContract } from 'wagmi/actions'; 2 | import { config } from '../../WalletProvider'; 3 | import { chainsMap } from '../constants'; 4 | 5 | // https://about.airswap.io/technology/protocols 6 | 7 | // registry addresses 8 | export const chainToId = { 9 | ethereum: '0x8F9DA6d38939411340b19401E8c54Ea1f51B8f95', 10 | bsc: '0x9F11691FA842856E44586380b27Ac331ab7De93d', 11 | polygon: '0x9F11691FA842856E44586380b27Ac331ab7De93d', 12 | avax: '0xE40feb39fcb941A633deC965Abc9921b3FE962b2' 13 | }; 14 | 15 | const swapContracts = { 16 | ethereum: '0x522D6F36c95A1b6509A14272C17747BbB582F2A6', 17 | bsc: '0x132F13C3896eAB218762B9e46F55C9c478905849', 18 | polygon: '0x6713C23261c8A9B7D84Dd6114E78d9a7B9863C1a', 19 | avax: '0xEc08261ac8b3D2164d236bD499def9f82ba9d13F' 20 | }; 21 | 22 | export const name = 'AirSwap'; 23 | export const token = 'AST'; 24 | 25 | // https://about.airswap.io/technology/deployments 26 | export function approvalAddress(chain: string) { 27 | return swapContracts[chain]; 28 | } 29 | 30 | export async function getQuote(chain: string, from: string, to: string, amount: string) { 31 | const [fromServers, toServers] = await Promise.all( 32 | [from, to].map((t) => 33 | readContract(config, { 34 | address: chainToId[chain], 35 | abi: [ 36 | { 37 | inputs: [{ internalType: 'address', name: 'token', type: 'address' }], 38 | name: 'getURLsForToken', 39 | outputs: [{ internalType: 'string[]', name: 'urls', type: 'string[]' }], 40 | stateMutability: 'view', 41 | type: 'function' 42 | } 43 | ], 44 | functionName: 'getURLsForToken', 45 | args: [t as `0x${string}`], 46 | chainId: chainsMap[chain] 47 | }) 48 | ) 49 | ); 50 | 51 | const overlappingServers = fromServers.filter((s) => toServers.includes(s)); 52 | 53 | const controller = new AbortController(); 54 | 55 | const offers = ( 56 | await Promise.all( 57 | overlappingServers.map((s) => 58 | fetch(s, { 59 | method: 'POST', 60 | body: JSON.stringify({ 61 | jsonrpc: '2.0', 62 | id: '78e032d0-2894-42fd-99ce-22dfd14cf65b', 63 | method: 'getSignerSideOrder', // getSignerSideOrder -> provide input amount, getSenderSideOrder -> provide output amount 64 | params: { 65 | signerToken: to, 66 | senderWallet: '0x3a0e257568cc9c6c5d767d5dc0cd8a9ac69cc3ae', // wrapper contract 67 | senderToken: from, 68 | senderAmount: amount, 69 | swapContract: swapContracts[chain] 70 | } 71 | }), 72 | signal: controller.signal, 73 | headers: { 74 | 'Content-Type': 'application/json' 75 | } 76 | }) 77 | .then((r) => r.json()) 78 | .then((r) => (r.error === undefined ? r.result : null)) 79 | .catch((e) => null) 80 | ) 81 | ) 82 | ).filter((r) => r !== null) as any[]; 83 | const bestOffer = offers.reduce((min, offer) => offer.signerAmount > min.signerAmount); 84 | return { 85 | amountReturned: bestOffer.signerAmount, 86 | estimatedGas: 200885, // based on a previous tx, needs fixing 87 | validTo: bestOffer.expiry, 88 | rawQuote: bestOffer 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/cowswap/abi.ts: -------------------------------------------------------------------------------- 1 | export const ABI = { 2 | nativeSwap: [ 3 | { 4 | inputs: [ 5 | { 6 | components: [ 7 | { internalType: 'contract IERC20', name: 'buyToken', type: 'address' }, 8 | { internalType: 'address', name: 'receiver', type: 'address' }, 9 | { internalType: 'uint256', name: 'sellAmount', type: 'uint256' }, 10 | { internalType: 'uint256', name: 'buyAmount', type: 'uint256' }, 11 | { internalType: 'bytes32', name: 'appData', type: 'bytes32' }, 12 | { internalType: 'uint256', name: 'feeAmount', type: 'uint256' }, 13 | { internalType: 'uint32', name: 'validTo', type: 'uint32' }, 14 | { internalType: 'bool', name: 'partiallyFillable', type: 'bool' }, 15 | { internalType: 'int64', name: 'quoteId', type: 'int64' } 16 | ], 17 | internalType: 'struct EthFlowOrder.Data', 18 | name: 'order', 19 | type: 'tuple' 20 | } 21 | ], 22 | name: 'createOrder', 23 | outputs: [{ internalType: 'bytes32', name: 'orderHash', type: 'bytes32' }], 24 | stateMutability: 'payable', 25 | type: 'function' 26 | } 27 | ], 28 | settlement: [ 29 | { 30 | anonymous: false, 31 | inputs: [{ indexed: true, internalType: 'address', name: 'solver', type: 'address' }], 32 | name: 'Settlement', 33 | type: 'event' 34 | }, 35 | { 36 | anonymous: false, 37 | inputs: [ 38 | { indexed: true, internalType: 'address', name: 'owner', type: 'address' }, 39 | { indexed: false, internalType: 'contract IERC20', name: 'sellToken', type: 'address' }, 40 | { indexed: false, internalType: 'contract IERC20', name: 'buyToken', type: 'address' }, 41 | { indexed: false, internalType: 'uint256', name: 'sellAmount', type: 'uint256' }, 42 | { indexed: false, internalType: 'uint256', name: 'buyAmount', type: 'uint256' }, 43 | { indexed: false, internalType: 'uint256', name: 'feeAmount', type: 'uint256' }, 44 | { indexed: false, internalType: 'bytes', name: 'orderUid', type: 'bytes' } 45 | ], 46 | name: 'Trade', 47 | type: 'event' 48 | } 49 | ] 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/firebird.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { chainsMap, defillamaReferrerAddress } from '../constants'; 3 | import { ExtraData } from '../types'; 4 | import { applyArbitrumFees } from '../utils/arbitrumFees'; 5 | import { sendTx } from '../utils/sendTx'; 6 | import { zeroAddress } from 'viem'; 7 | import { estimateGas } from 'wagmi/actions'; 8 | import { config } from '../../WalletProvider'; 9 | 10 | export const chainToId = { 11 | ethereum: chainsMap.ethereum, 12 | bsc: chainsMap.bsc, 13 | polygon: chainsMap.polygon, 14 | optimism: chainsMap.optimism, 15 | arbitrum: chainsMap.arbitrum, 16 | avax: chainsMap.avax, 17 | fantom: chainsMap.fantom, 18 | cronos: chainsMap.cronos, 19 | canto: chainsMap.canto, 20 | base: chainsMap.base, 21 | zksync: chainsMap.zksync 22 | //opBNB 23 | //pulse 24 | }; 25 | 26 | const approvalAddresses = { 27 | ethereum: '0xe0C38b2a8D09aAD53f1C67734B9A95E43d5981c0', 28 | bsc: '0x92e4F29Be975C1B1eB72E77De24Dccf11432a5bd', 29 | polygon: '0xb31D1B1eA48cE4Bf10ed697d44B747287E785Ad4', 30 | optimism: '0x0c6134Abc08A1EafC3E2Dc9A5AD023Bb08Da86C3', 31 | arbitrum: '0x0c6134Abc08A1EafC3E2Dc9A5AD023Bb08Da86C3', 32 | avax: '0xe0C38b2a8D09aAD53f1C67734B9A95E43d5981c0', 33 | fantom: '0xe0C38b2a8D09aAD53f1C67734B9A95E43d5981c0', 34 | cronos: '0x4A5a7331dA84d3834C030a9b8d4f3d687A3b788b', 35 | canto: '0x984742Be1901fcbed70d7B5847bee5BE006d91C8', 36 | base: '0x20f0b18BDDe8e3dd0e42C173062eBdd05C421151', 37 | zksync: '0xc593dcfD1E4605a6Cd466f5C6807D444414dBc97' 38 | }; 39 | 40 | export const name = 'Firebird'; 41 | export const token = 'HOPE'; 42 | 43 | export function approvalAddress(chain: string) { 44 | return approvalAddresses[chain]; 45 | } 46 | 47 | const routerAPI = 'https://router.firebird.finance/aggregator/v2'; 48 | const headers = { 49 | 'content-type': 'application/json', 50 | 'api-key': 'firebird_defillama' 51 | }; 52 | const nativeToken = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; 53 | 54 | export async function getQuote(chain: string, from: string, to: string, amount: string, extra: ExtraData) { 55 | const isFromNative = from === zeroAddress; 56 | const tokenFrom = isFromNative ? nativeToken : from; 57 | const tokenTo = to === zeroAddress ? nativeToken : to; 58 | const receiver = extra.userAddress || defillamaReferrerAddress; 59 | 60 | // amount should include decimals 61 | const result = await fetch( 62 | `${routerAPI}/quote?chainId=${ 63 | chainToId[chain] 64 | }&from=${tokenFrom}&to=${tokenTo}&amount=${amount}&receiver=${receiver}&slippage=${ 65 | +extra.slippage / 100 66 | }&source=defillama&ref=${defillamaReferrerAddress}`, 67 | { headers } 68 | ).then((r) => r.json()); 69 | const data = result.quoteData; 70 | 71 | const { encodedData } = await fetch(`${routerAPI}/encode`, { 72 | method: 'POST', 73 | headers, 74 | body: JSON.stringify(result) 75 | }).then((r) => r.json()); 76 | 77 | let estimatedGas; 78 | let value = isFromNative ? BigInt(amount) : undefined; 79 | try { 80 | estimatedGas = ( 81 | await estimateGas(config, { 82 | to: encodedData.router, 83 | data: encodedData.data, 84 | value, 85 | chainId: chainsMap[chain] 86 | }) 87 | ).toString(); 88 | } catch (e) { 89 | estimatedGas = data.maxReturn.totalGas; 90 | } 91 | 92 | if (estimatedGas) { 93 | if (chain === 'optimism') estimatedGas = BigNumber(3.5).times(estimatedGas).toFixed(0, 1); 94 | if (chain === 'arbitrum') 95 | estimatedGas = await applyArbitrumFees(encodedData.router, encodedData.data, estimatedGas); 96 | } 97 | 98 | return { 99 | amountReturned: data.maxReturn.totalTo, 100 | estimatedGas, 101 | tokenApprovalAddress: encodedData.router, 102 | rawQuote: { ...data, tx: { ...encodedData, from: receiver, value, gasLimit: estimatedGas } }, 103 | logo: 'https://assets.coingecko.com/markets/images/730/small/firebird-finance.png?1636117048' 104 | }; 105 | } 106 | 107 | export async function swap({ rawQuote, chain }) { 108 | const tx = await sendTx({ 109 | from: rawQuote.tx.from, 110 | to: rawQuote.tx.router, 111 | data: rawQuote.tx.data, 112 | value: rawQuote.tx.value, 113 | ...(chain === 'optimism' && { gas: rawQuote.gasLimit }) 114 | }); 115 | 116 | return tx; 117 | } 118 | 119 | export const getTxData = ({ rawQuote }) => rawQuote?.tx?.data; 120 | 121 | export const getTx = ({ rawQuote }) => ({ ...rawQuote?.tx, to: rawQuote?.tx?.router }); 122 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/hashflow/abi.ts: -------------------------------------------------------------------------------- 1 | export const ABI = [ 2 | { 3 | inputs: [ 4 | { 5 | components: [ 6 | { 7 | internalType: 'address', 8 | name: 'pool', 9 | type: 'address' 10 | }, 11 | { 12 | internalType: 'address', 13 | name: 'externalAccount', 14 | type: 'address' 15 | }, 16 | { 17 | internalType: 'address', 18 | name: 'trader', 19 | type: 'address' 20 | }, 21 | { 22 | internalType: 'address', 23 | name: 'effectiveTrader', 24 | type: 'address' 25 | }, 26 | { 27 | internalType: 'address', 28 | name: 'baseToken', 29 | type: 'address' 30 | }, 31 | { 32 | internalType: 'address', 33 | name: 'quoteToken', 34 | type: 'address' 35 | }, 36 | { 37 | internalType: 'uint256', 38 | name: 'effectiveBaseTokenAmount', 39 | type: 'uint256' 40 | }, 41 | { 42 | internalType: 'uint256', 43 | name: 'maxBaseTokenAmount', 44 | type: 'uint256' 45 | }, 46 | { 47 | internalType: 'uint256', 48 | name: 'maxQuoteTokenAmount', 49 | type: 'uint256' 50 | }, 51 | { 52 | internalType: 'uint256', 53 | name: 'quoteExpiry', 54 | type: 'uint256' 55 | }, 56 | { 57 | internalType: 'uint256', 58 | name: 'nonce', 59 | type: 'uint256' 60 | }, 61 | { 62 | internalType: 'bytes32', 63 | name: 'txid', 64 | type: 'bytes32' 65 | }, 66 | { 67 | internalType: 'bytes', 68 | name: 'signature', 69 | type: 'bytes' 70 | } 71 | ], 72 | internalType: 'struct IQuote.RFQTQuote', 73 | name: 'quote', 74 | type: 'tuple' 75 | } 76 | ], 77 | name: 'tradeSingleHop', 78 | outputs: [], 79 | stateMutability: 'payable', 80 | type: 'function' 81 | } 82 | ] as const; 83 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/hashflow/index.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { applyArbitrumFees } from '../../utils/arbitrumFees'; 3 | import { sendTx } from '../../utils/sendTx'; 4 | import { ABI } from './abi'; 5 | import { encodeFunctionData, zeroAddress } from 'viem'; 6 | 7 | export const chainToId = { 8 | ethereum: 1 9 | /* 10 | bsc: 56, 11 | polygon: 137, 12 | arbitrum: 42161, 13 | avax: 43114, 14 | optimism: 10 15 | */ 16 | }; 17 | 18 | export const name = 'Hashflow'; 19 | export const token = 'HFT'; 20 | export const isOutputAvailable = true; 21 | 22 | // from https://docs.hashflow.com/hashflow/taker/getting-started#5.-execute-quote-on-chain 23 | const routerAddress = { 24 | 1: '0xF6a94dfD0E6ea9ddFdFfE4762Ad4236576136613', 25 | 10: '0xb3999F658C0391d94A37f7FF328F3feC942BcADC', 26 | 56: '0x0ACFFB0fb2cddd9BD35d03d359F3D899E32FACc9', 27 | 137: '0x72550597dc0b2e0beC24e116ADd353599Eff2E35', 28 | 42161: '0x1F772fA3Bc263160ea09bB16CE1A6B8Fc0Fab36a', 29 | 43114: '0x64D2f9F44FE26C157d552aE7EAa613Ca6587B59e' 30 | }; 31 | 32 | export async function getQuote(chain: string, from: string, to: string, amount: string, extra) { 33 | const amountParam = 34 | extra.amountOut && extra.amountOut !== '0' ? { quoteTokenAmount: extra.amountOut } : { baseTokenAmount: amount }; 35 | 36 | const data = await fetch(`https://api.hashflow.com/taker/v2/rfq`, { 37 | method: 'POST', 38 | body: JSON.stringify({ 39 | networkId: chainToId[chain], 40 | source: 'defillama', 41 | rfqType: 0, 42 | baseToken: from, 43 | quoteToken: to, 44 | trader: extra.userAddress, 45 | ...amountParam 46 | }), 47 | headers: { 48 | 'Content-Type': 'application/json', 49 | Authorization: process.env.HASHFLOW_API_KEY as string 50 | } 51 | }).then((r) => r.json()); 52 | const gas = chain === 'optimism' ? BigNumber(3.5).times(data.gasEstimate).toFixed(0, 1) : data.gasEstimate; 53 | 54 | // https://docs.hashflow.com/hashflow/taker/getting-started#5.-execute-quote-on-chain 55 | const encodedData = encodeFunctionData({ 56 | abi: ABI, 57 | args: [ 58 | { 59 | pool: data.quoteData.pool as `0x${string}`, 60 | externalAccount: data.quoteData.eoa ?? (zeroAddress as `0x${string}`), 61 | trader: data.quoteData.trader as `0x${string}`, 62 | effectiveTrader: data.quoteData.effectiveTrader ?? (data.quoteData.trader as `0x${string}`), 63 | baseToken: data.quoteData.baseToken as `0x${string}`, 64 | quoteToken: data.quoteData.quoteToken as `0x${string}`, 65 | effectiveBaseTokenAmount: data.quoteData.baseTokenAmount as any, 66 | maxBaseTokenAmount: data.quoteData.baseTokenAmount as any, 67 | maxQuoteTokenAmount: data.quoteData.quoteTokenAmount as any, 68 | quoteExpiry: data.quoteData.quoteExpiry as any, 69 | nonce: data.quoteData.nonce as any, 70 | txid: data.quoteData.txid as `0x${string}`, 71 | signature: data.signature as `0x${string}` 72 | } 73 | ] 74 | }); 75 | 76 | const txData = { 77 | to: routerAddress[chainToId[chain]], 78 | data: encodedData 79 | }; 80 | 81 | let estimatedGas = gas; 82 | if (chain === 'arbitrum') estimatedGas = await applyArbitrumFees(txData.to, txData.data, gas); 83 | 84 | const timeTillExpiry = data.quoteData.quoteExpiry - Date.now() / 1000; 85 | if (timeTillExpiry < 40) { 86 | throw new Error('Expiry is too close'); 87 | } 88 | 89 | return { 90 | amountReturned: data?.quoteData?.quoteTokenAmount || 0, 91 | amountIn: data?.quoteData?.baseTokenAmount || 0, 92 | estimatedGas, 93 | tokenApprovalAddress: routerAddress[chainToId[chain]], 94 | validTo: data.quoteData.quoteExpiry, 95 | rawQuote: { 96 | ...data, 97 | gasLimit: estimatedGas, 98 | tx: { ...txData, ...(from === zeroAddress ? { value: data.quoteData.baseTokenAmount } : {}) } 99 | }, 100 | isMEVSafe: true 101 | }; 102 | } 103 | 104 | export async function swap({ rawQuote, chain }) { 105 | const tx = await sendTx({ 106 | ...rawQuote.tx, 107 | ...(chain === 'optimism' && { gas: rawQuote.tx.gasLimit }) 108 | }); 109 | 110 | return tx; 111 | } 112 | 113 | export const getTxData = ({ rawQuote }) => rawQuote?.tx?.data; 114 | 115 | export const getTx = ({ rawQuote }) => rawQuote.tx; 116 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/krystal.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | export const chainToId = { 4 | ethereum: 'ethereum', 5 | bsc: 'bsc', 6 | polygon: 'polygon', 7 | avax: 'avalanche', 8 | cronos: 'cronos', 9 | fantom: 'fantom', 10 | arbitrum: 'arbitrum', 11 | aurora: 'aurora', 12 | klaytn: 'klaytn' 13 | }; 14 | 15 | export const name = 'Krystal'; 16 | export const token = null; 17 | 18 | export function approvalAddress(chain: string) { 19 | return ''; // need to fix 20 | } 21 | 22 | export async function getQuote(chain: string, from: string, to: string, amount: string) { 23 | // gas token is 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 24 | const data = await fetch( 25 | `https://api.krystal.app/${chainToId[chain]}/v2/swap/allRates?src=${from}&srcAmount=${amount}&dest=${to}&platformWallet=0x168E4c3AC8d89B00958B6bE6400B066f0347DDc9` 26 | ).then((r) => r.json()); 27 | 28 | const estimatedGas = 29 | chain === 'optimism' ? BigNumber(3.5).times(data.rates[0].estimatedGas).toFixed(0, 1) : data.rates[0].estimatedGas; 30 | 31 | return { 32 | amountReturned: data.rates[0].amount, 33 | estimatedGas 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/kyberswap.ts: -------------------------------------------------------------------------------- 1 | import { ExtraData } from '../types'; 2 | import { sendTx } from '../utils/sendTx'; 3 | import { zeroAddress } from 'viem'; 4 | 5 | // https://docs.kyberswap.com/kyberswap-solutions/kyberswap-aggregator/aggregator-api-specification/evm-swaps 6 | export const chainToId = { 7 | ethereum: 'ethereum', 8 | bsc: 'bsc', 9 | polygon: 'polygon', 10 | optimism: 'optimism', 11 | arbitrum: 'arbitrum', 12 | avax: 'avalanche', 13 | fantom: 'fantom', 14 | zksync: 'zksync', 15 | polygonzkevm: 'polygon-zkevm', 16 | linea: 'linea', 17 | base: 'base', 18 | scroll: 'scroll', 19 | sonic: 'sonic', 20 | //mantle 21 | //blast 22 | 23 | // removed 24 | // cronos: 'cronos', 25 | // aurora: 'aurora', 26 | // bttc: 'bttc', 27 | }; 28 | 29 | const universalRouter = "0x6131b5fae19ea4f9d964eac0408e4408b66337b5" 30 | 31 | const routers = { 32 | ethereum: universalRouter, 33 | bsc: universalRouter, 34 | polygon: universalRouter, 35 | optimism: universalRouter, 36 | arbitrum: universalRouter, 37 | avax: universalRouter, 38 | fantom: universalRouter, 39 | zksync: '0x3F95eF3f2eAca871858dbE20A93c01daF6C2e923', 40 | polygonzkevm: universalRouter, 41 | linea: universalRouter, 42 | base: universalRouter, 43 | scroll: universalRouter, 44 | sonic: universalRouter, 45 | } 46 | 47 | export const name = 'KyberSwap'; 48 | export const token = 'KNC'; 49 | 50 | export function approvalAddress() { 51 | return universalRouter; 52 | } 53 | 54 | const nativeToken = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; 55 | const clientId = "llamaswap" 56 | 57 | export async function getQuote(chain: string, from: string, to: string, amount: string, extra: ExtraData) { 58 | const tokenFrom = from === zeroAddress ? nativeToken : from; 59 | const tokenTo = to === zeroAddress ? nativeToken : to; 60 | 61 | const quote = await fetch( 62 | `https://aggregator-api.kyberswap.com/${ 63 | chainToId[chain] 64 | }/api/v1/routes?tokenIn=${tokenFrom}&tokenOut=${tokenTo}&amountIn=${amount}&gasInclude=true`, 65 | { 66 | headers: { 67 | 'x-client-id': clientId 68 | } 69 | } 70 | ).then((r) => r.json()); 71 | 72 | const tx = extra.userAddress === zeroAddress? null : await fetch( 73 | `https://aggregator-api.kyberswap.com/${chainToId[chain]}/api/v1/route/build`, 74 | { 75 | headers: { 76 | 'x-client-id': clientId 77 | }, 78 | method: "POST", 79 | body: JSON.stringify({ 80 | routeSummary: quote.data.routeSummary, 81 | sender: extra.userAddress, 82 | recipient: extra.userAddress, 83 | slippageTolerance: +extra.slippage * 100, 84 | source: clientId 85 | }) 86 | } 87 | ).then((r) => r.json()); 88 | 89 | let gas = tx === null? quote.data.routeSummary.gas : tx.data.gas; 90 | 91 | if(tx !== null){ 92 | if(routers[chain].toLowerCase() !== tx.data.routerAddress.toLowerCase()){ 93 | throw new Error("Approval address doesn't match hardcoded one") 94 | } 95 | } 96 | 97 | return { 98 | amountReturned: tx === null? quote.data.routeSummary.amountOut: tx.data.amountOut, 99 | estimatedGas: gas, 100 | tokenApprovalAddress: routers[chain], 101 | rawQuote: tx === null? {} : tx.data, 102 | logo: 'https://assets.coingecko.com/coins/images/14899/small/RwdVsGcw_400x400.jpg?1618923851' 103 | }; 104 | } 105 | 106 | export async function swap({ fromAddress, from, rawQuote }) { 107 | const transactionOption: Record = { 108 | from: fromAddress, 109 | to: rawQuote.routerAddress, 110 | data: rawQuote.data, 111 | }; 112 | 113 | if (from === zeroAddress) transactionOption.value = rawQuote.amountIn; 114 | 115 | const tx = await sendTx(transactionOption); 116 | 117 | return tx; 118 | } 119 | export const getTxData = ({ rawQuote }) => rawQuote?.data; 120 | 121 | export const getTx = ({ rawQuote }) => ({ 122 | to: rawQuote.routerAddress, 123 | data: rawQuote.data 124 | }); 125 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/lifi.ts: -------------------------------------------------------------------------------- 1 | // Source https://docs.1inch.io/docs/aggregation-protocol/api/swagger 2 | 3 | import BigNumber from 'bignumber.js'; 4 | import { ExtraData } from '../types'; 5 | import { zeroAddress } from 'viem'; 6 | import { sendTx } from '../utils/sendTx'; 7 | 8 | export const chainToId = { 9 | ethereum: 'eth', 10 | polygon: 'pol', 11 | bsc: 'bsc', 12 | gnosis: 'dai', 13 | fantom: 'ftm', 14 | avax: 'ava', 15 | arbitrum: 'arb', 16 | optimism: 'opt', 17 | moonriver: 'mor', 18 | moonbeam: 'moo', 19 | celo: 'cel', 20 | fuse: 'fus', 21 | cronos: 'cro', 22 | velas: 'vel', 23 | aurora: 'aur' 24 | }; 25 | export const name = 'LI.FI'; 26 | export const token = null; 27 | 28 | const nativeToken = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; 29 | 30 | export async function getQuote(chain: string, from: string, to: string, amount: string, extra: ExtraData) { 31 | // ethereum = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE 32 | // amount should include decimals 33 | 34 | const tokenFrom = from === zeroAddress ? nativeToken : from; 35 | const tokenTo = to === zeroAddress ? nativeToken : to; 36 | const data = await fetch( 37 | `https://li.quest/v1/quote?fromChain=${chainToId[chain]}&toChain=${ 38 | chainToId[chain] 39 | }&fromToken=${tokenFrom}&toToken=${tokenTo}&fromAmount=${amount}&fromAddress=${ 40 | extra.userAddress === zeroAddress ? '0x1000000000000000000000000000000000000001' : extra.userAddress 41 | }&slippage=${+extra.slippage / 100}` 42 | ).then((r) => r.json()); 43 | 44 | const gas = data.estimate.gasCosts.reduce((acc, val) => acc + Number(val.estimate), 0); 45 | 46 | const estimatedGas = chain === 'optimism' ? BigNumber(3.5).times(gas).toFixed(0, 1) : gas; 47 | 48 | return { 49 | amountReturned: data.estimate.toAmount, 50 | estimatedGas, 51 | tokenApprovalAddress: data.estimate.approvalAddress, 52 | logo: '', 53 | rawQuote: { ...data, gasLimit: estimatedGas } 54 | }; 55 | } 56 | 57 | export async function swap({ rawQuote, chain }) { 58 | const tx = await sendTx({ 59 | from: rawQuote.transactionRequest.from, 60 | to: rawQuote.transactionRequest.to, 61 | data: rawQuote.transactionRequest.data, 62 | value: rawQuote.transactionRequest.value, 63 | ...(chain === 'optimism' && { gas: rawQuote.gasLimit }) 64 | }); 65 | 66 | return tx; 67 | } 68 | 69 | export const getTxData = ({ rawQuote }) => rawQuote?.transactionRequest?.data; 70 | 71 | export const getTx = ({ rawQuote }) => ({ 72 | from: rawQuote.transactionRequest.from, 73 | to: rawQuote.transactionRequest.to, 74 | data: rawQuote.transactionRequest.data, 75 | value: rawQuote.transactionRequest.value 76 | }); 77 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/llamazip/encode.ts: -------------------------------------------------------------------------------- 1 | function countBits(inputNum: bigint) { 2 | let bitlength = 0; 3 | while (inputNum !== 0n) { 4 | inputNum = inputNum / 2n; 5 | bitlength++; 6 | } 7 | return bitlength; 8 | } 9 | 10 | function removeFirstBit(word: bigint) { 11 | // To work this requires that word has a number of bits that is multiple of 8 + the starting bit 12 | return word.toString(16).slice(1); 13 | } 14 | 15 | export function encode( 16 | pair: string, 17 | token0IsTokenIn: boolean, 18 | expectedReturnAmount: string, 19 | slippage: string, 20 | inputIsETH: boolean, 21 | maxBalance?: boolean, 22 | inputAmount?: string 23 | ) { 24 | let word = ((1n << 4n) + BigInt(pair)) << 1n; 25 | if (token0IsTokenIn) { 26 | word = word + 1n; 27 | } 28 | word = word << 17n; 29 | let slippageZeroes = 0n; 30 | let slippageNum = BigInt(expectedReturnAmount); 31 | while (slippageNum > 131071n) { 32 | // 0b11111111111111111 33 | slippageZeroes++; 34 | slippageNum = slippageNum / 2n; 35 | } 36 | if (slippageNum < 131071n) { 37 | slippageNum = slippageNum + 1n; // round up 38 | } 39 | word = ((word + slippageNum) << 8n) + slippageZeroes; 40 | 41 | const slippageId = ['0.5', '0.1', '1', '5'].findIndex((slip) => slip === slippage); 42 | if (slippageId === -1) { 43 | throw new Error('Slippage number not supported'); 44 | } 45 | word = (word << 2n) + BigInt(slippageId); 46 | 47 | if (inputIsETH || maxBalance) { 48 | return removeFirstBit(word); // pad it so total number of bits is a multiple of 8 49 | } 50 | 51 | let inputZeroes = 0n; 52 | let inputNum = BigInt(inputAmount!); 53 | while ((inputNum % 10n) === 0n && inputNum !== 0n) { 54 | inputZeroes++; 55 | inputNum = inputNum / 10n; 56 | } 57 | word = (word << 5n) + inputZeroes; 58 | const inputBitlength = BigInt(countBits(inputNum)); 59 | const extraBits = inputBitlength % 8n; 60 | word = word << (inputBitlength + (extraBits <= 3n ? 3n - extraBits : 3n + 8n - extraBits)); 61 | return removeFirstBit(word + inputNum); 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/llamazip/index.ts: -------------------------------------------------------------------------------- 1 | import { sendTx } from '../../utils/sendTx'; 2 | import { encode } from './encode'; 3 | import { normalizeTokens, pairs } from './pairs'; 4 | import { zeroAddress } from 'viem'; 5 | import { simulateContract } from 'wagmi/actions'; 6 | import { config } from '../../../WalletProvider'; 7 | import { chainsMap } from '../../constants'; 8 | 9 | export const name = 'LlamaZip'; 10 | export const token = 'none'; 11 | 12 | export const chainToId = { 13 | optimism: '0x6f9d14Cf4A06Dd9C70766Bd161cf8d4387683E1b', 14 | arbitrum: '0x973bf562407766e77f885c1cd1a8060e5303C745' 15 | }; 16 | 17 | // https://docs.uniswap.org/contracts/v3/reference/deployments 18 | const quoter = { 19 | optimism: '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6', 20 | arbitrum: '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6' 21 | }; 22 | 23 | const weth = { 24 | optimism: '0x4200000000000000000000000000000000000006', 25 | arbitrum: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' 26 | }; 27 | 28 | function normalize(token: string, weth: string) { 29 | return (token === zeroAddress ? weth : token).toLowerCase(); 30 | } 31 | 32 | // https://docs.uniswap.org/sdk/v3/guides/quoting 33 | export async function getQuote(chain: string, from: string, to: string, amount: string, extra: any) { 34 | if (to.toLowerCase() === weth[chain].toLowerCase()) { 35 | return null; // We don't support swaps to WETH 36 | } 37 | 38 | const tokenFrom = normalize(from, weth[chain]); 39 | const tokenTo = normalize(to, weth[chain]); 40 | 41 | const token0isTokenIn = BigInt(tokenFrom) < BigInt(tokenTo); 42 | 43 | const possiblePairs = pairs[chain as keyof typeof pairs].filter( 44 | ({ name }) => name === normalizeTokens(tokenFrom, tokenTo).join('-') 45 | ); 46 | 47 | if (possiblePairs.length === 0) { 48 | return null; 49 | } 50 | 51 | const quotedAmountOuts = ( 52 | await Promise.all( 53 | possiblePairs.map(async (pair) => { 54 | try { 55 | // const callData = encodeFunctionData({}) 56 | return { 57 | output: ( 58 | await simulateContract(config, { 59 | address: quoter[chain], 60 | abi: [ 61 | { 62 | inputs: [ 63 | { internalType: 'address', name: 'tokenIn', type: 'address' }, 64 | { internalType: 'address', name: 'tokenOut', type: 'address' }, 65 | { internalType: 'uint24', name: 'fee', type: 'uint24' }, 66 | { internalType: 'uint256', name: 'amountIn', type: 'uint256' }, 67 | { internalType: 'uint160', name: 'sqrtPriceLimitX96', type: 'uint160' } 68 | ], 69 | name: 'quoteExactInputSingle', 70 | outputs: [{ internalType: 'uint256', name: 'amountOut', type: 'uint256' }], 71 | stateMutability: 'nonpayable', 72 | type: 'function' 73 | } 74 | ], 75 | functionName: 'quoteExactInputSingle', 76 | args: [tokenFrom as `0x${string}`, tokenTo as `0x${string}`, Number(pair.fee), BigInt(amount), 0n], 77 | chainId: chainsMap[chain] 78 | }) 79 | ).result, 80 | pair 81 | }; 82 | } catch (e) { 83 | console.log({ e }); 84 | if (pair.mayFail === true) return null; 85 | throw e; 86 | } 87 | }) 88 | ) 89 | ).filter((t) => t !== null); 90 | 91 | const bestPair = quotedAmountOuts.sort((a, b) => (b!.output > a!.output ? 1 : -1))[0]; 92 | const pair = bestPair!.pair; 93 | const quotedAmountOut = bestPair!.output.toString(); 94 | 95 | const inputIsETH = from === zeroAddress; 96 | const calldata = encode(pair.pairId, token0isTokenIn, quotedAmountOut, extra.slippage, inputIsETH, false, amount); 97 | if (calldata.length > 256 / 4 + 2) { 98 | return null; // LlamaZip doesn't support calldata that's bigger than one EVM word 99 | } 100 | 101 | return { 102 | amountReturned: quotedAmountOut.toString(), 103 | estimatedGas: (200e3).toString(), // random approximation 104 | rawQuote: { 105 | tx: { 106 | to: chainToId[chain], 107 | data: calldata, 108 | ...(inputIsETH ? { value: amount } : {}) 109 | } 110 | }, 111 | tokenApprovalAddress: chainToId[chain], 112 | logo: 'https://raw.githubusercontent.com/DefiLlama/memes/master/bussin.jpg' 113 | }; 114 | } 115 | 116 | export async function swap({ fromAddress, rawQuote }) { 117 | const tx = await sendTx({ 118 | from: fromAddress, 119 | to: rawQuote.tx.to, 120 | data: rawQuote.tx.data, 121 | value: rawQuote.tx.value 122 | }); 123 | return tx; 124 | } 125 | 126 | export const getTxData = ({ rawQuote }) => rawQuote?.tx?.data; 127 | 128 | export const getTx = ({ rawQuote }) => rawQuote?.tx; 129 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/llamazip/pairs.ts: -------------------------------------------------------------------------------- 1 | export const tokens = { 2 | optimism: { 3 | weth: '0x4200000000000000000000000000000000000006', 4 | usdc: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', 5 | op: '0x4200000000000000000000000000000000000042', 6 | snx: '0x8700dAec35aF8Ff88c16BdF0418774CB3D7599B4', 7 | dai: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', 8 | susd: '0x8c6f28f2F1A3C87F0f938b96d27520d9751ec8d9', 9 | wbtc: '0x68f180fcCe6836688e9084f035309E29Bf0A2095', 10 | thales: '0x217D47011b23BB961eB6D93cA9945B7501a5BB11', 11 | usdt: '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58', 12 | perp: '0x9e1028F5F1D5eDE59748FFceE5532509976840E0', 13 | velo: '0x3c8B650257cFb5f272f799F5e2b4e65093a11a05' 14 | }, 15 | arbitrum: { 16 | weth: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', 17 | usdc: '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8', 18 | usdt: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', 19 | gmx: '0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a', 20 | dai: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', 21 | wbtc: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f', 22 | gns: '0x18c11FD286C5EC11c3b683Caa813B77f5163A122', 23 | magic: '0x539bdE0d7Dbd336b79148AA742883198BBF60342', 24 | arb: '0x912CE59144191C1204E64559FE8253a0e49E6548' 25 | } 26 | }; 27 | 28 | export const normalizeTokens = (t0, t1) => 29 | BigInt(t0) < BigInt(t1) ? [t0.toLowerCase(), t1.toLowerCase()] : [t1.toLowerCase(), t0.toLowerCase()]; 30 | 31 | const createPair = (t0: string, t1: string, fee: string, pairId: string, mayFail: boolean = false) => { 32 | const [token0, token1] = normalizeTokens(t0, t1); 33 | 34 | return { 35 | name: `${token0}-${token1}`, 36 | pairId, 37 | token0, 38 | token1, 39 | fee, 40 | mayFail 41 | }; 42 | }; 43 | 44 | export const pairs = { 45 | optimism: (() => { 46 | const chainTokens = tokens.optimism; 47 | return [ 48 | createPair(chainTokens.weth, chainTokens.usdc, '500', '0'), 49 | createPair(chainTokens.weth, chainTokens.op, '3000', '1'), 50 | createPair(chainTokens.op, chainTokens.usdc, '3000', '2'), 51 | createPair(chainTokens.weth, chainTokens.op, '500', '3'), 52 | createPair(chainTokens.usdc, chainTokens.dai, '100', '4'), 53 | createPair(chainTokens.snx, chainTokens.weth, '3000', '5'), 54 | createPair(chainTokens.weth, chainTokens.dai, '3000', '6') 55 | ]; 56 | })(), 57 | arbitrum: (() => { 58 | const chainTokens = tokens.arbitrum; 59 | 60 | return [ 61 | createPair(chainTokens.weth, chainTokens.usdc, '500', '0'), 62 | createPair(chainTokens.weth, chainTokens.usdt, '500', '1'), 63 | createPair(chainTokens.weth, chainTokens.wbtc, '500', '2'), 64 | createPair(chainTokens.weth, chainTokens.gmx, '3000', '3'), 65 | createPair(chainTokens.weth, chainTokens.gns, '3000', '4'), 66 | createPair(chainTokens.weth, chainTokens.magic, '10000', '5'), 67 | createPair(chainTokens.weth, chainTokens.dai, '3000', '6'), 68 | createPair(chainTokens.weth, chainTokens.arb, '10000', '7'), 69 | createPair(chainTokens.weth, chainTokens.arb, '3000', '8', true), 70 | createPair(chainTokens.weth, chainTokens.arb, '500', '9', true), 71 | createPair(chainTokens.weth, chainTokens.arb, '100', '10', true) 72 | ]; 73 | })() 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/odos/index.ts: -------------------------------------------------------------------------------- 1 | import { sendTx } from '../../utils/sendTx'; 2 | 3 | // https://api.odos.xyz/info/chains 4 | export const chainToId = { 5 | ethereum: 1, 6 | arbitrum: 42161, 7 | optimism: 10, 8 | base: 8453, 9 | polygon: 137, 10 | avax: 43114, 11 | bsc: 56, 12 | fantom: 250, 13 | zksync: 324, 14 | //polygonzkevm: 1101 15 | //mantle 16 | //mode: 17 | linea: 59144, 18 | scroll: 534352, 19 | sonic: 146 20 | }; 21 | 22 | export const name = 'Odos'; 23 | export const token = 'ODOS'; 24 | 25 | const referralCode = 2101375859; 26 | 27 | // https://docs.odos.xyz/product/sor/v2/ 28 | const routers = { 29 | ethereum: '0xcf5540fffcdc3d510b18bfca6d2b9987b0772559', 30 | arbitrum: '0xa669e7a0d4b3e4fa48af2de86bd4cd7126be4e13', 31 | optimism: '0xca423977156bb05b13a2ba3b76bc5419e2fe9680', 32 | base: '0x19ceead7105607cd444f5ad10dd51356436095a1', 33 | polygon: '0x4e3288c9ca110bcc82bf38f09a7b425c095d92bf', 34 | avax: '0x88de50b233052e4fb783d4f6db78cc34fea3e9fc', 35 | bsc: '0x89b8aa89fdd0507a99d334cbe3c808fafc7d850e', 36 | fantom: '0xd0c22a5435f4e8e5770c1fafb5374015fc12f7cd', 37 | zksync: '0x4bBa932E9792A2b917D47830C93a9BC79320E4f7', 38 | linea: '0x2d8879046f1559E53eb052E949e9544bCB72f414', 39 | scroll: '0xbFe03C9E20a9Fc0b37de01A172F207004935E0b1', 40 | sonic: '0xac041df48df9791b0654f1dbbf2cc8450c5f2e9d', 41 | //polygonzkevm: '0x2b8B3f0949dfB616602109D2AAbBA11311ec7aEC' 42 | }; 43 | 44 | export function approvalAddress(chain) { 45 | return routers[chain]; 46 | } 47 | 48 | export async function getQuote(chain: string, from: string, to: string, amount: string, extra) { 49 | const quote = await fetch(`https://api.odos.xyz/sor/quote/v2`, { 50 | method: 'POST', 51 | headers: { 'Content-Type': 'application/json' }, 52 | body: JSON.stringify({ 53 | chainId: chainToId[chain], 54 | inputTokens: [ 55 | { 56 | tokenAddress: from, 57 | amount: amount 58 | } 59 | ], 60 | outputTokens: [ 61 | { 62 | tokenAddress: to, 63 | proportion: 1 64 | } 65 | ], 66 | userAddr: extra.userAddress, // checksummed user address 67 | slippageLimitPercent: extra.slippage, // set your slippage limit percentage (1 = 1%), 68 | referralCode, 69 | // optional: 70 | disableRFQs: true, 71 | compact: true 72 | }) 73 | }).then((res) => res.json()); 74 | 75 | const swapData = await fetch('https://api.odos.xyz/sor/assemble', { 76 | method: 'POST', 77 | headers: { 'Content-Type': 'application/json' }, 78 | body: JSON.stringify({ 79 | userAddr: extra.userAddress, // the checksummed address used to generate the quote 80 | pathId: quote.pathId // Replace with the pathId from quote response in step 1 81 | //simulate: true // this can be set to true if the user isn't doing their own estimate gas call for the transaction 82 | }) 83 | }).then((res) => res.json()); 84 | 85 | if (swapData.transaction.to.toLowerCase() !== routers[chain].toLowerCase()) { 86 | throw new Error(`Router address does not match`); 87 | } 88 | 89 | return { 90 | amountReturned: swapData.outputTokens[0].amount, 91 | estimatedGas: swapData.transaction.gas <= 0 ? swapData.gasEstimate : swapData.transaction.gas, 92 | rawQuote: swapData, 93 | tokenApprovalAddress: routers[chain] 94 | }; 95 | } 96 | 97 | export async function swap({ rawQuote }) { 98 | const tx = await sendTx({ 99 | from: rawQuote.transaction.from, 100 | to: rawQuote.transaction.to, 101 | data: rawQuote.transaction.data, 102 | value: rawQuote.transaction.value 103 | //gas: rawQuote.transaction.gas 104 | }); 105 | 106 | return tx; 107 | } 108 | 109 | export const getTxData = ({ rawQuote }) => rawQuote?.transaction.data; 110 | 111 | export const getTx = ({ rawQuote }) => ({ 112 | to: rawQuote.transaction.to, 113 | data: rawQuote.transaction.data, 114 | value: rawQuote.transaction.value 115 | }); 116 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/openocean.ts: -------------------------------------------------------------------------------- 1 | import { sendTx } from '../utils/sendTx'; 2 | import { zeroAddress } from 'viem'; 3 | 4 | export const chainToId = { 5 | //ethereum: 1, 6 | //bsc: 56, 7 | //polygon: 137, 8 | //optimism: 10, 9 | //arbitrum: 42161, 10 | gnosis: 100, 11 | //avax: 43114, 12 | fantom: 250, 13 | aurora: 1313161554, 14 | heco: 128, 15 | boba: 288, 16 | okexchain: 66, 17 | cronos: 25, 18 | moonriver: 1285, 19 | //ontology: 58, 20 | polygonzkevm: 1101, 21 | kava: 2222, 22 | metis: 1088, 23 | zksync: 324, 24 | linea: 59144, 25 | //base: 8453, 26 | //starknet 27 | //telos 28 | celo: 42220, 29 | scroll: 534352 30 | //harmony 31 | //tron 32 | }; 33 | 34 | // https://docs.openocean.finance/dev/contracts-of-chains#openoceans-current-contract-addresses 35 | const approvaAddressByChain = { 36 | polygonzkevm: '0x6dd434082EAB5Cd134B33719ec1FF05fE985B97b', 37 | zksync: '0x36A1aCbbCAfca2468b85011DDD16E7Cb4d673230', 38 | linea: '0x6352a56caadC4F1E25CD6c75970Fa768A3304e64', 39 | okexchain: '0xc0006Be82337585481044a7d11941c0828FFD2D4' 40 | }; 41 | 42 | export const name = 'OpenOcean'; 43 | export const token = 'OOE'; 44 | 45 | export function approvalAddress() { 46 | return '0x6352a56caadc4f1e25cd6c75970fa768a3304e64'; 47 | } 48 | 49 | // https://docs.openocean.finance/dev/openocean-api-3.0/quick-start 50 | // the api from their docs is broken 51 | // eg: https://open-api.openocean.finance/v3/eth/quote?inTokenAddress=0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9&outTokenAddress=0x8888801af4d980682e47f1a9036e589479e835c5&amount=100000000000000000000&gasPrice=400000000 52 | // returns a AAVE->MPH trade that returns 10.3k MPH, when in reality that trade only gets you 3.8k MPH 53 | // Replaced API with the one you get from snooping in their frontend, which works fine 54 | export async function getQuote(chain: string, from: string, to: string, amount: string, { slippage, userAddress }) { 55 | const gasPrice = await fetch(`https://ethapi.openocean.finance/v2/${chainToId[chain]}/gas-price`).then((r) => 56 | r.json() 57 | ); 58 | const data = await fetch( 59 | `https://ethapi.openocean.finance/v2/${ 60 | chainToId[chain] 61 | }/swap?inTokenAddress=${from}&outTokenAddress=${to}&amount=${amount}&gasPrice=${ 62 | gasPrice.fast?.maxFeePerGas ?? gasPrice.fast 63 | }&slippage=${+slippage * 100}&account=${ 64 | userAddress || zeroAddress 65 | }&referrer=0x5521c3dfd563d48ca64e132324024470f3498526` 66 | ).then((r) => r.json()); 67 | 68 | let gas = data.estimatedGas; 69 | 70 | return { 71 | amountReturned: data.outAmount, 72 | estimatedGas: gas, 73 | tokenApprovalAddress: approvaAddressByChain[chain] ?? '0x6352a56caadc4f1e25cd6c75970fa768a3304e64', 74 | rawQuote: { ...data, gasLimit: gas }, 75 | logo: 'https://assets.coingecko.com/coins/images/17014/small/ooe_log.png?1626074195' 76 | }; 77 | } 78 | 79 | export async function swap({ rawQuote, chain }) { 80 | const tx = await sendTx({ 81 | from: rawQuote.from, 82 | to: rawQuote.to, 83 | data: rawQuote.data, 84 | value: rawQuote.value, 85 | }); 86 | return tx; 87 | } 88 | 89 | export const getTxData = ({ rawQuote }) => rawQuote?.data; 90 | 91 | export const getTx = ({ rawQuote }) => ({ 92 | from: rawQuote.from, 93 | to: rawQuote.to, 94 | data: rawQuote.data, 95 | value: rawQuote.value 96 | }); 97 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/paraswap.ts: -------------------------------------------------------------------------------- 1 | // Source: https://developers.paraswap.network/api/master 2 | 3 | import { sendTx } from '../utils/sendTx'; 4 | import { defillamaReferrerAddress } from '../constants'; 5 | import { zeroAddress } from 'viem'; 6 | 7 | // api docs have an outdated chain list, need to check https://app.paraswap.io/# to find supported networks 8 | export const chainToId = { 9 | ethereum: 1, 10 | bsc: 56, 11 | polygon: 137, 12 | avax: 43114, 13 | arbitrum: 42161, 14 | fantom: 250, 15 | optimism: 10, 16 | polygonzkevm: 1101, 17 | base: 8453, 18 | gnosis: 100 19 | }; 20 | 21 | const approvers = { 22 | ethereum: "0x6a000f20005980200259b80c5102003040001068", 23 | bsc: "0x6a000f20005980200259b80c5102003040001068", 24 | polygon: "0x6a000f20005980200259b80c5102003040001068", 25 | avax: "0x6a000f20005980200259b80c5102003040001068", 26 | arbitrum: "0x6a000f20005980200259b80c5102003040001068", 27 | fantom: "0x6a000f20005980200259b80c5102003040001068", 28 | optimism: "0x6a000f20005980200259b80c5102003040001068", 29 | polygonzkevm: "0x6a000f20005980200259b80c5102003040001068", 30 | base: "0x6a000f20005980200259b80c5102003040001068", 31 | gnosis: "0x6a000f20005980200259b80c5102003040001068" 32 | } 33 | 34 | export const name = 'ParaSwap'; 35 | export const token = 'PSP'; 36 | export const partner = 'llamaswap'; 37 | export const isOutputAvailable = true; 38 | 39 | export function approvalAddress() { 40 | return '0x216b4b4ba9f3e719726886d34a177484278bfcae'; 41 | } 42 | const nativeToken = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; 43 | export async function getQuote( 44 | chain: string, 45 | from: string, 46 | to: string, 47 | amount: string, 48 | { fromToken, toToken, userAddress, slippage, amountOut } 49 | ) { 50 | // ethereum = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE 51 | // amount should include decimals 52 | 53 | const tokenFrom = from === zeroAddress ? nativeToken : from; 54 | const tokenTo = to === zeroAddress ? nativeToken : to; 55 | const side = amountOut && amountOut !== '0' ? 'BUY' : 'SELL'; 56 | const finalAmount = side === 'BUY' ? amountOut : amount; 57 | const data = await fetch( 58 | `https://apiv5.paraswap.io/prices/?srcToken=${tokenFrom}&destToken=${tokenTo}&amount=${finalAmount}&srcDecimals=${fromToken?.decimals}&destDecimals=${toToken?.decimals}&partner=${partner}&side=${side}&network=${chainToId[chain]}&excludeDEXS=ParaSwapPool,ParaSwapLimitOrders&version=6.2` 59 | ).then((r) => r.json()); 60 | 61 | const dataSwap = 62 | userAddress !== zeroAddress 63 | ? await fetch(`https://apiv5.paraswap.io/transactions/${chainToId[chain]}?ignoreChecks=true`, { 64 | method: 'POST', 65 | body: JSON.stringify({ 66 | srcToken: data.priceRoute.srcToken, 67 | srcDecimals: data.priceRoute.srcDecimals, 68 | destToken: data.priceRoute.destToken, 69 | destDecimals: data.priceRoute.destDecimals, 70 | slippage: slippage * 100, 71 | userAddress: userAddress, 72 | partner: partner, 73 | partnerAddress: defillamaReferrerAddress, 74 | takeSurplus: true, 75 | priceRoute: data.priceRoute, 76 | isCapSurplus: true, 77 | ...(side === 'BUY' ? { destAmount: data.priceRoute.destAmount } : { srcAmount: data.priceRoute.srcAmount }) 78 | }), 79 | headers: { 80 | 'Content-Type': 'application/json' 81 | } 82 | }).then((r) => r.json()) 83 | : null; 84 | 85 | if (dataSwap?.error) { 86 | throw new Error(dataSwap.error) 87 | } 88 | 89 | let gas = data.priceRoute.gasCost; 90 | 91 | if(data.priceRoute.tokenTransferProxy.toLowerCase() !== approvers[chain].toLowerCase()){ 92 | throw new Error("Approval address doesn't match") 93 | } 94 | 95 | return { 96 | amountReturned: data.priceRoute.destAmount, 97 | amountIn: data.priceRoute.srcAmount || 0, 98 | estimatedGas: gas, 99 | tokenApprovalAddress: data.priceRoute.tokenTransferProxy, 100 | rawQuote: { ...dataSwap, gasLimit: gas }, 101 | logo: 'https://assets.coingecko.com/coins/images/20403/small/ep7GqM19_400x400.jpg?1636979120' 102 | }; 103 | } 104 | 105 | export async function swap({ rawQuote, chain }) { 106 | const tx = await sendTx({ 107 | from: rawQuote.from, 108 | to: rawQuote.to, 109 | data: rawQuote.data, 110 | value: rawQuote.value, 111 | }); 112 | 113 | return tx; 114 | } 115 | 116 | export const getTxData = ({ rawQuote }) => rawQuote?.data; 117 | 118 | export const getTx = ({ rawQuote }) => ({ 119 | from: rawQuote.from, 120 | to: rawQuote.to, 121 | data: rawQuote.data, 122 | value: rawQuote.value 123 | }); 124 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/rango.ts: -------------------------------------------------------------------------------- 1 | // Source https://docs.1inch.io/docs/aggregation-protocol/api/swagger 2 | 3 | import BigNumber from 'bignumber.js'; 4 | import { zeroAddress } from 'viem'; 5 | import { estimateGas } from 'wagmi/actions'; 6 | import { config } from '../../WalletProvider'; 7 | import { chainsMap } from '../constants'; 8 | 9 | export const chainToId = { 10 | ethereum: 'ETH', 11 | bsc: 'BSC', 12 | polygon: 'POLYGON', 13 | optimism: 'OPTIMISM', 14 | arbitrum: 'ARBITRUM', 15 | gnosis: 'GNOSIS', 16 | avax: 'AVAX_CCHAIN', 17 | fantom: 'FANTOM', 18 | aurora: 'AURORA' 19 | }; 20 | 21 | export const name = 'Rango'; 22 | export const token = null; 23 | 24 | export async function getQuote( 25 | chain: string, 26 | from: string, 27 | to: string, 28 | amount: string, 29 | { userAddress, fromToken, toToken, slippage } 30 | ) { 31 | // ethereum = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE 32 | // amount should include decimals 33 | 34 | const tokenFrom = 35 | fromToken.address === zeroAddress 36 | ? `${chainToId[chain]}.${fromToken.symbol}` 37 | : `${chainToId[chain]}.${fromToken.symbol}--${fromToken.address}`; 38 | const tokenTo = 39 | toToken.address === zeroAddress 40 | ? `${chainToId[chain]}.${toToken.symbol}` 41 | : `${chainToId[chain]}.${toToken.symbol}--${toToken.address}`; 42 | const params = new URLSearchParams({ 43 | from: tokenFrom, 44 | to: tokenTo, 45 | amount: amount, 46 | fromAddress: userAddress || zeroAddress, 47 | toAddress: userAddress || zeroAddress, 48 | disableEstimate: 'true', 49 | apiKey: 'c0ed54c0-e85c-4547-8e11-7ff88775b90c', 50 | slippage: slippage || '1' 51 | }).toString(); 52 | 53 | const data = await fetch(`https://api.rango.exchange/basic/swap?${params}`).then((r) => r.json()); 54 | 55 | let estimatedGas; 56 | try { 57 | estimatedGas = ( 58 | await estimateGas(config, { 59 | to: data?.tx?.txTo, 60 | data: data?.tx?.txData, 61 | value: data?.tx?.value, 62 | chainId: chainsMap[chain] 63 | }) 64 | ).toString(); 65 | } catch (e) { 66 | estimatedGas = BigNumber(data?.tx?.gasLimit).toString(); 67 | } 68 | 69 | const gasPrice = 70 | chain === 'optimism' && estimatedGas ? BigNumber(3.5).times(estimatedGas).toFixed(0, 1) : estimatedGas; 71 | 72 | return { 73 | amountReturned: data?.route?.outputAmount, 74 | estimatedGas: gasPrice, 75 | tokenApprovalAddress: data?.tx?.txTo, 76 | rawQuote: { ...data, gasLimit: gasPrice }, 77 | logo: '' 78 | }; 79 | } 80 | 81 | export async function swap({ signer, rawQuote, chain }) { 82 | const fromAddress = await signer.getAddress(); 83 | 84 | const tx = await signer.sendTransaction({ 85 | from: fromAddress, 86 | to: rawQuote?.tx?.txTo, 87 | data: rawQuote?.tx?.txData, 88 | value: rawQuote?.tx?.value, 89 | ...(chain === 'optimism' && { gas: rawQuote.gasLimit }) 90 | }); 91 | 92 | return tx; 93 | } 94 | 95 | export const getTxData = ({ rawQuote }) => rawQuote?.tx?.txData; 96 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/unidex.ts: -------------------------------------------------------------------------------- 1 | // Unidex aggregates many aggregators but their api only supports fantom 2 | // Source: https://unidexexchange.gitbook.io/unidex/api-information/swap-aggregator/quote-swap 3 | // IMPORTANT: their api is broken, this integration is disabled since it doesnt work 4 | 5 | import BigNumber from 'bignumber.js'; 6 | 7 | export const chainToId = { 8 | fantom: 250 9 | }; 10 | 11 | export const name = 'UniDex'; 12 | export const token = 'UNIDX'; 13 | 14 | export function approvalAddress() { 15 | return '0x216b4b4ba9f3e719726886d34a177484278bfcae'; 16 | } 17 | 18 | export async function getQuote(chain: string, from: string, to: string, amount: string) { 19 | const data = await fetch( 20 | `https://unidexmirai.org/swap/v1/quote?sellToken=${from}&buyToken=${to}&sellAmount=${amount}` 21 | ).then((r) => r.json()); 22 | 23 | const estimatedGas = chain === 'optimism' ? BigNumber(3.5).times(data.estimatedGas).toFixed(0, 1) : data.estimatedGas; 24 | 25 | return { 26 | amountReturned: data.buyAmount, 27 | estimatedGas, 28 | tokenApprovalAddress: data.allowanceTarget 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/utils.ts: -------------------------------------------------------------------------------- 1 | export const redirectQuoteReq = async ( 2 | protocol: string, 3 | chain: string, 4 | from: string, 5 | to: string, 6 | amount: string, 7 | extra: any 8 | ) => { 9 | const data = await fetch( 10 | `https://swap-api.defillama.com/dexAggregatorQuote?protocol=${encodeURIComponent( 11 | protocol 12 | )}&chain=${chain}&from=${from}&to=${to}&amount=${amount}&api_key=nsr_UYWxuvj1hOCgHxJhDEKZ0g30c4Be3I5fOMBtFAA`, 13 | { 14 | method: 'POST', 15 | body: JSON.stringify(extra) 16 | } 17 | ).then((res) => res.json()); 18 | 19 | return data; 20 | }; 21 | 22 | interface SwapEvent { 23 | user: string; 24 | aggregator: string; 25 | isError: boolean; 26 | chain: string; 27 | from: string; 28 | to: string; 29 | quote: any; 30 | txUrl: string; 31 | amount: string; 32 | errorData: any; 33 | amountUsd: number | null; 34 | slippage: string; 35 | routePlace: string; 36 | route: any; 37 | reportedOutput?: number; 38 | realOutput?: number; 39 | } 40 | 41 | export const sendSwapEvent = async (event: SwapEvent) => { 42 | const data = await fetch(`https://llamaswap-stats.llama.fi/saveEvent`, { 43 | method: 'POST', 44 | body: JSON.stringify(event) 45 | }).then((res) => res.json()); 46 | 47 | return data; 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/yieldyak/abi.ts: -------------------------------------------------------------------------------- 1 | export const ABI = [ 2 | { 3 | inputs: [ 4 | { internalType: 'uint256', name: '_amountIn', type: 'uint256' }, 5 | { internalType: 'address', name: '_tokenIn', type: 'address' }, 6 | { internalType: 'address', name: '_tokenOut', type: 'address' }, 7 | { internalType: 'uint256', name: '_maxSteps', type: 'uint256' }, 8 | { internalType: 'uint256', name: '_gasPrice', type: 'uint256' } 9 | ], 10 | name: 'findBestPathWithGas', 11 | outputs: [ 12 | { 13 | components: [ 14 | { internalType: 'uint256[]', name: 'amounts', type: 'uint256[]' }, 15 | { internalType: 'address[]', name: 'adapters', type: 'address[]' }, 16 | { internalType: 'address[]', name: 'path', type: 'address[]' }, 17 | { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' } 18 | ], 19 | internalType: 'struct YakRouter.FormattedOfferWithGas', 20 | name: '', 21 | type: 'tuple' 22 | } 23 | ], 24 | stateMutability: 'view', 25 | type: 'function' 26 | }, 27 | { 28 | inputs: [ 29 | { 30 | components: [ 31 | { internalType: 'uint256', name: 'amountIn', type: 'uint256' }, 32 | { internalType: 'uint256', name: 'amountOut', type: 'uint256' }, 33 | { internalType: 'address[]', name: 'path', type: 'address[]' }, 34 | { internalType: 'address[]', name: 'adapters', type: 'address[]' } 35 | ], 36 | internalType: 'struct YakRouter.Trade', 37 | name: '_trade', 38 | type: 'tuple' 39 | }, 40 | { internalType: 'address', name: '_to', type: 'address' }, 41 | { internalType: 'uint256', name: '_fee', type: 'uint256' } 42 | ], 43 | name: 'swapNoSplit', 44 | outputs: [], 45 | stateMutability: 'nonpayable', 46 | type: 'function' 47 | }, 48 | { 49 | inputs: [ 50 | { 51 | components: [ 52 | { internalType: 'uint256', name: 'amountIn', type: 'uint256' }, 53 | { internalType: 'uint256', name: 'amountOut', type: 'uint256' }, 54 | { internalType: 'address[]', name: 'path', type: 'address[]' }, 55 | { internalType: 'address[]', name: 'adapters', type: 'address[]' } 56 | ], 57 | internalType: 'struct YakRouter.Trade', 58 | name: '_trade', 59 | type: 'tuple' 60 | }, 61 | { internalType: 'address', name: '_to', type: 'address' }, 62 | { internalType: 'uint256', name: '_fee', type: 'uint256' } 63 | ], 64 | name: 'swapNoSplitFromAVAX', 65 | outputs: [], 66 | stateMutability: 'payable', 67 | type: 'function' 68 | }, 69 | { 70 | inputs: [ 71 | { 72 | components: [ 73 | { internalType: 'uint256', name: 'amountIn', type: 'uint256' }, 74 | { internalType: 'uint256', name: 'amountOut', type: 'uint256' }, 75 | { internalType: 'address[]', name: 'path', type: 'address[]' }, 76 | { internalType: 'address[]', name: 'adapters', type: 'address[]' } 77 | ], 78 | internalType: 'struct YakRouter.Trade', 79 | name: '_trade', 80 | type: 'tuple' 81 | }, 82 | { internalType: 'address', name: '_to', type: 'address' }, 83 | { internalType: 'uint256', name: '_fee', type: 'uint256' } 84 | ], 85 | name: 'swapNoSplitToAVAX', 86 | outputs: [], 87 | stateMutability: 'nonpayable', 88 | type: 'function' 89 | } 90 | ]; 91 | -------------------------------------------------------------------------------- /src/components/Aggregator/adapters/yieldyak/index.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { sendTx } from '../../utils/sendTx'; 3 | import { ABI } from './abi'; 4 | import { encodeFunctionData, zeroAddress } from 'viem'; 5 | import { readContract } from 'wagmi/actions'; 6 | import { config } from '../../../WalletProvider'; 7 | import { chainsMap } from '../../constants'; 8 | 9 | // Source https://github.com/yieldyak/yak-aggregator 10 | export const chainToId = { 11 | avax: '0xC4729E56b831d74bBc18797e0e17A295fA77488c', 12 | canto: '0xE9A2a22c92949d52e963E43174127BEb50739dcF' 13 | }; 14 | 15 | export const name = 'YieldYak'; 16 | export const token = 'YAK'; 17 | 18 | export function approvalAddress(chain: string) { 19 | return chainToId[chain]; 20 | } 21 | 22 | const nativeToken = { 23 | avax: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', 24 | canto: '0x826551890dc65655a0aceca109ab11abdbd7a07b' 25 | }; 26 | 27 | export async function getQuote(chain: string, from: string, to: string, amount: string, extra: any) { 28 | const tokenFrom = from === zeroAddress ? nativeToken[chain] : from; 29 | const tokenTo = to === zeroAddress ? nativeToken[chain] : to; 30 | 31 | const gasPrice = extra.gasPriceData?.gasPrice ?? '1062500000000'; 32 | 33 | const data = (await readContract(config, { 34 | address: chainToId[chain], 35 | abi: ABI, 36 | functionName: 'findBestPathWithGas', 37 | args: [amount, tokenFrom, tokenTo, 3, gasPrice], 38 | chainId: chainsMap[chain] 39 | })) as { 40 | amounts: Array; 41 | adapters: Array<`0x${string}`>; 42 | gasEstimate: bigint; 43 | path: Array<`0x${string}`>; 44 | }; 45 | 46 | const expectedAmount = data.amounts[data.amounts.length - 1]; 47 | 48 | const minAmountOut = BigNumber(expectedAmount.toString()) 49 | .times(1 - Number(extra.slippage) / 100) 50 | .toFixed(0); 51 | 52 | const gas = data.gasEstimate + 21000n; 53 | 54 | return { 55 | amountReturned: expectedAmount.toString(), 56 | estimatedGas: gas.toString(), // Gas estimates only include gas-cost of swapping and querying on adapter and not intermediate logic. 57 | rawQuote: { 58 | // convert bigint to string so when we send swap event to our server, app doesn't crash serializing bigint values 59 | offer: { 60 | ...data, 61 | amounts: data.amounts.map((amount) => String(amount)), 62 | gasEstimate: String(data.gasEstimate) 63 | }, 64 | minAmountOut 65 | }, 66 | tokenApprovalAddress: chainToId[chain], 67 | logo: 'https://assets.coingecko.com/coins/images/17654/small/yieldyak.png?1665824438' 68 | }; 69 | } 70 | 71 | export async function swap({ chain, fromAddress, rawQuote, from, to }) { 72 | const data = encodeFunctionData({ 73 | abi: ABI, 74 | functionName: 75 | from === zeroAddress ? 'swapNoSplitFromAVAX' : to === zeroAddress ? 'swapNoSplitToAVAX' : 'swapNoSplit', 76 | args: [ 77 | [rawQuote.offer.amounts[0], rawQuote.minAmountOut, rawQuote.offer.path, rawQuote.offer.adapters], 78 | fromAddress, 79 | 0 80 | ] 81 | }); 82 | 83 | const tx = { 84 | to: chainToId[chain], 85 | data, 86 | ...(from === zeroAddress ? { value: rawQuote.offer.amounts[0] } : {}) 87 | }; 88 | 89 | const res = await sendTx(tx); 90 | 91 | return res; 92 | } 93 | -------------------------------------------------------------------------------- /src/components/Aggregator/chainToCoingeckoId.ts: -------------------------------------------------------------------------------- 1 | 2 | // Copied from @defillama/sdk/src/computeTVL/index.ts 3 | export const chainToCoingeckoId = { 4 | bsc: "binance-smart-chain", 5 | ethereum: "ethereum", 6 | polygon: "polygon-pos", 7 | avax: "avalanche", 8 | fantom: "fantom", 9 | xdai: "xdai", 10 | heco: "huobi-token", 11 | okexchain: "okex-chain", 12 | harmony: "harmony-shard-0", 13 | kcc: "kucoin-community-chain", 14 | celo: "celo", 15 | arbitrum: "arbitrum-one", 16 | iotex: "iotex", 17 | moonriver: "moonriver", 18 | solana: "solana", 19 | terra: "terra", 20 | tron: "tron", 21 | waves: "waves", 22 | klaytn: "klay-token", 23 | osmosis: "osmosis", 24 | kava: "kava", 25 | icon: "icon", 26 | optimism: "optimistic-ethereum", 27 | eos: "eos", 28 | secret: "secret", 29 | rsk: "rootstock", 30 | neo: "neo", 31 | tezos: "tezos", 32 | wan: "wanchain", 33 | ontology: "ontology", 34 | algorand: "algorand", 35 | zilliqa: "zilliqa", 36 | kardia: "kardiachain", 37 | cronos: "cronos", 38 | aurora: "aurora", 39 | boba: "boba", 40 | metis: "metis-andromeda", 41 | telos: "telos", 42 | moonbeam: "moonbeam", 43 | meter: "meter", 44 | sx: "sx-network", 45 | velas: "velas", 46 | milkomeda: "milkomeda-cardano", 47 | canto: "canto", 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Aggregator/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { useTokenApprove } from './useTokenApprove' 2 | 3 | export { useTokenApprove } 4 | -------------------------------------------------------------------------------- /src/components/Aggregator/hooks/useToken.tsx: -------------------------------------------------------------------------------- 1 | import { erc20Abi, getAddress } from 'viem'; 2 | import { useReadContracts } from 'wagmi'; 3 | 4 | export const useToken = ({ 5 | address, 6 | chainId, 7 | enabled 8 | }: { 9 | address: `0x${string}`; 10 | chainId: number; 11 | enabled: boolean; 12 | }) => { 13 | const { data, isLoading, error } = useReadContracts({ 14 | allowFailure: false, 15 | contracts: [ 16 | { 17 | address, 18 | abi: erc20Abi, 19 | functionName: 'name', 20 | chainId 21 | }, 22 | { 23 | address, 24 | abi: erc20Abi, 25 | functionName: 'symbol', 26 | chainId 27 | }, 28 | { 29 | address, 30 | abi: erc20Abi, 31 | functionName: 'decimals', 32 | chainId 33 | } 34 | ], 35 | query: { enabled } 36 | }); 37 | 38 | return { 39 | data: data ? { name: data[0], symbol: data[1], decimals: data[2], address: getAddress(address) } : null, 40 | isLoading, 41 | error 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/Aggregator/list.ts: -------------------------------------------------------------------------------- 1 | import * as matcha from './adapters/0x'; 2 | import * as inch from './adapters/1inch'; 3 | import * as cowswap from './adapters/cowswap'; 4 | //import * as firebird from './adapters/firebird'; 5 | import * as kyberswap from './adapters/kyberswap'; 6 | //import * as hashflow from './adapters/hashflow'; 7 | //import * as openocean from './adapters/openocean'; 8 | import * as paraswap from './adapters/paraswap'; 9 | // import * as lifi from './adapters/lifi'; 10 | // import * as rango from './adapters/rango'; 11 | 12 | // import * as unidex from "./adapters/unidex" - disabled, their api is broken 13 | // import * as airswap from './adapters/airswap' cors 14 | import * as odos from './adapters/odos'; 15 | // import * as yieldyak from './adapters/yieldyak'; 16 | // import * as llamazip from './adapters/llamazip'; 17 | // import * as krystal from './adapters/krystal' 18 | import * as matchaGasless from './adapters/0xGasless'; 19 | import * as matchaV2 from './adapters/0xV2'; 20 | 21 | export const adapters = [matcha, cowswap, paraswap, kyberswap, inch, matchaGasless, odos, matchaV2]; 22 | 23 | export const inifiniteApprovalAllowed = [matcha.name, cowswap.name, matchaGasless.name]; 24 | 25 | export const adaptersWithApiKeys = { 26 | [matcha.name]: true, 27 | [matchaGasless.name]: true, 28 | [matchaV2.name]: true, 29 | [inch.name]: true, 30 | //[hashflow.name]: true 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/Aggregator/router.ts: -------------------------------------------------------------------------------- 1 | import { allChains } from '../WalletProvider/chains'; 2 | import { chainNamesReplaced } from './constants'; 3 | import { adapters } from './list'; 4 | 5 | export const adaptersNames = adapters.map(({ name }) => name); 6 | 7 | const adaptersMap = adapters.reduce((acc, adapter) => ({ ...acc, [adapter.name]: adapter }), {}); 8 | 9 | export function getAllChains() { 10 | const chains = new Set(); 11 | for (const adapter of adapters) { 12 | Object.keys(adapter.chainToId).forEach((chain) => chains.add(chain)); 13 | } 14 | 15 | const chainsOptions = allChains 16 | .map((c) => { 17 | const isVisible = chains.has(c.network); 18 | if (!isVisible) return null; 19 | return { 20 | value: c.network, 21 | label: chainNamesReplaced[c.network] ?? c.name, 22 | chainId: c.id, 23 | logoURI: c?.iconUrl 24 | }; 25 | }) 26 | .filter(Boolean); 27 | return chainsOptions as Array<{ 28 | value: string; 29 | label: string; 30 | chainId: number; 31 | logoURI?: string | null; 32 | }>; 33 | } 34 | 35 | export async function swap({ 36 | chain, 37 | from, 38 | to, 39 | amount, 40 | fromAddress, 41 | slippage = '1', 42 | adapter, 43 | rawQuote, 44 | tokens, 45 | approvalData, 46 | signature 47 | }) { 48 | const aggregator = adaptersMap[adapter]; 49 | 50 | try { 51 | const res = await aggregator.swap({ 52 | chain, 53 | from, 54 | to, 55 | amount, 56 | fromAddress, 57 | slippage, 58 | rawQuote, 59 | tokens, 60 | approvalData, 61 | signature 62 | }); 63 | return res; 64 | } catch (e) { 65 | throw e; 66 | } 67 | } 68 | export async function gaslessApprove({ adapter, rawQuote, isInfiniteApproval }) { 69 | const aggregator = adaptersMap[adapter]; 70 | 71 | if (!aggregator.gaslessApprove) return; 72 | 73 | try { 74 | const res = await aggregator.gaslessApprove({ 75 | rawQuote, 76 | isInfiniteApproval 77 | }); 78 | return res; 79 | } catch (e) { 80 | throw e; 81 | } 82 | } 83 | 84 | export async function signatureForSwap({ signTypedDataAsync, adapter, rawQuote }) { 85 | const aggregator = adaptersMap[adapter]; 86 | 87 | if (!aggregator.signatureForSwap) return; 88 | 89 | try { 90 | const res = await aggregator.signatureForSwap({ 91 | signTypedDataAsync, 92 | rawQuote 93 | }); 94 | return res; 95 | } catch (e) { 96 | throw e; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/components/Aggregator/types.ts: -------------------------------------------------------------------------------- 1 | export interface ExtraData { 2 | userAddress: string; 3 | slippage: string; 4 | amountOut: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Aggregator/utils/arbitrumFees.ts: -------------------------------------------------------------------------------- 1 | import { readContract } from 'wagmi/actions'; 2 | import { config } from '../../WalletProvider'; 3 | import { arbitrum } from 'viem/chains'; 4 | 5 | export async function applyArbitrumFees(to: string, data: string, gas: string) { 6 | const gasData2 = await readContract(config, { 7 | address: '0x00000000000000000000000000000000000000C8', 8 | abi: [ 9 | { 10 | inputs: [ 11 | { internalType: 'address', name: 'to', type: 'address' }, 12 | { internalType: 'bool', name: 'contractCreation', type: 'bool' }, 13 | { internalType: 'bytes', name: 'data', type: 'bytes' } 14 | ], 15 | name: 'gasEstimateL1Component', 16 | outputs: [ 17 | { internalType: 'uint64', name: 'gasEstimateForL1', type: 'uint64' }, 18 | { internalType: 'uint256', name: 'baseFee', type: 'uint256' }, 19 | { internalType: 'uint256', name: 'l1BaseFeeEstimate', type: 'uint256' } 20 | ], 21 | stateMutability: 'view', 22 | type: 'function' 23 | } 24 | ], 25 | chainId: arbitrum.id, 26 | functionName: 'gasEstimateL1Component', 27 | args: [to as `0x${string}`, false, data as `0x${string}`] 28 | }); 29 | 30 | return Number(BigInt(gas) + gasData2[0]).toString(); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Aggregator/utils/getAllowance.ts: -------------------------------------------------------------------------------- 1 | import { erc20Abi, zeroAddress } from 'viem'; 2 | import { readContract } from 'wagmi/actions'; 3 | import { config } from '~/components/WalletProvider'; 4 | import { chainsMap } from '../constants'; 5 | 6 | // To change the approve amount you first have to reduce the addresses` 7 | // allowance to zero by calling `approve(_spender, 0)` if it is not 8 | // already 0 to mitigate the race condition described here: 9 | // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 10 | export const oldErc = [ 11 | '0xdAC17F958D2ee523a2206206994597C13D831ec7'.toLowerCase(), // USDT 12 | '0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32'.toLowerCase() // LDO 13 | ]; 14 | 15 | export async function getAllowance({ 16 | token, 17 | chain, 18 | address, 19 | spender 20 | }: { 21 | token?: string; 22 | chain?: string; 23 | address?: `0x${string}`; 24 | spender?: `0x${string}`; 25 | }) { 26 | if (!spender || !token || !address || token === zeroAddress || !chain) { 27 | return null; 28 | } 29 | try { 30 | const allowance = await readContract(config, { 31 | address: token as `0x${string}`, 32 | abi: erc20Abi, 33 | functionName: 'allowance', 34 | args: [address, spender], 35 | chainId: chainsMap[chain] 36 | }); 37 | 38 | return allowance; 39 | } catch (error) { 40 | throw new Error(error instanceof Error ? `[Allowance]:${error.message}` : '[Allowance]: Failed to fetch allowance'); 41 | } 42 | } -------------------------------------------------------------------------------- /src/components/Aggregator/utils/optimismFees.ts: -------------------------------------------------------------------------------- 1 | import { readContract } from 'wagmi/actions'; 2 | import { config } from '../../WalletProvider'; 3 | import { chainsMap } from '../constants'; 4 | 5 | const FEE_ADDRESS = '0x420000000000000000000000000000000000000F'; 6 | 7 | export const chainsWithOpFees = ['optimism', 'base']; 8 | 9 | export const getOptimismFee = async (txData, chain) => { 10 | if (!chain || !chainsMap[chain]) return 'Unknown'; 11 | 12 | try { 13 | const gas = await readContract(config, { 14 | address: FEE_ADDRESS, 15 | abi: [ 16 | { 17 | inputs: [{ internalType: 'bytes', name: '_data', type: 'bytes' }], 18 | name: 'getL1Fee', 19 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 20 | stateMutability: 'view', 21 | type: 'function' 22 | } 23 | ], 24 | functionName: 'getL1Fee', 25 | args: [txData], 26 | chainId: chainsMap[chain] 27 | }); 28 | 29 | return Number(gas) / 1e18; 30 | } catch (e) { 31 | console.log(e, txData); 32 | return 'Unknown'; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/Aggregator/utils/sendTx.ts: -------------------------------------------------------------------------------- 1 | import { estimateGas, sendTransaction } from 'wagmi/actions'; 2 | import { config } from '../../WalletProvider'; 3 | 4 | export async function sendTx(txObject: any) { 5 | if (txObject.data === '0x' || typeof txObject.to !== 'string') { 6 | throw new Error('Malformed tx'); // Should never happen 7 | } 8 | if (txObject.gas === undefined) { 9 | const gasPrediction = await estimateGas(config, txObject).catch(() => null); 10 | 11 | if (gasPrediction) { 12 | txObject.gas = (gasPrediction * 14n) / 10n; // Increase gas +40% 13 | } 14 | } 15 | 16 | return sendTransaction(config, txObject); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/CloseBtn/index.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton } from '@chakra-ui/react'; 2 | import { CrossIcon } from '../Icons'; 3 | 4 | export const CloseBtn = ({ onClick, ...props }) => ( 5 | } 11 | aria-label="close" 12 | onClick={onClick} 13 | {...props} 14 | /> 15 | ); 16 | -------------------------------------------------------------------------------- /src/components/FAQs/index.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Text } from '@chakra-ui/react'; 2 | 3 | export default function FaqWrapper() { 4 | return ( 5 | <> 6 | 7 | FAQ 8 | 9 | 10 | 11 |

12 | 13 | 14 | What is this? 15 | 16 | 17 | 18 |

19 | 20 | It's an aggregator of DEX aggregators, we query the price in 1inch, cowswap, matcha... and then offer you 21 | the best price among all of them 22 | 23 |
24 | 25 | 26 |

27 | 28 | 29 | Does DefiLlama take any fees? 30 | 31 | 32 | 33 |

34 | 35 | DefiLlama takes 0 fee on swaps. 36 |
37 |
You'll get the exact same price swapping through DefiLlama as what you'd get swapping through the 38 | chosen aggregator directly. 39 |
40 |
41 | We do add our referral code to swaps tho, so, for aggregators with revenue sharing, they will send us part 42 | of the fee they earn. This is not an extra fee, you'd be charged the same fee anyway, but now a small part 43 | of it is shared with DefiLlama. We also integrate aggregators with no fee sharing the best price, and in 44 | those cases we don't make any money. 45 |
46 |
47 | 48 |

49 | 50 | 51 | Is it safe? 52 | 53 | 54 | 55 |

56 | 57 | Our aggregator uses the router contract of each aggregator, we don't use any contracts developed by us. Thus 58 | you inherit the same security you'd get by swapping directly from their UI instead of ours. 59 | 60 |
61 | 62 |

63 | 64 | 65 | Why do gas fees in MetaMask not match what I see in the UI? 66 | 67 | 68 | 69 |

70 | 71 | We inflate gas limit of txs on MetaMask by +40% to ensure that there's nothing unexpected that could trigger out-of-gas reverts. This stacks 72 | on top of any increase your RPC might apply on gas estimations, along with possible different gas prices between your metamask and our estimation. 73 |
74 |
75 | All this together means that gas number you see on metamask will always be inflated, while in our UI we display the actual gas that the tx will consume. 76 | The extra gas that is not used is just refunded to the user when tx executes. 77 |
78 |
79 | 80 |

81 | 82 | 83 | Will I be eligible for aggregator airdrops if I swap through DefiLlama? 84 | 85 | 86 | 87 |

88 | 89 | We execute swaps directly against the router of each aggregator, so there's no difference between a swap 90 | executed directly from their UI and a swap executed from DefiLlama. 91 |
92 |
93 | Thus, if any of the aggregators we integrate does an airdrop in the future, all swaps made through them 94 | would be eligible for their airdrop. 95 |
96 |
97 | 98 |

99 | 100 | 101 | I swapped ETH on CoW Swap but it just disappeared, what happened? 102 | 103 | 104 | 105 |

106 | 107 | Some ETH orders on CoW Swap might not get filled because price moves against you too quickly, 108 | in those cases the ETH just sits in a contract until it is refunded 30 minutes after your tx. 109 | 110 |
111 |
112 | 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /src/components/Lending/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import llamasWifCoins from './llamas_wif_coins.png'; 2 | import llamaWifBinoculars from './llama_wif_binoculars.png'; 3 | 4 | import { Text } from '@chakra-ui/react'; 5 | 6 | const NotFound = ({ hasSelectedFilters = false, text, size = '150px' }) => { 7 | return ( 8 |
22 | llamas 27 | 28 | {text} 29 | 30 |
31 | ); 32 | }; 33 | 34 | export default NotFound; 35 | -------------------------------------------------------------------------------- /src/components/Lending/llama_wif_binoculars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/src/components/Lending/llama_wif_binoculars.png -------------------------------------------------------------------------------- /src/components/Lending/llamas_wif_coins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LlamaSwap/interface/70dd6221325dd25ffe749d887875d66f9496d92b/src/components/Lending/llamas_wif_coins.png -------------------------------------------------------------------------------- /src/components/MultiSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import Select, { Props } from 'react-select'; 2 | import styled from 'styled-components'; 3 | import { QuestionIcon } from '@chakra-ui/icons'; 4 | import { CSSProperties } from 'react'; 5 | 6 | interface IReactSelect extends Props { 7 | style?: CSSProperties; 8 | defaultOptions?: boolean; 9 | itemCount?: number; 10 | cacheOptions?: boolean; 11 | } 12 | 13 | const formatOptionLabel = ({ label, ...rest }) => { 14 | return ( 15 |
16 |
17 | {rest.logoURI ? ( 18 | 31 | ) : rest?.logoURI === false ? null : ( 32 | 33 | )} 34 |
35 |
{label}
36 |
37 | ); 38 | }; 39 | 40 | const Wrapper = styled.span` 41 | --background: ${({ theme }) => theme.bg6}; 42 | --menu-background: ${({ theme }) => theme.bg6}; 43 | --color: ${({ theme }) => theme.text1}; 44 | --placeholder: ${({ theme }) => theme.text3}; 45 | --bg-hover: ${({ theme }) => theme.bg2}; 46 | --option-bg: ${({ theme }) => theme.bg2}; 47 | 48 | & > * > * { 49 | box-shadow: 50 | 0px 24px 32px rgba(0, 0, 0, 0.04), 51 | 0px 16px 24px rgba(0, 0, 0, 0.04), 52 | 0px 4px 8px rgba(0, 0, 0, 0.04), 53 | 0px 0px 1px rgba(0, 0, 0, 0.04); 54 | border-radius: 12px; 55 | } 56 | @media screen and (max-width: ${({ theme }) => theme.bpMed}) { 57 | font-size: 16px; 58 | } 59 | `; 60 | 61 | const customStyles = { 62 | control: (provided) => ({ 63 | ...provided, 64 | background: 'var(--background)', 65 | padding: '4px 2px', 66 | borderRadius: '12px', 67 | border: 'none', 68 | color: 'var(--color)', 69 | boxShadow: 70 | '0px 24px 32px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 0px 1px rgba(0, 0, 0, 0.04)', 71 | margin: 0, 72 | zIndex: 0 73 | }), 74 | input: (provided) => ({ 75 | ...provided, 76 | color: 'var(--color)' 77 | }), 78 | menu: (provided) => ({ 79 | ...provided, 80 | background: 'var(--menu-background)', 81 | zIndex: 10 82 | }), 83 | menuList: (provided) => ({ 84 | ...provided, 85 | 'scrollbar-width': 'none', 86 | '-ms-overflow-style': 'none', 87 | '&::-webkit-scrollbar': { 88 | display: 'none' 89 | } 90 | }), 91 | option: (provided, state) => ({ 92 | ...provided, 93 | color: state.isActive ? 'black' : 'var(--color)' 94 | }), 95 | multiValue: (provided) => ({ 96 | ...provided, 97 | fontFamily: 'inherit', 98 | background: 'var(--option-bg)', 99 | padding: '2px' 100 | }), 101 | multiValueLabel: (styles) => ({ 102 | ...styles, 103 | color: 'var(--color)' 104 | }), 105 | placeholder: (provided) => ({ 106 | ...provided, 107 | color: 'var(--placeholder)' 108 | }), 109 | singleValue: (provided, state) => ({ 110 | ...provided, 111 | color: 'var(--color)' 112 | }) 113 | }; 114 | 115 | const ReactSelect = ({ options, style, ...props }: IReactSelect) => ( 116 | 117 | { 82 | setSlippage(val.target.value.replace(/[^0-9.,]/g, '')?.replace(/,/g, '.')); 83 | }} 84 | /> 85 | {warnings.length ? ( 86 | 87 | 88 | 89 | 90 | 91 | {warnings.map((warning, i) => ( 92 | {warning} 93 | ))} 94 | 95 | 96 | ) : null} 97 | {['0.01', '0.1', '0.5', '1'].map((slp) => ( 98 | 111 | ))} 112 | 113 | 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /src/components/Tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import styled from 'styled-components'; 4 | 5 | const TabList = styled.ul` 6 | display: flex; 7 | list-style: none; 8 | margin: 0; 9 | padding: 0; 10 | position: relative; 11 | z-index: 1000; 12 | gap: 8px; 13 | background-color: ${({ theme }) => theme.background}; 14 | box-shadow: 10px 0px 50px 10px rgba(26, 26, 26, 0.6); 15 | overflow: auto; 16 | border-radius: 12px; 17 | `; 18 | 19 | const ActiveTabBackground = styled.div` 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | height: 100%; 24 | background-color: ${({ theme }) => theme.primary1}; 25 | border-radius: 12px; 26 | transition: 27 | transform 0.3s ease, 28 | width 0.3s ease; 29 | will-change: transform, width; 30 | `; 31 | 32 | const Tab = styled.li<{ active: boolean }>` 33 | border-radius: 12px; 34 | padding: 0.625rem 1.25rem; 35 | cursor: pointer; 36 | color: ${({ active, theme }) => (active ? theme.white : theme.text1)}; 37 | transition: color 0.3s ease, background-color 0.3s ease; 38 | font-size: 16px; 39 | font-weight: bold; 40 | display: flex; 41 | align-items: center; 42 | z-index: 1; 43 | background-color: ${({ active, theme }) => (active ? theme.primary1 : 'transparent')}; 44 | position: relative; 45 | overflow: hidden; 46 | 47 | &:hover { 48 | background-color: rgba(33, 114, 229, 0.1); 49 | color: ${({ theme }) => theme.white}; 50 | } 51 | 52 | &:hover::before { 53 | content: ''; 54 | position: absolute; 55 | top: 0; 56 | left: 0; 57 | right: 0; 58 | bottom: 0; 59 | background-color: 60 | border-radius: 25px; 61 | } 62 | `; 63 | 64 | const Wrapper = styled.div` 65 | display: flex; 66 | flex-direction: column; 67 | align-items: center; 68 | gap: 24px; 69 | width: 100%; 70 | text-align: left; 71 | 72 | background-color: rgb(34, 36, 42); 73 | border-radius: 10px; 74 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 75 | 76 | @media screen and (max-width: ${({ theme }) => theme.bpMed}) { 77 | margin-top: 60px; 78 | } 79 | `; 80 | 81 | const TabPanels = styled.div` 82 | width: 100%; 83 | display: flex; 84 | justify-content: center; 85 | @media screen and (max-width: ${({ theme }) => theme.bpMed}) { 86 | padding: 0.5rem; 87 | margin-top: 0.25rem; 88 | } 89 | `; 90 | 91 | const Tabs = ({ 92 | tabs 93 | }: { 94 | tabs: Array<{ 95 | id: string; 96 | name: string; 97 | content: JSX.Element; 98 | }>; 99 | }) => { 100 | const router = useRouter(); 101 | const [isRouterReady, setIsRouterReady] = React.useState(false); 102 | const [activeTab, setActiveTab] = React.useState(''); 103 | const tabRefs = React.useRef>>([]); 104 | tabRefs.current = tabs.map((_, i) => tabRefs.current[i] ?? React.createRef()); 105 | 106 | React.useEffect(() => { 107 | if (router.isReady) { 108 | const activeTabId = router.query.tab || 'swap'; 109 | setActiveTab(activeTabId as string); 110 | setIsRouterReady(true); 111 | } 112 | }, [router.isReady, router.query.tab]); 113 | const handleTabChange = (index) => { 114 | const tabId = tabs[index].id; 115 | setActiveTab(tabId); 116 | router.push({ query: { tab: tabId } }, undefined, { shallow: true }); 117 | }; 118 | 119 | const getActiveTabStyles = () => { 120 | const activeIndex = tabs.findIndex((tab) => tab.id === activeTab); 121 | const activeTabRef = tabRefs.current[activeIndex]; 122 | const width = activeTabRef.current ? activeTabRef.current.offsetWidth : 0; 123 | const left = activeTabRef.current ? activeTabRef.current.offsetLeft : 0; 124 | 125 | return { 126 | width: `${width}px`, 127 | transform: `translateX(${left}px)` 128 | }; 129 | }; 130 | 131 | if (!isRouterReady) { 132 | return null; 133 | } 134 | 135 | return ( 136 | 137 | 138 | {tabs.map((tab, index) => ( 139 | handleTabChange(index)} 144 | > 145 | {tab.name} 146 | 147 | ))} 148 | 149 | 150 | 151 | {tabs.find((tab) => tab.id === activeTab)?.content} 152 | 153 | ); 154 | }; 155 | 156 | export default Tabs; 157 | -------------------------------------------------------------------------------- /src/components/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | import { Tooltip as AriaTooltip, TooltipAnchor, useTooltipState } from 'ariakit/tooltip' 4 | import Link from 'next/link' 5 | 6 | interface ITooltip { 7 | content: string | null | React.ReactNode 8 | href?: string 9 | shallow?: boolean 10 | onClick?: (e: any) => any 11 | style?: {} 12 | children: React.ReactNode 13 | as?: any 14 | } 15 | 16 | const TooltipPopver = styled(AriaTooltip)` 17 | font-size: 0.85rem; 18 | padding: 1rem; 19 | color: hsl(0, 0%, 100%); 20 | background: hsl(204, 3%, 12%); 21 | border: 1px solid hsl(204, 3%, 32%); 22 | border-radius: 8px; 23 | filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 40%)); 24 | max-width: 228px; 25 | ` 26 | 27 | const TooltipAnchor2 = styled(TooltipAnchor)` 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | white-space: nowrap; 31 | flex-shrink: 0; 32 | 33 | a { 34 | display: flex; 35 | } 36 | ` 37 | 38 | const Popover2 = styled(TooltipPopver)` 39 | padding: 12px; 40 | ` 41 | 42 | export default function Tooltip({ content, as, href, shallow, onClick, children, ...props }: ITooltip) { 43 | const tooltip = useTooltipState() 44 | 45 | if (!content || content === '') return <>{children} 46 | 47 | const triggerProps = { 48 | ...(onClick && { onClick }) 49 | } 50 | 51 | return ( 52 | <> 53 | 54 | {href ? ( 55 | 56 | {children} 57 | 58 | ) : ( 59 | children 60 | )} 61 | 62 | 63 | {content} 64 | 65 | 66 | ) 67 | } 68 | 69 | export function Tooltip2({ content, children, ...props }: ITooltip) { 70 | const tooltip = useTooltipState() 71 | 72 | if (!content || content === '') return <>{children} 73 | 74 | return ( 75 | <> 76 | {children} 77 | 78 | {content} 79 | 80 | 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/components/TransactionModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | ModalBody, 4 | ModalCloseButton, 5 | ModalContent, 6 | ModalOverlay, 7 | Link as ChakraLink, 8 | Text 9 | } from '@chakra-ui/react'; 10 | import { ExternalLinkIcon } from '@chakra-ui/icons'; 11 | import {} from 'react-feather'; 12 | 13 | export const TransactionModal = ({ open, setOpen, link }) => { 14 | return ( 15 | setOpen(false)} 21 | > 22 | 23 | 24 | 25 | 26 | 27 | 38 | 39 | 40 | 41 | 42 | 43 | Transaction Submitted 44 | 45 | 46 | 47 | 59 | View on explorer 60 | 61 | 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/components/WalletProvider/index.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultConfig } from '@rainbow-me/rainbowkit'; 2 | import { rpcsTransports } from '../Aggregator/rpcs'; 3 | import { allChains } from './chains'; 4 | import type { Config } from 'wagmi'; 5 | 6 | const projectId = 'b3d4ba9fb97949ab12267b470a6f31d2'; 7 | 8 | export const config = getDefaultConfig({ 9 | appName: 'LlamaSwap', 10 | projectId, 11 | chains: allChains as any, 12 | transports: rpcsTransports, 13 | ssr: false 14 | }) as Config; 15 | 16 | declare module 'wagmi' { 17 | interface Register { 18 | config: typeof config; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Yields/List.tsx: -------------------------------------------------------------------------------- 1 | import { Input, VStack, Image, Flex, InputLeftElement, InputGroup, Divider, Text } from '@chakra-ui/react'; 2 | import { useMemo, useRef, useState } from 'react'; 3 | import { useVirtualizer } from '@tanstack/react-virtual'; 4 | import styled from 'styled-components'; 5 | import { SearchIcon } from '@chakra-ui/icons'; 6 | 7 | const StyledRow = styled.div` 8 | padding: 10px 20px; 9 | background-color: ${({ theme }) => theme.bg1}; 10 | transition: 11 | background-color 0.3s, 12 | transform 0.3s; 13 | border-radius: 15px; 14 | margin: 5px 0; 15 | color: ${({ theme }) => theme.text1}; 16 | 17 | &:hover { 18 | background-color: ${({ theme }) => theme.bg2}; 19 | transform: scale(1.02); 20 | cursor: pointer; 21 | } 22 | `; 23 | 24 | const StyledRows = styled.div` 25 | margin-top: 24px; 26 | height: 440px; 27 | overflow-y: auto; 28 | overflow-x: hidden; 29 | width: 100%; 30 | ::-webkit-scrollbar { 31 | display: none; 32 | } 33 | ms-overflow-style: none; 34 | scrollbar-width: none; 35 | `; 36 | 37 | export const InfiniteList = ({ items, setToken, search, setIsSearch, isSearch }) => { 38 | const [searchTerm, setSearchTerm] = useState(''); 39 | const parentRef = useRef(null); 40 | 41 | const filteredItems = useMemo(() => { 42 | if (!searchTerm) return items; 43 | const filtered = items 44 | ? items.filter((item) => item.value.toLowerCase().includes(searchTerm?.trim().toLowerCase())) 45 | : []; 46 | const exactMatch = items 47 | ? items.find((item) => item.value.toLowerCase() === searchTerm?.trim().toLowerCase()) 48 | : null; 49 | 50 | const result = exactMatch ? [exactMatch, ...filtered.filter((item) => item.value !== exactMatch.value)] : filtered; 51 | return result; 52 | }, [searchTerm, items]); 53 | 54 | const rowVirtualizer = useVirtualizer({ 55 | count: filteredItems.length, 56 | getScrollElement: () => parentRef.current, 57 | estimateSize: () => 45, 58 | overscan: 5 59 | }); 60 | return ( 61 | 62 | 63 | {isSearch ? ( 64 | 65 | Select a Token 66 | 67 | ) : null} 68 | 69 | 70 | 71 | 72 | setSearchTerm(e.target.value)} 80 | onClick={() => { 81 | setIsSearch(true); 82 | }} 83 | value={!isSearch ? search : searchTerm} 84 | border="none" 85 | /> 86 | 87 | 88 | 89 | {isSearch ? ( 90 | 91 | ) : null} 92 | 93 | {isSearch ? ( 94 | 95 |
102 | {rowVirtualizer.getVirtualItems().map((virtualRow) => { 103 | const item = filteredItems[virtualRow.index]; 104 | return ( 105 |
115 | { 117 | setToken(item.value); 118 | setIsSearch(false); 119 | setSearchTerm(''); 120 | }} 121 | style={{ 122 | display: 'flex', 123 | gap: '8px', 124 | fontWeight: '400', 125 | fontSize: '16px', 126 | textOverflow: 'ellipsis', 127 | whiteSpace: 'nowrap' 128 | }} 129 | > 130 | 131 | {item.label} {item.value} 132 | 133 |
134 | ); 135 | })} 136 |
137 |
138 | ) : null} 139 |
140 | ); 141 | }; 142 | -------------------------------------------------------------------------------- /src/components/Yields/MenuList.tsx: -------------------------------------------------------------------------------- 1 | import { useVirtualizer } from '@tanstack/react-virtual'; 2 | import { useEffect, useRef } from 'react'; 3 | 4 | export const MenuList = (props) => { 5 | const { options, children, maxHeight, getValue } = props; 6 | const [value] = getValue(); 7 | const selectedIndex = options.findIndex((option) => option.value === value?.value); 8 | const listRef = useRef(null); 9 | 10 | const rowVirtualizer = useVirtualizer({ 11 | count: children.length, 12 | getScrollElement: () => listRef.current, 13 | estimateSize: () => 35, 14 | overscan: 5 15 | }); 16 | 17 | useEffect(() => { 18 | if (selectedIndex > -1) { 19 | rowVirtualizer.scrollToIndex(selectedIndex); 20 | } 21 | }, [selectedIndex, rowVirtualizer]); 22 | 23 | return ( 24 |
34 |
41 | {rowVirtualizer.getVirtualItems().map((virtualRow: any) => ( 42 |
54 | {children[virtualRow.index]} 55 |
56 | ))} 57 |
58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/Yields/Panel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Panel = ({ isVisible, children, setVisible }) => { 5 | const panelRef = useRef(null); 6 | const buttonRef = useRef(null); 7 | 8 | useEffect(() => { 9 | const handleClickOutside = (event) => { 10 | if ( 11 | isVisible && 12 | panelRef.current && 13 | !panelRef.current.contains(event.target) && 14 | event.target !== buttonRef.current 15 | ) { 16 | setVisible(false); 17 | } 18 | }; 19 | 20 | document.addEventListener('click', handleClickOutside, true); 21 | 22 | return () => { 23 | document.removeEventListener('click', handleClickOutside, true); 24 | }; 25 | }, [setVisible, isVisible]); 26 | 27 | return ( 28 | <> 29 | 30 |
31 | {children} 32 |
33 |
34 | 35 | 36 | ); 37 | }; 38 | 39 | const BlurWrapper = styled.div<{ isVisible: boolean }>` 40 | display: ${({ isVisible }) => (isVisible ? 'block' : 'none')}; 41 | opacity: ${({ isVisible }) => (isVisible ? 1 : 0)}; 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | width: 100%; 46 | height: 100%; 47 | z-index: 3; 48 | backdrop-filter: blur(2px); 49 | transition: opacity 0.3s ease-in; 50 | `; 51 | 52 | const PanelBody = styled.div<{ isVisible: boolean }>` 53 | position: absolute; 54 | top: 0; 55 | right: 8px; 56 | border-radius: 16px; 57 | max-width: ${({ isVisible }) => (isVisible ? '400px' : '0')}; 58 | height: 100%; 59 | background-color: ${(props) => props.theme.bg2}; 60 | padding: ${({ isVisible }) => (isVisible ? '0px 8px 0px 16px' : '0')}; 61 | z-index: 4; 62 | transition: width 0.3s ease; 63 | background-color: rgb(34, 36, 42); 64 | box-shadow: ${({ isVisible }) => (isVisible ? '-8px 0 8px rgba(0, 0, 0, 0.15)' : 'none')}; 65 | 66 | overflow-y: auto; 67 | ::-webkit-scrollbar { 68 | display: none; 69 | } 70 | -ms-overflow-style: none; 71 | scrollbar-width: none; 72 | `; 73 | 74 | const PanelContent = styled.div<{ isVisible: boolean }>` 75 | opacity: ${({ isVisible }) => (isVisible ? 1 : 0)}; 76 | visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')}; 77 | transition: 78 | opacity 0.3s ease-in, 79 | visibility 0.5s ease-in; 80 | `; 81 | 82 | export const PanelButton = styled.button<{ isVisible: boolean }>` 83 | position: absolute; 84 | top: 50%; 85 | right: ${({ isVisible }) => (isVisible ? '288px' : '-20px')}; 86 | font-size: 1rem; 87 | transform: translateY(-50%) rotate(270deg); 88 | background-color: rgb(34, 36, 42); 89 | color: ${(props) => props.theme.text1}; 90 | border: none; 91 | border-radius: 12px 12px 0px 0; 92 | padding: 2px 12px 2px 12px; 93 | cursor: pointer; 94 | border-top: 1px solid #2f333c; 95 | border-left: 1px solid #2f333c; 96 | border-right: 1px solid #2f333c; 97 | z-index: 100; 98 | 99 | transition: 100 | background-color 0.3s ease, 101 | right 0.3s ease; 102 | 103 | &:hover { 104 | background-color: ${(props) => props.theme.bg3}; 105 | } 106 | `; 107 | 108 | export default Panel; 109 | -------------------------------------------------------------------------------- /src/constants/breakpoints.ts: -------------------------------------------------------------------------------- 1 | export const sm = 480 / 16 2 | export const med = 812 / 16 3 | export const lg = 1024 / 16 4 | export const xl = 1400 / 16 5 | export const twoXl = 1536 / 16 6 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export const useIsClient = () => { 4 | const [isClient, setIsClient] = useState(false); 5 | 6 | const windowType = typeof window; 7 | 8 | useEffect(() => { 9 | if (windowType !== 'undefined') { 10 | setIsClient(true); 11 | } 12 | }, [windowType]); 13 | 14 | return isClient; 15 | }; 16 | -------------------------------------------------------------------------------- /src/hooks/useCountdown.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const useCountdown = (targetDate) => { 4 | const countDownDate = new Date(targetDate).getTime(); 5 | 6 | const [countDown, setCountDown] = useState(countDownDate - new Date().getTime()); 7 | 8 | useEffect(() => { 9 | const interval = setInterval(() => { 10 | setCountDown(countDownDate - new Date().getTime()); 11 | }, 1000); 12 | 13 | return () => clearInterval(interval); 14 | }, [countDownDate]); 15 | 16 | const seconds = Math.floor((countDown % (1000 * 60)) / 1000); 17 | 18 | return seconds < 0 || Number.isNaN(seconds) ? 0 : seconds; 19 | }; 20 | 21 | function useCountdownFull(msTillTraget) { 22 | const [millisecondsTillTargetTime, setMillisecondsTillTargetTime] = useState(msTillTraget); 23 | const [isStarted, setStarted] = useState(false); 24 | 25 | useEffect(() => { 26 | if (isStarted) { 27 | setMillisecondsTillTargetTime(msTillTraget); 28 | const interval = setInterval(() => { 29 | setMillisecondsTillTargetTime((ms) => ms - 1000); 30 | }, 1000); 31 | 32 | return () => clearInterval(interval); 33 | } 34 | return; 35 | }, [msTillTraget]); 36 | 37 | const start = () => setStarted(true); 38 | 39 | return { countdown: getReturnValues(millisecondsTillTargetTime), start, isStarted }; 40 | } 41 | 42 | const getReturnValues = (millisecondsTillTargetTime) => { 43 | const days = round(millisecondsTillTargetTime / (1000 * 60 * 60 * 24)); 44 | const hours = round((millisecondsTillTargetTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); 45 | const minutes = round((millisecondsTillTargetTime % (1000 * 60 * 60)) / (1000 * 60)); 46 | const seconds = round((millisecondsTillTargetTime % (1000 * 60)) / 1000); 47 | 48 | return { days, hours, minutes, seconds }; 49 | }; 50 | 51 | function round(value) { 52 | if (value > 0) { 53 | return Math.floor(value); 54 | } 55 | return Math.ceil(value); 56 | } 57 | 58 | export { useCountdown, useCountdownFull }; 59 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export function useDebounce(value, delay) { 4 | // State and setters for debounced value 5 | const [debouncedValue, setDebouncedValue] = useState(value); 6 | useEffect( 7 | () => { 8 | // Update debounced value after delay 9 | const handler = setTimeout(() => { 10 | setDebouncedValue(value); 11 | }, delay); 12 | // Cancel the timeout if value changes (also on delay change or unmount) 13 | // This is how we prevent debounced value from updating if value is changed ... 14 | // .. within the delay period. Timeout gets cleared and restarted. 15 | return () => { 16 | clearTimeout(handler); 17 | }; 18 | }, 19 | [value, delay] // Only re-call effect if value or delay changes 20 | ); 21 | return debouncedValue; 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useLocalStorage.tsx: -------------------------------------------------------------------------------- 1 | // adapted from https://usehooks.com/useLocalStorage/ 2 | 3 | import { useState } from 'react'; 4 | 5 | export function useLocalStorage(key: string, initialValue: T) { 6 | // State to store our value 7 | // Pass initial state function to useState so logic is only executed once 8 | const [storedValue, setStoredValue] = useState(() => { 9 | if (typeof window === 'undefined') { 10 | return initialValue; 11 | } 12 | try { 13 | // Get from local storage by key 14 | const item = window.localStorage.getItem(key); 15 | // Parse stored json or if none return initialValue 16 | return item ? JSON.parse(item) : initialValue; 17 | } catch (error) { 18 | // If error also return initialValue 19 | console.log(error); 20 | return initialValue; 21 | } 22 | }); 23 | // Return a wrapped version of useState's setter function that ... 24 | // ... persists the new value to localStorage. 25 | const setValue = (value: T | ((val: T) => T)) => { 26 | try { 27 | // Allow value to be a function so we have same API as useState 28 | const valueToStore = value instanceof Function ? value(storedValue) : value; 29 | // Save state 30 | setStoredValue(valueToStore); 31 | // Save to local storage 32 | if (typeof window !== 'undefined') { 33 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 34 | } 35 | } catch (error) { 36 | // A more advanced implementation would handle the error case 37 | console.log(error); 38 | } 39 | }; 40 | return [storedValue, setValue] as const; 41 | } 42 | -------------------------------------------------------------------------------- /src/hooks/useQueryParams.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { useEffect } from 'react'; 3 | import { zeroAddress } from 'viem'; 4 | import { useAccount } from 'wagmi'; 5 | import { getAllChains } from '~/components/Aggregator/router'; 6 | 7 | const chains = getAllChains(); 8 | 9 | export function useQueryParams() { 10 | const router = useRouter(); 11 | const { isConnected, chain: chainOnWallet } = useAccount(); 12 | 13 | const urlParams = new URLSearchParams(window.location.search); 14 | const toToken = urlParams.get('to'); 15 | const fromToken = urlParams.get('from'); 16 | const chainOnURL = urlParams.get('chain'); 17 | 18 | const { ...query } = router.query; 19 | 20 | const chainName = typeof chainOnURL === 'string' ? chainOnURL.toLowerCase() : 'ethereum'; 21 | const fromTokenAddress = typeof fromToken === 'string' ? fromToken.toLowerCase() : null; 22 | const toTokenAddress = typeof toToken === 'string' ? toToken.toLowerCase() : null; 23 | 24 | useEffect(() => { 25 | if (router.isReady && !chainOnURL) { 26 | const chain = chainOnWallet ? chains.find((c) => c.chainId === chainOnWallet.id) : null; 27 | 28 | // redirects to chain on wallet if supported 29 | if (isConnected && chainOnWallet && chain) { 30 | router.push( 31 | { 32 | pathname: '/', 33 | query: { ...query, chain: chain.value, from: zeroAddress, tab: 'swap' } 34 | }, 35 | undefined, 36 | { shallow: true } 37 | ); 38 | } else { 39 | // redirects to ethereum, when there is no chain query param in URl or if chain on wallet is not supported 40 | router.push( 41 | { 42 | pathname: '/', 43 | query: { ...query, chain: 'ethereum', from: zeroAddress, tab: 'swap' } 44 | }, 45 | undefined, 46 | { shallow: true } 47 | ); 48 | } 49 | } 50 | }, [chainOnURL, chainOnWallet, isConnected, router]); 51 | 52 | return { chainName, fromTokenAddress, toTokenAddress }; 53 | } 54 | -------------------------------------------------------------------------------- /src/hooks/useSelectedChainAndTokens.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { chainsMap } from '~/components/Aggregator/constants'; 3 | import { getAllChains } from '~/components/Aggregator/router'; 4 | import { IToken } from '~/types'; 5 | import { useQueryParams } from './useQueryParams'; 6 | import { useGetTokenListByChain } from '~/queries/useGetTokenList'; 7 | import { useToken } from '~/components/Aggregator/hooks/useToken'; 8 | import { isAddress, zeroAddress } from 'viem'; 9 | import { nativeTokens } from '~/components/Aggregator/nativeTokens'; 10 | import { formatAddress } from '~/utils/formatAddress'; 11 | 12 | const chains = getAllChains(); 13 | 14 | export function useSelectedChainAndTokens() { 15 | const { chainName, fromTokenAddress, toTokenAddress } = useQueryParams(); 16 | 17 | const { data: tokenList, isLoading: fetchingTokenList } = useGetTokenListByChain({ 18 | chainId: chainName ? chainsMap[chainName] : null 19 | }); 20 | 21 | const data = useMemo(() => { 22 | const selectedChain = chains.find((c) => c.value === chainName); 23 | 24 | const selectedFromToken = 25 | fromTokenAddress && isAddress(fromTokenAddress) 26 | ? (fromTokenAddress === zeroAddress && selectedChain 27 | ? nativeTokens.find((t) => t.chainId === selectedChain.chainId) 28 | : null) ?? 29 | tokenList?.[fromTokenAddress.toLowerCase()] ?? 30 | null 31 | : null; 32 | 33 | const selectedToToken = 34 | toTokenAddress && isAddress(toTokenAddress) 35 | ? (toTokenAddress === zeroAddress && selectedChain 36 | ? nativeTokens.find((t) => t.chainId === selectedChain.chainId) 37 | : null) ?? 38 | tokenList?.[toTokenAddress.toLowerCase()] ?? 39 | null 40 | : null; 41 | 42 | return { 43 | selectedChain: selectedChain ? { ...selectedChain, id: chainsMap[selectedChain.value] } : null, 44 | selectedFromToken: selectedFromToken 45 | ? { ...selectedFromToken, label: selectedFromToken.symbol, value: selectedFromToken.address } 46 | : null, 47 | selectedToToken: selectedToToken 48 | ? { ...selectedToToken, label: selectedToToken.symbol, value: selectedToToken.address } 49 | : null, 50 | chainTokenList: (tokenList ?? {}) as Record 51 | }; 52 | }, [chainName, fromTokenAddress, toTokenAddress, tokenList]); 53 | 54 | // data of selected token not in chain's tokenlist 55 | const { data: fromToken2, isLoading: fetchingFromToken2 } = useToken({ 56 | address: fromTokenAddress as `0x${string}`, 57 | chainId: data.selectedChain?.id, 58 | enabled: 59 | typeof fromTokenAddress === 'string' && 60 | isAddress(fromTokenAddress) && 61 | data.selectedChain && 62 | data.selectedFromToken === null && 63 | !fetchingTokenList 64 | ? true 65 | : false 66 | }); 67 | 68 | const { data: toToken2, isLoading: fetchingToToken2 } = useToken({ 69 | address: toTokenAddress as `0x${string}`, 70 | chainId: data.selectedChain?.id, 71 | enabled: 72 | typeof toTokenAddress === 'string' && 73 | isAddress(toTokenAddress) && 74 | data.selectedChain && 75 | data.selectedToToken === null && 76 | !fetchingTokenList 77 | ? true 78 | : false 79 | }); 80 | 81 | return useMemo(() => { 82 | const finalSelectedFromToken: IToken | null = 83 | data.selectedFromToken === null && fromToken2 84 | ? { 85 | name: fromToken2.name ?? formatAddress(fromToken2.address), 86 | label: fromToken2.symbol ?? formatAddress(fromToken2.address), 87 | symbol: fromToken2.symbol ?? '', 88 | address: fromToken2.address, 89 | value: fromToken2.address, 90 | decimals: fromToken2.decimals, 91 | logoURI: `https://token-icons.llamao.fi/icons/tokens/${data.selectedChain?.id ?? 1}/${ 92 | fromToken2.address 93 | }?h=20&w=20`, 94 | chainId: data.selectedChain?.id ?? 1, 95 | geckoId: null 96 | } 97 | : data.selectedFromToken; 98 | 99 | const finalSelectedToToken: IToken | null = 100 | data.selectedToToken === null && toToken2 101 | ? { 102 | name: toToken2.name ?? formatAddress(toToken2.address), 103 | label: toToken2.symbol ?? formatAddress(toToken2.address), 104 | symbol: toToken2.symbol ?? '', 105 | address: toToken2.address, 106 | value: toToken2.address, 107 | decimals: toToken2.decimals, 108 | logoURI: `https://token-icons.llamao.fi/icons/tokens/${data.selectedChain?.id ?? 1}/${ 109 | toToken2.address 110 | }?h=20&w=20`, 111 | chainId: data.selectedChain?.id ?? 1, 112 | geckoId: null 113 | } 114 | : data.selectedToToken; 115 | 116 | return { 117 | ...data, 118 | finalSelectedFromToken, 119 | finalSelectedToToken, 120 | fetchingTokenList, 121 | fetchingFromToken: fromTokenAddress && !finalSelectedFromToken && (fetchingTokenList || fetchingFromToken2) ? true : false, 122 | fetchingToToken: toTokenAddress && !finalSelectedToToken && (fetchingTokenList || fetchingToToken2) ? true : false 123 | }; 124 | }, [data, fromToken2, toToken2, fetchingTokenList]); 125 | } 126 | -------------------------------------------------------------------------------- /src/layout/Phishing.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, AlertIcon } from '@chakra-ui/react'; 2 | 3 | const Phishing = () => { 4 | return ( 5 | 6 | 7 | Please make sure you are on swap.defillama.com - check the URL carefully. 8 | 9 | ); 10 | }; 11 | 12 | export { Phishing }; 13 | -------------------------------------------------------------------------------- /src/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Head from 'next/head'; 3 | import styled from 'styled-components'; 4 | import ThemeProvider, { GlobalStyle } from '~/Theme'; 5 | import { Phishing } from './Phishing'; 6 | import ConnectButton from '~/components/Aggregator/ConnectButton'; 7 | import Header from '~/components/Aggregator/Header'; 8 | 9 | const PageWrapper = styled.div` 10 | flex: 1; 11 | display: flex; 12 | flex-direction: column; 13 | margin: 16px; 14 | isolation: isolate; 15 | 16 | @media screen and (min-width: ${({ theme }) => theme.bpLg}) { 17 | margin: 28px; 18 | } 19 | `; 20 | 21 | const Center = styled.main` 22 | flex: 1; 23 | display: flex; 24 | flex-direction: column; 25 | gap: 28px; 26 | width: 100%; 27 | min-height: 100%; 28 | margin: 0 auto; 29 | color: ${({ theme }) => theme.text1}; 30 | 31 | @media screen and (max-width: ${({ theme }) => theme.bpMed}) { 32 | gap: 0px; 33 | } 34 | `; 35 | 36 | interface ILayoutProps { 37 | title: string; 38 | children: React.ReactNode; 39 | defaultSEO?: boolean; 40 | backgroundColor?: string; 41 | style?: React.CSSProperties; 42 | } 43 | 44 | export default function Layout({ title, children, ...props }: ILayoutProps) { 45 | return ( 46 | <> 47 | 48 | {title} 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 | 57 |
58 | {children} 59 |
60 |
61 |
62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ChakraProvider, DarkMode } from '@chakra-ui/react'; 3 | import { HydrationBoundary, QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { WagmiProvider } from 'wagmi'; 5 | import styled from 'styled-components'; 6 | import { darkTheme, RainbowKitProvider } from '@rainbow-me/rainbowkit'; 7 | import '@rainbow-me/rainbowkit/styles.css'; 8 | import '~/Theme/globals.css'; 9 | import { config } from '~/components/WalletProvider'; 10 | 11 | const Provider = styled.div` 12 | width: 100%; 13 | & > div { 14 | width: 100%; 15 | } 16 | `; 17 | 18 | function App({ Component, pageProps }) { 19 | const [queryClient] = React.useState(() => new QueryClient()); 20 | 21 | const [isMounted, setIsMounted] = React.useState(false); 22 | 23 | React.useEffect(() => { 24 | setIsMounted(true); 25 | }, []); 26 | 27 | return ( 28 | 29 | 30 | 31 | 32 | {isMounted && ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | )} 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | 48 | export default App; 49 | -------------------------------------------------------------------------------- /src/pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 2 | import { ServerStyleSheet } from 'styled-components'; 3 | 4 | class MyDocument extends Document { 5 | static async getInitialProps(ctx) { 6 | const sheet = new ServerStyleSheet(); 7 | const originalRenderPage = ctx.renderPage; 8 | 9 | try { 10 | ctx.renderPage = () => 11 | originalRenderPage({ 12 | enhanceApp: (App) => (props) => sheet.collectStyles() 13 | }); 14 | 15 | const initialProps = await Document.getInitialProps(ctx); 16 | 17 | return { 18 | ...initialProps, 19 | styles: [initialProps.styles, sheet.getStyleElement()] 20 | }; 21 | } finally { 22 | sheet.seal(); 23 | } 24 | } 25 | 26 | render() { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | ); 41 | } 42 | } 43 | 44 | export default MyDocument; 45 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { AggregatorContainer } from '~/components/Aggregator'; 3 | import Lending from '~/components/Lending'; 4 | import SmolRefuel from '~/components/SmolRefuel'; 5 | import Tabs from '~/components/Tabs'; 6 | import Yields from '~/components/Yields'; 7 | import Layout from '~/layout'; 8 | 9 | export default function Aggregator() { 10 | const tabData = [ 11 | { id: 'swap', name: 'Swap', content: }, 12 | { id: 'earn', name: 'Earn', content: }, 13 | { id: 'borrow', name: 'Borrow', content: }, 14 | { id: 'refuel', name: 'Gas Refuel', content: } 15 | ]; 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/testAdapters.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { testAdapters } from '~/components/Aggregator/testAdapters.test'; 3 | import styled from 'styled-components'; 4 | 5 | const Table = styled.table` 6 | th, 7 | td { 8 | padding-left: 3em; 9 | } 10 | th { 11 | padding: 3em; 12 | } 13 | `; 14 | 15 | export default function Aggregator(props) { 16 | const [tests, setTests] = useState([]); 17 | const addTest = (test) => { 18 | tests.push(test); 19 | setTests([...tests]); 20 | }; 21 | useEffect(() => { 22 | testAdapters(addTest); 23 | }, []); 24 | return ( 25 | 26 | 27 | 28 | {['Adapter', 'Chain', 'Connected', 'From', 'To', 'Failure', 'Value', 'Median', 'Drop'].map((name) => ( 29 | 30 | ))} 31 | 32 | 33 | 34 | {tests.map(({ adapter, chain, userAddress, from, to, success, value, median, drop }, idx) => ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ))} 47 | 48 |
{name}
{adapter}{chain}{userAddress ? 'yes' : 'x'}{from}{to}{success}{value}{median}{drop}
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/token-liquidity.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import { Flex, FormControl, FormLabel, Heading, IconButton } from '@chakra-ui/react'; 4 | import Layout from '~/layout'; 5 | import { chainsMap } from '~/components/Aggregator/constants'; 6 | import ReactSelect from '~/components/MultiSelect'; 7 | import { getAllChains } from '~/components/Aggregator/router'; 8 | import { LiquidityByToken } from '~/components/LiquidityByToken'; 9 | import type { IToken } from '~/types'; 10 | import { ArrowRight } from 'react-feather'; 11 | import { getTokenList } from '~/props/getTokenList'; 12 | 13 | export async function getStaticProps() { 14 | const tokenlist = await getTokenList(); 15 | return { props: { tokenlist } }; 16 | } 17 | 18 | const chains = getAllChains(); 19 | 20 | export default function TokenLiquidity({ tokenlist }) { 21 | const router = useRouter(); 22 | 23 | const { chain, from: fromToken, to: toToken } = router.query; 24 | 25 | const chainName = typeof chain === 'string' ? chain.toLowerCase() : 'ethereum'; 26 | const fromTokenSymbol = typeof fromToken === 'string' ? fromToken.toLowerCase() : null; 27 | const toTokenSymbol = typeof toToken === 'string' ? toToken.toLowerCase() : null; 28 | 29 | const { selectedChain, selectedFromToken, selectedToToken, chainTokenList } = React.useMemo(() => { 30 | const tokenList: Array = tokenlist && chainName ? tokenlist[chainsMap[chainName]] || [] : null; 31 | 32 | const selectedChain = chains.find((c) => c.value === chainName); 33 | 34 | const selectedFromToken = tokenList?.find( 35 | (t) => t.symbol.toLowerCase() === fromTokenSymbol || t.address.toLowerCase() === fromTokenSymbol 36 | ); 37 | 38 | const selectedToToken = tokenList?.find( 39 | (t) => t.symbol.toLowerCase() === toTokenSymbol || t.address.toLowerCase() === toTokenSymbol 40 | ); 41 | 42 | return { 43 | selectedChain, 44 | selectedFromToken: selectedFromToken 45 | ? { ...selectedFromToken, label: selectedFromToken.symbol, value: selectedFromToken.address } 46 | : null, 47 | selectedToToken: selectedToToken 48 | ? { ...selectedToToken, label: selectedToToken.symbol, value: selectedToToken.address } 49 | : null, 50 | chainTokenList: tokenList 51 | }; 52 | }, [chainName, fromTokenSymbol, toTokenSymbol, tokenlist]); 53 | 54 | const onChainChange = (chain) => { 55 | router.push({ pathname: router.pathname, query: { ...router.query, chain: chain.value } }, undefined, { 56 | shallow: true 57 | }); 58 | }; 59 | 60 | const onFromTokenChange = (token) => { 61 | router.push({ pathname: router.pathname, query: { ...router.query, from: token.symbol } }, undefined, { 62 | shallow: true 63 | }); 64 | }; 65 | 66 | const onToTokenChange = (token) => { 67 | router.push({ pathname: router.pathname, query: { ...router.query, to: token.symbol } }, undefined, { 68 | shallow: true 69 | }); 70 | }; 71 | 72 | return ( 73 | 74 | Token Liquidity 75 | 76 | 77 | 78 | 79 | Chain 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | From 88 | 89 | 95 | 96 | 98 | router.push( 99 | { 100 | pathname: router.pathname, 101 | query: { ...router.query, to: fromToken, from: toToken } 102 | }, 103 | undefined, 104 | { shallow: true } 105 | ) 106 | } 107 | bg="none" 108 | icon={} 109 | aria-label="Switch Tokens" 110 | marginTop="auto" 111 | /> 112 | 113 | 114 | 115 | To 116 | 117 | 123 | 124 | 125 | 126 | 127 | {selectedChain && selectedFromToken && selectedToToken && ( 128 | 129 | )} 130 | 131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /src/props/getSandwichList.ts: -------------------------------------------------------------------------------- 1 | import { chunk } from 'lodash'; 2 | import { normalizeTokens } from '~/utils'; 3 | import { getTopTokensByChain } from './getTokenList'; 4 | 5 | const LIQUDITY_THRESHOLD_USD = 1_500_000; 6 | const PERCENT_SANDWICHED_TRADES = 5; 7 | 8 | export const getSandwichList = async () => { 9 | try { 10 | const { data: sandwichData } = await fetch( 11 | `https://public.api.eigenphi.io/?path=/ethereum/30d/sandwiched_pool&apikey=${process.env.EIGEN_API_KEY}` 12 | ).then((res) => res.json()); 13 | 14 | const [_, topTokens] = await getTopTokensByChain(1); 15 | 16 | const topPairs = 17 | topTokens 18 | ?.filter((pair) => Number(pair?.attributes?.reserve_in_usd) > LIQUDITY_THRESHOLD_USD) 19 | .reduce( 20 | (acc, pair) => ({ 21 | ...acc, 22 | [(normalizeTokens(pair.token0?.address, pair?.token1?.address) as Array).join('')]: true 23 | }), 24 | {} 25 | ) ?? {}; 26 | 27 | const poolAddresses = chunk( 28 | sandwichData?.map(({ address }) => address), 29 | 30 30 | ); 31 | const pairsData = ( 32 | await Promise.allSettled( 33 | poolAddresses.map( 34 | async (pools) => 35 | await fetch(`https://api.dexscreener.com/latest/dex/pairs/ethereum/${pools.join(',')}`).then((r) => 36 | r.json() 37 | ) 38 | ) 39 | ) 40 | ) 41 | .filter(({ status }) => status === 'fulfilled') 42 | .map(({ value }: any) => value.pairs) 43 | .flat() 44 | .sort((a, b) => b.liquidity?.usd - a?.liquidity?.usd); 45 | 46 | const highLiqPairs = pairsData 47 | .filter((pair) => pair?.liquidity?.usd > LIQUDITY_THRESHOLD_USD) 48 | .map((pair) => ({ 49 | ...pair, 50 | id: (normalizeTokens(pair?.baseToken?.address, pair?.quoteToken?.address) as Array).join('') 51 | })); 52 | 53 | const sandwichList = { 54 | ethereum: pairsData.reduce((acc, pair) => { 55 | const pairData = sandwichData.find(({ address }) => address.toLowerCase() === pair?.pairAddress?.toLowerCase()); 56 | const pairId = ( 57 | normalizeTokens(pairData?.tokens[0]?.address, pairData?.tokens[1]?.address) as Array 58 | ).join(''); 59 | if ( 60 | !pairData || 61 | (pairData.sandwiched / pairData.trades) * 100 < PERCENT_SANDWICHED_TRADES || 62 | topPairs[pairId] || 63 | highLiqPairs.find(({ id }) => id === pairId) 64 | ) 65 | return acc; 66 | 67 | return { ...acc, [pairId]: pairData }; 68 | }, {}) 69 | }; 70 | 71 | return sandwichList; 72 | } catch (e) { 73 | console.log(e); 74 | return []; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/props/getTokenList.ts: -------------------------------------------------------------------------------- 1 | import { geckoTerminalChainsMap } from '~/components/Aggregator/constants'; 2 | 3 | export async function getTokenList(chainId?: number) { 4 | return fetch(chainId ? `https://d3g10bzo9rdluh.cloudfront.net/tokenlists-${chainId}.json` : `https://d3g10bzo9rdluh.cloudfront.net/tokenlists.json`).then((r) => r.json()); 5 | } 6 | 7 | export const getTopTokensByChain = async (chainId) => { 8 | try { 9 | if (!geckoTerminalChainsMap[chainId]) { 10 | return [chainId, []]; 11 | } 12 | 13 | const resData: any[] = []; 14 | 15 | for (let i = 1; i <= 5; i++) { 16 | const prevRes = await fetch( 17 | `https://api.geckoterminal.com/api/v2/networks/${geckoTerminalChainsMap[chainId]}/pools?include=dex%2Cdex.network%2Cdex.network.network_metric%2Ctokens&page=${i}&include_network_metrics=true` 18 | ) 19 | .then((r) => r.json()) 20 | .catch(() => ({ data: [], included: [] })); 21 | 22 | resData.push(...prevRes.data); 23 | } 24 | 25 | const result = resData.map((pool) => { 26 | return { ...pool, baseToken: pool.relationships.base_token.data.id.split('_')[1] }; 27 | }); 28 | 29 | return [chainId, result]; 30 | } catch (error) { 31 | return [chainId, []]; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/props/getTokensMaps.ts: -------------------------------------------------------------------------------- 1 | import { IToken } from '~/types'; 2 | 3 | const mapTokensByKey = (tokens: Record>, key: Array) => { 4 | return Object.fromEntries( 5 | Object.entries(tokens).map(([chain, tokens]) => { 6 | return [ 7 | chain, 8 | Object.fromEntries( 9 | tokens 10 | .map((token) => { 11 | const value = key.map((k) => token[k]).filter(Boolean)[0]; 12 | 13 | return value ? [token.address.toLowerCase(), value] : null; 14 | }) 15 | .filter(Boolean) as Array<[string, string]> 16 | ) 17 | ]; 18 | }) 19 | ); 20 | }; 21 | 22 | export const getTokensMaps = (tokenlist) => { 23 | const tokensUrlMap = mapTokensByKey(tokenlist, ['logoURI', 'logoURI2']); 24 | const tokensSymbolsMap = mapTokensByKey(tokenlist, ['symbol']); 25 | 26 | return { tokensUrlMap, tokensSymbolsMap }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/props/getYieldsProps.ts: -------------------------------------------------------------------------------- 1 | export async function getYieldsProps() { 2 | const yields = await fetch('https://yields.llama.fi/pools') 3 | .then((res) => res.json()) 4 | .then((res) => res.data) 5 | .then((pools) => pools.filter((pool) => pool?.ilRisk === 'no')); 6 | const yieldsConfig = await fetch('https://api.llama.fi/config/yields') 7 | .then((res) => res.json()) 8 | .then((c) => c.protocols); 9 | 10 | return { 11 | data: yields, 12 | config: yieldsConfig 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/queries/useBalance.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { erc20Abi, formatUnits, zeroAddress } from 'viem'; 3 | import { getBalance, readContracts } from 'wagmi/actions'; 4 | import { nativeAddress } from '~/components/Aggregator/constants'; 5 | 6 | import { config } from '~/components/WalletProvider'; 7 | 8 | interface IGetBalance { 9 | address?: string; 10 | chainId?: number; 11 | token?: string; 12 | } 13 | 14 | export const getTokenBalance = async ({ address, chainId, token }: IGetBalance) => { 15 | try { 16 | if (!address || !chainId || !token) { 17 | return null; 18 | } 19 | 20 | if ([zeroAddress, nativeAddress.toLowerCase()].includes(token.toLowerCase())) { 21 | const data = await getBalance(config, { 22 | address: address as `0x${string}`, 23 | chainId 24 | }); 25 | 26 | return data; 27 | } 28 | 29 | const result = await readContracts(config, { 30 | allowFailure: false, 31 | contracts: [ 32 | { 33 | address: token as `0x${string}`, 34 | abi: erc20Abi, 35 | functionName: 'balanceOf', 36 | args: [address as `0x${string}`], 37 | chainId 38 | }, 39 | { 40 | address: token as `0x${string}`, 41 | abi: erc20Abi, 42 | functionName: 'decimals', 43 | chainId 44 | } 45 | ] 46 | }); 47 | 48 | return { value: result[0], formatted: formatUnits(result[0], result[1]), decimals: result[1] }; 49 | } catch (error) { 50 | console.log(error); 51 | return null; 52 | } 53 | }; 54 | 55 | export const useBalance = ({ address, chainId, token }) => { 56 | const queryData = useQuery({ 57 | queryKey: ['balance', address, chainId, token], 58 | queryFn: () => getTokenBalance({ address, chainId, token }), 59 | refetchInterval: 10_000 60 | }); 61 | 62 | return queryData; 63 | }; 64 | -------------------------------------------------------------------------------- /src/queries/useGetMCap.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | interface IGetMcap { 4 | id: string | null; 5 | } 6 | 7 | export async function getMcap({ id }: IGetMcap) { 8 | try { 9 | if (!id) { 10 | return null; 11 | } 12 | 13 | const data = 14 | await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${id}&vs_currencies=usd&include_market_cap=true 15 | `).then((res) => res.json()); 16 | 17 | return data ? Object.values(data)?.[0]?.['usd_market_cap'] ?? null : null; 18 | } catch (error) { 19 | console.log(`Failed to fetch mcap of ${id}`, error); 20 | } 21 | } 22 | 23 | export function useGetMcap({ id }: IGetMcap) { 24 | return useQuery({ 25 | queryKey: ['mcap', id], 26 | queryFn: () => getMcap({ id }), 27 | staleTime: 5 * 60 * 1000 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/queries/useGetSavedTokens.tsx: -------------------------------------------------------------------------------- 1 | import { getSavedTokens } from '~/utils'; 2 | 3 | function fetchSavedTokens(chainId?: number | null) { 4 | if (!chainId) return []; 5 | 6 | const savedTokens = getSavedTokens(); 7 | 8 | return Object.fromEntries((savedTokens[chainId] ?? []).map((token) => [token.address.toLowerCase(), token])); 9 | } 10 | 11 | export function useGetSavedTokens(chainId?: number | null) { 12 | return fetchSavedTokens(chainId); 13 | } 14 | -------------------------------------------------------------------------------- /src/queries/useGetTokenList.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { getTokenList } from '~/props/getTokenList'; 3 | 4 | export async function getList() { 5 | try { 6 | const data = await getTokenList(); 7 | return data; 8 | } catch (error) { 9 | throw new Error(error instanceof Error ? error.message : 'Failed to fetch'); 10 | } 11 | } 12 | 13 | export const useGetTokenList = () => { 14 | return useQuery({ queryKey: ['token-list'], queryFn: getList }); 15 | }; 16 | 17 | export async function getTokenListByChain({ chainId }: { chainId: number | null | undefined }) { 18 | if (!chainId) return {}; 19 | try { 20 | const data = await getTokenList(chainId).catch(() => null); 21 | 22 | if (!data) { 23 | const dataAllChains = await getTokenList(); 24 | 25 | if (!dataAllChains[chainId]) return {}; 26 | 27 | const tokens = {}; 28 | 29 | for (const token of dataAllChains[chainId]) { 30 | tokens[token.address] = token; 31 | } 32 | 33 | return tokens; 34 | } 35 | 36 | return data; 37 | } catch (error) { 38 | throw new Error(error instanceof Error ? error.message : 'Failed to fetch'); 39 | } 40 | } 41 | 42 | export const useGetTokenListByChain = ({ chainId }: { chainId: number | null | undefined }) => { 43 | return useQuery({ 44 | queryKey: ['token-list', chainId], 45 | queryFn: () => getTokenListByChain({ chainId }), 46 | staleTime: 60 * 60 * 1000, 47 | refetchInterval: 60 * 60 * 1000, 48 | refetchOnWindowFocus: false 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/queries/useLendingProps.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { getLendingProps } from '~/props/getLendingProps'; 3 | 4 | export const useLendingProps = () => { 5 | const res = useQuery({ queryKey: ['lendingProps'], queryFn: getLendingProps }); 6 | return { ...res, data: res.data || { yields: [], chainList: [], categoryList: [], allPools: [], tokens: [] } }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/queries/useSwapsHistory.tsx: -------------------------------------------------------------------------------- 1 | import { multiCall } from '@defillama/sdk/build/abi'; 2 | import { useQuery } from '@tanstack/react-query'; 3 | import { uniq } from 'lodash'; 4 | import { chainIdToName, chainsMap } from '~/components/Aggregator/constants'; 5 | import { useQueryParams } from '~/hooks/useQueryParams'; 6 | 7 | const getSwapsHistory = async ({ userId, chain: chainId, tokensUrlMap, tokensSymbolsMap }) => { 8 | if (!chainId) return []; 9 | const chain = chainIdToName(chainId); 10 | 11 | const chainTokensSymbols = tokensSymbolsMap[chainId]; 12 | const chainTokensUrls = tokensUrlMap[chainId]; 13 | const history = await fetch(`https://llamaswap-stats.llama.fi/history?chain=${chain}&userId=${userId}`).then((r) => 14 | r.json() 15 | ); 16 | 17 | const tokens = uniq(history.map((tx) => [tx.from?.toLowerCase(), tx.to?.toLowerCase()]).flat()) as Array; 18 | const unknownSymbolsAddresses: Array = []; 19 | tokens.forEach((tokenAddress) => { 20 | if (!chainTokensSymbols[tokenAddress]) unknownSymbolsAddresses.push(tokenAddress); 21 | }); 22 | 23 | let onChainSymbols: Record = {}; 24 | if (unknownSymbolsAddresses.length) { 25 | const { output: symbols } = await multiCall({ 26 | abi: 'erc20:symbol', 27 | chain: chain, 28 | calls: unknownSymbolsAddresses.map((token) => ({ target: token })) 29 | }); 30 | 31 | onChainSymbols = Object.fromEntries(unknownSymbolsAddresses.map((address, i) => [address, symbols[i]?.output])); 32 | } 33 | 34 | return history.map((tx) => ({ 35 | ...tx, 36 | fromIcon: chainTokensUrls[tx?.from?.toLowerCase()] || '/placeholder.png', 37 | toIcon: chainTokensUrls[tx?.to?.toLowerCase()] || '/placeholder.png', 38 | fromSymbol: chainTokensSymbols[tx?.from?.toLowerCase()] || onChainSymbols?.[tx?.from?.toLowerCase()], 39 | toSymbol: chainTokensSymbols[tx?.to?.toLowerCase()] || onChainSymbols?.[tx?.to?.toLowerCase()] 40 | })); 41 | }; 42 | 43 | export function useSwapsHistory({ userId, tokensUrlMap, tokensSymbolsMap, isOpen }) { 44 | const { chainName } = useQueryParams(); 45 | const chain = chainsMap[chainName]; 46 | return useQuery({ 47 | queryKey: ['getSwapsHistory', userId, chain], 48 | queryFn: () => getSwapsHistory({ userId, chain, tokensUrlMap, tokensSymbolsMap }), 49 | enabled: !!userId && !!chain && isOpen, 50 | staleTime: 25_000 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/queries/useTokenBalances.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { erc20Abi, zeroAddress } from 'viem'; 3 | import { chainIdToName, wrappedTokensByChain } from '~/components/Aggregator/constants'; 4 | import { getBalance, readContract } from 'wagmi/actions'; 5 | import { config } from '~/components/WalletProvider'; 6 | 7 | type Balances = Record; 8 | 9 | function scramble(str: string) { 10 | return str.split('').reduce((a, b) => { 11 | return a + String.fromCharCode(b.charCodeAt(0) + 2); 12 | }, ''); 13 | } 14 | 15 | async function getTokensBalancesAndPrices(address:string, chainId:any, chainName:string){ 16 | const balances: any = await fetch( 17 | `https://peluche.llamao.fi/balances?addresses=${address}&chainId=${chainId}&type=erc20`, 18 | { 19 | headers:{ 20 | "x-api-key": scramble('_RqMaPV5)37j3HUOp41RbJrqOoq4wi6eB_J64fjiLrsKL?hhe_h_r0wh7fgEOh_d') 21 | } 22 | } 23 | ).then((r) => r.json()); 24 | 25 | const gasToken = chainName + ":" + zeroAddress 26 | const tokensToPrice:string[] = balances.balances.filter(b=>b.whitelist).map(a=>chainName + ":" + a.address).concat(gasToken) 27 | const pricePromises:any[] = [] 28 | for(let i=0; i r.json())) 32 | } 33 | 34 | const prices = (await Promise.all(pricePromises)).reduce((all, prom)=>({ 35 | ...all, 36 | ...prom.coins 37 | }), {}) 38 | 39 | return {balances, prices} 40 | } 41 | 42 | const getBalances = async (address, chainId): Promise => { 43 | if (!address || !chainId) return {}; 44 | 45 | try { 46 | const chainName = chainIdToName(chainId) 47 | 48 | const [{balances, prices}, gasBalance, wrappedTokenBalance] = await Promise.all([ 49 | getTokensBalancesAndPrices(address, chainId, chainName!).catch(() => { 50 | return {balances: { balances: [] }, prices: { coins: {} }} 51 | }), 52 | getBalance(config, { 53 | address: address as `0x${string}`, 54 | chainId 55 | }).catch(e=>null), 56 | wrappedTokensByChain[chainId] ? readContract(config, { 57 | address: wrappedTokensByChain[chainId] as `0x${string}`, 58 | abi: erc20Abi, 59 | functionName: 'balanceOf', 60 | args: [address as `0x${string}`], 61 | chainId 62 | }).catch(e=>null) : null 63 | ]) 64 | 65 | const finalBalances = balances.balances.concat([{ 66 | total_amount: gasBalance?.value?.toString(), 67 | address: zeroAddress 68 | }]).reduce((all: Balances, t: any) => { 69 | const price = prices[`${chainName}:${t.address}`] ?? {} 70 | all[t.address.toLowerCase()] = { 71 | decimals: price.decimals, 72 | symbol: price.symbol ?? 'UNKNOWN', 73 | price: price.price, 74 | amount: t.total_amount, 75 | balanceUSD: price.price !== undefined && t.total_amount != null ? price.price*t.total_amount/(10**price.decimals) : 0 76 | }; 77 | return all; 78 | }, {}); 79 | 80 | if (wrappedTokenBalance != null){ 81 | const price = prices[`${chainName}:${zeroAddress}`] ?? {} 82 | finalBalances[wrappedTokensByChain[chainId].toLowerCase()] = { 83 | decimals: price.decimals, 84 | symbol: `W${price.symbol}`, 85 | price: price.price, 86 | amount: wrappedTokenBalance.toString(), 87 | balanceUSD: price.price !== undefined ? price.price*Number(wrappedTokenBalance)/(10**price.decimals) : 0 88 | } 89 | } 90 | 91 | return finalBalances 92 | } catch(e) { 93 | console.log(`Couldn't find balances for ${chainId}:${address}`, e) 94 | return {}; 95 | } 96 | }; 97 | 98 | export const useTokenBalances = (address, chain) => { 99 | return useQuery({ 100 | queryKey: ['balances', address, chain], 101 | queryFn: () => getBalances(address, chain), 102 | staleTime: 60 * 1000, 103 | refetchInterval: 60 * 1000 104 | }); 105 | }; 106 | -------------------------------------------------------------------------------- /src/queries/useYieldProps.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { getYieldsProps } from '~/props/getYieldsProps'; 3 | 4 | export const useYieldProps = () => { 5 | const res = useQuery({ queryKey: ['yieldProps'], queryFn: getYieldsProps }); 6 | return { ...res, data: res.data || { data: [], config: {} } }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface IToken { 2 | address: string; 3 | label: string; 4 | value: string; 5 | logoURI: string; 6 | logoURI2?: string | null; 7 | symbol: string; 8 | decimals: number; 9 | name: string; 10 | chainId: number; 11 | amount?: string | number; 12 | balanceUSD?: number; 13 | geckoId: string | null; 14 | isGeckoToken?: boolean; 15 | isMultichain?: boolean; 16 | } 17 | 18 | export interface IPool { 19 | apu: number; 20 | apy: number; 21 | apyBase: number | null; 22 | apyBase7d: number | null; 23 | apyBaseBorrow: number; 24 | apyBaseInception: number | null; 25 | apyBorrow: number; 26 | apyMean30d: number; 27 | apyPct1D: number; 28 | apyPct7D: number; 29 | apyPct30D: number; 30 | apyReward: number | null; 31 | apyRewardBorrow: number | null; 32 | borrowFactor: null; 33 | borrowable: true; 34 | category: string; 35 | chain: string; 36 | count: number; 37 | exposure: string; 38 | il7d: number | null; 39 | ilRisk: string; 40 | lsdApy: number; 41 | ltv: number; 42 | mintedCoin: string | null; 43 | mu: number; 44 | outlier: boolean; 45 | pool: string; 46 | poolMeta: null; 47 | predictions: { predictedClass: string; predictedProbability: number; binnedConfidence: number }; 48 | project: string; 49 | rewardTokens: null; 50 | sigma: number; 51 | stablecoin: boolean; 52 | symbol: string; 53 | totalAvailableUsd: number; 54 | totalBorrowUsd: number; 55 | totalSupplyUsd: number; 56 | tvlUsd: number; 57 | underlyingTokens: Array; 58 | volumeUsd1d: number | null; 59 | volumeUsd7d: number | null; 60 | config: { name: string; category: string }; 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/formatAddress.tsx: -------------------------------------------------------------------------------- 1 | export const formatAddress = (address: string, length: number = 4) => { 2 | return address.slice(0, length) + '...' + address.slice(-length); 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/formatAmount.ts: -------------------------------------------------------------------------------- 1 | export const formatAmount = (amount: string | number) => amount.toString().trim().split(' ').join(''); 2 | 3 | export const formatAmountString = (value, prefix = '') => { 4 | if (isNaN(value)) return value; 5 | const formatter = Intl.NumberFormat('en', { notation: 'compact' }); 6 | return prefix + formatter.format(value); 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/formatToast.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | export const formatSuccessToast = (variables) => { 4 | const fromToken = variables.tokens.fromToken; 5 | const toToken = variables.tokens.toToken; 6 | 7 | const inAmount = variables.rawQuote?.inAmount ?? variables.rawQuote?.inputAmount ?? variables.rawQuote?.sellAmount; 8 | const outAmount = variables.rawQuote?.outAmount ?? variables.rawQuote?.outputAmount ?? variables.rawQuote?.buyAmount; 9 | return { 10 | title: 'Transaction Success', 11 | description: `Swapped ${ 12 | inAmount 13 | ? BigNumber(inAmount) 14 | .div(10 ** Number(fromToken.decimals || 18)) 15 | .toFixed(3) 16 | : '' 17 | } ${fromToken.symbol} for ${ 18 | outAmount 19 | ? BigNumber(outAmount) 20 | .div(10 ** Number(toToken.decimals || 18)) 21 | .toFixed(3) 22 | : '' 23 | } ${toToken.symbol} via ${variables.adapter}`, 24 | status: 'success', 25 | duration: 10000, 26 | isClosable: true, 27 | position: 'top-right', 28 | containerStyle: { 29 | width: '100%', 30 | maxWidth: '300px' 31 | } 32 | } as const; 33 | }; 34 | 35 | const SLIPPAGE_ERRORS = [ 36 | ' { 43 | const isSlippage = SLIPPAGE_ERRORS.some((text) => error?.reason?.includes(text)); 44 | let errorMsg = 'Something went wrong'; 45 | 46 | if (isFailed) errorMsg = 'Transaction Failed'; 47 | else if (isSlippage) errorMsg = 'Slippage is too low, try again with higher slippage'; 48 | else if (error?.reason) errorMsg = error.reason; 49 | 50 | return { 51 | title: 'Transaction Failed', 52 | description: errorMsg, 53 | status: 'error', 54 | duration: 10000, 55 | isClosable: true, 56 | position: 'top-right', 57 | containerStyle: { 58 | width: '100%', 59 | maxWidth: '300px' 60 | } 61 | } as const; 62 | }; 63 | 64 | export const formatUnknownErrorToast = ({ title, message }) => { 65 | return { 66 | title: title, 67 | description: message, 68 | status: 'error', 69 | duration: 10000, 70 | isClosable: true, 71 | position: 'top-right', 72 | containerStyle: { 73 | width: '100%', 74 | maxWidth: '300px' 75 | } 76 | } as const; 77 | }; 78 | 79 | export const formatSubmittedToast = (variables) => { 80 | const fromToken = variables.tokens.fromToken; 81 | const toToken = variables.tokens.toToken; 82 | 83 | const inAmount = variables.rawQuote?.inAmount ?? variables.rawQuote?.inputAmount ?? variables.rawQuote?.sellAmount; 84 | const outAmount = variables.rawQuote?.outAmount ?? variables.rawQuote?.outputAmount ?? variables.rawQuote?.buyAmount; 85 | return { 86 | title: 'Transaction Submitted', 87 | description: `Swap ${ 88 | inAmount 89 | ? BigNumber(inAmount) 90 | .div(10 ** Number(fromToken.decimals || 18)) 91 | .toFixed(3) 92 | : '' 93 | } ${fromToken.symbol} for ${ 94 | outAmount 95 | ? BigNumber(outAmount) 96 | .div(10 ** Number(toToken.decimals || 18)) 97 | .toFixed(3) 98 | : '' 99 | } ${toToken.symbol} via ${variables.adapter}`, 100 | status: 'success', 101 | duration: 10000, 102 | isClosable: true, 103 | position: 'top-right', 104 | containerStyle: { 105 | width: '100%', 106 | maxWidth: '300px' 107 | } 108 | } as const; 109 | }; 110 | -------------------------------------------------------------------------------- /src/utils/getChartData.tsx: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { initialLiquidity } from '~/components/Aggregator/constants'; 3 | 4 | export function getChartData({ routes, price, fromTokenDecimals, toTokenDecimals, minimumSlippage, maximumSlippage }) { 5 | const liquidityAt = price || 500; 6 | // price at $500 liquidity 7 | const currentPrice = routes?.find((route) => route[0] === liquidityAt)?.[1]?.price?.amountReturned ?? null; 8 | 9 | const chartData: Array<[number, number, number, string, number]> = []; 10 | 11 | const newLiquidityValues: Array = []; 12 | 13 | if (currentPrice) { 14 | routes?.forEach(([netOutUSD, { name, price, fromAmount }], index) => { 15 | const amountReturned = price?.amountReturned ?? null; 16 | 17 | if (amountReturned) { 18 | const expectedPrice = Number(currentPrice) * (Number(netOutUSD) / liquidityAt); 19 | 20 | const slippage = 21 | netOutUSD < liquidityAt 22 | ? 0 23 | : Number(Math.abs(((Number(amountReturned) - expectedPrice) / expectedPrice) * 100).toFixed(2)); 24 | 25 | const prevValue = chartData[index - 1]; 26 | 27 | if (chartData.length && prevValue && slippage - prevValue[1] > 1) { 28 | const newLiq = Math.floor((Number(prevValue[0]) + Number(netOutUSD)) / 2); 29 | 30 | if ( 31 | !newLiquidityValues.includes(newLiq) && 32 | !initialLiquidity.includes(newLiq) && 33 | newLiq > 500 && 34 | newLiq < 500_000_000 && 35 | routes.length <= 100 36 | ) { 37 | newLiquidityValues.push(newLiq); 38 | } 39 | } 40 | 41 | chartData.push([ 42 | Number(netOutUSD), 43 | slippage, 44 | Number( 45 | BigNumber(amountReturned) 46 | .div(10 ** Number(toTokenDecimals || 18)) 47 | .toFixed(3) 48 | ), 49 | name, 50 | Number( 51 | BigNumber(fromAmount) 52 | .div(10 ** Number(fromTokenDecimals || 18)) 53 | .toFixed(3) 54 | ) 55 | ]); 56 | } 57 | }); 58 | } 59 | 60 | const minIndex = getMinIndexRoute(chartData, minimumSlippage || 0); 61 | const maxIndex = getMaxIndexRoute(chartData, maximumSlippage || 100); 62 | 63 | const dataInRange = chartData.slice(minIndex, maxIndex + 1); 64 | 65 | return { 66 | chartData: dataInRange.filter((values, index) => 67 | dataInRange[index + 1] ? values[1] < dataInRange[index + 1][1] : true 68 | ), 69 | newLiquidityValues: newLiquidityValues.sort((a, b) => a - b) 70 | }; 71 | } 72 | 73 | function getMinIndexRoute(arr, minSlippage) { 74 | let left = 0, 75 | right = arr.length - 1; 76 | 77 | while (left <= right) { 78 | // Process until it is last element 79 | 80 | let mid = Math.floor((left + (right + 1)) / 2); // using floor as we may get floating numbers 81 | 82 | if (arr[mid][1] >= minSlippage && (mid > 0 ? arr[mid - 1][1] < minSlippage : true)) { 83 | // element found at mid 84 | return mid; // no need to process further 85 | } 86 | 87 | if (minSlippage < arr[mid][1]) { 88 | // element might be in first half 89 | right = mid - 1; // right is mid - 1 because we know that mid is not correct element 90 | } else { 91 | // element might be in second half 92 | left = mid + 1; // left is mid + 1 because we know that mid is not correct element 93 | } 94 | } 95 | 96 | return 0; // if not found, return 0 index 97 | } 98 | 99 | function getMaxIndexRoute(arr, maxSlippage) { 100 | let left = 0, 101 | right = arr.length - 1; 102 | 103 | while (left <= right) { 104 | // Process until it is last element 105 | 106 | let mid = Math.floor((left + (right + 1)) / 2); // using floor as we may get floating numbers 107 | 108 | if (arr[mid][1] <= maxSlippage && (mid < arr.length - 1 ? arr[mid + 1][1] > maxSlippage : true)) { 109 | // element found at mid 110 | return mid; // no need to process further 111 | } 112 | 113 | if (maxSlippage < arr[mid][1]) { 114 | // element might be in first half 115 | right = mid - 1; // right is mid - 1 because we know that mid is not correct element 116 | } else { 117 | // element might be in second half 118 | left = mid + 1; // left is mid + 1 because we know that mid is not correct element 119 | } 120 | } 121 | 122 | return arr.length; // if not found, return array length 123 | } 124 | -------------------------------------------------------------------------------- /src/utils/getTopRoute.tsx: -------------------------------------------------------------------------------- 1 | import { zeroAddress } from 'viem'; 2 | 3 | export function getTopRoute({ routes, gasPriceData, gasTokenPrice, fromToken, toToken, toTokenPrice }) { 4 | const sortedRoutes = routes 5 | .map((route) => { 6 | if (route.price) { 7 | let gasUsd = 8 | route.price?.estimatedGas && gasPriceData.gasPrice && gasTokenPrice 9 | ? (gasTokenPrice * +route.price.estimatedGas * gasPriceData.gasPrice) / 1e18 10 | : 0; 11 | 12 | // CowSwap native token swap 13 | gasUsd = 14 | route.price.feeAmount && fromToken.address === zeroAddress 15 | ? (Number(route.price.feeAmount) / 1e18) * gasTokenPrice 16 | : gasUsd; 17 | const amount = +route.price.amountReturned / 10 ** +toToken?.decimals; 18 | const amountUsd = (amount * toTokenPrice).toFixed(2); 19 | const netOut = +amountUsd - gasUsd; 20 | 21 | return { ...route, netOut }; 22 | } 23 | return { ...route, netOut: 0 }; 24 | }) 25 | .sort((a, b) => Number(b.netOut ?? 0) - Number(a.netOut ?? 0)); 26 | 27 | const topRoute = sortedRoutes.length > 0 ? sortedRoutes[0] : null; 28 | 29 | return topRoute; 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/index.tsx: -------------------------------------------------------------------------------- 1 | export const capitalizeFirstLetter = (word) => word.charAt(0).toUpperCase() + word.slice(1); 2 | 3 | const ICONS_CDN = 'https://icons.llamao.fi/icons'; 4 | 5 | export function protoclIconUrl(protocol) { 6 | return `${ICONS_CDN}/protocols/${protocol}?w=48&h=48`; 7 | } 8 | 9 | export function getSavedTokens() { 10 | return JSON.parse(localStorage.getItem('savedTokens') || '{}'); 11 | } 12 | 13 | export const median = (arr: number[]): number => { 14 | const s = [...arr].sort((a, b) => a - b); 15 | const mid = Math.floor(s.length / 2); 16 | return s.length % 2 === 0 ? (s[mid - 1] + s[mid]) / 2 : s[mid]; 17 | }; 18 | 19 | export const formattedNum = (number, symbol = false, acceptNegatives = false) => { 20 | let currencySymbol; 21 | if (symbol === true) { 22 | currencySymbol = '$'; 23 | } else if (symbol === false) { 24 | currencySymbol = ''; 25 | } else { 26 | currencySymbol = symbol; 27 | } 28 | if (!number || number === '' || Number.isNaN(Number(number))) { 29 | return symbol ? `${currencySymbol}0` : 0; 30 | } 31 | let formattedNum = String(); 32 | let num = parseFloat(number); 33 | // const isNegative = num < 0; 34 | 35 | // const currencyMark = isNegative ? `${currencySymbol}-` : currencySymbol 36 | // const normalMark = isNegative ? '-' : '' 37 | 38 | // if (num > 10000000) { 39 | // return (symbol ? currencyMark : normalMark) + toK(num.toFixed(0), true) 40 | // } 41 | 42 | // if (num === 0) { 43 | // return symbol ? `${currencySymbol}0` : 0 44 | // } 45 | 46 | // if (num < 0.0001 && num > 0) { 47 | // return symbol ? `< ${currencySymbol}0.0001` : '< 0.0001' 48 | // } 49 | 50 | let maximumFractionDigits = num < 1 ? 8 : 4; 51 | maximumFractionDigits = num > 100000 ? 2 : maximumFractionDigits; 52 | formattedNum = num.toLocaleString('en-US', { maximumFractionDigits }); 53 | 54 | return String(formattedNum); 55 | }; 56 | 57 | export const normalizeTokens = (t0 = '0', t1 = '0') => { 58 | if (!t0 || !t1) return null; 59 | 60 | return Number(t0) < Number(t1) ? [t0.toLowerCase(), t1.toLowerCase()] : [t1.toLowerCase(), t0.toLowerCase()]; 61 | }; 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "allowJs": true, 6 | "baseUrl": ".", 7 | "paths": { 8 | "~/*": ["./src/*"], 9 | "~/public/*": ["./public/*"] 10 | }, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "ESNext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "incremental": true, 22 | "downlevelIteration": true, 23 | "allowSyntheticDefaultImports": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noImplicitAny": false, 26 | "strictNullChecks": true, 27 | "noImplicitReturns": true, 28 | "noImplicitThis": true, 29 | "noUnusedLocals": true, 30 | }, 31 | "exclude": ["node_modules", "server"], 32 | "include": ["**/*.js", "**/*.ts", "**/*.tsx"] 33 | } 34 | --------------------------------------------------------------------------------