├── .gitignore
├── .idea
├── .gitignore
├── modules.xml
├── raydium-test.iml
└── vcs.xml
├── README.md
├── package.json
├── public
└── index.html
├── src
├── App.tsx
├── components
│ ├── AlertDismissable.tsx
│ └── Main.tsx
├── constant.ts
├── index.css
├── index.tsx
└── utils
│ └── index.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.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 | /dist
14 |
15 | # misc
16 | .DS_Store
17 | .env
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | .vscode
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/raydium-test.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Raydium Frontend App
2 | A simple token swap frontend app to connect wallet and swap sol to ray.
3 |
4 | ## Reminder
5 | This project is an example project that's built for learning and sharing purpose only. As this project was developed in 2022, I believe that Raydium SDK has quite some significant upgrades and Raydium team should have provided developer guide. If you encounter any technical issue, it's best to contact Raydium's tech support for help.
6 |
7 | ## Reference
8 | [Raydium SDK](https://github.com/raydium-io/raydium-sdk)
9 |
10 | [Raydium Frontend](https://github.com/raydium-io/raydium-frontend)
11 |
12 | [Solana Starter Projects](https://solana-labs.github.io/wallet-adapter/#starter-projects)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "raydium-test",
3 | "homepage": "https://abbylow.github.io/raydium-test",
4 | "version": "1.0.0",
5 | "main": "index.js",
6 | "license": "MIT",
7 | "dependencies": {
8 | "@raydium-io/raydium-sdk": "^1.1.0-beta.1",
9 | "@solana/wallet-adapter-base": "^0.9.5",
10 | "@solana/wallet-adapter-react": "^0.15.5",
11 | "@solana/wallet-adapter-react-ui": "^0.9.7",
12 | "@solana/wallet-adapter-wallets": "^0.16.1",
13 | "@solana/web3.js": "^1.43.0",
14 | "bootstrap": "^5.1.3",
15 | "core-js": "^3.22.5",
16 | "lodash.debounce": "^4.0.8",
17 | "react": "^18.1.0",
18 | "react-bootstrap": "^2.4.0",
19 | "react-dom": "^18.1.0"
20 | },
21 | "devDependencies": {
22 | "@types/lodash.debounce": "^4.0.7",
23 | "@types/react-dom": "^18.0.4",
24 | "crypto-browserify": "^3.12.0",
25 | "css-loader": "^6.7.1",
26 | "gh-pages": "^4.0.0",
27 | "html-webpack-plugin": "^5.5.0",
28 | "mini-css-extract-plugin": "^2.6.0",
29 | "os-browserify": "^0.3.0",
30 | "stream-browserify": "^3.0.0",
31 | "ts-loader": "^9.3.0",
32 | "typescript": "^4.6.4",
33 | "webpack": "^5.72.1",
34 | "webpack-cli": "^4.9.2",
35 | "webpack-dev-server": "^4.9.0"
36 | },
37 | "scripts": {
38 | "start": "webpack serve --port 3000",
39 | "build": "NODE_ENV=production webpack",
40 | "predeploy": "npm run build",
41 | "deploy": "gh-pages -d build"
42 | },
43 | "eslintConfig": {
44 | "extends": "react-app"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Raydium Frontend Test
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useMemo } from 'react';
2 | import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
3 | import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
4 | import {
5 | GlowWalletAdapter,
6 | PhantomWalletAdapter,
7 | SlopeWalletAdapter,
8 | SolflareWalletAdapter,
9 | TorusWalletAdapter,
10 | } from '@solana/wallet-adapter-wallets';
11 | import {
12 | WalletModalProvider,
13 | WalletMultiButton
14 | } from '@solana/wallet-adapter-react-ui';
15 | import { clusterApiUrl } from '@solana/web3.js';
16 |
17 | import Main from './components/Main';
18 |
19 | // Default styles that can be overridden by your app
20 | require('@solana/wallet-adapter-react-ui/styles.css');
21 |
22 |
23 | const App: FC = () => {
24 | // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
25 | const network = WalletAdapterNetwork.Mainnet;
26 |
27 | // You can also provide a custom RPC endpoint.
28 | // const endpoint = useMemo(() => clusterApiUrl(network), [network]);
29 | const endpoint = "https://solana-api.projectserum.com"; // mainnet has rate limit so use Project Serum-hosted api node
30 |
31 | // @solana/wallet-adapter-wallets includes all the adapters but supports tree shaking and lazy loading --
32 | // Only the wallets you configure here will be compiled into your application, and only the dependencies
33 | // of wallets that your users connect to will be loaded.
34 | const wallets = useMemo(
35 | () => [
36 | new PhantomWalletAdapter(),
37 | new GlowWalletAdapter(),
38 | new SlopeWalletAdapter(),
39 | new SolflareWalletAdapter({ network }),
40 | new TorusWalletAdapter(),
41 | ],
42 | [network]
43 | );
44 |
45 | return (
46 |
47 |
48 |
49 |
50 |
54 |
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default App
--------------------------------------------------------------------------------
/src/components/AlertDismissable.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { Alert } from 'react-bootstrap';
3 |
4 | type AlertProps = {
5 | heading: string;
6 | content: string;
7 | type: string;
8 | show: boolean;
9 | setShow: (isShow: boolean) => void;
10 | }
11 |
12 | const AlertDismissable: FC = (props) => {
13 | const { heading, content, type, show, setShow } = props;
14 |
15 | if (show) {
16 | return (
17 | setShow(false)} dismissible>
18 |
19 | {heading}
20 |
21 |
22 | {content}
23 |
24 |
25 | );
26 | }
27 | return null;
28 | }
29 |
30 | export default AlertDismissable;
31 |
--------------------------------------------------------------------------------
/src/components/Main.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | FC,
3 | useState,
4 | ChangeEvent,
5 | MouseEventHandler,
6 | useEffect
7 | } from 'react';
8 | import { useConnection, useWallet } from '@solana/wallet-adapter-react';
9 | import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'
10 | import {
11 | Liquidity,
12 | LiquidityPoolKeys,
13 | jsonInfo2PoolKeys,
14 | LiquidityPoolJsonInfo,
15 | TokenAccount,
16 | } from "@raydium-io/raydium-sdk";
17 | import AlertDismissable from './AlertDismissable';
18 | import { getTokenAccountsByOwner, calcAmountOut } from '../utils';
19 | import {
20 | SOL_IMG,
21 | RAY_IMG,
22 | RAY_SOL_LP_V4_POOL_KEY,
23 | RAYDIUM_LIQUIDITY_JSON,
24 | RAY_TOKEN_MINT
25 | } from '../constant';
26 |
27 | const Main: FC = () => {
28 | const { publicKey, sendTransaction } = useWallet();
29 | const { connection } = useConnection();
30 |
31 | const [solBalance, setSolBalance] = useState(0);
32 | const [rayBalance, setRayBalance] = useState(0);
33 | const [exchangeRate, setExchangeRate] = useState('');
34 |
35 | const [raySolPoolKey, setRaySolPoolKey] = useState();
36 | const [tokenAccounts, setTokenAccounts] = useState([]);
37 |
38 | const [alertHeading, setAlertHeading] = useState('');
39 | const [alertContent, setAlertContent] = useState('');
40 | const [alertType, setAlertType] = useState('danger');
41 | const [alertShow, setAlertShow] = useState(false);
42 |
43 | const [input, setInput] = useState('0');
44 | const [output, setOutput] = useState('0');
45 |
46 | const [swapInDirection, setSwapInDirection] = useState(false); // IN: RAY to SOL; OUT: SOL to RAY
47 |
48 | useEffect(() => {
49 | const getAccountInfo = async () => {
50 | if (publicKey !== null) {
51 | const balance = await connection.getBalance(publicKey); // get SOL balance
52 | setSolBalance(balance / LAMPORTS_PER_SOL);
53 |
54 | const tokenAccs = await getTokenAccountsByOwner(connection, publicKey as PublicKey); // get all token accounts
55 | setTokenAccounts(tokenAccs);
56 |
57 | let rayTokenAddress: PublicKey;
58 | tokenAccs.filter(acc => acc.accountInfo.mint.toBase58() === RAY_TOKEN_MINT).map(async (acc) => {
59 | rayTokenAddress = acc.pubkey;
60 | const accBalance = await connection.getTokenAccountBalance(rayTokenAddress);
61 | const rayBal = accBalance.value.uiAmount || 0;
62 | setRayBalance(rayBal);
63 | });
64 |
65 | }
66 | };
67 | const getPoolInfo = async () => {
68 | const liquidityJsonResp = await fetch(RAYDIUM_LIQUIDITY_JSON);
69 | if (!(await liquidityJsonResp).ok) return []
70 | const liquidityJson = await liquidityJsonResp.json();
71 | const allPoolKeysJson = [...(liquidityJson?.official ?? []), ...(liquidityJson?.unOfficial ?? [])]
72 | const poolKeysRaySolJson: LiquidityPoolJsonInfo = allPoolKeysJson.filter((item) => item.lpMint === RAY_SOL_LP_V4_POOL_KEY)?.[0] || null;
73 | const raySolPk = jsonInfo2PoolKeys(poolKeysRaySolJson);
74 | setRaySolPoolKey(raySolPk);
75 | }
76 | getAccountInfo();
77 | getPoolInfo();
78 | }, [publicKey, connection]);
79 |
80 | useEffect(() => {
81 | const getInitialRate = async () => {
82 | if (raySolPoolKey && publicKey) {
83 | const { executionPrice } = await calcAmountOut(connection, raySolPoolKey, 1, swapInDirection);
84 | const rate = executionPrice?.toFixed() || '0';
85 | setExchangeRate(rate);
86 | }
87 | }
88 | getInitialRate();
89 | }, [publicKey, raySolPoolKey, swapInDirection]);
90 |
91 | useEffect(() => {
92 | // update estimated output
93 | if (exchangeRate) {
94 | const inputNum: number = parseFloat(input);
95 | const calculatedOutput: number = inputNum * parseFloat(exchangeRate);
96 | const processedOutput: string = isNaN(calculatedOutput) ? '0' : String(calculatedOutput);
97 | setOutput(processedOutput);
98 | }
99 | }, [exchangeRate, input]);
100 |
101 | const handleChange = async (e: ChangeEvent) => {
102 | setInput(e.target.value);
103 | };
104 |
105 | const handleSwap: MouseEventHandler = async () => {
106 | const inputNumber = parseFloat(input);
107 | if (raySolPoolKey && publicKey) {
108 | try {
109 | const { amountIn, minAmountOut } = await calcAmountOut(connection, raySolPoolKey, inputNumber, swapInDirection);
110 |
111 | const { transaction, signers } = await Liquidity.makeSwapTransaction({
112 | connection,
113 | poolKeys: raySolPoolKey,
114 | userKeys: {
115 | tokenAccounts,
116 | owner: publicKey,
117 | },
118 | amountIn,
119 | amountOut: minAmountOut,
120 | fixedSide: "in"
121 | });
122 | const txid = await sendTransaction(transaction, connection, { signers, skipPreflight: true });
123 |
124 | setAlertHeading('Transaction sent');
125 | setAlertContent(`Check it at https://solscan.io/tx/${txid}`);
126 | setAlertType('success');
127 | setAlertShow(true);
128 | } catch (err: any) {
129 | console.error('tx failed => ', err);
130 | setAlertHeading('Something went wrong');
131 | if (err?.code && err?.message) {
132 | setAlertContent(`${err.code}: ${err.message}`)
133 | } else {
134 | setAlertContent(JSON.stringify(err));
135 | }
136 | setAlertType('danger');
137 | setAlertShow(true);
138 | }
139 | }
140 | };
141 |
142 | const handleSwitchDirection: MouseEventHandler = () => {
143 | const newDirection = !swapInDirection;
144 | setSwapInDirection(newDirection);
145 | };
146 |
147 | return (
148 |
232 | );
233 | }
234 |
235 | export default Main;
--------------------------------------------------------------------------------
/src/constant.ts:
--------------------------------------------------------------------------------
1 | export const SOL_IMG = 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png';
2 |
3 | export const RAY_IMG = 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R/logo.png';
4 |
5 | export const RAY_SOL_LP_V4_POOL_KEY = '89ZKE4aoyfLBe2RuV6jM3JGNhaV18Nxh8eNtjRcndBip'; // https://solscan.io/token/89ZKE4aoyfLBe2RuV6jM3JGNhaV18Nxh8eNtjRcndBip
6 |
7 | export const RAYDIUM_LIQUIDITY_JSON = 'https://api.raydium.io/v2/sdk/liquidity/mainnet.json';
8 |
9 | export const RAY_TOKEN_MINT = '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R';
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: 'DM Sans', 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
4 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 |
4 | import App from './App';
5 |
6 | import 'bootstrap/dist/css/bootstrap.css';
7 | import './index.css';
8 |
9 | const container = document.getElementById('root');
10 | const root = createRoot(container!); // createRoot(container!) if you use TypeScript
11 | root.render();
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 |
2 | import { Connection, PublicKey,} from "@solana/web3.js";
3 |
4 | import { LiquidityPoolKeys, Liquidity, TokenAmount, Token, Percent, TOKEN_PROGRAM_ID, SPL_ACCOUNT_LAYOUT, TokenAccount } from "@raydium-io/raydium-sdk";
5 |
6 | export async function getTokenAccountsByOwner(
7 | connection: Connection,
8 | owner: PublicKey,
9 | ) {
10 | const tokenResp = await connection.getTokenAccountsByOwner(
11 | owner,
12 | {
13 | programId: TOKEN_PROGRAM_ID
14 | },
15 | );
16 |
17 | const accounts: TokenAccount[] = [];
18 |
19 | for (const { pubkey, account } of tokenResp.value) {
20 | accounts.push({
21 | pubkey,
22 | accountInfo:SPL_ACCOUNT_LAYOUT.decode(account.data)
23 | });
24 | }
25 |
26 | return accounts;
27 | }
28 |
29 | /**
30 | * swapInDirection: used to determine the direction of the swap
31 | * Eg: RAY_SOL_LP_V4_POOL_KEY is using SOL as quote token, RAY as base token
32 | * If the swapInDirection is true, currencyIn is RAY and currencyOut is SOL
33 | * vice versa
34 | */
35 | export async function calcAmountOut(connection: Connection, poolKeys: LiquidityPoolKeys, rawAmountIn: number, swapInDirection: boolean) {
36 | const poolInfo = await Liquidity.fetchInfo({ connection, poolKeys });
37 | let currencyInMint = poolKeys.baseMint;
38 | let currencyInDecimals = poolInfo.baseDecimals;
39 | let currencyOutMint = poolKeys.quoteMint;
40 | let currencyOutDecimals = poolInfo.quoteDecimals;
41 |
42 | if (!swapInDirection) {
43 | currencyInMint = poolKeys.quoteMint;
44 | currencyInDecimals = poolInfo.quoteDecimals;
45 | currencyOutMint = poolKeys.baseMint;
46 | currencyOutDecimals = poolInfo.baseDecimals;
47 | }
48 |
49 | const currencyIn = new Token(currencyInMint, currencyInDecimals);
50 | const amountIn = new TokenAmount(currencyIn, rawAmountIn, false);
51 | const currencyOut = new Token(currencyOutMint, currencyOutDecimals);
52 | const slippage = new Percent(5, 100); // 5% slippage
53 |
54 | const {
55 | amountOut,
56 | minAmountOut,
57 | currentPrice,
58 | executionPrice,
59 | priceImpact,
60 | fee,
61 | } = Liquidity.computeAmountOut({ poolKeys, poolInfo, amountIn, currencyOut, slippage, });
62 |
63 | return {
64 | amountIn,
65 | amountOut,
66 | minAmountOut,
67 | currentPrice,
68 | executionPrice,
69 | priceImpact,
70 | fee,
71 | };
72 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "jsx": "react",
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "lib": [
8 | "dom",
9 | "esnext"
10 | ],
11 | "strict": true,
12 | "sourceMap": true,
13 | "target": "esnext",
14 | },
15 | "include": [
16 | "src"
17 | ],
18 | "exclude": [
19 | "node_modules"
20 | ]
21 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 | const prod = process.env.NODE_ENV === 'production';
4 |
5 | const HtmlWebpackPlugin = require('html-webpack-plugin');
6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
7 |
8 | module.exports = {
9 | mode: prod ? 'production' : 'development',
10 | entry: './src/index.tsx',
11 | output: {
12 | path: __dirname + '/build/',
13 | },
14 | module: {
15 | rules: [
16 | {
17 | test: /\.(ts|tsx)$/,
18 | exclude: /node_modules/,
19 | resolve: {
20 | extensions: ['.ts', '.tsx', '.js', '.json'],
21 | },
22 | use: 'ts-loader',
23 | },
24 | {
25 | test: /\.css$/,
26 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
27 | },
28 | ]
29 | },
30 | devtool: prod ? undefined : 'source-map',
31 | plugins: [
32 | new HtmlWebpackPlugin({
33 | template: 'public/index.html',
34 | }),
35 | new MiniCssExtractPlugin(),
36 | new webpack.ProvidePlugin({
37 | // Make a global `process` variable that points to the `process` package,
38 | // because the `util` package expects there to be a global variable named `process`.
39 | // Thanks to https://stackoverflow.com/a/65018686/14239942
40 | process: 'process/browser'
41 | }),
42 | ],
43 | resolve: {
44 | fallback: {
45 | "crypto": require.resolve("crypto-browserify"),
46 | "stream": require.resolve("stream-browserify"),
47 | "os": false, // "os": require.resolve("os-browserify/browser"),
48 | }
49 | },
50 | };
--------------------------------------------------------------------------------