├── .env ├── .env.production ├── .gitignore ├── README.md ├── craco.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.less ├── App.test.tsx ├── App.tsx ├── ant-custom.less ├── buffer-layout.d.ts ├── components │ ├── accountInfo.tsx │ ├── currencyInput │ │ ├── index.tsx │ │ └── styles.less │ ├── exchange.tsx │ ├── identicon │ │ ├── index.tsx │ │ └── style.less │ ├── info.tsx │ ├── labels.tsx │ ├── numericInput.tsx │ ├── pool │ │ ├── add.less │ │ ├── add.tsx │ │ ├── config.tsx │ │ ├── remove.tsx │ │ ├── supplyOverview.tsx │ │ ├── view.less │ │ └── view.tsx │ ├── settings.tsx │ ├── slippage │ │ └── style.less │ ├── tokenIcon │ │ └── index.tsx │ └── trade │ │ ├── index.tsx │ │ └── trade.less ├── index.css ├── index.tsx ├── models │ ├── account.ts │ ├── index.ts │ ├── pool.ts │ └── tokenSwap.ts ├── react-app-env.d.ts ├── routes.tsx ├── serviceWorker.ts ├── setupTests.ts ├── sol-wallet-adapter.d.ts └── utils │ ├── accounts.tsx │ ├── connection.tsx │ ├── currencyPair.tsx │ ├── ids.tsx │ ├── notifications.tsx │ ├── pools.tsx │ ├── token-list.json │ ├── utils.ts │ └── wallet.tsx ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | # Program Owner in this file needs to match program owner that is part of on-chain swap program 2 | # Applicable only to token-swap programs compiled with feature flag: ('program-owner-fees') 3 | SWAP_PROGRAM_OWNER_FEE_ADDRESS='' 4 | 5 | # HOST Public Key used for additional swap fees 6 | SWAP_HOST_FEE_ADDRESS='' 7 | 8 | # Rewired variables to comply with CRA restrictions 9 | REACT_APP_SWAP_HOST_FEE_ADDRESS=$SWAP_HOST_FEE_ADDRESS 10 | REACT_APP_SWAP_PROGRAM_OWNER_FEE_ADDRESS=$SWAP_PROGRAM_OWNER_FEE_ADDRESS 11 | 12 | 13 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP = false 2 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .idea 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ⚠️ Warning 2 | 3 | Any content produced by Solana, or developer resources that Solana provides, are for educational and inspiration purposes only. Solana does not encourage, induce or sanction the deployment of any such applications in violation of applicable laws or regulations. 4 | 5 | ## Deployment 6 | 7 | App is using to enviroment variables that can be set before deployment: 8 | * `SWAP_PROGRAM_OWNER_FEE_ADDRESS` used to distribute fees to owner of the pool program (Note: this varibale reuqires special version of token-swap program) 9 | * `SWAP_HOST_FEE_ADDRESS` used to distribute fees to host of the application 10 | 11 | To inject varibles to the app, set the SWAP_PROGRAM_OWNER_FEE_ADDRESS and/or SWAP_HOST_FEE_ADDRESS environment variables to the addresses of your SOL accounts. 12 | 13 | You may want to put these in local environment files (e.g. .env.development.local, .env.production.local). See the documentation on environment variables for more information. 14 | 15 | NOTE: remember to re-build your app before deploying for your referral addresses to be reflected. -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const CracoLessPlugin = require("craco-less"); 2 | 3 | module.exports = { 4 | plugins: [ 5 | { 6 | plugin: CracoLessPlugin, 7 | options: { 8 | lessLoaderOptions: { 9 | lessOptions: { 10 | modifyVars: { "@primary-color": "#2abdd2" }, 11 | javascriptEnabled: true, 12 | }, 13 | }, 14 | }, 15 | }, 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "token-swap-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^5.7.0", 7 | "@project-serum/serum": "^0.13.7", 8 | "@project-serum/sol-wallet-adapter": "^0.1.1", 9 | "@solana/spl-token": "0.0.11", 10 | "@solana/spl-token-swap": "0.0.2", 11 | "@solana/web3.js": "^0.78.2", 12 | "@testing-library/jest-dom": "^4.2.4", 13 | "@testing-library/react": "^9.5.0", 14 | "@testing-library/user-event": "^7.2.1", 15 | "@types/react-router-dom": "^5.1.6", 16 | "antd": "^4.6.6", 17 | "bn.js": "^5.1.3", 18 | "bs58": "^4.0.1", 19 | "buffer-layout": "^1.2.0", 20 | "craco-less": "^1.17.0", 21 | "identicon.js": "^2.3.3", 22 | "jazzicon": "^1.5.0", 23 | "react": "^16.13.1", 24 | "react-dom": "^16.13.1", 25 | "react-github-btn": "^1.2.0", 26 | "react-router-dom": "^5.2.0", 27 | "react-scripts": "3.4.3", 28 | "recharts": "^1.8.5", 29 | "typescript": "^4.0.0" 30 | }, 31 | "scripts": { 32 | "start": "craco start", 33 | "build": "craco build", 34 | "test": "craco test", 35 | "eject": "react-scripts eject", 36 | "localnet:update": "solana-localnet update", 37 | "localnet:up": "rm client/util/store/config.json; set -x; solana-localnet down; set -e; solana-localnet up", 38 | "localnet:down": "solana-localnet down", 39 | "localnet:logs": "solana-localnet logs -f", 40 | "predeploy": "git pull --ff-only && yarn && yarn build", 41 | "deploy": "gh-pages -d build", 42 | "deploy:ar": "arweave deploy-dir build --key-file " 43 | }, 44 | "eslintConfig": { 45 | "extends": "react-app" 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "homepage": ".", 60 | "devDependencies": { 61 | "@types/bn.js": "^4.11.6", 62 | "@types/bs58": "^4.0.1", 63 | "@types/identicon.js": "^2.3.0", 64 | "@types/jest": "^24.9.1", 65 | "@types/node": "^12.12.62", 66 | "@types/react": "^16.9.50", 67 | "@types/react-dom": "^16.9.8", 68 | "@types/recharts": "^1.8.16", 69 | "arweave-deploy": "^1.9.1", 70 | "gh-pages": "^3.1.0", 71 | "prettier": "^2.1.2" 72 | } 73 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/oyster-swap/82e29aaf4578898710bbeaf183b494e8b503815f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Swap | Solana 25 | 51 | 54 | 55 | 56 | 57 | 58 |
59 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Swap | Serum", 3 | "name": "Swap | Serum", 4 | "icons": [ 5 | { 6 | "src": "icon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.less: -------------------------------------------------------------------------------- 1 | @import "~antd/dist/antd.dark.less"; 2 | @import "./ant-custom.less"; 3 | 4 | body { 5 | --row-highlight: @background-color-base; 6 | } 7 | 8 | .App-logo { 9 | background-image: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjZDgzYWViIiB4bWxuczp4PSJodHRwOi8vbnMuYWRvYmUuY29tL0V4dGVuc2liaWxpdHkvMS4wLyIgeG1sbnM6aT0iaHR0cDovL25zLmFkb2JlLmNvbS9BZG9iZUlsbHVzdHJhdG9yLzEwLjAvIiB4bWxuczpncmFwaD0iaHR0cDovL25zLmFkb2JlLmNvbS9HcmFwaHMvMS4wLyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iLTIwNSAyMDcgMTAwIDEwMCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAtMjA1IDIwNyAxMDAgMTAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHN3aXRjaD48Zm9yZWlnbk9iamVjdCByZXF1aXJlZEV4dGVuc2lvbnM9Imh0dHA6Ly9ucy5hZG9iZS5jb20vQWRvYmVJbGx1c3RyYXRvci8xMC4wLyIgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSI+PC9mb3JlaWduT2JqZWN0PjxnIGk6ZXh0cmFuZW91cz0ic2VsZiI+PHBhdGggZD0iTS0xODMuMywyNzMuMWMwLjMsMS0wLjIsMi4xLTEuMiwyLjRjLTAuMiwwLjEtMC40LDAuMS0wLjYsMC4xYy0wLjgsMC0xLjUtMC41LTEuOC0xLjNjLTAuMy0xLDAuMi0yLjEsMS4yLTIuNCAgICBDLTE4NC42LDI3MS41LTE4My42LDI3Mi4xLTE4My4zLDI3My4xeiBNLTE4MC41LDI2MS40YzAuNCwwLDAuOC0wLjEsMS4xLTAuNGMwLjgtMC42LDEtMS44LDAuNC0yLjZjLTAuNi0wLjgtMS44LTEtMi42LTAuNCAgICBjMCwwLDAsMCwwLDBjLTAuOCwwLjYtMSwxLjgtMC40LDIuNkMtMTgxLjYsMjYxLjEtMTgxLDI2MS40LTE4MC41LDI2MS40eiBNLTE4NS42LDI2Ny42YzAuMiwwLjEsMC40LDAuMSwwLjYsMC4xICAgIGMwLjgsMCwxLjUtMC41LDEuOC0xLjNjMC4zLTEtMC4yLTIuMS0xLjItMi40Yy0xLTAuMy0yLjEsMC4yLTIuNCwxLjJDLTE4Ny4yLDI2Ni4yLTE4Ni42LDI2Ny4zLTE4NS42LDI2Ny42eiBNLTE3OS4zLDI3OC41ICAgIGMtMC44LTAuNi0yLTAuNC0yLjYsMC40Yy0wLjYsMC44LTAuNCwyLDAuNCwyLjZjMC4zLDAuMiwwLjcsMC40LDEuMSwwLjRjMC42LDAsMS4yLTAuMywxLjUtMC44ICAgIEMtMTc4LjMsMjgwLjMtMTc4LjUsMjc5LjEtMTc5LjMsMjc4LjV6IE0tMTczLDI1OC45YzAuNSwwLDEtMC4yLDEuMy0wLjVjMC4zLTAuNCwwLjYtMC44LDAuNi0xLjNjMC0wLjUtMC4yLTEtMC42LTEuMyAgICBjLTAuNC0wLjQtMC44LTAuNi0xLjMtMC42Yy0wLjUsMC0xLDAuMi0xLjMsMC42Yy0wLjMsMC4zLTAuNiwwLjgtMC42LDEuM2MwLDAuNSwwLjIsMSwwLjYsMS4zQy0xNzQsMjU4LjctMTczLjUsMjU4LjktMTczLDI1OC45eiAgICAgTS0xNTguNiwyNTUuMmMtMSwwLTEuOSwwLjgtMS45LDEuOWMwLDEsMC45LDEuOSwxLjksMS45YzEsMCwxLjktMC44LDEuOS0xLjlDLTE1Ni43LDI1Ni0xNTcuNiwyNTUuMi0xNTguNiwyNTUuMnogTS0xNTMuMywyNTcuMSAgICBjMCwxLDAuOCwxLjksMS45LDEuOWMxLDAsMS45LTAuOCwxLjktMS45YzAtMS0wLjgtMS45LTEuOS0xLjlDLTE1Mi40LDI1NS4yLTE1My4zLDI1Ni0xNTMuMywyNTcuMXogTS0xNDYuMSwyNTcuMSAgICBjMCwxLDAuOCwxLjksMS45LDEuOWMxLDAsMS45LTAuOCwxLjktMS45YzAtMS0wLjgtMS45LTEuOS0xLjlDLTE0NS4zLDI1NS4yLTE0Ni4xLDI1Ni0xNDYuMSwyNTcuMXogTS0xNjcuNywyNTcuMSAgICBjMCwxLDAuOCwxLjksMS45LDEuOXMxLjktMC44LDEuOS0xLjljMC0xLTAuOC0xLjktMS45LTEuOVMtMTY3LjcsMjU2LTE2Ny43LDI1Ny4xeiBNLTEzNywyNTUuMmMtMC41LDAtMSwwLjItMS4zLDAuNiAgICBjLTAuMywwLjMtMC42LDAuOC0wLjYsMS4zYzAsMC41LDAuMiwxLDAuNiwxLjNjMC4zLDAuNCwwLjgsMC42LDEuMywwLjZjMC41LDAsMS0wLjIsMS4zLTAuNmMwLjMtMC4zLDAuNi0wLjgsMC42LTEuMyAgICBjMC0wLjUtMC4yLTEtMC42LTEuM0MtMTM2LDI1NS40LTEzNi41LDI1NS4yLTEzNywyNTUuMnogTS0xMjQuNCwyNDYuNWMtMS0wLjMtMi4xLDAuMi0yLjQsMS4ybDAsMGMtMC4zLDEsMC4yLDIuMSwxLjIsMi40ICAgIGMwLjIsMC4xLDAuNCwwLjEsMC42LDAuMWMwLjgsMCwxLjUtMC41LDEuOC0xLjNjMCwwLDAsMCwwLDBDLTEyMi44LDI0Ny45LTEyMy40LDI0Ni44LTEyNC40LDI0Ni41eiBNLTEzMC43LDI1My4xTC0xMzAuNywyNTMuMSAgICBjLTAuOCwwLjYtMSwxLjgtMC40LDIuNmMwLjQsMC41LDAuOSwwLjgsMS41LDAuOGMwLjQsMCwwLjgtMC4xLDEuMS0wLjRjMC44LTAuNiwxLTEuOCwwLjQtMi42ICAgIEMtMTI4LjYsMjUyLjctMTI5LjgsMjUyLjUtMTMwLjcsMjUzLjF6IE0tMTMwLjcsMjM1LjZjMC4zLDAuMiwwLjcsMC40LDEuMSwwLjRjMC42LDAsMS4yLTAuMywxLjUtMC44YzAuNi0wLjgsMC40LTItMC40LTIuNiAgICBjLTAuOC0wLjYtMi0wLjQtMi42LDAuNEMtMTMxLjcsMjMzLjgtMTMxLjUsMjM1LTEzMC43LDIzNS42eiBNLTEyNC45LDI0Mi4zYzAuMiwwLDAuNCwwLDAuNi0wLjFjMS0wLjMsMS41LTEuNCwxLjItMi40ICAgIGMtMC4zLTEtMS40LTEuNS0yLjQtMS4yYy0xLDAuMy0xLjUsMS40LTEuMiwyLjRDLTEyNi41LDI0MS44LTEyNS43LDI0Mi4zLTEyNC45LDI0Mi4zeiBNLTE0MS4zLDI4MC45bC0xMC43LTEwLjcgICAgYy0wLjctMC43LTEuOS0wLjctMi43LDBjLTAuNywwLjctMC43LDEuOSwwLDIuN2w3LjYsNy42aC0yNmMtMSwwLTEuOSwwLjgtMS45LDEuOXMwLjgsMS45LDEuOSwxLjloMjUuOGwtNy40LDcuNCAgICBjLTAuNywwLjctMC43LDEuOSwwLDIuN2MwLjQsMC40LDAuOSwwLjYsMS4zLDAuNnMxLTAuMiwxLjMtMC42bDEwLjctMTAuN2MwLjQtMC40LDAuNi0wLjksMC41LTEuNCAgICBDLTE0MC43LDI4MS44LTE0MC45LDI4MS4zLTE0MS4zLDI4MC45eiBNLTE3MSwyMzMuMWwxMC43LDEwLjdjMC40LDAuNCwwLjksMC42LDEuMywwLjZzMS0wLjIsMS4zLTAuNmMwLjctMC43LDAuNy0xLjksMC0yLjcgICAgbC03LjUtNy41aDI4LjJjMSwwLDEuOS0wLjgsMS45LTEuOXMtMC44LTEuOS0xLjktMS45aC0yOC4ybDcuNS03LjVjMC43LTAuNywwLjctMS45LDAtMi43Yy0wLjctMC43LTEuOS0wLjctMi43LDBsLTEwLjcsMTAuNyAgICBjLTAuNCwwLjQtMC42LDAuOS0wLjUsMS40Qy0xNzEuNiwyMzIuMi0xNzEuNCwyMzIuNy0xNzEsMjMzLjF6Ij48L3BhdGg+PC9nPjwvc3dpdGNoPjwvc3ZnPg=="); 10 | height: 40px; 11 | pointer-events: none; 12 | background-repeat: no-repeat; 13 | background-size: 50px; 14 | width: 40px; 15 | } 16 | 17 | .Banner { 18 | min-height: 30px; 19 | width: 100%; 20 | background-color: #fff704; 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | // z-index: 10; 25 | } 26 | 27 | .Banner-description { 28 | color: black; 29 | text-align: center; 30 | width: 100%; 31 | } 32 | 33 | .App-Bar { 34 | display: grid; 35 | grid-template-columns: 1fr 120px; 36 | -webkit-box-pack: justify; 37 | justify-content: space-between; 38 | -webkit-box-align: center; 39 | align-items: center; 40 | flex-direction: row; 41 | width: 100%; 42 | top: 0px; 43 | position: relative; 44 | padding: 1rem; 45 | z-index: 2; 46 | } 47 | 48 | .App-Bar-left { 49 | box-sizing: border-box; 50 | margin: 0px; 51 | min-width: 0px; 52 | display: flex; 53 | padding: 0px; 54 | -webkit-box-align: center; 55 | align-items: center; 56 | width: fit-content; 57 | } 58 | 59 | .App-Bar-right { 60 | display: flex; 61 | flex-direction: row; 62 | -webkit-box-align: center; 63 | align-items: center; 64 | justify-self: flex-end; 65 | } 66 | 67 | .ant-tabs-nav-scroll { 68 | display: flex; 69 | justify-content: center; 70 | } 71 | 72 | .discord { 73 | font-size: 30px; 74 | color: #7289da; 75 | } 76 | 77 | .discord:hover { 78 | color: #8ea1e1; 79 | } 80 | 81 | .telegram { 82 | color: #32afed; 83 | font-size: 28px; 84 | background-color: white; 85 | border-radius: 30px; 86 | display: flex; 87 | width: 27px; 88 | height: 27px; 89 | } 90 | 91 | .telegram:hover { 92 | color: #2789de !important; 93 | } 94 | 95 | .App-header { 96 | background-color: #282c34; 97 | min-height: 100vh; 98 | display: flex; 99 | flex-direction: column; 100 | align-items: center; 101 | justify-content: center; 102 | font-size: calc(10px + 2vmin); 103 | color: white; 104 | } 105 | 106 | .App-link { 107 | color: #61dafb; 108 | } 109 | 110 | .social-buttons { 111 | margin-top: auto; 112 | margin-left: auto; 113 | margin-bottom: 0.5rem; 114 | margin-right: 1rem; 115 | gap: 0.3rem; 116 | display: flex; 117 | } 118 | 119 | .wallet-wrapper { 120 | background: @background-color-base; 121 | padding-left: 0.7rem; 122 | border-radius: 0.5rem; 123 | display: flex; 124 | align-items: center; 125 | white-space: nowrap; 126 | } 127 | 128 | .wallet-key { 129 | background: @background-color-base; 130 | padding: 0.1rem 0.5rem 0.1rem 0.7rem; 131 | margin-left: 0.3rem; 132 | border-radius: 0.5rem; 133 | display: flex; 134 | align-items: center; 135 | } 136 | 137 | .exchange-card { 138 | border-radius: 20px; 139 | box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 10px 0px; 140 | width: 450px; 141 | margin: 4px auto; 142 | padding: 0px; 143 | 144 | .ant-tabs-tab { 145 | width: 50%; 146 | margin: 0px; 147 | justify-content: center; 148 | border-radius: 20px 20px 0px 0px; 149 | } 150 | 151 | .ant-tabs-tab-active { 152 | background-color: @background-color-light; 153 | } 154 | 155 | .ant-tabs-nav-list { 156 | width: 100% !important; 157 | } 158 | } 159 | 160 | @media only screen and (max-width: 600px) { 161 | .exchange-card { 162 | width: 360px; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import App from "./App"; 4 | 5 | test("renders learn react link", () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.less"; 3 | import GitHubButton from "react-github-btn"; 4 | import { Routes } from "./routes"; 5 | 6 | function App() { 7 | return ( 8 |
9 |
10 |
11 | Swap is unaudited software. Use at your own risk. 12 |
13 |
14 | 15 |
16 | 24 | Star 25 | 26 | 32 | Fork 33 | 34 |
35 |
36 | ); 37 | } 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /src/ant-custom.less: -------------------------------------------------------------------------------- 1 | @import "~antd/dist/antd.css"; 2 | @import "~antd/dist/antd.dark.less"; 3 | @primary-color: #ff00a8; 4 | @popover-background: #1a2029; 5 | -------------------------------------------------------------------------------- /src/buffer-layout.d.ts: -------------------------------------------------------------------------------- 1 | declare module "buffer-layout" { 2 | const magic: any; 3 | export = magic; 4 | } 5 | 6 | declare module "jazzicon" { 7 | const magic: any; 8 | export = magic; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/accountInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useWallet } from "./../utils/wallet"; 3 | import { shortenAddress } from "./../utils/utils"; 4 | import { Identicon } from "./identicon"; 5 | import { useNativeAccount } from "./../utils/accounts"; 6 | import { LAMPORTS_PER_SOL } from "@solana/web3.js"; 7 | 8 | export const AccountInfo = (props: {}) => { 9 | const { wallet } = useWallet(); 10 | const { account } = useNativeAccount(); 11 | 12 | if (!wallet || !wallet.publicKey) { 13 | return null; 14 | } 15 | 16 | return ( 17 |
18 | 19 | {((account?.lamports || 0) / LAMPORTS_PER_SOL).toFixed(6)} SOL 20 | 21 |
22 | {shortenAddress(`${wallet.publicKey}`)} 23 | 27 |
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/currencyInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Card, Select } from "antd"; 3 | import { NumericInput } from "../numericInput"; 4 | import { 5 | getPoolName, 6 | getTokenName, 7 | isKnownMint, 8 | KnownToken, 9 | } from "../../utils/utils"; 10 | import { useUserAccounts, useMint, useCachedPool } from "../../utils/accounts"; 11 | import "./styles.less"; 12 | import { useConnectionConfig } from "../../utils/connection"; 13 | import { PoolIcon, TokenIcon } from "../tokenIcon"; 14 | import PopularTokens from "../../utils/token-list.json"; 15 | import { PublicKey } from "@solana/web3.js"; 16 | import { PoolInfo, TokenAccount } from "../../models"; 17 | 18 | const { Option } = Select; 19 | 20 | export const CurrencyInput = (props: { 21 | mint?: string; 22 | amount?: string; 23 | title?: string; 24 | onInputChange?: (val: number) => void; 25 | onMintChange?: (account: string) => void; 26 | }) => { 27 | const { userAccounts } = useUserAccounts(); 28 | const { pools } = useCachedPool(); 29 | const mint = useMint(props.mint); 30 | 31 | const { env } = useConnectionConfig(); 32 | 33 | const tokens = PopularTokens[env] as KnownToken[]; 34 | 35 | const renderPopularTokens = tokens.map((item) => { 36 | return ( 37 | 50 | ); 51 | }); 52 | 53 | // TODO: expand nested pool names ...? 54 | 55 | // group accounts by mint and use one with biggest balance 56 | const grouppedUserAccounts = userAccounts 57 | .sort((a, b) => { 58 | return b.info.amount.toNumber() - a.info.amount.toNumber(); 59 | }) 60 | .reduce((map, acc) => { 61 | const mint = acc.info.mint.toBase58(); 62 | if (isKnownMint(env, mint)) { 63 | return map; 64 | } 65 | 66 | const pool = pools.find((p) => p && p.pubkeys.mint.toBase58() === mint); 67 | 68 | map.set(mint, (map.get(mint) || []).concat([{ account: acc, pool }])); 69 | 70 | return map; 71 | }, new Map()); 72 | 73 | // TODO: group multple accounts of same time and select one with max amount 74 | const renderAdditionalTokens = [...grouppedUserAccounts.keys()].map( 75 | (mint) => { 76 | const list = grouppedUserAccounts.get(mint); 77 | if (!list || list.length <= 0) { 78 | return undefined; 79 | } 80 | 81 | const account = list[0]; 82 | 83 | if (account.account.info.amount.eqn(0)) { 84 | return undefined; 85 | } 86 | 87 | let name: string; 88 | let icon: JSX.Element; 89 | if (account.pool) { 90 | name = getPoolName(env, account.pool); 91 | 92 | const sorted = account.pool.pubkeys.holdingMints 93 | .map((a: PublicKey) => a.toBase58()) 94 | .sort(); 95 | icon = ; 96 | } else { 97 | name = getTokenName(env, mint); 98 | icon = ; 99 | } 100 | 101 | return ( 102 | 112 | ); 113 | } 114 | ); 115 | 116 | const userUiBalance = () => { 117 | const currentAccount = userAccounts?.find( 118 | (a) => a.info.mint.toBase58() === props.mint 119 | ); 120 | if (currentAccount && mint) { 121 | return ( 122 | currentAccount.info.amount.toNumber() / Math.pow(10, mint.decimals) 123 | ); 124 | } 125 | 126 | return 0; 127 | }; 128 | 129 | return ( 130 | 135 |
136 |
{props.title}
137 | 138 |
141 | props.onInputChange && props.onInputChange(userUiBalance()) 142 | } 143 | > 144 | Balance: {userUiBalance().toFixed(6)} 145 |
146 |
147 |
148 | { 151 | if (props.onInputChange) { 152 | props.onInputChange(val); 153 | } 154 | }} 155 | style={{ 156 | fontSize: 20, 157 | boxShadow: "none", 158 | borderColor: "transparent", 159 | outline: "transpaernt", 160 | }} 161 | placeholder="0.00" 162 | /> 163 | 164 |
165 | 180 |
181 |
182 |
183 | ); 184 | }; 185 | -------------------------------------------------------------------------------- /src/components/currencyInput/styles.less: -------------------------------------------------------------------------------- 1 | .ccy-input { 2 | .ant-select-selector, 3 | .ant-select-selector:focus, 4 | .ant-select-selector:active { 5 | border-color: transparent !important; 6 | box-shadow: none !important; 7 | } 8 | } 9 | 10 | .ccy-input-header { 11 | display: grid; 12 | 13 | grid-template-columns: repeat(2, 1fr); 14 | grid-column-gap: 10px; 15 | 16 | -webkit-box-pack: justify; 17 | justify-content: space-between; 18 | -webkit-box-align: center; 19 | align-items: center; 20 | flex-direction: row; 21 | padding: 10px 20px 0px 20px; 22 | } 23 | 24 | .ccy-input-header-left { 25 | width: 100%; 26 | box-sizing: border-box; 27 | margin: 0px; 28 | min-width: 0px; 29 | display: flex; 30 | padding: 0px; 31 | -webkit-box-align: center; 32 | align-items: center; 33 | width: fit-content; 34 | } 35 | 36 | .ccy-input-header-right { 37 | width: 100%; 38 | display: flex; 39 | flex-direction: row; 40 | -webkit-box-align: center; 41 | align-items: center; 42 | justify-self: flex-end; 43 | justify-content: flex-end; 44 | } 45 | -------------------------------------------------------------------------------- /src/components/exchange.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Button, Card, Popover } from "antd"; 3 | import { TradeEntry } from "./trade"; 4 | import { AddToLiquidity } from "./pool/add"; 5 | import { PoolAccounts } from "./pool/view"; 6 | import { useWallet } from "../utils/wallet"; 7 | import { AccountInfo } from "./accountInfo"; 8 | import { Settings } from "./settings"; 9 | import { SettingOutlined } from "@ant-design/icons"; 10 | 11 | export const ExchangeView = (props: {}) => { 12 | const { connected, wallet } = useWallet(); 13 | const tabStyle: React.CSSProperties = { width: 120 }; 14 | const tabList = [ 15 | { 16 | key: "trade", 17 | tab:
Trade
, 18 | render: () => { 19 | return ; 20 | }, 21 | }, 22 | { 23 | key: "pool", 24 | tab:
Pool
, 25 | render: () => { 26 | return ; 27 | }, 28 | }, 29 | ]; 30 | 31 | const [activeTab, setActiveTab] = useState(tabList[0].key); 32 | 33 | const TopBar = ( 34 |
35 |
36 |
37 |
38 |
39 | 48 | 49 | {connected && ( 50 | } 53 | trigger="click" 54 | > 55 | 56 | 57 | )} 58 |
59 | {!connected && ( 60 | 68 | )} 69 | {connected && ( 70 | 75 | )} 76 |
77 | { 78 | } 82 | trigger="click" 83 | > 84 |
93 |
94 | ); 95 | 96 | return ( 97 | <> 98 | {TopBar} 99 | { 108 | setActiveTab(key); 109 | }} 110 | > 111 | {tabList.find((t) => t.key === activeTab)?.render()} 112 | 113 | 114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /src/components/identicon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | 3 | import Jazzicon from "jazzicon"; 4 | 5 | import "./style.less"; 6 | 7 | export const Identicon = (props: { 8 | address?: string; 9 | style?: React.CSSProperties; 10 | }) => { 11 | const { address } = props; 12 | const ref = useRef(); 13 | 14 | useEffect(() => { 15 | if (address && ref.current) { 16 | ref.current.innerHTML = ""; 17 | ref.current.appendChild(Jazzicon(16, parseInt(address.slice(0, 10), 16))); 18 | } 19 | }, [address]); 20 | 21 | return ( 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/identicon/style.less: -------------------------------------------------------------------------------- 1 | .identicon-wrapper { 2 | display: flex; 3 | height: 1rem; 4 | width: 1rem; 5 | border-radius: 1.125rem; 6 | margin: 0.2rem 0.2rem 0.2rem 0.1rem; 7 | /* background-color: ${({ theme }) => theme.bg4}; */ 8 | } 9 | -------------------------------------------------------------------------------- /src/components/info.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Popover } from "antd"; 2 | import React from "react"; 3 | 4 | import { InfoCircleOutlined } from "@ant-design/icons"; 5 | 6 | export const Info = (props: { 7 | text: React.ReactElement; 8 | style?: React.CSSProperties; 9 | }) => { 10 | return ( 11 | {props.text}
} 14 | > 15 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/labels.tsx: -------------------------------------------------------------------------------- 1 | import { ENV } from "../utils/connection"; 2 | import { CurrencyContextState } from "../utils/currencyPair"; 3 | import { getTokenName } from "../utils/utils"; 4 | 5 | export const CREATE_POOL_LABEL = "Create Liquidity Pool"; 6 | export const INSUFFICIENT_FUNDS_LABEL = (tokenName: string) => 7 | `Insufficient ${tokenName} funds`; 8 | export const POOL_NOT_AVAILABLE = (tokenA: string, tokenB: string) => 9 | `Pool ${tokenA}/${tokenB} doesn't exsist`; 10 | export const ADD_LIQUIDITY_LABEL = "Provide Liquidity"; 11 | export const SWAP_LABEL = "Swap"; 12 | export const CONNECT_LABEL = "Connect Wallet"; 13 | export const SELECT_TOKEN_LABEL = "Select a token"; 14 | export const ENTER_AMOUNT_LABEL = "Enter an amount"; 15 | 16 | export const generateActionLabel = ( 17 | action: string, 18 | connected: boolean, 19 | env: ENV, 20 | A: CurrencyContextState, 21 | B: CurrencyContextState, 22 | ignoreToBalance: boolean = false 23 | ) => { 24 | return !connected 25 | ? CONNECT_LABEL 26 | : !A.mintAddress 27 | ? SELECT_TOKEN_LABEL 28 | : !A.amount 29 | ? ENTER_AMOUNT_LABEL 30 | : !B.mintAddress 31 | ? SELECT_TOKEN_LABEL 32 | : !B.amount 33 | ? ENTER_AMOUNT_LABEL 34 | : !A.sufficientBalance() 35 | ? INSUFFICIENT_FUNDS_LABEL(getTokenName(env, A.mintAddress)) 36 | : ignoreToBalance || B.sufficientBalance() 37 | ? action 38 | : INSUFFICIENT_FUNDS_LABEL(getTokenName(env, B.mintAddress)); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/numericInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Input } from "antd"; 3 | 4 | export class NumericInput extends React.Component { 5 | onChange = (e: any) => { 6 | const { value } = e.target; 7 | const reg = /^-?\d*(\.\d*)?$/; 8 | if ((!isNaN(value) && reg.test(value)) || value === "" || value === "-") { 9 | this.props.onChange(value); 10 | } 11 | }; 12 | 13 | // '.' at the end or only '-' in the input box. 14 | onBlur = () => { 15 | const { value, onBlur, onChange } = this.props; 16 | let valueTemp = value; 17 | if (value.charAt(value.length - 1) === "." || value === "-") { 18 | valueTemp = value.slice(0, -1); 19 | } 20 | onChange(valueTemp.replace(/0*(\d+)/, "$1")); 21 | if (onBlur) { 22 | onBlur(); 23 | } 24 | }; 25 | 26 | render() { 27 | return ( 28 | 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/pool/add.less: -------------------------------------------------------------------------------- 1 | .pool-settings-grid { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | gap: 0.5em 1em; 5 | align-items: center; 6 | text-align: right; 7 | 8 | input { 9 | text-align: right; 10 | } 11 | } 12 | 13 | .add-button { 14 | width: 100%; 15 | position: relative; 16 | 17 | :first-child { 18 | width: 100%; 19 | position: relative; 20 | } 21 | } 22 | 23 | .add-spinner { 24 | position: absolute; 25 | right: 5px; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/pool/add.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { addLiquidity, usePoolForBasket } from "../../utils/pools"; 3 | import { Button, Dropdown, Popover } from "antd"; 4 | import { useWallet } from "../../utils/wallet"; 5 | import { 6 | useConnection, 7 | useConnectionConfig, 8 | useSlippageConfig, 9 | } from "../../utils/connection"; 10 | import { Spin } from "antd"; 11 | import { LoadingOutlined } from "@ant-design/icons"; 12 | import { notify } from "../../utils/notifications"; 13 | import { SupplyOverview } from "./supplyOverview"; 14 | import { CurrencyInput } from "../currencyInput"; 15 | import { DEFAULT_DENOMINATOR, PoolConfigCard } from "./config"; 16 | import "./add.less"; 17 | import { PoolConfig } from "../../models"; 18 | import { SWAP_PROGRAM_OWNER_FEE_ADDRESS } from "../../utils/ids"; 19 | import { useCurrencyPairState } from "../../utils/currencyPair"; 20 | import { 21 | CREATE_POOL_LABEL, 22 | ADD_LIQUIDITY_LABEL, 23 | generateActionLabel, 24 | } from "../labels"; 25 | 26 | const antIcon = ; 27 | 28 | export const AddToLiquidity = () => { 29 | const { wallet, connected } = useWallet(); 30 | const connection = useConnection(); 31 | const [pendingTx, setPendingTx] = useState(false); 32 | const { A, B, setLastTypedAccount } = useCurrencyPairState(); 33 | const pool = usePoolForBasket([A?.mintAddress, B?.mintAddress]); 34 | const { slippage } = useSlippageConfig(); 35 | const { env } = useConnectionConfig(); 36 | const [options, setOptions] = useState({ 37 | curveType: 0, 38 | tradeFeeNumerator: 25, 39 | tradeFeeDenominator: DEFAULT_DENOMINATOR, 40 | ownerTradeFeeNumerator: 5, 41 | ownerTradeFeeDenominator: DEFAULT_DENOMINATOR, 42 | ownerWithdrawFeeNumerator: 0, 43 | ownerWithdrawFeeDenominator: DEFAULT_DENOMINATOR, 44 | }); 45 | 46 | const executeAction = !connected 47 | ? wallet.connect 48 | : async () => { 49 | if (A.account && B.account && A.mint && B.mint) { 50 | setPendingTx(true); 51 | const components = [ 52 | { 53 | account: A.account, 54 | mintAddress: A.mintAddress, 55 | amount: A.convertAmount(), 56 | }, 57 | { 58 | account: B.account, 59 | mintAddress: B.mintAddress, 60 | amount: B.convertAmount(), 61 | }, 62 | ]; 63 | 64 | addLiquidity(connection, wallet, components, slippage, pool, options) 65 | .then(() => { 66 | setPendingTx(false); 67 | }) 68 | .catch((e) => { 69 | console.log("Transaction failed", e); 70 | notify({ 71 | description: 72 | "Please try again and approve transactions from your wallet", 73 | message: "Adding liquidity cancelled.", 74 | type: "error", 75 | }); 76 | setPendingTx(false); 77 | }); 78 | } 79 | }; 80 | 81 | const hasSufficientBalance = A.sufficientBalance() && B.sufficientBalance(); 82 | 83 | const createPoolButton = SWAP_PROGRAM_OWNER_FEE_ADDRESS ? ( 84 | 96 | ) : ( 97 | } 107 | > 108 | {generateActionLabel(CREATE_POOL_LABEL, connected, env, A, B)} 109 | {pendingTx && } 110 | 111 | ); 112 | 113 | return ( 114 |
115 | 119 | Liquidity providers earn a fixed percentage fee on all trades 120 | proportional to their share of the pool. Fees are added to the pool, 121 | accrue in real time and can be claimed by withdrawing your 122 | liquidity. 123 |
124 | } 125 | > 126 | 127 | 128 | 129 | { 132 | if (A.amount !== val) { 133 | setLastTypedAccount(A.mintAddress); 134 | } 135 | A.setAmount(val); 136 | }} 137 | amount={A.amount} 138 | mint={A.mintAddress} 139 | onMintChange={(item) => { 140 | A.setMint(item); 141 | }} 142 | /> 143 |
+
144 | { 147 | if (B.amount !== val) { 148 | setLastTypedAccount(B.mintAddress); 149 | } 150 | 151 | B.setAmount(val); 152 | }} 153 | amount={B.amount} 154 | mint={B.mintAddress} 155 | onMintChange={(item) => { 156 | B.setMint(item); 157 | }} 158 | /> 159 | 163 | {pool && ( 164 | 181 | )} 182 | {!pool && createPoolButton} 183 |
184 | ); 185 | }; 186 | -------------------------------------------------------------------------------- /src/components/pool/config.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Card, Select } from "antd"; 3 | import { NumericInput } from "../numericInput"; 4 | import "./add.less"; 5 | import { PoolConfig } from "../../models"; 6 | 7 | const Option = Select.Option; 8 | 9 | export const DEFAULT_DENOMINATOR = 10_000; 10 | 11 | const FeeInput = (props: { 12 | numerator: number; 13 | denominator: number; 14 | set: (numerator: number, denominator: number) => void; 15 | }) => { 16 | const [value, setValue] = useState( 17 | ((props.numerator / props.denominator) * 100).toString() 18 | ); 19 | 20 | return ( 21 |
22 | { 34 | setValue(x); 35 | 36 | const val = parseFloat(x); 37 | if (Number.isFinite(val)) { 38 | const numerator = (val * DEFAULT_DENOMINATOR) / 100; 39 | props.set(numerator, DEFAULT_DENOMINATOR); 40 | } 41 | }} 42 | /> 43 | % 44 |
45 | ); 46 | }; 47 | 48 | // sets fee in the pool to 0.3% 49 | // see for fees details: https://uniswap.org/docs/v2/advanced-topics/fees/ 50 | export const PoolConfigCard = (props: { 51 | options: PoolConfig; 52 | setOptions: (config: PoolConfig) => void; 53 | }) => { 54 | const { 55 | tradeFeeNumerator, 56 | tradeFeeDenominator, 57 | ownerTradeFeeNumerator, 58 | ownerTradeFeeDenominator, 59 | ownerWithdrawFeeNumerator, 60 | ownerWithdrawFeeDenominator, 61 | } = props.options; 62 | 63 | return ( 64 | 65 |
66 | <> 67 | LPs Trading Fee: 68 | 72 | props.setOptions({ 73 | ...props.options, 74 | tradeFeeNumerator: numerator, 75 | tradeFeeDenominator: denominator, 76 | }) 77 | } 78 | /> 79 | 80 | <> 81 | Owner Trading Fee: 82 | 86 | props.setOptions({ 87 | ...props.options, 88 | ownerTradeFeeNumerator: numerator, 89 | ownerTradeFeeDenominator: denominator, 90 | }) 91 | } 92 | /> 93 | 94 | <> 95 | Withdraw Fee: 96 | 100 | props.setOptions({ 101 | ...props.options, 102 | ownerWithdrawFeeNumerator: numerator, 103 | ownerWithdrawFeeDenominator: denominator, 104 | }) 105 | } 106 | /> 107 | 108 | <> 109 | Curve Type: 110 | 123 | 124 |
125 |
126 | ); 127 | }; 128 | -------------------------------------------------------------------------------- /src/components/pool/remove.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Button } from "antd"; 3 | 4 | import { removeLiquidity } from "../../utils/pools"; 5 | import { useWallet } from "../../utils/wallet"; 6 | import { useConnection } from "../../utils/connection"; 7 | import { PoolInfo, TokenAccount } from "../../models"; 8 | import { notify } from "../../utils/notifications"; 9 | 10 | export const RemoveLiquidity = (props: { 11 | instance: { account: TokenAccount; pool: PoolInfo }; 12 | }) => { 13 | const { account, pool } = props.instance; 14 | const [pendingTx, setPendingTx] = useState(false); 15 | const { wallet } = useWallet(); 16 | const connection = useConnection(); 17 | 18 | const onRemove = async () => { 19 | try { 20 | setPendingTx(true); 21 | // TODO: calculate percentage based on user input 22 | let liquidityAmount = account.info.amount.toNumber(); 23 | await removeLiquidity(connection, wallet, liquidityAmount, account, pool); 24 | } catch { 25 | notify({ 26 | description: 27 | "Please try again and approve transactions from your wallet", 28 | message: "Removing liquidity cancelled.", 29 | type: "error", 30 | }); 31 | } finally { 32 | setPendingTx(false); 33 | // TODO: force refresh of pool accounts? 34 | } 35 | }; 36 | 37 | return ( 38 | <> 39 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/pool/supplyOverview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from "react"; 2 | import { Card } from "antd"; 3 | import { getTokenName, formatTokenAmount, convert } from "../../utils/utils"; 4 | import { PieChart, Pie, Cell } from "recharts"; 5 | import { useMint, useAccount } from "../../utils/accounts"; 6 | import { 7 | ENDPOINTS, 8 | useConnection, 9 | useConnectionConfig, 10 | } from "../../utils/connection"; 11 | import { PoolInfo } from "../../models"; 12 | import { MARKETS, TOKEN_MINTS, Market } from "@project-serum/serum"; 13 | import { Connection } from "@solana/web3.js"; 14 | 15 | const RADIAN = Math.PI / 180; 16 | const renderCustomizedLabel = (props: any, data: any) => { 17 | const { cx, cy, midAngle, innerRadius, outerRadius, index } = props; 18 | const radius = innerRadius + (outerRadius - innerRadius) * 0.5; 19 | const x = cx + radius * Math.cos(-midAngle * RADIAN); 20 | const y = cy + radius * Math.sin(-midAngle * RADIAN); 21 | 22 | return ( 23 | cx ? "start" : "end"} 28 | dominantBaseline="central" 29 | > 30 | {data[index].name} 31 | 32 | ); 33 | }; 34 | 35 | const STABLE_COINS = new Set(["USDC", "wUSDC", "USDT"]); 36 | 37 | const useMidPriceInUSD = (mint: string) => { 38 | const connection = useMemo( 39 | () => new Connection(ENDPOINTS[0].endpoint, "recent"), 40 | [] 41 | ); 42 | const [price, setPrice] = useState(undefined); 43 | const [isBase, setIsBase] = useState(false); 44 | 45 | useEffect(() => { 46 | setIsBase(true); 47 | setPrice(undefined); 48 | 49 | const SERUM_TOKEN = TOKEN_MINTS.find((a) => a.address.toBase58() === mint); 50 | const marketName = `${SERUM_TOKEN?.name}/USDC`; 51 | const marketInfo = MARKETS.find((m) => m.name === marketName); 52 | 53 | if (STABLE_COINS.has(SERUM_TOKEN?.name || "")) { 54 | setIsBase(true); 55 | setPrice(1.0); 56 | return; 57 | } 58 | 59 | if (!marketInfo?.programId) { 60 | return; 61 | } 62 | 63 | (async () => { 64 | let market = await Market.load( 65 | connection, 66 | marketInfo.address, 67 | undefined, 68 | marketInfo.programId 69 | ); 70 | 71 | const bids = await market.loadBids(connection); 72 | const asks = await market.loadAsks(connection); 73 | const bestBid = bids.getL2(1); 74 | const bestAsk = asks.getL2(1); 75 | 76 | setIsBase(false); 77 | 78 | if (bestBid.length > 0 && bestAsk.length > 0) { 79 | setPrice((bestBid[0][0] + bestAsk[0][0]) / 2.0); 80 | } 81 | })(); 82 | }, [connection, mint, setIsBase, setPrice]); 83 | 84 | return { price, isBase }; 85 | }; 86 | 87 | export const SupplyOverview = (props: { 88 | mintAddress: string[]; 89 | pool?: PoolInfo; 90 | }) => { 91 | const { mintAddress, pool } = props; 92 | const connection = useConnection(); 93 | const mintA = useMint(mintAddress[0]); 94 | const mintB = useMint(mintAddress[1]); 95 | const accountA = useAccount( 96 | pool?.pubkeys.holdingMints[0].toBase58() === mintAddress[0] 97 | ? pool?.pubkeys.holdingAccounts[0] 98 | : pool?.pubkeys.holdingAccounts[1] 99 | ); 100 | const accountB = useAccount( 101 | pool?.pubkeys.holdingMints[0].toBase58() === mintAddress[0] 102 | ? pool?.pubkeys.holdingAccounts[1] 103 | : pool?.pubkeys.holdingAccounts[0] 104 | ); 105 | const { env } = useConnectionConfig(); 106 | const [data, setData] = useState< 107 | { name: string; value: number; color: string }[] 108 | >([]); 109 | const { price: priceA, isBase: isBaseA } = useMidPriceInUSD(mintAddress[0]); 110 | const { price: priceB, isBase: isBaseB } = useMidPriceInUSD(mintAddress[1]); 111 | 112 | const hasBothPrices = priceA !== undefined && priceB !== undefined; 113 | 114 | useEffect(() => { 115 | if (!mintAddress || !accountA || !accountB) { 116 | return; 117 | } 118 | 119 | (async () => { 120 | let chart = [ 121 | { 122 | name: getTokenName(env, mintAddress[0]), 123 | value: convert(accountA, mintA, hasBothPrices ? priceA : undefined), 124 | color: "#6610f2", 125 | }, 126 | { 127 | name: getTokenName(env, mintAddress[1]), 128 | value: convert(accountB, mintB, hasBothPrices ? priceB : undefined), 129 | color: "#d83aeb", 130 | }, 131 | ]; 132 | 133 | setData(chart); 134 | })(); 135 | }, [ 136 | accountA, 137 | accountB, 138 | mintA, 139 | mintB, 140 | connection, 141 | env, 142 | mintAddress, 143 | hasBothPrices, 144 | priceA, 145 | priceB, 146 | ]); 147 | 148 | if (!pool || !accountA || !accountB || data.length < 1) { 149 | return null; 150 | } 151 | 152 | return ( 153 | 154 |
155 | 156 | renderCustomizedLabel(props, data)} 164 | outerRadius={60} 165 | > 166 | {data.map((entry, index) => ( 167 | 168 | ))} 169 | 170 | 171 |
181 |
182 | {data[0].name}: {formatTokenAmount(accountA, mintA)}{" "} 183 | {!isBaseA && formatTokenAmount(accountA, mintA, priceA, "($", ")")} 184 |
185 |
186 | {data[1].name}: {formatTokenAmount(accountB, mintB)}{" "} 187 | {!isBaseB && 188 | priceB && 189 | formatTokenAmount(accountB, mintB, priceB, "($", ")")} 190 |
191 |
192 |
193 |
194 | ); 195 | }; 196 | -------------------------------------------------------------------------------- /src/components/pool/view.less: -------------------------------------------------------------------------------- 1 | .pools-grid { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .pool-item-row { 7 | display: flex; 8 | position: relative; 9 | width: 100%; 10 | align-items: center; 11 | text-align: right; 12 | cursor: pointer; 13 | } 14 | 15 | .pool-item-row:hover { 16 | background-color: var(--row-highlight); 17 | } 18 | 19 | .pool-item-amount { 20 | min-width: 100px; 21 | } 22 | 23 | .pool-item-type { 24 | min-width: 20px; 25 | } 26 | 27 | .pool-item-name { 28 | padding-right: 0.5em; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/pool/view.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ConfigProvider, Empty } from "antd"; 3 | import { useOwnedPools } from "../../utils/pools"; 4 | import { RemoveLiquidity } from "./remove"; 5 | import { getPoolName } from "../../utils/utils"; 6 | import { useMint } from "../../utils/accounts"; 7 | import { useConnectionConfig } from "../../utils/connection"; 8 | import { PoolIcon } from "../tokenIcon"; 9 | import { PoolInfo, TokenAccount } from "../../models"; 10 | import { useCurrencyPairState } from "../../utils/currencyPair"; 11 | import "./view.less"; 12 | 13 | const PoolItem = (props: { 14 | item: { pool: PoolInfo; isFeeAccount: boolean; account: TokenAccount }; 15 | }) => { 16 | const { env } = useConnectionConfig(); 17 | const { A, B } = useCurrencyPairState(); 18 | const item = props.item; 19 | const mint = useMint(item.account.info.mint.toBase58()); 20 | const amount = 21 | item.account.info.amount.toNumber() / Math.pow(10, mint?.decimals || 0); 22 | 23 | if (!amount) { 24 | return null; 25 | } 26 | 27 | const setPair = () => { 28 | A.setMint(props.item.pool.pubkeys.holdingMints[0].toBase58()); 29 | B.setMint(props.item.pool.pubkeys.holdingMints[1].toBase58()); 30 | }; 31 | 32 | const sorted = item.pool.pubkeys.holdingMints.map((a) => a.toBase58()).sort(); 33 | 34 | if (item) { 35 | return ( 36 |
41 |
{amount.toFixed(4)}
42 |
43 | {item.isFeeAccount ? " (F) " : " "} 44 |
45 | 50 |
{getPoolName(env, item.pool)}
51 | 52 |
53 | ); 54 | } 55 | 56 | return null; 57 | }; 58 | 59 | export const PoolAccounts = () => { 60 | const pools = useOwnedPools(); 61 | 62 | return ( 63 | <> 64 |
Your Liquidity
65 | ( 67 | 71 | )} 72 | > 73 |
74 | {pools.map((p) => ( 75 | 76 | ))} 77 |
78 |
79 | 80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /src/components/settings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Button, Select } from "antd"; 3 | import { 4 | ENDPOINTS, 5 | useConnectionConfig, 6 | useSlippageConfig, 7 | } from "../utils/connection"; 8 | import { useWallet, WALLET_PROVIDERS } from "../utils/wallet"; 9 | import { NumericInput } from "./numericInput"; 10 | 11 | const Slippage = (props: {}) => { 12 | const { slippage, setSlippage } = useSlippageConfig(); 13 | const slippagePct = slippage * 100; 14 | const [value, setValue] = useState(slippagePct.toString()); 15 | 16 | useEffect(() => { 17 | setValue(slippagePct.toString()); 18 | }, [slippage, slippagePct]); 19 | 20 | const isSelected = (val: number) => { 21 | return val === slippagePct ? "primary" : "default"; 22 | }; 23 | 24 | const itemStyle: React.CSSProperties = { 25 | margin: 5, 26 | }; 27 | 28 | return ( 29 |
32 | {[0.1, 0.5, 1.0].map((item) => { 33 | return ( 34 | 42 | ); 43 | })} 44 |
45 | { 58 | setValue(x); 59 | const newValue = parseFloat(x) / 100.0; 60 | if (Number.isFinite(newValue)) { 61 | setSlippage(newValue); 62 | } 63 | }} 64 | /> 65 | % 66 |
67 |
68 | ); 69 | }; 70 | 71 | export const Settings = () => { 72 | const { providerUrl, setProvider } = useWallet(); 73 | const { endpoint, setEndpoint } = useConnectionConfig(); 74 | 75 | return ( 76 | <> 77 |
78 | Transactions: Settings: 79 |
80 | Slippage: 81 | 82 |
83 |
84 |
85 | Network:{" "} 86 | 97 |
98 |
99 | Wallet:{" "} 100 | 107 |
108 | 109 | ); 110 | }; 111 | -------------------------------------------------------------------------------- /src/components/slippage/style.less: -------------------------------------------------------------------------------- 1 | .slippage-input { 2 | } 3 | -------------------------------------------------------------------------------- /src/components/tokenIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import { Identicon } from "./../identicon"; 2 | import React from "react"; 3 | import { getTokenIcon } from "../../utils/utils"; 4 | import { useConnectionConfig } from "../../utils/connection"; 5 | 6 | export const TokenIcon = (props: { 7 | mintAddress: string; 8 | style?: React.CSSProperties; 9 | }) => { 10 | const { env } = useConnectionConfig(); 11 | const icon = getTokenIcon(env, props.mintAddress); 12 | 13 | if (icon) { 14 | return ( 15 | Token icon 30 | ); 31 | } 32 | 33 | return ( 34 | 38 | ); 39 | }; 40 | 41 | export const PoolIcon = (props: { 42 | mintA: string; 43 | mintB: string; 44 | style?: React.CSSProperties; 45 | }) => { 46 | return ( 47 |
48 | 52 | 53 |
54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/trade/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Spin } from "antd"; 2 | import React, { useState } from "react"; 3 | import { 4 | useConnection, 5 | useConnectionConfig, 6 | useSlippageConfig, 7 | } from "../../utils/connection"; 8 | import { useWallet } from "../../utils/wallet"; 9 | import { CurrencyInput } from "../currencyInput"; 10 | import { LoadingOutlined } from "@ant-design/icons"; 11 | import { swap, usePoolForBasket } from "../../utils/pools"; 12 | import { notify } from "../../utils/notifications"; 13 | import { useCurrencyPairState } from "../../utils/currencyPair"; 14 | import { generateActionLabel, POOL_NOT_AVAILABLE, SWAP_LABEL } from "../labels"; 15 | import "./trade.less"; 16 | import { getTokenName } from "../../utils/utils"; 17 | 18 | const antIcon = ; 19 | 20 | // TODO: 21 | // Compute price breakdown with/without fee 22 | // Show slippage 23 | // Show fee information 24 | 25 | export const TradeEntry = () => { 26 | const { wallet, connected } = useWallet(); 27 | const connection = useConnection(); 28 | const [pendingTx, setPendingTx] = useState(false); 29 | const { A, B, setLastTypedAccount } = useCurrencyPairState(); 30 | const pool = usePoolForBasket([A?.mintAddress, B?.mintAddress]); 31 | const { slippage } = useSlippageConfig(); 32 | const { env } = useConnectionConfig(); 33 | 34 | const swapAccounts = () => { 35 | const tempMint = A.mintAddress; 36 | const tempAmount = A.amount; 37 | A.setMint(B.mintAddress); 38 | A.setAmount(B.amount); 39 | B.setMint(tempMint); 40 | B.setAmount(tempAmount); 41 | }; 42 | 43 | const handleSwap = async () => { 44 | if (A.account && B.mintAddress) { 45 | try { 46 | setPendingTx(true); 47 | 48 | const components = [ 49 | { 50 | account: A.account, 51 | mintAddress: A.mintAddress, 52 | amount: A.convertAmount(), 53 | }, 54 | { 55 | mintAddress: B.mintAddress, 56 | amount: B.convertAmount(), 57 | }, 58 | ]; 59 | 60 | await swap(connection, wallet, components, slippage, pool); 61 | } catch { 62 | notify({ 63 | description: 64 | "Please try again and approve transactions from your wallet", 65 | message: "Swap trade cancelled.", 66 | type: "error", 67 | }); 68 | } finally { 69 | setPendingTx(false); 70 | } 71 | } 72 | }; 73 | 74 | return ( 75 | <> 76 |
77 | { 80 | if (A.amount !== val) { 81 | setLastTypedAccount(A.mintAddress); 82 | } 83 | 84 | A.setAmount(val); 85 | }} 86 | amount={A.amount} 87 | mint={A.mintAddress} 88 | onMintChange={(item) => { 89 | A.setMint(item); 90 | }} 91 | /> 92 | 95 | { 98 | if (B.amount !== val) { 99 | setLastTypedAccount(B.mintAddress); 100 | } 101 | 102 | B.setAmount(val); 103 | }} 104 | amount={B.amount} 105 | mint={B.mintAddress} 106 | onMintChange={(item) => { 107 | B.setMint(item); 108 | }} 109 | /> 110 |
111 | 142 | 143 | ); 144 | }; 145 | -------------------------------------------------------------------------------- /src/components/trade/trade.less: -------------------------------------------------------------------------------- 1 | .trade-button { 2 | width: 100%; 3 | position: relative; 4 | 5 | :first-child { 6 | width: 100%; 7 | position: relative; 8 | } 9 | } 10 | 11 | .trade-spinner { 12 | position: absolute; 13 | right: 5px; 14 | } 15 | 16 | .swap-button { 17 | border-radius: 2em; 18 | width: 32px; 19 | padding-left: 8px; 20 | } 21 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | import { WalletProvider } from "./utils/wallet"; 7 | import { ConnectionProvider } from "./utils/connection"; 8 | import { AccountsProvider } from "./utils/accounts"; 9 | import { CurrencyPairProvider } from "./utils/currencyPair"; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | , 23 | document.getElementById("root") 24 | ); 25 | 26 | // If you want your app to work offline and load faster, you can change 27 | // unregister() to register() below. Note this comes with some pitfalls. 28 | // Learn more about service workers: https://bit.ly/CRA-PWA 29 | serviceWorker.unregister(); 30 | -------------------------------------------------------------------------------- /src/models/account.ts: -------------------------------------------------------------------------------- 1 | import { AccountInfo, PublicKey } from "@solana/web3.js"; 2 | 3 | import { AccountInfo as TokenAccountInfo } from "@solana/spl-token"; 4 | 5 | export interface TokenAccount { 6 | pubkey: PublicKey; 7 | account: AccountInfo; 8 | info: TokenAccountInfo; 9 | } 10 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pool"; 2 | export * from "./account"; 3 | export * from "./tokenSwap"; 4 | -------------------------------------------------------------------------------- /src/models/pool.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { TokenAccount } from "./account"; 3 | 4 | export interface PoolInfo { 5 | pubkeys: { 6 | program: PublicKey; 7 | account: PublicKey; 8 | holdingAccounts: PublicKey[]; 9 | holdingMints: PublicKey[]; 10 | mint: PublicKey; 11 | feeAccount?: PublicKey; 12 | }; 13 | legacy: boolean; 14 | raw: any; 15 | } 16 | 17 | export interface LiquidityComponent { 18 | amount: number; 19 | account?: TokenAccount; 20 | mintAddress: string; 21 | } 22 | 23 | export interface PoolConfig { 24 | curveType: 0 | 1; 25 | tradeFeeNumerator: number; 26 | tradeFeeDenominator: number; 27 | ownerTradeFeeNumerator: number; 28 | ownerTradeFeeDenominator: number; 29 | ownerWithdrawFeeNumerator: number; 30 | ownerWithdrawFeeDenominator: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/models/tokenSwap.ts: -------------------------------------------------------------------------------- 1 | import { Numberu64 } from "@solana/spl-token-swap"; 2 | import { PublicKey, Account, TransactionInstruction } from "@solana/web3.js"; 3 | import * as BufferLayout from "buffer-layout"; 4 | 5 | export { TokenSwap } from "@solana/spl-token-swap"; 6 | 7 | /** 8 | * Layout for a public key 9 | */ 10 | export const publicKey = (property: string = "publicKey"): Object => { 11 | return BufferLayout.blob(32, property); 12 | }; 13 | 14 | /** 15 | * Layout for a 64bit unsigned value 16 | */ 17 | export const uint64 = (property: string = "uint64"): Object => { 18 | return BufferLayout.blob(8, property); 19 | }; 20 | 21 | export const TokenSwapLayoutLegacyV0 = BufferLayout.struct([ 22 | BufferLayout.u8("isInitialized"), 23 | BufferLayout.u8("nonce"), 24 | publicKey("tokenAccountA"), 25 | publicKey("tokenAccountB"), 26 | publicKey("tokenPool"), 27 | uint64("feesNumerator"), 28 | uint64("feesDenominator"), 29 | ]); 30 | 31 | export const TokenSwapLayout: typeof BufferLayout.Structure = BufferLayout.struct( 32 | [ 33 | BufferLayout.u8("isInitialized"), 34 | BufferLayout.u8("nonce"), 35 | publicKey("tokenProgramId"), 36 | publicKey("tokenAccountA"), 37 | publicKey("tokenAccountB"), 38 | publicKey("tokenPool"), 39 | publicKey("mintA"), 40 | publicKey("mintB"), 41 | publicKey("feeAccount"), 42 | BufferLayout.u8("curveType"), 43 | uint64("tradeFeeNumerator"), 44 | uint64("tradeFeeDenominator"), 45 | uint64("ownerTradeFeeNumerator"), 46 | uint64("ownerTradeFeeDenominator"), 47 | uint64("ownerWithdrawFeeNumerator"), 48 | uint64("ownerWithdrawFeeDenominator"), 49 | BufferLayout.blob(16, "padding"), 50 | ] 51 | ); 52 | 53 | export const createInitSwapInstruction = ( 54 | tokenSwapAccount: Account, 55 | authority: PublicKey, 56 | tokenAccountA: PublicKey, 57 | tokenAccountB: PublicKey, 58 | tokenPool: PublicKey, 59 | feeAccount: PublicKey, 60 | tokenAccountPool: PublicKey, 61 | tokenProgramId: PublicKey, 62 | swapProgramId: PublicKey, 63 | nonce: number, 64 | curveType: number, 65 | tradeFeeNumerator: number, 66 | tradeFeeDenominator: number, 67 | ownerTradeFeeNumerator: number, 68 | ownerTradeFeeDenominator: number, 69 | ownerWithdrawFeeNumerator: number, 70 | ownerWithdrawFeeDenominator: number 71 | ): TransactionInstruction => { 72 | const keys = [ 73 | { pubkey: tokenSwapAccount.publicKey, isSigner: false, isWritable: true }, 74 | { pubkey: authority, isSigner: false, isWritable: false }, 75 | { pubkey: tokenAccountA, isSigner: false, isWritable: false }, 76 | { pubkey: tokenAccountB, isSigner: false, isWritable: false }, 77 | { pubkey: tokenPool, isSigner: false, isWritable: true }, 78 | { pubkey: feeAccount, isSigner: false, isWritable: false }, 79 | { pubkey: tokenAccountPool, isSigner: false, isWritable: true }, 80 | { pubkey: tokenProgramId, isSigner: false, isWritable: false }, 81 | ]; 82 | 83 | const commandDataLayout = BufferLayout.struct([ 84 | BufferLayout.u8("instruction"), 85 | BufferLayout.u8("nonce"), 86 | BufferLayout.u8("curveType"), 87 | BufferLayout.nu64("tradeFeeNumerator"), 88 | BufferLayout.nu64("tradeFeeDenominator"), 89 | BufferLayout.nu64("ownerTradeFeeNumerator"), 90 | BufferLayout.nu64("ownerTradeFeeDenominator"), 91 | BufferLayout.nu64("ownerWithdrawFeeNumerator"), 92 | BufferLayout.nu64("ownerWithdrawFeeDenominator"), 93 | BufferLayout.blob(16, "padding"), 94 | ]); 95 | let data = Buffer.alloc(1024); 96 | { 97 | const encodeLength = commandDataLayout.encode( 98 | { 99 | instruction: 0, // InitializeSwap instruction 100 | nonce, 101 | curveType, 102 | tradeFeeNumerator, 103 | tradeFeeDenominator, 104 | ownerTradeFeeNumerator, 105 | ownerTradeFeeDenominator, 106 | ownerWithdrawFeeNumerator, 107 | ownerWithdrawFeeDenominator, 108 | }, 109 | data 110 | ); 111 | data = data.slice(0, encodeLength); 112 | } 113 | return new TransactionInstruction({ 114 | keys, 115 | programId: swapProgramId, 116 | data, 117 | }); 118 | }; 119 | 120 | export const depositInstruction = ( 121 | tokenSwap: PublicKey, 122 | authority: PublicKey, 123 | sourceA: PublicKey, 124 | sourceB: PublicKey, 125 | intoA: PublicKey, 126 | intoB: PublicKey, 127 | poolToken: PublicKey, 128 | poolAccount: PublicKey, 129 | swapProgramId: PublicKey, 130 | tokenProgramId: PublicKey, 131 | poolTokenAmount: number | Numberu64, 132 | maximumTokenA: number | Numberu64, 133 | maximumTokenB: number | Numberu64 134 | ): TransactionInstruction => { 135 | const dataLayout = BufferLayout.struct([ 136 | BufferLayout.u8("instruction"), 137 | uint64("poolTokenAmount"), 138 | uint64("maximumTokenA"), 139 | uint64("maximumTokenB"), 140 | ]); 141 | 142 | const data = Buffer.alloc(dataLayout.span); 143 | dataLayout.encode( 144 | { 145 | instruction: 2, // Deposit instruction 146 | poolTokenAmount: new Numberu64(poolTokenAmount).toBuffer(), 147 | maximumTokenA: new Numberu64(maximumTokenA).toBuffer(), 148 | maximumTokenB: new Numberu64(maximumTokenB).toBuffer(), 149 | }, 150 | data 151 | ); 152 | 153 | const keys = [ 154 | { pubkey: tokenSwap, isSigner: false, isWritable: false }, 155 | { pubkey: authority, isSigner: false, isWritable: false }, 156 | { pubkey: sourceA, isSigner: false, isWritable: true }, 157 | { pubkey: sourceB, isSigner: false, isWritable: true }, 158 | { pubkey: intoA, isSigner: false, isWritable: true }, 159 | { pubkey: intoB, isSigner: false, isWritable: true }, 160 | { pubkey: poolToken, isSigner: false, isWritable: true }, 161 | { pubkey: poolAccount, isSigner: false, isWritable: true }, 162 | { pubkey: tokenProgramId, isSigner: false, isWritable: false }, 163 | ]; 164 | return new TransactionInstruction({ 165 | keys, 166 | programId: swapProgramId, 167 | data, 168 | }); 169 | }; 170 | 171 | export const withdrawInstruction = ( 172 | tokenSwap: PublicKey, 173 | authority: PublicKey, 174 | poolMint: PublicKey, 175 | feeAccount: PublicKey | undefined, 176 | sourcePoolAccount: PublicKey, 177 | fromA: PublicKey, 178 | fromB: PublicKey, 179 | userAccountA: PublicKey, 180 | userAccountB: PublicKey, 181 | swapProgramId: PublicKey, 182 | tokenProgramId: PublicKey, 183 | poolTokenAmount: number | Numberu64, 184 | minimumTokenA: number | Numberu64, 185 | minimumTokenB: number | Numberu64 186 | ): TransactionInstruction => { 187 | const dataLayout = BufferLayout.struct([ 188 | BufferLayout.u8("instruction"), 189 | uint64("poolTokenAmount"), 190 | uint64("minimumTokenA"), 191 | uint64("minimumTokenB"), 192 | ]); 193 | 194 | const data = Buffer.alloc(dataLayout.span); 195 | dataLayout.encode( 196 | { 197 | instruction: 3, // Withdraw instruction 198 | poolTokenAmount: new Numberu64(poolTokenAmount).toBuffer(), 199 | minimumTokenA: new Numberu64(minimumTokenA).toBuffer(), 200 | minimumTokenB: new Numberu64(minimumTokenB).toBuffer(), 201 | }, 202 | data 203 | ); 204 | 205 | const keys = [ 206 | { pubkey: tokenSwap, isSigner: false, isWritable: false }, 207 | { pubkey: authority, isSigner: false, isWritable: false }, 208 | { pubkey: poolMint, isSigner: false, isWritable: true }, 209 | { pubkey: sourcePoolAccount, isSigner: false, isWritable: true }, 210 | { pubkey: fromA, isSigner: false, isWritable: true }, 211 | { pubkey: fromB, isSigner: false, isWritable: true }, 212 | { pubkey: userAccountA, isSigner: false, isWritable: true }, 213 | { pubkey: userAccountB, isSigner: false, isWritable: true }, 214 | ]; 215 | 216 | if (feeAccount) { 217 | keys.push({ pubkey: feeAccount, isSigner: false, isWritable: true }); 218 | } 219 | keys.push({ pubkey: tokenProgramId, isSigner: false, isWritable: false }); 220 | 221 | return new TransactionInstruction({ 222 | keys, 223 | programId: swapProgramId, 224 | data, 225 | }); 226 | }; 227 | 228 | export const swapInstruction = ( 229 | tokenSwap: PublicKey, 230 | authority: PublicKey, 231 | userSource: PublicKey, 232 | poolSource: PublicKey, 233 | poolDestination: PublicKey, 234 | userDestination: PublicKey, 235 | poolMint: PublicKey, 236 | feeAccount: PublicKey, 237 | swapProgramId: PublicKey, 238 | tokenProgramId: PublicKey, 239 | amountIn: number | Numberu64, 240 | minimumAmountOut: number | Numberu64, 241 | programOwner?: PublicKey 242 | ): TransactionInstruction => { 243 | const dataLayout = BufferLayout.struct([ 244 | BufferLayout.u8("instruction"), 245 | uint64("amountIn"), 246 | uint64("minimumAmountOut"), 247 | ]); 248 | 249 | const keys = [ 250 | { pubkey: tokenSwap, isSigner: false, isWritable: false }, 251 | { pubkey: authority, isSigner: false, isWritable: false }, 252 | { pubkey: userSource, isSigner: false, isWritable: true }, 253 | { pubkey: poolSource, isSigner: false, isWritable: true }, 254 | { pubkey: poolDestination, isSigner: false, isWritable: true }, 255 | { pubkey: userDestination, isSigner: false, isWritable: true }, 256 | { pubkey: poolMint, isSigner: false, isWritable: true }, 257 | { pubkey: feeAccount, isSigner: false, isWritable: true }, 258 | { pubkey: tokenProgramId, isSigner: false, isWritable: false }, 259 | ]; 260 | 261 | // optional depending on the build of token-swap program 262 | if (programOwner) { 263 | keys.push({ pubkey: programOwner, isSigner: false, isWritable: true }); 264 | } 265 | 266 | const data = Buffer.alloc(dataLayout.span); 267 | dataLayout.encode( 268 | { 269 | instruction: 1, // Swap instruction 270 | amountIn: new Numberu64(amountIn).toBuffer(), 271 | minimumAmountOut: new Numberu64(minimumAmountOut).toBuffer(), 272 | }, 273 | data 274 | ); 275 | 276 | return new TransactionInstruction({ 277 | keys, 278 | programId: swapProgramId, 279 | data, 280 | }); 281 | }; 282 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { HashRouter, Route } from "react-router-dom"; 2 | import React from "react"; 3 | import { ExchangeView } from "./components/exchange"; 4 | 5 | export function Routes() { 6 | // TODO: add simple view for sharing ... 7 | return ( 8 | <> 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === "localhost" || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === "[::1]" || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 32 | if (publicUrl.origin !== window.location.origin) { 33 | // Our service worker won't work if PUBLIC_URL is on a different origin 34 | // from what our page is served on. This might happen if a CDN is used to 35 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 36 | return; 37 | } 38 | 39 | window.addEventListener("load", () => { 40 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 41 | 42 | if (isLocalhost) { 43 | // This is running on localhost. Let's check if a service worker still exists or not. 44 | checkValidServiceWorker(swUrl, config); 45 | 46 | // Add some additional logging to localhost, pointing developers to the 47 | // service worker/PWA documentation. 48 | navigator.serviceWorker.ready.then(() => { 49 | console.log( 50 | "This web app is being served cache-first by a service " + 51 | "worker. To learn more, visit https://bit.ly/CRA-PWA" 52 | ); 53 | }); 54 | } else { 55 | // Is not localhost. Just register service worker 56 | registerValidSW(swUrl, config); 57 | } 58 | }); 59 | } 60 | } 61 | 62 | function registerValidSW(swUrl: string, config?: Config) { 63 | navigator.serviceWorker 64 | .register(swUrl) 65 | .then((registration) => { 66 | registration.onupdatefound = () => { 67 | const installingWorker = registration.installing; 68 | if (installingWorker == null) { 69 | return; 70 | } 71 | installingWorker.onstatechange = () => { 72 | if (installingWorker.state === "installed") { 73 | if (navigator.serviceWorker.controller) { 74 | // At this point, the updated precached content has been fetched, 75 | // but the previous service worker will still serve the older 76 | // content until all client tabs are closed. 77 | console.log( 78 | "New content is available and will be used when all " + 79 | "tabs for this page are closed. See https://bit.ly/CRA-PWA." 80 | ); 81 | 82 | // Execute callback 83 | if (config && config.onUpdate) { 84 | config.onUpdate(registration); 85 | } 86 | } else { 87 | // At this point, everything has been precached. 88 | // It's the perfect time to display a 89 | // "Content is cached for offline use." message. 90 | console.log("Content is cached for offline use."); 91 | 92 | // Execute callback 93 | if (config && config.onSuccess) { 94 | config.onSuccess(registration); 95 | } 96 | } 97 | } 98 | }; 99 | }; 100 | }) 101 | .catch((error) => { 102 | console.error("Error during service worker registration:", error); 103 | }); 104 | } 105 | 106 | function checkValidServiceWorker(swUrl: string, config?: Config) { 107 | // Check if the service worker can be found. If it can't reload the page. 108 | fetch(swUrl, { 109 | headers: { "Service-Worker": "script" }, 110 | }) 111 | .then((response) => { 112 | // Ensure service worker exists, and that we really are getting a JS file. 113 | const contentType = response.headers.get("content-type"); 114 | if ( 115 | response.status === 404 || 116 | (contentType != null && contentType.indexOf("javascript") === -1) 117 | ) { 118 | // No service worker found. Probably a different app. Reload the page. 119 | navigator.serviceWorker.ready.then((registration) => { 120 | registration.unregister().then(() => { 121 | window.location.reload(); 122 | }); 123 | }); 124 | } else { 125 | // Service worker found. Proceed as normal. 126 | registerValidSW(swUrl, config); 127 | } 128 | }) 129 | .catch(() => { 130 | console.log( 131 | "No internet connection found. App is running in offline mode." 132 | ); 133 | }); 134 | } 135 | 136 | export function unregister() { 137 | if ("serviceWorker" in navigator) { 138 | navigator.serviceWorker.ready 139 | .then((registration) => { 140 | registration.unregister(); 141 | }) 142 | .catch((error) => { 143 | console.error(error.message); 144 | }); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom/extend-expect"; 6 | -------------------------------------------------------------------------------- /src/sol-wallet-adapter.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@project-serum/sol-wallet-adapter" { 2 | const magic: any; 3 | export = magic; 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/accounts.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext, useEffect, useState } from "react"; 2 | import { useConnection } from "./connection"; 3 | import { useWallet } from "./wallet"; 4 | import { AccountInfo, Connection, PublicKey } from "@solana/web3.js"; 5 | import { programIds, SWAP_HOST_FEE_ADDRESS, WRAPPED_SOL_MINT } from "./ids"; 6 | import { AccountLayout, u64, MintInfo, MintLayout } from "@solana/spl-token"; 7 | import { usePools } from "./pools"; 8 | import { TokenAccount, PoolInfo } from "./../models"; 9 | import { notify } from "./notifications"; 10 | 11 | const AccountsContext = React.createContext(null); 12 | 13 | class AccountUpdateEvent extends Event { 14 | static type = "AccountUpdate"; 15 | id: string; 16 | constructor(id: string) { 17 | super(AccountUpdateEvent.type); 18 | this.id = id; 19 | } 20 | } 21 | 22 | class EventEmitter extends EventTarget { 23 | raiseAccountUpdated(id: string) { 24 | this.dispatchEvent(new AccountUpdateEvent(id)); 25 | } 26 | } 27 | 28 | const accountEmitter = new EventEmitter(); 29 | 30 | const mintCache = new Map>(); 31 | const pendingAccountCalls = new Map>(); 32 | const accountsCache = new Map(); 33 | 34 | const getAccountInfo = async (connection: Connection, pubKey: PublicKey) => { 35 | const info = await connection.getAccountInfo(pubKey); 36 | if (info === null) { 37 | throw new Error("Failed to find mint account"); 38 | } 39 | 40 | const buffer = Buffer.from(info.data); 41 | 42 | const data = deserializeAccount(buffer); 43 | 44 | const details = { 45 | pubkey: pubKey, 46 | account: { 47 | ...info, 48 | }, 49 | info: data, 50 | } as TokenAccount; 51 | 52 | return details; 53 | }; 54 | 55 | const getMintInfo = async (connection: Connection, pubKey: PublicKey) => { 56 | const info = await connection.getAccountInfo(pubKey); 57 | if (info === null) { 58 | throw new Error("Failed to find mint account"); 59 | } 60 | 61 | const data = Buffer.from(info.data); 62 | 63 | return deserializeMint(data); 64 | }; 65 | 66 | export const cache = { 67 | getAccount: async (connection: Connection, pubKey: string | PublicKey) => { 68 | let id: PublicKey; 69 | if (typeof pubKey === "string") { 70 | id = new PublicKey(pubKey); 71 | } else { 72 | id = pubKey; 73 | } 74 | 75 | const address = id.toBase58(); 76 | 77 | let account = accountsCache.get(address); 78 | if (account) { 79 | return account; 80 | } 81 | 82 | let query = pendingAccountCalls.get(address); 83 | if (query) { 84 | return query; 85 | } 86 | 87 | query = getAccountInfo(connection, id).then((data) => { 88 | pendingAccountCalls.delete(address); 89 | accountsCache.set(address, data); 90 | return data; 91 | }) as Promise; 92 | pendingAccountCalls.set(address, query as any); 93 | 94 | return query; 95 | }, 96 | getMint: async (connection: Connection, pubKey: string | PublicKey) => { 97 | let id: PublicKey; 98 | if (typeof pubKey === "string") { 99 | id = new PublicKey(pubKey); 100 | } else { 101 | id = pubKey; 102 | } 103 | 104 | let mint = mintCache.get(id.toBase58()); 105 | if (mint) { 106 | return mint; 107 | } 108 | 109 | let query = getMintInfo(connection, id); 110 | 111 | mintCache.set(id.toBase58(), query as any); 112 | 113 | return query; 114 | }, 115 | }; 116 | 117 | export const getCachedAccount = ( 118 | predicate: (account: TokenAccount) => boolean 119 | ) => { 120 | for (const account of accountsCache.values()) { 121 | if (predicate(account)) { 122 | return account as TokenAccount; 123 | } 124 | } 125 | }; 126 | 127 | function wrapNativeAccount( 128 | pubkey: PublicKey, 129 | account?: AccountInfo 130 | ): TokenAccount | undefined { 131 | if (!account) { 132 | return undefined; 133 | } 134 | 135 | return { 136 | pubkey: pubkey, 137 | account, 138 | info: { 139 | mint: WRAPPED_SOL_MINT, 140 | owner: pubkey, 141 | amount: new u64(account.lamports), 142 | delegate: null, 143 | delegatedAmount: new u64(0), 144 | isInitialized: true, 145 | isFrozen: false, 146 | isNative: true, 147 | rentExemptReserve: null, 148 | closeAuthority: null, 149 | }, 150 | }; 151 | } 152 | 153 | const UseNativeAccount = () => { 154 | const connection = useConnection(); 155 | const { wallet } = useWallet(); 156 | 157 | const [nativeAccount, setNativeAccount] = useState>(); 158 | useEffect(() => { 159 | if (!connection || !wallet?.publicKey) { 160 | return; 161 | } 162 | 163 | connection.getAccountInfo(wallet.publicKey).then((acc) => { 164 | if (acc) { 165 | setNativeAccount(acc); 166 | } 167 | }); 168 | connection.onAccountChange(wallet.publicKey, (acc) => { 169 | if (acc) { 170 | setNativeAccount(acc); 171 | } 172 | }); 173 | }, [setNativeAccount, wallet, wallet.publicKey, connection]); 174 | 175 | return { nativeAccount }; 176 | }; 177 | 178 | const PRECACHED_OWNERS = new Set(); 179 | const precacheUserTokenAccounts = async ( 180 | connection: Connection, 181 | owner?: PublicKey 182 | ) => { 183 | if (!owner) { 184 | return; 185 | } 186 | 187 | // used for filtering account updates over websocket 188 | PRECACHED_OWNERS.add(owner.toBase58()); 189 | 190 | // user accounts are update via ws subscription 191 | const accounts = await connection.getTokenAccountsByOwner(owner, { 192 | programId: programIds().token, 193 | }); 194 | accounts.value 195 | .map((info) => { 196 | const data = deserializeAccount(info.account.data); 197 | // need to query for mint to get decimals 198 | 199 | // TODO: move to web3.js for decoding on the client side... maybe with callback 200 | const details = { 201 | pubkey: info.pubkey, 202 | account: { 203 | ...info.account, 204 | }, 205 | info: data, 206 | } as TokenAccount; 207 | 208 | return details; 209 | }) 210 | .forEach((acc) => { 211 | accountsCache.set(acc.pubkey.toBase58(), acc); 212 | }); 213 | }; 214 | 215 | export function AccountsProvider({ children = null as any }) { 216 | const connection = useConnection(); 217 | const { wallet, connected } = useWallet(); 218 | const [tokenAccounts, setTokenAccounts] = useState([]); 219 | const [userAccounts, setUserAccounts] = useState([]); 220 | const { nativeAccount } = UseNativeAccount(); 221 | const { pools } = usePools(); 222 | 223 | const selectUserAccounts = useCallback(() => { 224 | return [...accountsCache.values()].filter( 225 | (a) => a.info.owner.toBase58() === wallet.publicKey.toBase58() 226 | ); 227 | }, [wallet]); 228 | 229 | useEffect(() => { 230 | setUserAccounts( 231 | [ 232 | wrapNativeAccount(wallet.publicKey, nativeAccount), 233 | ...tokenAccounts, 234 | ].filter((a) => a !== undefined) as TokenAccount[] 235 | ); 236 | }, [nativeAccount, wallet, tokenAccounts]); 237 | 238 | useEffect(() => { 239 | if (!connection || !wallet || !wallet.publicKey) { 240 | setTokenAccounts([]); 241 | } else { 242 | // cache host accounts to avoid query during swap 243 | precacheUserTokenAccounts(connection, SWAP_HOST_FEE_ADDRESS); 244 | 245 | precacheUserTokenAccounts(connection, wallet.publicKey).then(() => { 246 | setTokenAccounts(selectUserAccounts()); 247 | }); 248 | 249 | // This can return different types of accounts: token-account, mint, multisig 250 | // TODO: web3.js expose ability to filter. discuss filter syntax 251 | const tokenSubID = connection.onProgramAccountChange( 252 | programIds().token, 253 | (info) => { 254 | // TODO: fix type in web3.js 255 | const id = (info.accountId as unknown) as string; 256 | // TODO: do we need a better way to identify layout (maybe a enum identifing type?) 257 | if (info.accountInfo.data.length === AccountLayout.span) { 258 | const data = deserializeAccount(info.accountInfo.data); 259 | // TODO: move to web3.js for decoding on the client side... maybe with callback 260 | const details = { 261 | pubkey: new PublicKey((info.accountId as unknown) as string), 262 | account: { 263 | ...info.accountInfo, 264 | }, 265 | info: data, 266 | } as TokenAccount; 267 | 268 | if ( 269 | PRECACHED_OWNERS.has(details.info.owner.toBase58()) || 270 | accountsCache.has(id) 271 | ) { 272 | accountsCache.set(id, details); 273 | setTokenAccounts(selectUserAccounts()); 274 | accountEmitter.raiseAccountUpdated(id); 275 | } 276 | } else if (info.accountInfo.data.length === MintLayout.span) { 277 | if (mintCache.has(id)) { 278 | const data = Buffer.from(info.accountInfo.data); 279 | const mint = deserializeMint(data); 280 | mintCache.set(id, new Promise((resolve) => resolve(mint))); 281 | accountEmitter.raiseAccountUpdated(id); 282 | } 283 | 284 | accountEmitter.raiseAccountUpdated(id); 285 | } 286 | }, 287 | "singleGossip" 288 | ); 289 | 290 | return () => { 291 | connection.removeProgramAccountChangeListener(tokenSubID); 292 | }; 293 | } 294 | }, [connection, connected, wallet?.publicKey]); 295 | 296 | return ( 297 | 304 | {children} 305 | 306 | ); 307 | } 308 | 309 | export function useNativeAccount() { 310 | const context = useContext(AccountsContext); 311 | return { 312 | account: context.nativeAccount as AccountInfo, 313 | }; 314 | } 315 | 316 | export function useMint(id?: string) { 317 | const connection = useConnection(); 318 | const [mint, setMint] = useState(); 319 | 320 | useEffect(() => { 321 | if (!id) { 322 | return; 323 | } 324 | 325 | cache 326 | .getMint(connection, id) 327 | .then(setMint) 328 | .catch((err) => 329 | notify({ 330 | message: err.message, 331 | type: "error", 332 | }) 333 | ); 334 | const onAccountEvent = (e: Event) => { 335 | const event = e as AccountUpdateEvent; 336 | if (event.id === id) { 337 | cache.getMint(connection, id).then(setMint); 338 | } 339 | }; 340 | 341 | accountEmitter.addEventListener(AccountUpdateEvent.type, onAccountEvent); 342 | return () => { 343 | accountEmitter.removeEventListener( 344 | AccountUpdateEvent.type, 345 | onAccountEvent 346 | ); 347 | }; 348 | }, [connection, id]); 349 | 350 | return mint; 351 | } 352 | 353 | export function useUserAccounts() { 354 | const context = useContext(AccountsContext); 355 | return { 356 | userAccounts: context.userAccounts as TokenAccount[], 357 | }; 358 | } 359 | 360 | export function useAccount(pubKey?: PublicKey) { 361 | const connection = useConnection(); 362 | const [account, setAccount] = useState(); 363 | 364 | const key = pubKey?.toBase58(); 365 | useEffect(() => { 366 | const query = async () => { 367 | try { 368 | if (!key) { 369 | return; 370 | } 371 | 372 | const acc = await cache.getAccount(connection, key).catch((err) => 373 | notify({ 374 | message: err.message, 375 | type: "error", 376 | }) 377 | ); 378 | if (acc) { 379 | setAccount(acc); 380 | } 381 | } catch (err) { 382 | console.error(err); 383 | } 384 | }; 385 | 386 | query(); 387 | 388 | const onAccountEvent = (e: Event) => { 389 | const event = e as AccountUpdateEvent; 390 | if (event.id === key) { 391 | query(); 392 | } 393 | }; 394 | 395 | accountEmitter.addEventListener(AccountUpdateEvent.type, onAccountEvent); 396 | return () => { 397 | accountEmitter.removeEventListener( 398 | AccountUpdateEvent.type, 399 | onAccountEvent 400 | ); 401 | }; 402 | }, [connection, key]); 403 | 404 | return account; 405 | } 406 | 407 | export function useCachedPool() { 408 | const context = useContext(AccountsContext); 409 | return { 410 | pools: context.pools as PoolInfo[], 411 | }; 412 | } 413 | 414 | export const useSelectedAccount = (account: string) => { 415 | const { userAccounts } = useUserAccounts(); 416 | const index = userAccounts.findIndex( 417 | (acc) => acc.pubkey.toBase58() === account 418 | ); 419 | 420 | if (index !== -1) { 421 | return userAccounts[index]; 422 | } 423 | 424 | return; 425 | }; 426 | 427 | export const useAccountByMint = (mint: string) => { 428 | const { userAccounts } = useUserAccounts(); 429 | const index = userAccounts.findIndex( 430 | (acc) => acc.info.mint.toBase58() === mint 431 | ); 432 | 433 | if (index !== -1) { 434 | return userAccounts[index]; 435 | } 436 | 437 | return; 438 | }; 439 | 440 | // TODO: expose in spl package 441 | const deserializeAccount = (data: Buffer) => { 442 | const accountInfo = AccountLayout.decode(data); 443 | accountInfo.mint = new PublicKey(accountInfo.mint); 444 | accountInfo.owner = new PublicKey(accountInfo.owner); 445 | accountInfo.amount = u64.fromBuffer(accountInfo.amount); 446 | 447 | if (accountInfo.delegateOption === 0) { 448 | accountInfo.delegate = null; 449 | accountInfo.delegatedAmount = new u64(0); 450 | } else { 451 | accountInfo.delegate = new PublicKey(accountInfo.delegate); 452 | accountInfo.delegatedAmount = u64.fromBuffer(accountInfo.delegatedAmount); 453 | } 454 | 455 | accountInfo.isInitialized = accountInfo.state !== 0; 456 | accountInfo.isFrozen = accountInfo.state === 2; 457 | 458 | if (accountInfo.isNativeOption === 1) { 459 | accountInfo.rentExemptReserve = u64.fromBuffer(accountInfo.isNative); 460 | accountInfo.isNative = true; 461 | } else { 462 | accountInfo.rentExemptReserve = null; 463 | accountInfo.isNative = false; 464 | } 465 | 466 | if (accountInfo.closeAuthorityOption === 0) { 467 | accountInfo.closeAuthority = null; 468 | } else { 469 | accountInfo.closeAuthority = new PublicKey(accountInfo.closeAuthority); 470 | } 471 | 472 | return accountInfo; 473 | }; 474 | 475 | // TODO: expose in spl package 476 | const deserializeMint = (data: Buffer) => { 477 | if (data.length !== MintLayout.span) { 478 | throw new Error("Not a valid Mint"); 479 | } 480 | 481 | const mintInfo = MintLayout.decode(data); 482 | 483 | if (mintInfo.mintAuthorityOption === 0) { 484 | mintInfo.mintAuthority = null; 485 | } else { 486 | mintInfo.mintAuthority = new PublicKey(mintInfo.mintAuthority); 487 | } 488 | 489 | mintInfo.supply = u64.fromBuffer(mintInfo.supply); 490 | mintInfo.isInitialized = mintInfo.isInitialized !== 0; 491 | 492 | if (mintInfo.freezeAuthorityOption === 0) { 493 | mintInfo.freezeAuthority = null; 494 | } else { 495 | mintInfo.freezeAuthority = new PublicKey(mintInfo.freezeAuthority); 496 | } 497 | 498 | return mintInfo as MintInfo; 499 | }; 500 | -------------------------------------------------------------------------------- /src/utils/connection.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStorageState } from "./utils"; 2 | import { 3 | Account, 4 | clusterApiUrl, 5 | Connection, 6 | Transaction, 7 | TransactionInstruction, 8 | } from "@solana/web3.js"; 9 | import React, { useContext, useEffect, useMemo } from "react"; 10 | import { setProgramIds } from "./ids"; 11 | import { notify } from "./notifications"; 12 | 13 | export type ENV = "mainnet-beta" | "testnet" | "devnet" | "localnet"; 14 | 15 | export const ENDPOINTS = [ 16 | { 17 | name: "mainnet-beta" as ENV, 18 | endpoint: "https://solana-api.projectserum.com/", 19 | }, 20 | { name: "testnet" as ENV, endpoint: clusterApiUrl("testnet") }, 21 | { name: "devnet" as ENV, endpoint: clusterApiUrl("devnet") }, 22 | { name: "localnet" as ENV, endpoint: "http://127.0.0.1:8899" }, 23 | ]; 24 | 25 | const DEFAULT = ENDPOINTS[0].endpoint; 26 | const DEFAULT_SLIPPAGE = 0.25; 27 | 28 | interface ConnectionConfig { 29 | connection: Connection; 30 | sendConnection: Connection; 31 | endpoint: string; 32 | slippage: number; 33 | setSlippage: (val: number) => void; 34 | env: ENV; 35 | setEndpoint: (val: string) => void; 36 | } 37 | 38 | const ConnectionContext = React.createContext({ 39 | endpoint: DEFAULT, 40 | setEndpoint: () => {}, 41 | slippage: DEFAULT_SLIPPAGE, 42 | setSlippage: (val: number) => {}, 43 | connection: new Connection(DEFAULT, "recent"), 44 | sendConnection: new Connection(DEFAULT, "recent"), 45 | env: ENDPOINTS[0].name, 46 | }); 47 | 48 | export function ConnectionProvider({ children = undefined as any }) { 49 | const [endpoint, setEndpoint] = useLocalStorageState( 50 | "connectionEndpts", 51 | ENDPOINTS[0].endpoint 52 | ); 53 | 54 | const [slippage, setSlippage] = useLocalStorageState( 55 | "slippage", 56 | DEFAULT_SLIPPAGE.toString() 57 | ); 58 | 59 | const connection = useMemo(() => new Connection(endpoint, "recent"), [ 60 | endpoint, 61 | ]); 62 | const sendConnection = useMemo(() => new Connection(endpoint, "recent"), [ 63 | endpoint, 64 | ]); 65 | 66 | const env = 67 | ENDPOINTS.find((end) => end.endpoint === endpoint)?.name || 68 | ENDPOINTS[0].name; 69 | 70 | setProgramIds(env); 71 | 72 | // The websocket library solana/web3.js uses closes its websocket connection when the subscription list 73 | // is empty after opening its first time, preventing subsequent subscriptions from receiving responses. 74 | // This is a hack to prevent the list from every getting empty 75 | useEffect(() => { 76 | const id = connection.onAccountChange(new Account().publicKey, () => {}); 77 | return () => { 78 | connection.removeAccountChangeListener(id); 79 | }; 80 | }, [connection]); 81 | 82 | useEffect(() => { 83 | const id = connection.onSlotChange(() => null); 84 | return () => { 85 | connection.removeSlotChangeListener(id); 86 | }; 87 | }, [connection]); 88 | 89 | useEffect(() => { 90 | const id = sendConnection.onAccountChange( 91 | new Account().publicKey, 92 | () => {} 93 | ); 94 | return () => { 95 | sendConnection.removeAccountChangeListener(id); 96 | }; 97 | }, [sendConnection]); 98 | 99 | useEffect(() => { 100 | const id = sendConnection.onSlotChange(() => null); 101 | return () => { 102 | sendConnection.removeSlotChangeListener(id); 103 | }; 104 | }, [sendConnection]); 105 | 106 | return ( 107 | setSlippage(val.toString()), 113 | connection, 114 | sendConnection, 115 | env, 116 | }} 117 | > 118 | {children} 119 | 120 | ); 121 | } 122 | 123 | export function useConnection() { 124 | return useContext(ConnectionContext).connection as Connection; 125 | } 126 | 127 | export function useSendConnection() { 128 | return useContext(ConnectionContext)?.sendConnection; 129 | } 130 | 131 | export function useConnectionConfig() { 132 | const context = useContext(ConnectionContext); 133 | return { 134 | endpoint: context.endpoint, 135 | setEndpoint: context.setEndpoint, 136 | env: context.env, 137 | }; 138 | } 139 | 140 | export function useSlippageConfig() { 141 | const { slippage, setSlippage } = useContext(ConnectionContext); 142 | return { slippage, setSlippage }; 143 | } 144 | 145 | export const sendTransaction = async ( 146 | connection: any, 147 | wallet: any, 148 | instructions: TransactionInstruction[], 149 | signers: Account[], 150 | awaitConfirmation = true 151 | ) => { 152 | let transaction = new Transaction(); 153 | instructions.forEach((instruction) => transaction.add(instruction)); 154 | transaction.recentBlockhash = ( 155 | await connection.getRecentBlockhash("max") 156 | ).blockhash; 157 | transaction.setSigners( 158 | // fee payied by the wallet owner 159 | wallet.publicKey, 160 | ...signers.map((s) => s.publicKey) 161 | ); 162 | if (signers.length > 0) { 163 | transaction.partialSign(...signers); 164 | } 165 | transaction = await wallet.signTransaction(transaction); 166 | const rawTransaction = transaction.serialize(); 167 | let options = { 168 | skipPreflight: true, 169 | commitment: "singleGossip", 170 | }; 171 | 172 | const txid = await connection.sendRawTransaction(rawTransaction, options); 173 | 174 | if (awaitConfirmation) { 175 | const status = ( 176 | await connection.confirmTransaction(txid, options && options.commitment) 177 | ).value; 178 | 179 | if (status.err) { 180 | // TODO: notify 181 | notify({ 182 | message: "Transaction failed...", 183 | description: `${txid}`, 184 | type: "error", 185 | }); 186 | 187 | throw new Error( 188 | `Raw transaction ${txid} failed (${JSON.stringify(status)})` 189 | ); 190 | } 191 | } 192 | 193 | return txid; 194 | }; 195 | -------------------------------------------------------------------------------- /src/utils/currencyPair.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext, useEffect, useState } from "react"; 2 | import { calculateDependentAmount, usePoolForBasket } from "./pools"; 3 | import { useMint, useAccountByMint } from "./accounts"; 4 | import { MintInfo } from "@solana/spl-token"; 5 | import { useConnection } from "./connection"; 6 | import { TokenAccount } from "../models"; 7 | import { convert } from "./utils"; 8 | 9 | export interface CurrencyContextState { 10 | mintAddress: string; 11 | account?: TokenAccount; 12 | mint?: MintInfo; 13 | amount: string; 14 | setAmount: (val: string) => void; 15 | setMint: (mintAddress: string) => void; 16 | convertAmount: () => number; 17 | sufficientBalance: () => boolean; 18 | } 19 | 20 | export interface CurrencyPairContextState { 21 | A: CurrencyContextState; 22 | B: CurrencyContextState; 23 | setLastTypedAccount: (mintAddress: string) => void; 24 | } 25 | 26 | const CurrencyPairContext = React.createContext( 27 | null 28 | ); 29 | 30 | export function CurrencyPairProvider({ children = null as any }) { 31 | const connection = useConnection(); 32 | const [amountA, setAmountA] = useState(""); 33 | const [amountB, setAmountB] = useState(""); 34 | const [mintAddressA, setMintAddressA] = useState(""); 35 | const [mintAddressB, setMintAddressB] = useState(""); 36 | const [lastTypedAccount, setLastTypedAccount] = useState(""); 37 | const accountA = useAccountByMint(mintAddressA); 38 | const accountB = useAccountByMint(mintAddressB); 39 | const mintA = useMint(mintAddressA); 40 | const mintB = useMint(mintAddressB); 41 | const pool = usePoolForBasket([mintAddressA, mintAddressB]); 42 | 43 | const calculateDependent = useCallback(async () => { 44 | if (pool && mintAddressA && mintAddressB) { 45 | let setDependent; 46 | let amount; 47 | let independent; 48 | if (lastTypedAccount === mintAddressA) { 49 | independent = mintAddressA; 50 | setDependent = setAmountB; 51 | amount = parseFloat(amountA); 52 | } else { 53 | independent = mintAddressB; 54 | setDependent = setAmountA; 55 | amount = parseFloat(amountB); 56 | } 57 | 58 | const result = await calculateDependentAmount( 59 | connection, 60 | independent, 61 | amount, 62 | pool 63 | ); 64 | if (result !== undefined && Number.isFinite(result)) { 65 | setDependent(result.toFixed(2)); 66 | } else { 67 | setDependent(""); 68 | } 69 | } 70 | }, [ 71 | pool, 72 | mintAddressA, 73 | mintAddressB, 74 | setAmountA, 75 | setAmountB, 76 | amountA, 77 | amountB, 78 | connection, 79 | lastTypedAccount, 80 | ]); 81 | 82 | useEffect(() => { 83 | calculateDependent(); 84 | }, [amountB, amountA, lastTypedAccount, calculateDependent]); 85 | 86 | const convertAmount = (amount: string, mint?: MintInfo) => { 87 | return parseFloat(amount) * Math.pow(10, mint?.decimals || 0); 88 | }; 89 | 90 | return ( 91 | convertAmount(amountA, mintA), 101 | sufficientBalance: () => 102 | accountA !== undefined && 103 | convert(accountA, mintA) >= parseFloat(amountA), 104 | }, 105 | B: { 106 | mintAddress: mintAddressB, 107 | account: accountB, 108 | mint: mintB, 109 | amount: amountB, 110 | setAmount: setAmountB, 111 | setMint: setMintAddressB, 112 | convertAmount: () => convertAmount(amountB, mintB), 113 | sufficientBalance: () => 114 | accountB !== undefined && 115 | convert(accountB, mintB) >= parseFloat(amountB), 116 | }, 117 | setLastTypedAccount, 118 | }} 119 | > 120 | {children} 121 | 122 | ); 123 | } 124 | 125 | export const useCurrencyPairState = () => { 126 | const context = useContext(CurrencyPairContext); 127 | return context as CurrencyPairContextState; 128 | }; 129 | -------------------------------------------------------------------------------- /src/utils/ids.tsx: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | 3 | export const WRAPPED_SOL_MINT = new PublicKey( 4 | "So11111111111111111111111111111111111111112" 5 | ); 6 | let TOKEN_PROGRAM_ID = new PublicKey( 7 | "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" 8 | ); 9 | 10 | let SWAP_PROGRAM_ID: PublicKey; 11 | let SWAP_PROGRAM_LEGACY_IDS: PublicKey[]; 12 | 13 | export const SWAP_HOST_FEE_ADDRESS = process.env.REACT_APP_SWAP_HOST_FEE_ADDRESS 14 | ? new PublicKey(`${process.env.REACT_APP_SWAP_HOST_FEE_ADDRESS}`) 15 | : undefined; 16 | export const SWAP_PROGRAM_OWNER_FEE_ADDRESS = new PublicKey( 17 | "HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN" 18 | ); 19 | 20 | console.debug(`Host address: ${SWAP_HOST_FEE_ADDRESS?.toBase58()}`); 21 | console.debug(`Owner address: ${SWAP_PROGRAM_OWNER_FEE_ADDRESS?.toBase58()}`); 22 | 23 | // legacy pools are used to show users contributions in those pools to allow for withdrawals of funds 24 | export const PROGRAM_IDS = [ 25 | { 26 | name: "mainnet-beta", 27 | swap: () => ({ 28 | current: new PublicKey("9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL"), 29 | legacy: [], 30 | }), 31 | }, 32 | { 33 | name: "testnet", 34 | swap: () => ({ 35 | current: new PublicKey("2n2dsFSgmPcZ8jkmBZLGUM2nzuFqcBGQ3JEEj6RJJcEg"), 36 | legacy: [ 37 | new PublicKey("9tdctNJuFsYZ6VrKfKEuwwbPp4SFdFw3jYBZU8QUtzeX"), 38 | new PublicKey("CrRvVBS4Hmj47TPU3cMukurpmCUYUrdHYxTQBxncBGqw"), 39 | ], 40 | }), 41 | }, 42 | { 43 | name: "devnet", 44 | swap: () => ({ 45 | current: new PublicKey("BSfTAcBdqmvX5iE2PW88WFNNp2DHhLUaBKk5WrnxVkcJ"), 46 | legacy: [ 47 | new PublicKey("H1E1G7eD5Rrcy43xvDxXCsjkRggz7MWNMLGJ8YNzJ8PM"), 48 | new PublicKey("CMoteLxSPVPoc7Drcggf3QPg3ue8WPpxYyZTg77UGqHo"), 49 | new PublicKey("EEuPz4iZA5reBUeZj6x1VzoiHfYeHMppSCnHZasRFhYo"), 50 | ], 51 | }), 52 | }, 53 | { 54 | name: "localnet", 55 | swap: () => ({ 56 | current: new PublicKey("5rdpyt5iGfr68qt28hkefcFyF4WtyhTwqKDmHSBG8GZx"), 57 | legacy: [], 58 | }), 59 | }, 60 | ]; 61 | 62 | export const setProgramIds = (envName: string) => { 63 | let instance = PROGRAM_IDS.find((env) => env.name === envName); 64 | if (!instance) { 65 | return; 66 | } 67 | 68 | let swap = instance.swap(); 69 | 70 | SWAP_PROGRAM_ID = swap.current; 71 | SWAP_PROGRAM_LEGACY_IDS = swap.legacy; 72 | }; 73 | 74 | export const programIds = () => { 75 | return { 76 | token: TOKEN_PROGRAM_ID, 77 | swap: SWAP_PROGRAM_ID, 78 | swap_legacy: SWAP_PROGRAM_LEGACY_IDS, 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /src/utils/notifications.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { notification } from "antd"; 3 | // import Link from '../components/Link'; 4 | 5 | export function notify({ 6 | message = "", 7 | description = undefined as any, 8 | txid = "", 9 | type = "info", 10 | placement = "bottomLeft", 11 | }) { 12 | if (txid) { 13 | // 18 | // View transaction {txid.slice(0, 8)}...{txid.slice(txid.length - 8)} 19 | // 20 | 21 | description = <>; 22 | } 23 | (notification as any)[type]({ 24 | message: {message}, 25 | description: ( 26 | {description} 27 | ), 28 | placement, 29 | style: { 30 | backgroundColor: "white", 31 | }, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/pools.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Account, 3 | Connection, 4 | PublicKey, 5 | SystemProgram, 6 | TransactionInstruction, 7 | } from "@solana/web3.js"; 8 | import { sendTransaction, useConnection } from "./connection"; 9 | import { useEffect, useState } from "react"; 10 | import { Token, MintLayout, AccountLayout } from "@solana/spl-token"; 11 | import { notify } from "./notifications"; 12 | import { 13 | cache, 14 | getCachedAccount, 15 | useUserAccounts, 16 | useCachedPool, 17 | } from "./accounts"; 18 | import { 19 | programIds, 20 | SWAP_HOST_FEE_ADDRESS, 21 | SWAP_PROGRAM_OWNER_FEE_ADDRESS, 22 | WRAPPED_SOL_MINT, 23 | } from "./ids"; 24 | import { 25 | LiquidityComponent, 26 | PoolInfo, 27 | TokenAccount, 28 | createInitSwapInstruction, 29 | TokenSwapLayout, 30 | depositInstruction, 31 | withdrawInstruction, 32 | TokenSwapLayoutLegacyV0, 33 | swapInstruction, 34 | PoolConfig, 35 | } from "./../models"; 36 | 37 | const LIQUIDITY_TOKEN_PRECISION = 8; 38 | 39 | export const removeLiquidity = async ( 40 | connection: Connection, 41 | wallet: any, 42 | liquidityAmount: number, 43 | account: TokenAccount, 44 | pool?: PoolInfo 45 | ) => { 46 | if (!pool) { 47 | return; 48 | } 49 | 50 | notify({ 51 | message: "Removing Liquidity...", 52 | description: "Please review transactions to approve.", 53 | type: "warn", 54 | }); 55 | 56 | // TODO get min amounts based on total supply and liquidity 57 | const minAmount0 = 0; 58 | const minAmount1 = 0; 59 | 60 | const poolMint = await cache.getMint(connection, pool.pubkeys.mint); 61 | const accountA = await cache.getAccount( 62 | connection, 63 | pool.pubkeys.holdingAccounts[0] 64 | ); 65 | const accountB = await cache.getAccount( 66 | connection, 67 | pool.pubkeys.holdingAccounts[1] 68 | ); 69 | if (!poolMint.mintAuthority) { 70 | throw new Error("Mint doesnt have authority"); 71 | } 72 | const authority = poolMint.mintAuthority; 73 | 74 | const signers: Account[] = []; 75 | const instructions: TransactionInstruction[] = []; 76 | const cleanupInstructions: TransactionInstruction[] = []; 77 | 78 | const accountRentExempt = await connection.getMinimumBalanceForRentExemption( 79 | AccountLayout.span 80 | ); 81 | 82 | // TODO: check if one of to accounts needs to be native sol ... if yes unwrap it ... 83 | const toAccounts: PublicKey[] = [ 84 | await findOrCreateAccountByMint( 85 | wallet.publicKey, 86 | wallet.publicKey, 87 | instructions, 88 | cleanupInstructions, 89 | accountRentExempt, 90 | accountA.info.mint, 91 | signers 92 | ), 93 | await findOrCreateAccountByMint( 94 | wallet.publicKey, 95 | wallet.publicKey, 96 | instructions, 97 | cleanupInstructions, 98 | accountRentExempt, 99 | accountB.info.mint, 100 | signers 101 | ), 102 | ]; 103 | 104 | instructions.push( 105 | Token.createApproveInstruction( 106 | programIds().token, 107 | account.pubkey, 108 | authority, 109 | wallet.publicKey, 110 | [], 111 | liquidityAmount 112 | ) 113 | ); 114 | 115 | // withdraw 116 | instructions.push( 117 | withdrawInstruction( 118 | pool.pubkeys.account, 119 | authority, 120 | pool.pubkeys.mint, 121 | pool.pubkeys.feeAccount, 122 | account.pubkey, 123 | pool.pubkeys.holdingAccounts[0], 124 | pool.pubkeys.holdingAccounts[1], 125 | toAccounts[0], 126 | toAccounts[1], 127 | pool.pubkeys.program, 128 | programIds().token, 129 | liquidityAmount, 130 | minAmount0, 131 | minAmount1 132 | ) 133 | ); 134 | 135 | let tx = await sendTransaction( 136 | connection, 137 | wallet, 138 | instructions.concat(cleanupInstructions), 139 | signers 140 | ); 141 | 142 | notify({ 143 | message: "Liquidity Returned. Thank you for your support.", 144 | type: "success", 145 | description: `Transaction - ${tx}`, 146 | }); 147 | }; 148 | 149 | export const swap = async ( 150 | connection: Connection, 151 | wallet: any, 152 | components: LiquidityComponent[], 153 | SLIPPAGE: number, 154 | pool?: PoolInfo 155 | ) => { 156 | if (!pool || !components[0].account) { 157 | notify({ 158 | type: "error", 159 | message: `Pool doesn't exsist.`, 160 | description: `Swap trade cancelled`, 161 | }); 162 | return; 163 | } 164 | 165 | // Uniswap whitepaper: https://uniswap.org/whitepaper.pdf 166 | // see: https://uniswap.org/docs/v2/advanced-topics/pricing/ 167 | // as well as native uniswap v2 oracle: https://uniswap.org/docs/v2/core-concepts/oracles/ 168 | const amountIn = components[0].amount; // these two should include slippage 169 | const minAmountOut = components[1].amount * (1 - SLIPPAGE); 170 | const holdingA = 171 | pool.pubkeys.holdingMints[0].toBase58() === 172 | components[0].account.info.mint.toBase58() 173 | ? pool.pubkeys.holdingAccounts[0] 174 | : pool.pubkeys.holdingAccounts[1]; 175 | const holdingB = 176 | holdingA === pool.pubkeys.holdingAccounts[0] 177 | ? pool.pubkeys.holdingAccounts[1] 178 | : pool.pubkeys.holdingAccounts[0]; 179 | 180 | const poolMint = await cache.getMint(connection, pool.pubkeys.mint); 181 | if (!poolMint.mintAuthority || !pool.pubkeys.feeAccount) { 182 | throw new Error("Mint doesnt have authority"); 183 | } 184 | const authority = poolMint.mintAuthority; 185 | 186 | const instructions: TransactionInstruction[] = []; 187 | const cleanupInstructions: TransactionInstruction[] = []; 188 | const signers: Account[] = []; 189 | 190 | const accountRentExempt = await connection.getMinimumBalanceForRentExemption( 191 | AccountLayout.span 192 | ); 193 | 194 | const fromAccount = getWrappedAccount( 195 | instructions, 196 | cleanupInstructions, 197 | components[0].account, 198 | wallet.publicKey, 199 | amountIn + accountRentExempt, 200 | signers 201 | ); 202 | 203 | let toAccount = findOrCreateAccountByMint( 204 | wallet.publicKey, 205 | wallet.publicKey, 206 | instructions, 207 | cleanupInstructions, 208 | accountRentExempt, 209 | new PublicKey(components[1].mintAddress), 210 | signers 211 | ); 212 | 213 | // create approval for transfer transactions 214 | instructions.push( 215 | Token.createApproveInstruction( 216 | programIds().token, 217 | fromAccount, 218 | authority, 219 | wallet.publicKey, 220 | [], 221 | amountIn 222 | ) 223 | ); 224 | 225 | let hostFeeAccount = SWAP_HOST_FEE_ADDRESS 226 | ? findOrCreateAccountByMint( 227 | wallet.publicKey, 228 | SWAP_HOST_FEE_ADDRESS, 229 | instructions, 230 | cleanupInstructions, 231 | accountRentExempt, 232 | pool.pubkeys.mint, 233 | signers 234 | ) 235 | : undefined; 236 | 237 | // swap 238 | instructions.push( 239 | swapInstruction( 240 | pool.pubkeys.account, 241 | authority, 242 | fromAccount, 243 | holdingA, 244 | holdingB, 245 | toAccount, 246 | pool.pubkeys.mint, 247 | pool.pubkeys.feeAccount, 248 | pool.pubkeys.program, 249 | programIds().token, 250 | amountIn, 251 | minAmountOut, 252 | hostFeeAccount 253 | ) 254 | ); 255 | 256 | let tx = await sendTransaction( 257 | connection, 258 | wallet, 259 | instructions.concat(cleanupInstructions), 260 | signers 261 | ); 262 | 263 | notify({ 264 | message: "Trade executed.", 265 | type: "success", 266 | description: `Transaction - ${tx}`, 267 | }); 268 | }; 269 | 270 | export const addLiquidity = async ( 271 | connection: Connection, 272 | wallet: any, 273 | components: LiquidityComponent[], 274 | slippage: number, 275 | pool?: PoolInfo, 276 | options?: PoolConfig 277 | ) => { 278 | if (!pool) { 279 | if (!options) { 280 | throw new Error("Options are required to create new pool."); 281 | } 282 | 283 | await _addLiquidityNewPool(wallet, connection, components, options); 284 | } else { 285 | await _addLiquidityExistingPool( 286 | pool, 287 | components, 288 | connection, 289 | wallet, 290 | slippage 291 | ); 292 | } 293 | }; 294 | 295 | const getHoldings = (connection: Connection, accounts: string[]) => { 296 | return accounts.map((acc) => 297 | cache.getAccount(connection, new PublicKey(acc)) 298 | ); 299 | }; 300 | 301 | const toPoolInfo = (item: any, program: PublicKey, toMerge?: PoolInfo) => { 302 | const mint = new PublicKey(item.data.tokenPool); 303 | return { 304 | pubkeys: { 305 | account: item.pubkey, 306 | program: program, 307 | mint, 308 | holdingMints: [] as PublicKey[], 309 | holdingAccounts: [item.data.tokenAccountA, item.data.tokenAccountB].map( 310 | (a) => new PublicKey(a) 311 | ), 312 | }, 313 | legacy: false, 314 | raw: item, 315 | } as PoolInfo; 316 | }; 317 | 318 | export const usePools = () => { 319 | const connection = useConnection(); 320 | const [pools, setPools] = useState([]); 321 | 322 | // initial query 323 | useEffect(() => { 324 | setPools([]); 325 | 326 | const queryPools = async (swapId: PublicKey, isLegacy = false) => { 327 | let poolsArray: PoolInfo[] = []; 328 | (await connection.getProgramAccounts(swapId)) 329 | .filter( 330 | (item) => 331 | item.account.data.length === TokenSwapLayout.span || 332 | item.account.data.length === TokenSwapLayoutLegacyV0.span 333 | ) 334 | .map((item) => { 335 | let result = { 336 | data: undefined as any, 337 | account: item.account, 338 | pubkey: item.pubkey, 339 | init: async () => {}, 340 | }; 341 | 342 | // handling of legacy layout can be removed soon... 343 | if (item.account.data.length === TokenSwapLayoutLegacyV0.span) { 344 | result.data = TokenSwapLayoutLegacyV0.decode(item.account.data); 345 | let pool = toPoolInfo(result, swapId); 346 | pool.legacy = isLegacy; 347 | poolsArray.push(pool as PoolInfo); 348 | 349 | result.init = async () => { 350 | try { 351 | // TODO: this is not great 352 | // Ideally SwapLayout stores hash of all the mints to make finding of pool for a pair easier 353 | const holdings = await Promise.all( 354 | getHoldings(connection, [ 355 | result.data.tokenAccountA, 356 | result.data.tokenAccountB, 357 | ]) 358 | ); 359 | 360 | pool.pubkeys.holdingMints = [ 361 | holdings[0].info.mint, 362 | holdings[1].info.mint, 363 | ] as PublicKey[]; 364 | } catch (err) { 365 | console.log(err); 366 | } 367 | }; 368 | } else { 369 | result.data = TokenSwapLayout.decode(item.account.data); 370 | let pool = toPoolInfo(result, swapId); 371 | pool.legacy = isLegacy; 372 | pool.pubkeys.feeAccount = new PublicKey(result.data.feeAccount); 373 | pool.pubkeys.holdingMints = [ 374 | new PublicKey(result.data.mintA), 375 | new PublicKey(result.data.mintB), 376 | ] as PublicKey[]; 377 | 378 | poolsArray.push(pool as PoolInfo); 379 | } 380 | 381 | return result; 382 | }); 383 | 384 | return poolsArray; 385 | }; 386 | 387 | Promise.all([ 388 | queryPools(programIds().swap), 389 | ...programIds().swap_legacy.map((leg) => queryPools(leg, true)), 390 | ]).then((all) => { 391 | setPools(all.flat()); 392 | }); 393 | }, [connection]); 394 | 395 | useEffect(() => { 396 | const subID = connection.onProgramAccountChange( 397 | programIds().swap, 398 | async (info) => { 399 | const id = (info.accountId as unknown) as string; 400 | if (info.accountInfo.data.length === TokenSwapLayout.span) { 401 | const account = info.accountInfo; 402 | const updated = { 403 | data: TokenSwapLayout.decode(account.data), 404 | account: account, 405 | pubkey: new PublicKey(id), 406 | }; 407 | 408 | const index = 409 | pools && 410 | pools.findIndex((p) => p.pubkeys.account.toBase58() === id); 411 | if (index && index >= 0 && pools) { 412 | // TODO: check if account is empty? 413 | 414 | const filtered = pools.filter((p, i) => i !== index); 415 | setPools([...filtered, toPoolInfo(updated, programIds().swap)]); 416 | } else { 417 | let pool = toPoolInfo(updated, programIds().swap); 418 | 419 | pool.pubkeys.feeAccount = new PublicKey(updated.data.feeAccount); 420 | pool.pubkeys.holdingMints = [ 421 | new PublicKey(updated.data.mintA), 422 | new PublicKey(updated.data.mintB), 423 | ] as PublicKey[]; 424 | 425 | setPools([...pools, pool]); 426 | } 427 | } 428 | }, 429 | "singleGossip" 430 | ); 431 | 432 | return () => { 433 | connection.removeProgramAccountChangeListener(subID); 434 | }; 435 | }, [connection, pools]); 436 | 437 | return { pools }; 438 | }; 439 | 440 | export const usePoolForBasket = (mints: (string | undefined)[]) => { 441 | const connection = useConnection(); 442 | const { pools } = useCachedPool(); 443 | const [pool, setPool] = useState(); 444 | const sortedMints = [...mints].sort(); 445 | useEffect(() => { 446 | (async () => { 447 | // reset pool during query 448 | setPool(undefined); 449 | 450 | let matchingPool = pools 451 | .filter((p) => !p.legacy) 452 | .filter((p) => 453 | p.pubkeys.holdingMints 454 | .map((a) => a.toBase58()) 455 | .sort() 456 | .every((address, i) => address === sortedMints[i]) 457 | ); 458 | 459 | for (let i = 0; i < matchingPool.length; i++) { 460 | const p = matchingPool[i]; 461 | 462 | const account = await cache.getAccount( 463 | connection, 464 | p.pubkeys.holdingAccounts[0] 465 | ); 466 | 467 | if (!account.info.amount.eqn(0)) { 468 | setPool(p); 469 | return; 470 | } 471 | } 472 | })(); 473 | }, [connection, ...sortedMints, pools]); 474 | 475 | return pool; 476 | }; 477 | 478 | export const useOwnedPools = () => { 479 | const { pools } = useCachedPool(); 480 | const { userAccounts } = useUserAccounts(); 481 | 482 | const map = userAccounts.reduce((acc, item) => { 483 | const key = item.info.mint.toBase58(); 484 | acc.set(key, [...(acc.get(key) || []), item]); 485 | return acc; 486 | }, new Map()); 487 | 488 | return pools 489 | .filter((p) => map.has(p.pubkeys.mint.toBase58())) 490 | .map((item) => { 491 | let feeAccount = item.pubkeys.feeAccount?.toBase58(); 492 | return map.get(item.pubkeys.mint.toBase58())?.map((a) => { 493 | return { 494 | account: a as TokenAccount, 495 | isFeeAccount: feeAccount === a.pubkey.toBase58(), 496 | pool: item, 497 | }; 498 | }); 499 | }) 500 | .flat(); 501 | }; 502 | 503 | async function _addLiquidityExistingPool( 504 | pool: PoolInfo, 505 | components: LiquidityComponent[], 506 | connection: Connection, 507 | wallet: any, 508 | SLIPPAGE: number 509 | ) { 510 | notify({ 511 | message: "Adding Liquidity...", 512 | description: "Please review transactions to approve.", 513 | type: "warn", 514 | }); 515 | 516 | const poolMint = await cache.getMint(connection, pool.pubkeys.mint); 517 | if (!poolMint.mintAuthority) { 518 | throw new Error("Mint doesnt have authority"); 519 | } 520 | 521 | if (!pool.pubkeys.feeAccount) { 522 | throw new Error("Invald fee account"); 523 | } 524 | 525 | const accountA = await cache.getAccount( 526 | connection, 527 | pool.pubkeys.holdingAccounts[0] 528 | ); 529 | const accountB = await cache.getAccount( 530 | connection, 531 | pool.pubkeys.holdingAccounts[1] 532 | ); 533 | 534 | const reserve0 = accountA.info.amount.toNumber(); 535 | const reserve1 = accountB.info.amount.toNumber(); 536 | const fromA = 537 | accountA.info.mint.toBase58() === components[0].mintAddress 538 | ? components[0] 539 | : components[1]; 540 | const fromB = fromA === components[0] ? components[1] : components[0]; 541 | 542 | if (!fromA.account || !fromB.account) { 543 | throw new Error("Missing account info."); 544 | } 545 | 546 | const supply = poolMint.supply.toNumber(); 547 | const authority = poolMint.mintAuthority; 548 | 549 | // Uniswap whitepaper: https://uniswap.org/whitepaper.pdf 550 | // see: https://uniswap.org/docs/v2/advanced-topics/pricing/ 551 | // as well as native uniswap v2 oracle: https://uniswap.org/docs/v2/core-concepts/oracles/ 552 | const amount0 = fromA.amount; 553 | const amount1 = fromB.amount; 554 | 555 | const liquidity = Math.min( 556 | (amount0 * (1 - SLIPPAGE) * supply) / reserve0, 557 | (amount1 * (1 - SLIPPAGE) * supply) / reserve1 558 | ); 559 | const instructions: TransactionInstruction[] = []; 560 | const cleanupInstructions: TransactionInstruction[] = []; 561 | 562 | const signers: Account[] = []; 563 | 564 | const accountRentExempt = await connection.getMinimumBalanceForRentExemption( 565 | AccountLayout.span 566 | ); 567 | const fromKeyA = getWrappedAccount( 568 | instructions, 569 | cleanupInstructions, 570 | fromA.account, 571 | wallet.publicKey, 572 | amount0 + accountRentExempt, 573 | signers 574 | ); 575 | const fromKeyB = getWrappedAccount( 576 | instructions, 577 | cleanupInstructions, 578 | fromB.account, 579 | wallet.publicKey, 580 | amount1 + accountRentExempt, 581 | signers 582 | ); 583 | 584 | let toAccount = findOrCreateAccountByMint( 585 | wallet.publicKey, 586 | wallet.publicKey, 587 | instructions, 588 | [], 589 | accountRentExempt, 590 | pool.pubkeys.mint, 591 | signers, 592 | new Set([pool.pubkeys.feeAccount.toBase58()]) 593 | ); 594 | 595 | // create approval for transfer transactions 596 | instructions.push( 597 | Token.createApproveInstruction( 598 | programIds().token, 599 | fromKeyA, 600 | authority, 601 | wallet.publicKey, 602 | [], 603 | amount0 604 | ) 605 | ); 606 | 607 | instructions.push( 608 | Token.createApproveInstruction( 609 | programIds().token, 610 | fromKeyB, 611 | authority, 612 | wallet.publicKey, 613 | [], 614 | amount1 615 | ) 616 | ); 617 | 618 | // depoist 619 | instructions.push( 620 | depositInstruction( 621 | pool.pubkeys.account, 622 | authority, 623 | fromKeyA, 624 | fromKeyB, 625 | pool.pubkeys.holdingAccounts[0], 626 | pool.pubkeys.holdingAccounts[1], 627 | pool.pubkeys.mint, 628 | toAccount, 629 | pool.pubkeys.program, 630 | programIds().token, 631 | liquidity, 632 | amount0, 633 | amount1 634 | ) 635 | ); 636 | 637 | let tx = await sendTransaction( 638 | connection, 639 | wallet, 640 | instructions.concat(cleanupInstructions), 641 | signers 642 | ); 643 | 644 | notify({ 645 | message: "Pool Funded. Happy trading.", 646 | type: "success", 647 | description: `Transaction - ${tx}`, 648 | }); 649 | } 650 | 651 | function findOrCreateAccountByMint( 652 | payer: PublicKey, 653 | owner: PublicKey, 654 | instructions: TransactionInstruction[], 655 | cleanupInstructions: TransactionInstruction[], 656 | accountRentExempt: number, 657 | mint: PublicKey, // use to identify same type 658 | signers: Account[], 659 | excluded?: Set 660 | ): PublicKey { 661 | const accountToFind = mint.toBase58(); 662 | const account = getCachedAccount( 663 | (acc) => 664 | acc.info.mint.toBase58() === accountToFind && 665 | acc.info.owner.toBase58() === owner.toBase58() && 666 | (excluded === undefined || !excluded.has(acc.pubkey.toBase58())) 667 | ); 668 | const isWrappedSol = accountToFind === WRAPPED_SOL_MINT.toBase58(); 669 | 670 | let toAccount: PublicKey; 671 | if (account && !isWrappedSol) { 672 | toAccount = account.pubkey; 673 | } else { 674 | // creating depositor pool account 675 | const newToAccount = createSplAccount( 676 | instructions, 677 | payer, 678 | accountRentExempt, 679 | mint, 680 | owner, 681 | AccountLayout.span 682 | ); 683 | 684 | toAccount = newToAccount.publicKey; 685 | signers.push(newToAccount); 686 | 687 | if (isWrappedSol) { 688 | cleanupInstructions.push( 689 | Token.createCloseAccountInstruction( 690 | programIds().token, 691 | toAccount, 692 | payer, 693 | payer, 694 | [] 695 | ) 696 | ); 697 | } 698 | } 699 | 700 | return toAccount; 701 | } 702 | 703 | export async function calculateDependentAmount( 704 | connection: Connection, 705 | independent: string, 706 | amount: number, 707 | pool: PoolInfo 708 | ): Promise { 709 | const poolMint = await cache.getMint(connection, pool.pubkeys.mint); 710 | const accountA = await cache.getAccount( 711 | connection, 712 | pool.pubkeys.holdingAccounts[0] 713 | ); 714 | const accountB = await cache.getAccount( 715 | connection, 716 | pool.pubkeys.holdingAccounts[1] 717 | ); 718 | if (!poolMint.mintAuthority) { 719 | throw new Error("Mint doesnt have authority"); 720 | } 721 | 722 | if (poolMint.supply.eqn(0)) { 723 | return; 724 | } 725 | 726 | const mintA = await cache.getMint(connection, accountA.info.mint); 727 | const mintB = await cache.getMint(connection, accountB.info.mint); 728 | 729 | if (!mintA || !mintB) { 730 | return; 731 | } 732 | 733 | const isFirstIndependent = accountA.info.mint.toBase58() === independent; 734 | const depPrecision = Math.pow( 735 | 10, 736 | isFirstIndependent ? mintB.decimals : mintA.decimals 737 | ); 738 | const indPrecision = Math.pow( 739 | 10, 740 | isFirstIndependent ? mintA.decimals : mintB.decimals 741 | ); 742 | const adjAmount = amount * indPrecision; 743 | 744 | const dependentTokenAmount = isFirstIndependent 745 | ? (accountB.info.amount.toNumber() / accountA.info.amount.toNumber()) * 746 | adjAmount 747 | : (accountA.info.amount.toNumber() / accountB.info.amount.toNumber()) * 748 | adjAmount; 749 | 750 | return dependentTokenAmount / depPrecision; 751 | } 752 | 753 | // TODO: add ui to customize curve type 754 | async function _addLiquidityNewPool( 755 | wallet: any, 756 | connection: Connection, 757 | components: LiquidityComponent[], 758 | options: PoolConfig 759 | ) { 760 | notify({ 761 | message: "Creating new pool...", 762 | description: "Please review transactions to approve.", 763 | type: "warn", 764 | }); 765 | 766 | if (components.some((c) => !c.account)) { 767 | notify({ 768 | message: "You need to have balance for all legs in the basket...", 769 | description: "Please review inputs.", 770 | type: "error", 771 | }); 772 | return; 773 | } 774 | 775 | let instructions: TransactionInstruction[] = []; 776 | let cleanupInstructions: TransactionInstruction[] = []; 777 | 778 | const liquidityTokenAccount = new Account(); 779 | // Create account for pool liquidity token 780 | instructions.push( 781 | SystemProgram.createAccount({ 782 | fromPubkey: wallet.publicKey, 783 | newAccountPubkey: liquidityTokenAccount.publicKey, 784 | lamports: await connection.getMinimumBalanceForRentExemption( 785 | MintLayout.span 786 | ), 787 | space: MintLayout.span, 788 | programId: programIds().token, 789 | }) 790 | ); 791 | 792 | const tokenSwapAccount = new Account(); 793 | 794 | const [authority, nonce] = await PublicKey.findProgramAddress( 795 | [tokenSwapAccount.publicKey.toBuffer()], 796 | programIds().swap 797 | ); 798 | 799 | // create mint for pool liquidity token 800 | instructions.push( 801 | Token.createInitMintInstruction( 802 | programIds().token, 803 | liquidityTokenAccount.publicKey, 804 | LIQUIDITY_TOKEN_PRECISION, 805 | // pass control of liquidity mint to swap program 806 | authority, 807 | // swap program can freeze liquidity token mint 808 | null 809 | ) 810 | ); 811 | 812 | // Create holding accounts for 813 | const accountRentExempt = await connection.getMinimumBalanceForRentExemption( 814 | AccountLayout.span 815 | ); 816 | const holdingAccounts: Account[] = []; 817 | let signers: Account[] = []; 818 | 819 | components.forEach((leg) => { 820 | if (!leg.account) { 821 | return; 822 | } 823 | 824 | const mintPublicKey = leg.account.info.mint; 825 | // component account to store tokens I of N in liquidity poll 826 | holdingAccounts.push( 827 | createSplAccount( 828 | instructions, 829 | wallet.publicKey, 830 | accountRentExempt, 831 | mintPublicKey, 832 | authority, 833 | AccountLayout.span 834 | ) 835 | ); 836 | }); 837 | 838 | // creating depositor pool account 839 | const depositorAccount = createSplAccount( 840 | instructions, 841 | wallet.publicKey, 842 | accountRentExempt, 843 | liquidityTokenAccount.publicKey, 844 | wallet.publicKey, 845 | AccountLayout.span 846 | ); 847 | 848 | // creating fee pool account its set from env variable or to creater of the pool 849 | // creater of the pool is not allowed in some versions of token-swap program 850 | const feeAccount = createSplAccount( 851 | instructions, 852 | wallet.publicKey, 853 | accountRentExempt, 854 | liquidityTokenAccount.publicKey, 855 | SWAP_PROGRAM_OWNER_FEE_ADDRESS || wallet.publicKey, 856 | AccountLayout.span 857 | ); 858 | 859 | // create all accounts in one transaction 860 | let tx = await sendTransaction(connection, wallet, instructions, [ 861 | liquidityTokenAccount, 862 | depositorAccount, 863 | feeAccount, 864 | ...holdingAccounts, 865 | ...signers, 866 | ]); 867 | 868 | notify({ 869 | message: "Accounts created", 870 | description: `Transaction ${tx}`, 871 | type: "success", 872 | }); 873 | 874 | notify({ 875 | message: "Adding Liquidity...", 876 | description: "Please review transactions to approve.", 877 | type: "warn", 878 | }); 879 | 880 | signers = []; 881 | instructions = []; 882 | cleanupInstructions = []; 883 | 884 | instructions.push( 885 | SystemProgram.createAccount({ 886 | fromPubkey: wallet.publicKey, 887 | newAccountPubkey: tokenSwapAccount.publicKey, 888 | lamports: await connection.getMinimumBalanceForRentExemption( 889 | TokenSwapLayout.span 890 | ), 891 | space: TokenSwapLayout.span, 892 | programId: programIds().swap, 893 | }) 894 | ); 895 | 896 | components.forEach((leg, i) => { 897 | if (!leg.account) { 898 | return; 899 | } 900 | 901 | // create temporary account for wrapped sol to perform transfer 902 | const from = getWrappedAccount( 903 | instructions, 904 | cleanupInstructions, 905 | leg.account, 906 | wallet.publicKey, 907 | leg.amount + accountRentExempt, 908 | signers 909 | ); 910 | 911 | instructions.push( 912 | Token.createTransferInstruction( 913 | programIds().token, 914 | from, 915 | holdingAccounts[i].publicKey, 916 | wallet.publicKey, 917 | [], 918 | leg.amount 919 | ) 920 | ); 921 | }); 922 | 923 | instructions.push( 924 | createInitSwapInstruction( 925 | tokenSwapAccount, 926 | authority, 927 | holdingAccounts[0].publicKey, 928 | holdingAccounts[1].publicKey, 929 | liquidityTokenAccount.publicKey, 930 | feeAccount.publicKey, 931 | depositorAccount.publicKey, 932 | programIds().token, 933 | programIds().swap, 934 | nonce, 935 | options.curveType, 936 | options.tradeFeeNumerator, 937 | options.tradeFeeDenominator, 938 | options.ownerTradeFeeNumerator, 939 | options.ownerTradeFeeDenominator, 940 | options.ownerWithdrawFeeNumerator, 941 | options.ownerWithdrawFeeDenominator 942 | ) 943 | ); 944 | 945 | // All instructions didn't fit in single transaction 946 | // initialize and provide inital liquidity to swap in 2nd (this prevents loss of funds) 947 | tx = await sendTransaction( 948 | connection, 949 | wallet, 950 | instructions.concat(cleanupInstructions), 951 | [tokenSwapAccount, ...signers] 952 | ); 953 | 954 | notify({ 955 | message: "Pool Funded. Happy trading.", 956 | type: "success", 957 | description: `Transaction - ${tx}`, 958 | }); 959 | } 960 | 961 | function getWrappedAccount( 962 | instructions: TransactionInstruction[], 963 | cleanupInstructions: TransactionInstruction[], 964 | toCheck: TokenAccount, 965 | payer: PublicKey, 966 | amount: number, 967 | signers: Account[] 968 | ) { 969 | if (!toCheck.info.isNative) { 970 | return toCheck.pubkey; 971 | } 972 | 973 | const account = new Account(); 974 | instructions.push( 975 | SystemProgram.createAccount({ 976 | fromPubkey: payer, 977 | newAccountPubkey: account.publicKey, 978 | lamports: amount, 979 | space: AccountLayout.span, 980 | programId: programIds().token, 981 | }) 982 | ); 983 | 984 | instructions.push( 985 | Token.createInitAccountInstruction( 986 | programIds().token, 987 | WRAPPED_SOL_MINT, 988 | account.publicKey, 989 | payer 990 | ) 991 | ); 992 | 993 | cleanupInstructions.push( 994 | Token.createCloseAccountInstruction( 995 | programIds().token, 996 | account.publicKey, 997 | payer, 998 | payer, 999 | [] 1000 | ) 1001 | ); 1002 | 1003 | signers.push(account); 1004 | 1005 | return account.publicKey; 1006 | } 1007 | 1008 | function createSplAccount( 1009 | instructions: TransactionInstruction[], 1010 | payer: PublicKey, 1011 | accountRentExempt: number, 1012 | mint: PublicKey, 1013 | owner: PublicKey, 1014 | space: number 1015 | ) { 1016 | const account = new Account(); 1017 | instructions.push( 1018 | SystemProgram.createAccount({ 1019 | fromPubkey: payer, 1020 | newAccountPubkey: account.publicKey, 1021 | lamports: accountRentExempt, 1022 | space, 1023 | programId: programIds().token, 1024 | }) 1025 | ); 1026 | 1027 | instructions.push( 1028 | Token.createInitAccountInstruction( 1029 | programIds().token, 1030 | mint, 1031 | account.publicKey, 1032 | owner 1033 | ) 1034 | ); 1035 | 1036 | return account; 1037 | } 1038 | -------------------------------------------------------------------------------- /src/utils/token-list.json: -------------------------------------------------------------------------------- 1 | { 2 | "mainnet-beta": [ 3 | { 4 | "tokenSymbol": "SOL", 5 | "mintAddress": "So11111111111111111111111111111111111111112", 6 | "tokenName": "Solana", 7 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/solana/info/logo.png" 8 | }, 9 | { 10 | "tokenSymbol": "BTC", 11 | "mintAddress": "9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E", 12 | "tokenName": "Wrapped Bitcoin", 13 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/bitcoin/info/logo.png" 14 | }, 15 | { 16 | "tokenSymbol": "ETH", 17 | "mintAddress": "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk", 18 | "tokenName": "Wrapped Ethereum", 19 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" 20 | }, 21 | { 22 | "tokenSymbol": "USDC", 23 | "mintAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 24 | "tokenName": "USDC", 25 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/f3ffd0b9ae2165336279ce2f8db1981a55ce30f8/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" 26 | }, 27 | { 28 | "tokenSymbol": "YFI", 29 | "mintAddress": "3JSf5tPeuscJGtaCp5giEiDhv51gQ4v3zWg8DGgyLfAB", 30 | "tokenName": "Wrapped YFI", 31 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e/logo.png" 32 | }, 33 | { 34 | "tokenSymbol": "LINK", 35 | "mintAddress": "CWE8jPTUYhdCTZYWPTe1o5DFqfdjzWKc9WKz6rSjQUdG", 36 | "tokenName": "Wrapped Chainlink", 37 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png" 38 | }, 39 | { 40 | "tokenSymbol": "XRP", 41 | "mintAddress": "Ga2AXHpfAF6mv2ekZwcsJFqu7wB4NV331qNH7fW9Nst8", 42 | "tokenName": "Wrapped XRP", 43 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ripple/info/logo.png" 44 | }, 45 | { 46 | "tokenSymbol": "USDT", 47 | "mintAddress": "BQcdHdAQW1hczDbBi9hiegXAR7A98Q9jx3X3iBBBDiq4", 48 | "tokenName": "Wrapped USDT", 49 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/f3ffd0b9ae2165336279ce2f8db1981a55ce30f8/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png" 50 | }, 51 | { 52 | "tokenSymbol": "SUSHI", 53 | "mintAddress": "AR1Mtgh7zAtxuxGd2XPovXPVjcSdY3i4rQYisNadjfKy", 54 | "tokenName": "Wrapped SUSHI", 55 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B3595068778DD592e39A122f4f5a5cF09C90fE2/logo.png" 56 | }, 57 | { 58 | "tokenSymbol": "ALEPH", 59 | "mintAddress": "CsZ5LZkDS7h9TDKjrbL7VAwQZ9nsRu8vJLhRYfmGaN8K", 60 | "tokenName": "Wrapped ALEPH", 61 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/6996a371cd02f516506a8f092eeb29888501447c/blockchains/nuls/assets/NULSd6HgyZkiqLnBzTaeSQfx1TNg2cqbzq51h/logo.png" 62 | }, 63 | { 64 | "tokenSymbol": "SXP", 65 | "mintAddress": "SF3oTvfWzEP3DTwGSvUXRrGTvr75pdZNnBLAH9bzMuX", 66 | "tokenName": "Wrapped SXP", 67 | "icon": "https://github.com/trustwallet/assets/raw/b0ab88654fe64848da80d982945e4db06e197d4f/blockchains/ethereum/assets/0x8CE9137d39326AD0cD6491fb5CC0CbA0e089b6A9/logo.png" 68 | }, 69 | { 70 | "tokenSymbol": "HGET", 71 | "mintAddress": "BtZQfWqDGbk9Wf2rXEiWyQBdBY1etnUUn6zEphvVS7yN", 72 | "tokenName": "Wrapped HGET" 73 | }, 74 | { 75 | "tokenSymbol": "CREAM", 76 | "mintAddress": "5Fu5UUgbjpUvdBveb3a1JTNirL8rXtiYeSMWvKjtUNQv", 77 | "tokenName": "Wrapped CREAM", 78 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/4c82c2a409f18a4dd96a504f967a55a8fe47026d/blockchains/smartchain/assets/0xd4CB328A82bDf5f03eB737f37Fa6B370aef3e888/logo.png" 79 | }, 80 | { 81 | "tokenSymbol": "UBXT", 82 | "mintAddress": "873KLxCbz7s9Kc4ZzgYRtNmhfkQrhfyWGZJBmyCbC3ei", 83 | "tokenName": "Wrapped UBXT" 84 | }, 85 | { 86 | "tokenSymbol": "HNT", 87 | "mintAddress": "HqB7uswoVg4suaQiDP3wjxob1G5WdZ144zhdStwMCq7e", 88 | "tokenName": "Wrapped HNT" 89 | }, 90 | { 91 | "tokenSymbol": "FRONT", 92 | "mintAddress": "9S4t2NEAiJVMvPdRYKVrfJpBafPBLtvbvyS3DecojQHw", 93 | "tokenName": "Wrapped FRONT", 94 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/6e375e4e5fb0ffe09ed001bae1ef8ca1d6c86034/blockchains/ethereum/assets/0xf8C3527CC04340b208C854E985240c02F7B7793f/logo.png" 95 | }, 96 | { 97 | "tokenSymbol": "AKRO", 98 | "mintAddress": "6WNVCuxCGJzNjmMZoKyhZJwvJ5tYpsLyAtagzYASqBoF", 99 | "tokenName": "Wrapped AKRO", 100 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/878dcab0fab90e6593bcb9b7d941be4915f287dc/blockchains/ethereum/assets/0xb2734a4Cec32C81FDE26B0024Ad3ceB8C9b34037/logo.png" 101 | }, 102 | { 103 | "tokenSymbol": "HXRO", 104 | "mintAddress": "DJafV9qemGp7mLMEn5wrfqaFwxsbLgUsGVS16zKRk9kc", 105 | "tokenName": "Wrapped HXRO" 106 | }, 107 | { 108 | "tokenSymbol": "UNI", 109 | "mintAddress": "DEhAasscXF4kEGxFgJ3bq4PpVGp5wyUxMRvn6TzGVHaw", 110 | "tokenName": "Wrapped UNI", 111 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/08d734b5e6ec95227dc50efef3a9cdfea4c398a1/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png" 112 | }, 113 | { 114 | "mintAddress": "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", 115 | "tokenName": "Serum", 116 | "tokenSymbol": "SRM", 117 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x476c5E26a75bd202a9683ffD34359C0CC15be0fF/logo.png" 118 | }, 119 | { 120 | "tokenSymbol": "FTT", 121 | "mintAddress": "AGFEad2et2ZJif9jaGpdMixQqvW5i81aBdvKe7PHNfz3", 122 | "tokenName": "Wrapped FTT", 123 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/f3ffd0b9ae2165336279ce2f8db1981a55ce30f8/blockchains/ethereum/assets/0x50D1c9771902476076eCFc8B2A83Ad6b9355a4c9/logo.png" 124 | }, 125 | { 126 | "mintAddress": "MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L", 127 | "tokenName": "MegaSerum", 128 | "tokenSymbol": "MSRM", 129 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x476c5E26a75bd202a9683ffD34359C0CC15be0fF/logo.png" 130 | }, 131 | { 132 | "tokenSymbol": "WUSDC", 133 | "mintAddress": "BXXkv6z8ykpG1yuvUDPgh732wzVHB69RnB9YgSYh3itW", 134 | "tokenName": "Wrapped USDC", 135 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/f3ffd0b9ae2165336279ce2f8db1981a55ce30f8/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" 136 | } 137 | ], 138 | "testnet": [ 139 | { 140 | "tokenSymbol": "SOL", 141 | "mintAddress": "So11111111111111111111111111111111111111112", 142 | "tokenName": "Solana", 143 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/solana/info/logo.png" 144 | }, 145 | { 146 | "tokenSymbol": "ABC", 147 | "mintAddress": "D4fdoY5d2Bn1Cmjqy6J6shRHjcs7QNuBPzwEzTLrf7jm", 148 | "tokenName": "ABC Test", 149 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/bitcoin/info/logo.png" 150 | } 151 | ], 152 | "devnet": [ 153 | { 154 | "tokenSymbol": "SOL", 155 | "mintAddress": "So11111111111111111111111111111111111111112", 156 | "tokenName": "Solana", 157 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/solana/info/logo.png" 158 | }, 159 | { 160 | "tokenSymbol": "XYZ", 161 | "mintAddress": "DEhAasscXF4kEGxFgJ3bq4PpVGp5wyUxMRvn6TzGVHaw", 162 | "tokenName": "XYZ Test", 163 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/08d734b5e6ec95227dc50efef3a9cdfea4c398a1/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png" 164 | }, 165 | { 166 | "tokenSymbol": "ABC", 167 | "mintAddress": "6z83b76xbSm5UhdG33ePh7QCbLS8YaXCQ9up86tDTCUH", 168 | "tokenName": "ABC Test", 169 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/08d734b5e6ec95227dc50efef3a9cdfea4c398a1/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png" 170 | }, 171 | { 172 | "tokenSymbol": "DEF", 173 | "mintAddress": "3pyeDv6AV1RQuA6KzsqkZrpsNn4b3hooHrQhGs7K2TYa", 174 | "tokenName": "DEF Test", 175 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/08d734b5e6ec95227dc50efef3a9cdfea4c398a1/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png" 176 | } 177 | ], 178 | "localnet": [ 179 | { 180 | "tokenSymbol": "SOL", 181 | "mintAddress": "So11111111111111111111111111111111111111112", 182 | "tokenName": "Solana", 183 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/solana/info/logo.png" 184 | } 185 | ] 186 | } 187 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import { MintInfo } from "@solana/spl-token"; 3 | 4 | import PopularTokens from "./token-list.json"; 5 | import { ENV } from "./connection"; 6 | import { PoolInfo, TokenAccount } from "./../models"; 7 | 8 | export interface KnownToken { 9 | tokenSymbol: string; 10 | tokenName: string; 11 | icon: string; 12 | mintAddress: string; 13 | } 14 | 15 | const AddressToToken = Object.keys(PopularTokens).reduce((map, key) => { 16 | const tokens = PopularTokens[key as ENV] as KnownToken[]; 17 | const knownMints = tokens.reduce((map, item) => { 18 | map.set(item.mintAddress, item); 19 | return map; 20 | }, new Map()); 21 | 22 | map.set(key as ENV, knownMints); 23 | 24 | return map; 25 | }, new Map>()); 26 | 27 | export function useLocalStorageState(key: string, defaultState?: string) { 28 | const [state, setState] = useState(() => { 29 | // NOTE: Not sure if this is ok 30 | const storedState = localStorage.getItem(key); 31 | if (storedState) { 32 | return JSON.parse(storedState); 33 | } 34 | return defaultState; 35 | }); 36 | 37 | const setLocalStorageState = useCallback( 38 | (newState) => { 39 | const changed = state !== newState; 40 | if (!changed) { 41 | return; 42 | } 43 | setState(newState); 44 | if (newState === null) { 45 | localStorage.removeItem(key); 46 | } else { 47 | localStorage.setItem(key, JSON.stringify(newState)); 48 | } 49 | }, 50 | [state, key] 51 | ); 52 | 53 | return [state, setLocalStorageState]; 54 | } 55 | 56 | // shorten the checksummed version of the input address to have 0x + 4 characters at start and end 57 | export function shortenAddress(address: string, chars = 4): string { 58 | return `0x${address.substring(0, chars)}...${address.substring(44 - chars)}`; 59 | } 60 | 61 | export function getTokenName(env: ENV, mintAddress: string): string { 62 | const knownSymbol = AddressToToken.get(env)?.get(mintAddress)?.tokenSymbol; 63 | if (knownSymbol) { 64 | return knownSymbol; 65 | } 66 | 67 | return shortenAddress(mintAddress).substring(10).toUpperCase(); 68 | } 69 | 70 | export function getTokenIcon( 71 | env: ENV, 72 | mintAddress: string 73 | ): string | undefined { 74 | return AddressToToken.get(env)?.get(mintAddress)?.icon; 75 | } 76 | 77 | export function getPoolName(env: ENV, pool: PoolInfo) { 78 | const sorted = pool.pubkeys.holdingMints.map((a) => a.toBase58()).sort(); 79 | return sorted.map((item) => getTokenName(env, item)).join("/"); 80 | } 81 | 82 | export function isKnownMint(env: ENV, mintAddress: string) { 83 | return !!AddressToToken.get(env)?.get(mintAddress); 84 | } 85 | 86 | export function convert( 87 | account?: TokenAccount, 88 | mint?: MintInfo, 89 | rate: number = 1.0 90 | ): number { 91 | if (!account) { 92 | return 0; 93 | } 94 | 95 | const precision = Math.pow(10, mint?.decimals || 0); 96 | return (account.info.amount?.toNumber() / precision) * rate; 97 | } 98 | 99 | export function formatTokenAmount( 100 | account?: TokenAccount, 101 | mint?: MintInfo, 102 | rate: number = 1.0, 103 | prefix = "", 104 | suffix = "" 105 | ): string { 106 | if (!account) { 107 | return ""; 108 | } 109 | 110 | return `${[prefix]}${convert(account, mint, rate).toFixed(6)}${suffix}`; 111 | } 112 | -------------------------------------------------------------------------------- /src/utils/wallet.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useMemo, useState } from "react"; 2 | import Wallet from "@project-serum/sol-wallet-adapter"; 3 | import { notify } from "./notifications"; 4 | import { useConnectionConfig } from "./connection"; 5 | import { useLocalStorageState } from "./utils"; 6 | 7 | export const WALLET_PROVIDERS = [ 8 | { name: "sollet.io", url: "https://www.sollet.io" }, 9 | { name: "solflare.com", url: "https://solflare.com/access-wallet" }, 10 | { name: "mathwallet.org", url: "https://www.mathwallet.org" }, 11 | ]; 12 | 13 | const WalletContext = React.createContext(null); 14 | 15 | export function WalletProvider({ children = null as any }) { 16 | const { endpoint } = useConnectionConfig(); 17 | const [providerUrl, setProviderUrl] = useLocalStorageState( 18 | "walletProvider", 19 | "https://www.sollet.io" 20 | ); 21 | const wallet = useMemo(() => new Wallet(providerUrl, endpoint), [ 22 | providerUrl, 23 | endpoint, 24 | ]); 25 | 26 | const [connected, setConnected] = useState(false); 27 | useEffect(() => { 28 | console.log("trying to connect"); 29 | wallet.on("connect", () => { 30 | console.log("connected"); 31 | setConnected(true); 32 | let walletPublicKey = wallet.publicKey.toBase58(); 33 | let keyToDisplay = 34 | walletPublicKey.length > 20 35 | ? `${walletPublicKey.substring(0, 7)}.....${walletPublicKey.substring( 36 | walletPublicKey.length - 7, 37 | walletPublicKey.length 38 | )}` 39 | : walletPublicKey; 40 | 41 | notify({ 42 | message: "Wallet update", 43 | description: "Connected to wallet " + keyToDisplay, 44 | }); 45 | }); 46 | wallet.on("disconnect", () => { 47 | setConnected(false); 48 | notify({ 49 | message: "Wallet update", 50 | description: "Disconnected from wallet", 51 | }); 52 | }); 53 | return () => { 54 | wallet.disconnect(); 55 | setConnected(false); 56 | }; 57 | }, [wallet]); 58 | return ( 59 | url === providerUrl)?.name ?? 67 | providerUrl, 68 | }} 69 | > 70 | {children} 71 | 72 | ); 73 | } 74 | 75 | export function useWallet() { 76 | const context = useContext(WalletContext); 77 | return { 78 | connected: context.connected, 79 | wallet: context.wallet, 80 | providerUrl: context.providerUrl, 81 | setProvider: context.setProviderUrl, 82 | providerName: context.providerName, 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "downlevelIteration": true, 14 | "resolveJsonModule": true, 15 | "noEmit": true, 16 | "typeRoots": ["./types"], 17 | "jsx": "react", 18 | "isolatedModules": true 19 | }, 20 | "include": ["src"] 21 | } 22 | --------------------------------------------------------------------------------