SolMeet
Search…
BUIDL a Swap UI on Solana
[Updated at 2022.3.31]
See the example repo here
See the demo DApp here

TL; DR

  • Use Next.js + React.js + @solana/web3.js
  • Raydium AMM swap
  • Jupiter SDK swap

Introduction

  • What is Solana
    • Solana is a fast, low cost, decentralized blockchain with thousands of projects spanning DeFi, NFTs, Web3 and more.
  • What is Raydium
    • Raydium is an Automated Market Maker (AMM) and liquidity provider built on the Solana blockchain.
  • What is Jupiter
    • Jupiter is the key swap aggregator for Solana, offering the best route discovery between any token pair.

Overview

What does web3.js do?

  • web3.js library
  • Solana tx
  • Solana ix

How to find the program interface?

Structure

1
├── 📂 pages
2
│ │
3
│ ├── 📂 api
4
│ │
5
│ ├── 📄 _app.tsx
6
│ │
7
│ ├── 📄 index.tsx
8
│ │
9
│ ├── 📄 jupiter.tsx
10
│ │
11
│ └── 📄 raydium.tsx
12
13
└── 📂 views
14
│ │
15
│ ├── 📂 commons
16
│ │
17
│ ├── 📂 jupiter
18
│ │
19
│ └── 📂 raydium
20
21
├── 📂 utils
22
23
├── 📂 styles
24
25
├── 📂 chakra
26
│ │
27
│ └── 📄 style.js
28
29
├── 📂 public
30
31
│── 📄 next.config.js
32
33
└── ...
Copied!

Setup

First, let's start a brand new next.js project:
1
$ npx [email protected] solmeet-4-swap-ui --typescript
Copied!
Remove package-lock.json since we will use yarn through this entire tutorial:
1
$ rm package-lock.json
Copied!

Install Dependencies

Next, let's install all the dependencies. This includes:
  • Solana wallet adapter
  • Solana web3.js
  • Solana SPL token
  • Serum
  • Sass
  • Jupiter SDK
  • Next.js config plugins
  • Chakra (UI lib)
  • Lodash
Let's update package.json directly:
1
{
2
"name": "solmeet-4-swap-ui",
3
"private": true,
4
"scripts": {
5
"dev": "next dev",
6
"build": "next build",
7
"start": "next start",
8
"lint": "next lint"
9
},
10
"dependencies": {
11
"@chakra-ui/icons": "^1.1.1",
12
"@chakra-ui/react": "^1.7.4",
13
"@emotion/react": "^11",
14
"@emotion/styled": "^11",
15
"@jup-ag/react-hook": "^1.0.0-beta.2",
16
"@project-serum/borsh": "^0.2.3",
17
"@project-serum/serum": "^0.13.61",
18
"@solana/spl-token-registry": "^0.2.1733",
19
"@solana/wallet-adapter-base": "^0.9.2",
20
"@solana/wallet-adapter-react": "^0.15.2",
21
"@solana/wallet-adapter-react-ui": "^0.9.4",
22
"@solana/wallet-adapter-wallets": "^0.14.2",
23
"@solana/web3.js": "^1.32.0",
24
"framer-motion": "^5",
25
"lodash-es": "^4.17.21",
26
"next": "12.0.8",
27
"next-compose-plugins": "^2.2.1",
28
"next-transpile-modules": "^9.0.0",
29
"react": "17.0.2",
30
"react-dom": "17.0.2",
31
"sass": "^1.49.0"
32
},
33
"devDependencies": {
34
"@types/lodash-es": "^4.17.5",
35
"@types/node": "17.0.10",
36
"@types/react": "17.0.38",
37
"eslint": "8.7.0",
38
"eslint-config-next": "12.0.8",
39
"typescript": "4.5.5"
40
},
41
"resolutions": {
42
"@solana/buffer-layout": "^3.0.0"
43
}
44
}
Copied!
Then run the installation:
1
$ yarn
2
...
Copied!
Note: make sure the version of buffer-layout is locked at ^3.0.0

Scaffold

Populates folders and files for later update:
1
$ mkdir utils && touch utils/{ids.ts,layouts.ts,liquidity.ts,pools.ts,safe-math.ts,swap.ts,tokenList.ts,tokens.ts,web3.ts}
2
$ mkdir views && mkdir views/{commons,jupiter,raydium}
3
$ touch views/commons/{Navigator.tsx,WalletProvider.tsx,SplTokenList.tsx,Notify.tsx} && touch views/jupiter/{FeeInfo.tsx,JupiterForm.tsx,JupiterProvider.tsx} && touch views/raydium/{index.tsx,SlippageSetting.tsx,SwapOperateContainer.tsx,TokenList.tsx,TokenSelect.tsx,TitleRow.tsx}
4
$ touch styles/{swap.module.sass,color.module.sass,navigator.module.sass,jupiter.module.sass}
5
$ touch pages/{index.tsx,jupiter.tsx,raydium.tsx}
6
$ mkdir chakra && touch chakra/style.js
Copied!

