├── .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 | You need to enable JavaScript to run this app.
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 |
42 |
46 |
47 | {item.tokenSymbol}
48 |
49 |
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 |
107 |
108 | {icon}
109 | {name}
110 |
111 |
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 | {
173 | if (props.onMintChange) {
174 | props.onMintChange(item);
175 | }
176 | }}
177 | >
178 | {[...renderPopularTokens, ...renderAdditionalTokens]}
179 |
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 |
38 |
39 |
40 |
45 | Trade
46 |
47 |
48 |
49 | {connected && (
50 |
}
53 | trigger="click"
54 | >
55 |
My Pools
56 |
57 | )}
58 |
59 | {!connected && (
60 |
66 | Connect
67 |
68 | )}
69 | {connected && (
70 |
75 | )}
76 |
77 | {
78 |
}
82 | trigger="click"
83 | >
84 |
}
89 | />
90 |
91 | }
92 |
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 |
16 |
17 |
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 |
93 | {generateActionLabel(CREATE_POOL_LABEL, connected, env, A, B)}
94 | {pendingTx && }
95 |
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 | Read more about providing liquidity.
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 |
178 | {generateActionLabel(ADD_LIQUIDITY_LABEL, connected, env, A, B)}
179 | {pendingTx && }
180 |
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 |
114 | props.setOptions({
115 | ...props.options,
116 | curveType: parseInt(val) as any,
117 | })
118 | }
119 | >
120 | Constant Product
121 | Flat
122 |
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 |
40 | Remove
41 |
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 |
setSlippage(item / 100.0)}
39 | >
40 | {item}%
41 |
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 |
91 | {ENDPOINTS.map(({ name, endpoint }) => (
92 |
93 | {name}
94 |
95 | ))}
96 |
97 |
98 |
99 | Wallet:{" "}
100 |
101 | {WALLET_PROVIDERS.map(({ name, url }) => (
102 |
103 | {name}
104 |
105 | ))}
106 |
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 |
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 |
93 | ⇅
94 |
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 |
127 | {generateActionLabel(
128 | !pool
129 | ? POOL_NOT_AVAILABLE(
130 | getTokenName(env, A.mintAddress),
131 | getTokenName(env, B.mintAddress)
132 | )
133 | : SWAP_LABEL,
134 | connected,
135 | env,
136 | A,
137 | B,
138 | true
139 | )}
140 | {pendingTx && }
141 |
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 |
--------------------------------------------------------------------------------