Add Common Components

There are 4 common components:
  • Navigator
  • Notify
  • SplTokenList
  • WalletProvider
Add the following code in ./views/commons/Navigator.tsx:
1
import { FunctionComponent } from "react";
2
import Link from "next/link";
3
import {
4
WalletModalProvider,
5
WalletDisconnectButton,
6
WalletMultiButton
7
} from "@solana/wallet-adapter-react-ui";
8
import { useWallet } from "@solana/wallet-adapter-react";
9
import style from "../../styles/navigator.module.sass";
10
11
const Navigator: FunctionComponent = () => {
12
const wallet = useWallet();
13
return (
14
<div className={style.sidebar}>
15
<div className={style.routesBlock}>
16
<Link href="/" passHref>
17
<a href="https://ibb.co/yP2vCNL">
18
<img
19
src="https://i.ibb.co/g9Yq8rs/logo-v4-horizontal-transparent.png"
20
alt="logo-v4-horizontal-transparent"
21
className={style.dappioLogo}
22
/>
23
</a>
24
</Link>
25
<Link href="/jupiter">
26
<a className={style.route}>Jupiter</a>
27
</Link>
28
<Link href="/raydium">
29
<a className={style.route}>Raydium</a>
30
</Link>
31
</div>
32
<WalletModalProvider>
33
{wallet.connected ? <WalletDisconnectButton /> : <WalletMultiButton />}
34
</WalletModalProvider>
35
</div>
36
);
37
};
38
39
export default Navigator;
Copied!

Notify

Add the following code in ./views/commons/Notify.tsx:
1
import { FunctionComponent } from "react";
2
import {
3
Alert,
4
AlertIcon,
5
AlertTitle,
6
AlertDescription,
7
AlertStatus
8
} from "@chakra-ui/react";
9
import style from "../../styles/swap.module.sass";
10
11
export interface INotify {
12
status: AlertStatus;
13
title: string;
14
description: string;
15
link?: string;
16
}
17
interface NotifyProps {
18
message: {
19
status: AlertStatus;
20
title: string;
21
description: string;
22
link?: string;
23
};
24
}
25
26
const Notify: FunctionComponent<NotifyProps> = props => {
27
return (
28
<Alert status={props.message.status} className={style.notifyContainer}>
29
<div className={style.notifyTitleRow}>
30
<AlertIcon boxSize="2rem" />
31
<AlertTitle className={style.title}>{props.message.title}</AlertTitle>
32
</div>
33
<AlertDescription className={style.notifyDescription}>
34
{props.message.description}
35
</AlertDescription>
36
{props.message.link ? (
37
<a
38
href={props.message.link}
39
style={{ color: "#fbae21", textDecoration: "underline" }}
40
>
41
Check Explorer
42
</a>
43
) : (
44
""
45
)}
46
</Alert>
47
);
48
};
49
50
export default Notify;
Copied!

SplTokenList

Add the following code in ./views/commons/SplTokenList.tsx:
1
import { FunctionComponent } from "react";
2
import style from "../../styles/swap.module.sass";
3
import { TOKENS } from "../../utils/tokens";
4
import { ISplToken } from "../../utils/web3";
5
6
interface ISplTokenProps {
7
splTokenData: ISplToken[];
8
}
9
10
interface SplTokenDisplayData {
11
symbol: string;
12
mint: string;
13
pubkey: string;
14
amount: number;
15
}
16
17
const SplTokenList: FunctionComponent<ISplTokenProps> = (
18
props
19
): JSX.Element => {
20
let tokenList: SplTokenDisplayData[] = [];
21
if (props.splTokenData.length === 0) {
22
return <></>;
23
}
24
25
for (const [_, value] of Object.entries(TOKENS)) {
26
let spl: ISplToken | undefined = props.splTokenData.find(
27
(t: ISplToken) => t.parsedInfo.mint === value.mintAddress
28
);
29
if (spl) {
30
let token = {} as SplTokenDisplayData;
31
token["symbol"] = value.symbol;
32
token["mint"] = spl?.parsedInfo.mint;
33
token["pubkey"] = spl?.pubkey;
34
token["amount"] = spl?.amount;
35
tokenList.push(token);
36
}
37
}
38
39
let tokens = tokenList.map((item: SplTokenDisplayData) => {
40
return (
41
<div key={item.mint} className={style.splTokenItem}>
42
<div>
43
<span style={{ marginRight: "1rem", fontWeight: "600" }}>
44
{item.symbol}
45
</span>
46
<span>- {item.amount}</span>
47
</div>
48
<div style={{ opacity: ".25" }}>
49
<div>Mint: {item.mint}</div>
50
<div>Pubkey: {item.pubkey}</div>
51
</div>
52
</div>
53
);
54
});
55
56
return (
57
<div className={style.splTokenContainer}>
58
<div className={style.splTokenListTitle}>Your Tokens</div>
59
{tokens}
60
</div>
61
);
62
};
63
64
export default SplTokenList;
Copied!

WalletProvider

Add the following code in ./views/commons/WalletProvider.tsx:
1
import React, { FunctionComponent, useMemo } from "react";
2
import {
3
ConnectionProvider,
4
WalletProvider
5
} from "@solana/wallet-adapter-react";
6
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
7
import {
8
LedgerWalletAdapter,
9
PhantomWalletAdapter,
10
SlopeWalletAdapter,
11
SolflareWalletAdapter,
12
SolletExtensionWalletAdapter,
13
SolletWalletAdapter,
14
TorusWalletAdapter
15
} from "@solana/wallet-adapter-wallets";
16
import { clusterApiUrl } from "@solana/web3.js";
17
18
// Default styles that can be overridden by your app
19
require("@solana/wallet-adapter-react-ui/styles.css");
20
21
export const Wallet: FunctionComponent = props => {
22
// // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
23
const network = WalletAdapterNetwork.Mainnet;
24
25
// // You can also provide a custom RPC endpoint.
26
const endpoint = "https://rpc-mainnet-fork.dappio.xyz";
27
28
// @solana/wallet-adapter-wallets includes all the adapters but supports tree shaking and lazy loading --
29
// Only the wallets you configure here will be compiled into your application, and only the dependencies
30
// of wallets that your users connect to will be loaded.
31
const wallets = useMemo(
32
() => [
33
new PhantomWalletAdapter(),
34
new SlopeWalletAdapter(),
35
new SolflareWalletAdapter(),
36
new TorusWalletAdapter(),
37
new LedgerWalletAdapter(),
38
new SolletWalletAdapter({ network }),
39
new SolletExtensionWalletAdapter({ network })
40
],
41
[network]
42
);
43
44
return (
45
<ConnectionProvider endpoint={endpoint}>
46
<WalletProvider wallets={wallets} autoConnect>
47
{props.children}
48
</WalletProvider>
49
</ConnectionProvider>
50
);
51
};
Copied!

Add Pages for Raydium and Jupiter

Add the following code in ./pages/raydium.tsx:
1
import { FunctionComponent } from "react";
2
3
const RaydiumPage: FunctionComponent = () => {
4
return <div>This is Raydium Page</div>;
5
};
6
7
export default RaydiumPage;
Copied!
Add the following code in ./pages/jupiter.tsx:
1
import { FunctionComponent } from "react";
2
3
const JupiterPage: FunctionComponent = () => {
4
return <div>This is Jupiter Page</div>;
5
};
6
7
export default JupiterPage;
Copied!

Update Styles

Theere are 3 style sheets to be updated:
  • globals.css
  • navigator.module.sass
  • color.module.sass

globals.css

Add the following code in ./styles/globals.css:
1
html,
2
body {
3
font-size: 10px;
4
background-color: rgb(19, 27, 51);
5
color: #eee
6
}
7
8
.wallet-adapter-modal-list-more {
9
color: #eee
10
}
11
.wallet-adapter-button-trigger {
12
background-color: #fbae21 !important;
13
color: black !important
14
}
Copied!
Add the following code in ./styles/navigator.module.sass:
1
@import './color.module.sass'
2
3
.dappioLogo
4
flex: 2
5
text-align: center
6
width: 12rem
7
margin-right: 10rem
8
cursor: pointer
9
.sidebar
10
display: flex
11
align-items: center
12
font-size: 2rem
13
height: 7rem
14
border-bottom: 1px solid rgb(29, 40, 76)
15
background-color: $main_blue
16
padding: 0 4rem
17
justify-content: space-between
18
letter-spacing: .1rem
19
font-weight: 500
20
.routesBlock
21
display: flex
22
align-items: center
23
justify-content: space-around
24
color: $white
25
font-size: 1.5rem
26
.route
27
margin-right: 5rem
Copied!

color.module.sass

Add the following code in ./styles/color.module.sass:
1
$white: #eee
2
$main_blue: rgb(19, 27, 51)
3
$swap_card_bgc: #131a35
4
$coin_select_block_bgc: #000829
5
$placeholder_grey: #f1f1f2
6
$swap_btn_border_color: #5ac4be
7
$token_list_bgc: #1c274f
8
$slippage_setting_warning_red: #f5222d
Copied!

Update app

Replace pages/_app.tsx with following code:
1
import "../styles/globals.css";
2
import type { AppProps } from "next/app";
3
import { Wallet } from "../views/commons/WalletProvider";
4
import Navigator from "../views/commons/Navigator";
5
6
function SwapUI({ Component, pageProps }: AppProps) {
7
return (
8
<>
9
<Wallet>
10
<Navigator />
11
<Component {...pageProps} />
12
</Wallet>
13
</>
14
);
15
}
16
17
export default SwapUI;
Copied!
Start the dev server. For now you should see jupiter and raydium page with only plain text and one wallet connecting button:
1
$ yarn dev
Copied!

Part 1: Build a Swap on Raydium

What we need to implement Raydium swap?

  1. 1.
    Token list
  2. 2.
    Slippage setting
  3. 3.
    Price out
  4. 4.
    Amm pools info
  5. 5.
    Interact with on-chain program

Add Raydium Utils

Let's update each component one by one:

Add Components

We will update the following components:
  • SlippageSetting
  • SwapOperateContainer
  • TitleRow
  • TokenList
  • TokenSelect
  • index

TitleRow.tsx

Add the following code in ./views/raydium/TitleRow.tsx:
1
import style from "../../styles/swap.module.sass";
2
import {
3
Tooltip,
4
Popover,
5
PopoverTrigger,
6
PopoverContent,
7
PopoverBody,
8
PopoverArrow
9
} from "@chakra-ui/react";
10
import { SettingsIcon, InfoOutlineIcon } from "@chakra-ui/icons";
11
import { useState, useEffect, FunctionComponent } from "react";
12
import { TokenData, ITokenInfo } from ".";
13
14
interface ITitleProps {
15
toggleSlippageSetting: Function;
16
fromData: TokenData;
17
toData: TokenData;
18
updateSwapOutAmount: Function;
19
}
20
21
interface IAddressInfoProps {
22
type: string;
23
}
24
25
const TitleRow: FunctionComponent<ITitleProps> = (props): JSX.Element => {
26
const [second, setSecond] = useState<number>(0);
27
const [percentage, setPercentage] = useState<number>(0);
28
29
useEffect(() => {
30
let id = setInterval(() => {
31
setSecond(second + 1);
32
setPercentage((second * 100) / 60);
33
if (second === 60) {
34
setSecond(0);
35
props.updateSwapOutAmount();
36
}
37
}, 1000);
38
return () => clearInterval(id);
39
});
40
41
const AddressInfo: FunctionComponent<IAddressInfoProps> = (
42
addressProps
43
): JSX.Element => {
44
let fromToData = {} as ITokenInfo;
45
if (addressProps.type === "From") {
46
fromToData = props.fromData.tokenInfo;
47
} else {
48
fromToData = props.toData.tokenInfo;
49
}
50
51
return (
52
<>
53
<span className={style.symbol}>{fromToData?.symbol}</span>
54
<span className={style.address}>
55
<span>{fromToData?.mintAddress.substring(0, 14)}</span>
56
<span>{fromToData?.mintAddress ? "..." : ""}</span>
57
{fromToData?.mintAddress.substr(-14)}
58
</span>
59
</>
60
);
61
};
62
63
return (
64
<div className={style.titleContainer}>
65
<div className={style.title}>Swap</div>
66
<div className={style.iconContainer}>
67
<Tooltip
68
hasArrow
69
label={`Displayed data will auto-refresh after ${
70
60 - second
71
} seconds. Click this circle to update manually.`}
72
color="white"
73
bg="brand.100"
74
padding="3"
75
>
76
<svg
77
viewBox="0 0 36 36"
78
className={`${style.percentageCircle} ${style.icon}`}
79
>
80
<path
81
className={style.circleBg}
82
d="M18 2.0845
83
a 15.9155 15.9155 0 0 1 0 31.831
84
a 15.9155 15.9155 0 0 1 0 -31.831"
85
/>
86
<path
87
d="M18 2.0845
88
a 15.9155 15.9155 0 0 1 0 31.831
89
a 15.9155 15.9155 0 0 1 0 -31.831"
90
fill="none"
91
stroke="rgb(20, 120, 227)"
92
strokeWidth="3"
93
// @ts-ignore
94
strokeDasharray={[percentage, 100]}
95
/>
96
</svg>
97
</Tooltip>
98
<Popover trigger="hover">
99
<PopoverTrigger>
100
<div className={style.icon}>
101
<InfoOutlineIcon w={18} h={18} />
102
</div>
103
</PopoverTrigger>
104
<PopoverContent
105
color="white"
106
bg="brand.100"
107
border="none"
108
w="auto"
109
className={style.popover}
110
>
111
<PopoverArrow bg="brand.100" className={style.popover} />
112
<PopoverBody>
113
<div className={style.selectTokenAddressTitle}>
114
Program Addresses (DO NOT DEPOSIT)
115
</div>
116
<div className={style.selectTokenAddress}>
117
{props.fromData.tokenInfo?.symbol ? (
118
<AddressInfo type="From" />
119
) : (
120
""
121
)}
122
</div>
123
<div className={style.selectTokenAddress}>
124
{props.toData.tokenInfo?.symbol ? (
125
<AddressInfo type="To" />
126
) : (
127
""
128
)}
129
</div>
130
</PopoverBody>
131
</PopoverContent>
132
</Popover>
133
<div
134
className={style.icon}
135
onClick={() => props.toggleSlippageSetting()}
136
>
137
<SettingsIcon w={18} h={18} />
138
</div>
139
</div>
140
</div>
141
);
142
};
143
144
export default TitleRow;
Copied!

TokenList.tsx

Add the following code in ./views/raydium/TokenList.tsx:
1
import { FunctionComponent, useEffect, useRef, useState } from "react";
2
import { CloseIcon } from "@chakra-ui/icons";
3
import SPLTokenRegistrySource from "../../utils/tokenList";
4
import { TOKENS } from "../../utils/tokens";
5
import { ITokenInfo } from ".";
6
import style from "../../styles/swap.module.sass";
7
8
interface TokenListProps {
9
showTokenList: boolean;
10
toggleTokenList: (event?: React.MouseEvent<HTMLDivElement>) => void;
11
getTokenInfo: Function;
12
}
13
14
const TokenList: FunctionComponent<TokenListProps> = props => {
15
const [initialList, setList] = useState<ITokenInfo[]>([]);
16
const [searchedList, setSearchList] = useState<ITokenInfo[]>([]);
17
const searchRef = useRef<any>();
18
19
useEffect(() => {
20
SPLTokenRegistrySource().then((res: any) => {
21
let list: ITokenInfo[] = [];
22
res.map((item: any) => {
23
let token = {} as ITokenInfo;
24
if (
25
TOKENS[item.symbol] &&
26
!list.find(
27
(t: ITokenInfo) => t.mintAddress === TOKENS[item.symbol].mintAddress
28
)
29
) {
30
token = TOKENS[item.symbol];
31
token["logoURI"] = item.logoURI;
32
list.push(token);
33
}
34
});
35
setList(() => list);
36
props.getTokenInfo(
37
list.find((item: ITokenInfo) => item.symbol === "SOL")
38
);
39
});
40
}, []);
41
42
useEffect(() => {
43
setSearchList(() => initialList);
44
}, [initialList]);
45
46
const setTokenInfo = (item: ITokenInfo) => {
47
props.getTokenInfo(item);
48
props.toggleTokenList();
49
};
50
51
useEffect(() => {
52
if (!props.showTokenList) {
53
setSearchList(initialList);
54
searchRef.current.value = "";
55
}
56
}, [props.showTokenList]);
57
58
const listItems = (data: ITokenInfo[]) => {
59
return data.map((item: ITokenInfo) => {
60
return (
61
<div
62
className={style.tokenRow}
63
key={item.mintAddress}
64
onClick={() => setTokenInfo(item)}
65
>
66
<img src={item.logoURI} alt="" className={style.tokenLogo} />
67
<div>{item.symbol}</div>
68
</div>
69
);
70
});
71
};
72
73
const searchToken = (e: any) => {
74
let key = e.target.value.toUpperCase();
75
let newList: ITokenInfo[] = [];
76
initialList.map((item: ITokenInfo) => {
77
if (item.symbol.includes(key)) {
78
newList.push(item);
79
}
80
});
81
setSearchList(() => newList);
82
};
83
84
let tokeListComponentStyle;
85
if (!props.showTokenList) {
86
tokeListComponentStyle = {
87
display: "none"
88
};
89
} else {
90
tokeListComponentStyle = {
91
display: "block"
92
};
93
}
94
95
return (
96
<div className={style.tokeListComponent} style={tokeListComponentStyle}>
97
<div className={style.tokeListContainer}>
98
<div className={style.header}>
99
<div>Select a token</div>
100
<div className={style.closeIcon} onClick={props.toggleTokenList}>
101
<CloseIcon w={5} h={5} />
102
</div>
103
</div>
104
<div className={style.inputBlock}>
105
<input
106
type="text"
107
placeholder="Search name or mint address"
108
ref={searchRef}
109
className={style.searchTokenInput}
110
onChange={searchToken}
111
/>
112
<div className={style.tokenListTitleRow}>
113
<div>Token name</div>
114
</div>
115
</div>
116
<div className={style.list}>{listItems(searchedList)}</div>
117
<div className={style.tokenListSetting}>View Token List</div>
118
</div>
119
</div>
120
);
121
};
122
123
export default TokenList;
Copied!

SlippageSetting.tsx

Add the following code in ./views/raydium/SlippageSetting.tsx:
1
import { useState, useEffect, FunctionComponent } from "react";
2
import { CloseIcon } from "@chakra-ui/icons";
3
import style from "../../styles/swap.module.sass";
4
5
interface SlippageSettingProps {
6
showSlippageSetting: boolean;
7
toggleSlippageSetting: Function;
8
getSlippageValue: Function;
9
slippageValue: number;
10
}
11
12
const SlippageSetting: FunctionComponent<SlippageSettingProps> = props => {
13
const rate = [0.1, 0.5, 1];
14
const [warningText, setWarningText] = useState("");
15
16
const setSlippageBtn = (item: number) => {
17
props.getSlippageValue(item);
18
};
19
20
useEffect(() => {
21
Options();
22
23
if (props.slippageValue < 0) {
24
setWarningText("Please enter a valid slippage percentage");
25
} else if (props.slippageValue < 1) {
26
setWarningText("Your transaction may fail");
27
} else {
28
setWarningText("");
29
}
30
}, [props.slippageValue]);
31
32
const Options = (): JSX.Element => {
33
return (
34
<>
35
{rate.map(item => {
36
return (
37
<button
38
className={`${style.optionBtn} ${
39
item === props.slippageValue
40
? style.selectedSlippageRateBtn
41
: ""
42
}`}
43
key={item}
44
onClick={() => setSlippageBtn(item)}
45
>
46
{item}%
47
</button>
48
);
49
})}
50
</>
51
);
52
};
53
54
const updateInputRate = (e: React.FormEvent<HTMLInputElement>) => {
55
props.getSlippageValue(e.currentTarget.value);
56
};
57
58
const close = () => {
59
if (props.slippageValue < 0) {
60
return;
61
}
62
props.toggleSlippageSetting();
63
};
64
65
if (!props.showSlippageSetting) {
66
return null;
67
}
68
69
return (
70
<div className={style.slippageSettingComponent}>
71
<div className={style.slippageSettingContainer}>
72
<div className={style.header}>
73
<div>Setting</div>
74
<div className={style.closeIcon} onClick={close}>
75
<CloseIcon w={5} h={5} />
76
</div>
77
</div>
78
<div className={style.settingSelectBlock}>
79
<div className={style.title}>Slippage tolerance</div>
80
<div className={style.optionsBlock}>
81
<Options />
82
<button className={`${style.optionBtn} ${style.inputBtn}`}>
83
<input
84
type="number"
85
placeholder="0%"
86
className={style.input}
87
value={props.slippageValue}
88
onChange={updateInputRate}
89
/>
90
%
91
</button>
92
</div>
93
<div className={style.warning}>{warningText}</div>
94
</div>
95
</div>
96
</div>
97
);
98
};
99
100
export default SlippageSetting;
Copied!

TokenSelect.tsx

Add the following code in ./views/raydium/TokenSelect.tsx:
1
import { FunctionComponent, useEffect, useState } from "react";
2
import { ArrowDownIcon } from "@chakra-ui/icons";
3
import { useWallet } from "@solana/wallet-adapter-react";
4
import { TokenData } from "./index";
5
import { ISplToken } from "../../utils/web3";
6
import style from "../../styles/swap.module.sass";
7
8
interface TokenSelectProps {
9
type: string;
10
toggleTokenList: Function;
11
tokenData: TokenData;
12
updateAmount: Function;
13
wallet: Object;
14
splTokenData: ISplToken[];
15
}
16
17
export interface IUpdateAmountData {
18
type: string;
19
amount: number;
20
}
21
22
interface SelectTokenProps {
23
propsData: {
24
tokenData: TokenData;
25
};
26
}
27
28
const TokenSelect: FunctionComponent<TokenSelectProps> = props => {
29
let wallet = useWallet();
30
const [tokenBalance, setTokenBalance] = useState<number | null>(null);
31
32
const updateAmount = (e: any) => {
33
e.preventDefault();
34
35
const amountData: IUpdateAmountData = {
36
amount: e.target.value,
37
type: props.type
38
};
39
props.updateAmount(amountData);
40
};
41
42
const selectToken = () => {
43
props.toggleTokenList(props.type);
44
};
45
46
useEffect(() => {
47
const getTokenBalance = () => {
48
let data: ISplToken | undefined = props.splTokenData.find(
49
(t: ISplToken) =>
50
t.parsedInfo.mint === props.tokenData.tokenInfo?.mintAddress
51
);
52
53
if (data) {
54
//@ts-ignore
55
setTokenBalance(data.amount);
56
}
57
};
58
getTokenBalance();
59
}, [props.splTokenData]);
60
61
const SelectTokenBtn: FunctionComponent<
62
SelectTokenProps
63
> = selectTokenProps => {
64
if (selectTokenProps.propsData.tokenData.tokenInfo?.symbol) {
65
return (
66
<>
67
<img
68
src={selectTokenProps.propsData.tokenData.tokenInfo?.logoURI}
69
alt="logo"
70
className={style.img}
71
/>
72
<div className={style.coinNameBlock}>
73
<span className={style.coinName}>
74
{selectTokenProps.propsData.tokenData.tokenInfo?.symbol}
75
</span>
76
<ArrowDownIcon w={5} h={5} />
77
</div>
78
</>
79
);
80
}
81
return (
82
<>
83
<span>Select a token</span>
84
<ArrowDownIcon w={5} h={5} />
85
</>
86
);
87
};
88
89
return (
90
<div className={style.coinSelect}>
91
<div className={style.noteText}>
92
<div>
93
{props.type === "To" ? `${props.type} (Estimate)` : props.type}
94
</div>
95
<div>
96
{wallet.connected && tokenBalance
97
? `Balance: ${tokenBalance.toFixed(4)}`
98
: ""}
99
</div>
100
</div>
101
<div className={style.coinAmountRow}>
102
{props.type !== "From" ? (
103
<div className={style.input}>
104
{props.tokenData.amount ? props.tokenData.amount : "-"}
105
</div>
106
) : (
107
<input
108
type="number"
109
className={style.input}
110
placeholder="0.00"
111
onChange={updateAmount}
112
disabled={props.type !== "From"}
113
/>
114
)}
115
116
<div className={style.selectTokenBtn} onClick={selectToken}>
117
<SelectTokenBtn propsData={props} />
118
</div>
119
</div>
120
</div>
121
);
122
};
123
124
export default TokenSelect;
Copied!

SwapOperateContainer.tsx

Add the following code in ./views/raydium/SwapOperateContainer.tsx:
1
import { FunctionComponent } from "react";
2
import { ArrowUpDownIcon, QuestionOutlineIcon } from "@chakra-ui/icons";
3
import { Tooltip } from "@chakra-ui/react";
4
import { useWallet } from "@solana/wallet-adapter-react";
5
import {
6
WalletModalProvider,
7
WalletMultiButton
8
} from "@solana/wallet-adapter-react-ui";
9
import { TokenData } from ".";
10
import TokenSelect from "./TokenSelect";
11
import { ISplToken } from "../../utils/web3";
12
import style from "../../styles/swap.module.sass";
13
14
interface SwapOperateContainerProps {
15
toggleTokenList: Function;
16
fromData: TokenData;
17
toData: TokenData;
18
updateAmount: Function;
19
switchFromAndTo: (event?: React.MouseEvent<HTMLDivElement>) => void;
20
slippageValue: number;
21
sendSwapTransaction: (event?: React.MouseEvent<HTMLButtonElement>) => void;
22
splTokenData: ISplToken[];
23
}
24
25
interface SwapDetailProps {
26
title: string;
27
tooltipContent: string;
28
value: string;
29
}
30
31
const SwapOperateContainer: FunctionComponent<
32
SwapOperateContainerProps
33
> = props => {
34
let wallet = useWallet();
35
const SwapBtn = (swapProps: any) => {
36
if (wallet.connected) {
37
if (
38
!swapProps.props.fromData.tokenInfo?.symbol ||
39
!swapProps.props.toData.tokenInfo?.symbol
40
) {
41
return (
42
<button
43
className={`${style.operateBtn} ${style.disabledBtn}`}
44
disabled
45
>
46
Select a token
47
</button>
48
);
49
}
50
if (
51
swapProps.props.fromData.tokenInfo?.symbol &&
52
swapProps.props.toData.tokenInfo?.symbol
53
) {
54
if (
55
!swapProps.props.fromData.amount ||
56
!swapProps.props.toData.amount
57
) {
58
return (
59
<button
60
className={`${style.operateBtn} ${style.disabledBtn}`}
61
disabled
62
>
63
Enter an amount
64
</button>
65
);
66
}
67
}
68
69
return (
70
<button
71
className={style.operateBtn}
72
onClick={props.sendSwapTransaction}
73
>
74
Swap
75
</button>
76
);
77
} else {
78
return (
79
<div className={style.selectWallet}>
80
<WalletModalProvider>
81
<WalletMultiButton />
82
</WalletModalProvider>
83
</div>
84
);
85
}
86
};
87
88
const SwapDetailPreview: FunctionComponent<SwapDetailProps> = props => {
89
return (
90
<div className={style.slippageRow}>
91
<div className={style.slippageTooltipBlock}>
92
<div>{props.title}</div>
93
<Tooltip
94
hasArrow
95
label={props.tooltipContent}
96
color="white"
97
bg="brand.100"
98
padding="3"
99
>
100
<QuestionOutlineIcon
101
w={5}
102
h={5}
103
className={`${style.icon} ${style.icon}`}
104
/>
105
</Tooltip>
106
</div>
107
<div>{props.value}</div>
108
</div>
109
);
110
};
111
112
const SwapDetailPreviewList = (): JSX.Element => {
113
return (
114
<>
115
<SwapDetailPreview
116
title="Swapping Through"
117
tooltipContent="This venue gave the best price for your trade"
118
value={`${props.fromData.tokenInfo.symbol} > ${props.toData.tokenInfo.symbol}`}
119
/>
120
</>
121
);
122
};
123
124
return (
125
<div className={style.swapCard}>
126
<div className={style.cardBody}>
127
<TokenSelect
128
type="From"
129
toggleTokenList={props.toggleTokenList}
130
tokenData={props.fromData}
131
updateAmount={props.updateAmount}
132
wallet={wallet}
133
splTokenData={props.splTokenData}
134
/>
135
<div
136
className={`${style.switchIcon} ${style.icon}`}
137
onClick={props.switchFromAndTo}
138
>
139
<ArrowUpDownIcon w={5} h={5} />
140
</div>
141
<TokenSelect
142
type="To"
143
toggleTokenList={props.toggleTokenList}
144
tokenData={props.toData}
145
updateAmount={props.updateAmount}
146
wallet={wallet}
147
splTokenData={props.splTokenData}
148
/>
149
<div className={style.slippageRow}>
150
<div className={style.slippageTooltipBlock}>
151
<div>Slippage Tolerance </div>
152
<Tooltip
153
hasArrow
154
label="The maximum difference between your estimated price and execution price."
155
color="white"
156
bg="brand.100"
157
padding="3"
158
>
159
<QuestionOutlineIcon
160
w={5}
161
h={5}
162
className={`${style.icon} ${style.icon}`}
163
/>
164
</Tooltip>
165
</div>
166
<div>{props.slippageValue}%</div>
167
</div>
168
{props.fromData.amount! > 0 &&
169
props.fromData.tokenInfo.symbol &&
170
props.toData.amount! > 0 &&
171
props.toData.tokenInfo.symbol ? (
172
<SwapDetailPreviewList />
173
) : (
174
""
175
)}
176
<SwapBtn props={props} />
177
</div>
178
</div>
179
);
180
};
181
182
export default SwapOperateContainer;
Copied!

index

Add the following code in ./views/raydium/index.tsx:
1
import { useState, useEffect, FunctionComponent } from "react";
2
import TokenList from "./TokenList";
3
import TitleRow from "./TitleRow";
4
import SlippageSetting from "./SlippageSetting";
5
import SwapOperateContainer from "./SwapOperateContainer";
6
import { Connection } from "@solana/web3.js";
7
import { Spinner } from "@chakra-ui/react";
8
import { useWallet, WalletContextState } from "@solana/wallet-adapter-react";
9
import { getPoolByTokenMintAddresses } from "../../utils/pools";
10
import { swap, getSwapOutAmount, setupPools } from "../../utils/swap";
11
import { getSPLTokenData } from "../../utils/web3";
12
import Notify from "../commons/Notify";
13
import { INotify } from "../commons/Notify";
14
import SplTokenList from "../commons/SplTokenList";
15
import { ISplToken } from "../../utils/web3";
16
import { IUpdateAmountData } from "./TokenSelect";
17
import style from "../../styles/swap.module.sass";
18
19
export interface ITokenInfo {
20
symbol: string;
21
mintAddress: string;
22
logoURI: string;
23
}
24
export interface TokenData {
25
amount: number | null;
26
tokenInfo: ITokenInfo;
27
}
28
29
const SwapPage: FunctionComponent = () => {
30
const [showTokenList, setShowTokenList] = useState(false);
31
const [showSlippageSetting, setShowSlippageSetting] = useState(false);
32
const [selectType, setSelectType] = useState<string>("From");
33
const [fromData, setFromData] = useState<TokenData>({} as TokenData);
34
const [toData, setToData] = useState<TokenData>({} as TokenData);
35
const [slippageValue, setSlippageValue] = useState(1);
36
const [splTokenData, setSplTokenData] = useState<ISplToken[]>([]);
37
const [liquidityPools, setLiquidityPools] = useState<any>("");
38
const [isLoading, setIsLoading] = useState<boolean>(false);
39
const [notify, setNotify] = useState<INotify>({
40
status: "info",
41
title: "",
42
description: "",
43
link: ""
44
});
45
const [showNotify, toggleNotify] = useState<Boolean>(false);
46
47
let wallet: WalletContextState = useWallet();
48
const connection = new Connection("https://rpc-mainnet-fork.dappio.xyz", {
49
wsEndpoint: "wss://rpc-mainnet-fork.dappio.xyz/ws",
50
commitment: "processed"
51
});
52
53
useEffect(() => {
54
setIsLoading(true);
55
setupPools(connection).then(data => {
56
setLiquidityPools(data);
57
setIsLoading(false);
58
});
59
return () => {
60
setLiquidityPools("");
61
};
62
}, []);
63
64
useEffect(() => {
65
if (wallet.connected) {
66
getSPLTokenData(wallet, connection).then((tokenList: ISplToken[]) => {
67
if (tokenList) {
68
setSplTokenData(() => tokenList.filter(t => t !== undefined));
69
}
70
});
71
}
72
}, [wallet.connected]);
73
74
const updateAmount = (e: IUpdateAmountData) => {
75
if (e.type === "From") {
76
setFromData((old: TokenData) => ({
77
...old,
78
amount: e.amount
79
}));
80
81
if (!e.amount) {
82
setToData((old: TokenData) => ({
83
...old,
84
amount: 0
85
}));
86
}
87
}
88
};