SolMeet
Search…
⌃K

#4 - 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

├── 📂 pages
│ │
│ ├── 📂 api
│ │
│ ├── 📄 _app.tsx
│ │
│ ├── 📄 index.tsx
│ │
│ ├── 📄 jupiter.tsx
│ │
│ └── 📄 raydium.tsx
└── 📂 views
│ │
│ ├── 📂 commons
│ │
│ ├── 📂 jupiter
│ │
│ └── 📂 raydium
├── 📂 utils
├── 📂 styles
├── 📂 chakra
│ │
│ └── 📄 style.js
├── 📂 public
│── 📄 next.config.js
└── ...

Setup

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

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:
{
"name": "solmeet-4-swap-ui",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@chakra-ui/icons": "^1.1.1",
"@chakra-ui/react": "^1.7.4",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@jup-ag/react-hook": "^1.0.0-beta.2",
"@project-serum/borsh": "^0.2.3",
"@project-serum/serum": "^0.13.61",
"@solana/spl-token-registry": "^0.2.1733",
"@solana/wallet-adapter-base": "^0.9.2",
"@solana/wallet-adapter-react": "^0.15.2",
"@solana/wallet-adapter-react-ui": "^0.9.4",
"@solana/wallet-adapter-wallets": "^0.14.2",
"@solana/web3.js": "^1.32.0",
"framer-motion": "^5",
"lodash-es": "^4.17.21",
"next": "12.0.8",
"next-compose-plugins": "^2.2.1",
"next-transpile-modules": "^9.0.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"sass": "^1.49.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.5",
"@types/node": "17.0.10",
"@types/react": "17.0.38",
"eslint": "8.7.0",
"eslint-config-next": "12.0.8",
"typescript": "4.5.5"
},
"resolutions": {
"@solana/buffer-layout": "^3.0.0"
}
}
Then run the installation:
$ yarn
...
Note: make sure the version of buffer-layout is locked at ^3.0.0

Scaffold

Populates folders and files for later update:
$ mkdir utils && touch utils/{ids.ts,layouts.ts,liquidity.ts,pools.ts,safe-math.ts,swap.ts,tokenList.ts,tokens.ts,web3.ts}
$ mkdir views && mkdir views/{commons,jupiter,raydium}
$ 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}
$ touch styles/{swap.module.sass,color.module.sass,navigator.module.sass,jupiter.module.sass}
$ touch pages/{index.tsx,jupiter.tsx,raydium.tsx}
$ mkdir chakra && touch chakra/style.js

Add Common Components

There are 4 common components:
  • Navigator
  • Notify
  • SplTokenList
  • WalletProvider
Add the following code in ./views/commons/Navigator.tsx:
import { FunctionComponent } from "react";
import Link from "next/link";
import {
WalletModalProvider,
WalletDisconnectButton,
WalletMultiButton
} from "@solana/wallet-adapter-react-ui";
import { useWallet } from "@solana/wallet-adapter-react";
import style from "../../styles/navigator.module.sass";
const Navigator: FunctionComponent = () => {
const wallet = useWallet();
return (
<div className={style.sidebar}>
<div className={style.routesBlock}>
<Link href="/" passHref>
<a href="https://ibb.co/yP2vCNL">
<img
src="https://i.ibb.co/g9Yq8rs/logo-v4-horizontal-transparent.png"
alt="logo-v4-horizontal-transparent"
className={style.dappioLogo}
/>
</a>
</Link>
<Link href="/jupiter">
<a className={style.route}>Jupiter</a>
</Link>
<Link href="/raydium">
<a className={style.route}>Raydium</a>
</Link>
</div>
<WalletModalProvider>
{wallet.connected ? <WalletDisconnectButton /> : <WalletMultiButton />}
</WalletModalProvider>
</div>
);
};
export default Navigator;

Notify

Add the following code in ./views/commons/Notify.tsx:
import { FunctionComponent } from "react";
import {
Alert,
AlertIcon,
AlertTitle,
AlertDescription,
AlertStatus
} from "@chakra-ui/react";
import style from "../../styles/swap.module.sass";
export interface INotify {
status: AlertStatus;
title: string;
description: string;
link?: string;
}
interface NotifyProps {
message: {
status: AlertStatus;
title: string;
description: string;
link?: string;
};
}
const Notify: FunctionComponent<NotifyProps> = props => {
return (
<Alert status={props.message.status} className={style.notifyContainer}>
<div className={style.notifyTitleRow}>
<AlertIcon boxSize="2rem" />
<AlertTitle className={style.title}>{props.message.title}</AlertTitle>
</div>
<AlertDescription className={style.notifyDescription}>
{props.message.description}
</AlertDescription>
{props.message.link ? (
<a
href={props.message.link}
style={{ color: "#fbae21", textDecoration: "underline" }}
>
Check Explorer
</a>
) : (
""
)}
</Alert>
);
};
export default Notify;

SplTokenList

Add the following code in ./views/commons/SplTokenList.tsx:
import { FunctionComponent } from "react";
import style from "../../styles/swap.module.sass";
import { TOKENS } from "../../utils/tokens";
import { ISplToken } from "../../utils/web3";
interface ISplTokenProps {
splTokenData: ISplToken[];
}
interface SplTokenDisplayData {
symbol: string;
mint: string;
pubkey: string;
amount: number;
}
const SplTokenList: FunctionComponent<ISplTokenProps> = (
props
): JSX.Element => {
let tokenList: SplTokenDisplayData[] = [];
if (props.splTokenData.length === 0) {
return <></>;
}
for (const [_, value] of Object.entries(TOKENS)) {
let spl: ISplToken | undefined = props.splTokenData.find(
(t: ISplToken) => t.parsedInfo.mint === value.mintAddress
);
if (spl) {
let token = {} as SplTokenDisplayData;
token["symbol"] = value.symbol;
token["mint"] = spl?.parsedInfo.mint;
token["pubkey"] = spl?.pubkey;
token["amount"] = spl?.amount;
tokenList.push(token);
}
}
let tokens = tokenList.map((item: SplTokenDisplayData) => {
return (
<div key={item.mint} className={style.splTokenItem}>
<div>
<span style={{ marginRight: "1rem", fontWeight: "600" }}>
{item.symbol}
</span>
<span>- {item.amount}</span>
</div>
<div style={{ opacity: ".25" }}>
<div>Mint: {item.mint}</div>
<div>Pubkey: {item.pubkey}</div>
</div>
</div>
);
});
return (
<div className={style.splTokenContainer}>
<div className={style.splTokenListTitle}>Your Tokens</div>
{tokens}
</div>
);
};
export default SplTokenList;

WalletProvider

Add the following code in ./views/commons/WalletProvider.tsx:
import React, { FunctionComponent, useMemo } from "react";
import {
ConnectionProvider,
WalletProvider
} from "@solana/wallet-adapter-react";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import {
LedgerWalletAdapter,
PhantomWalletAdapter,
SlopeWalletAdapter,
SolflareWalletAdapter,
SolletExtensionWalletAdapter,
SolletWalletAdapter,
TorusWalletAdapter
} from "@solana/wallet-adapter-wallets";
import { clusterApiUrl } from "@solana/web3.js";
// Default styles that can be overridden by your app
require("@solana/wallet-adapter-react-ui/styles.css");
export const Wallet: FunctionComponent = props => {
// // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
const network = WalletAdapterNetwork.Mainnet;
// // You can also provide a custom RPC endpoint.
const endpoint = "https://rpc-mainnet-fork.dappio.xyz";
// @solana/wallet-adapter-wallets includes all the adapters but supports tree shaking and lazy loading --
// Only the wallets you configure here will be compiled into your application, and only the dependencies
// of wallets that your users connect to will be loaded.
const wallets = useMemo(
() => [
new PhantomWalletAdapter(),
new SlopeWalletAdapter(),
new SolflareWalletAdapter(),
new TorusWalletAdapter(),
new LedgerWalletAdapter(),
new SolletWalletAdapter({ network }),
new SolletExtensionWalletAdapter({ network })
],
[network]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
{props.children}
</WalletProvider>
</ConnectionProvider>
);
};

Add Pages for Raydium and Jupiter

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

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:
html,
body {
font-size: 10px;
background-color: rgb(19, 27, 51);
color: #eee
}
.wallet-adapter-modal-list-more {
color: #eee
}
.wallet-adapter-button-trigger {
background-color: #fbae21 !important;
color: black !important
}
Add the following code in ./styles/navigator.module.sass:
@import './color.module.sass'
.dappioLogo
flex: 2
text-align: center
width: 12rem
margin-right: 10rem
cursor: pointer
.sidebar
display: flex
align-items: center
font-size: 2rem
height: 7rem
border-bottom: 1px solid rgb(29, 40, 76)
background-color: $main_blue
padding: 0 4rem
justify-content: space-between
letter-spacing: .1rem
font-weight: 500
.routesBlock
display: flex
align-items: center
justify-content: space-around
color: $white
font-size: 1.5rem
.route
margin-right: 5rem

color.module.sass

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

Update app

Replace pages/_app.tsx with following code:
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { Wallet } from "../views/commons/WalletProvider";
import Navigator from "../views/commons/Navigator";
function SwapUI({ Component, pageProps }: AppProps) {
return (
<>
<Wallet>
<Navigator />
<Component {...pageProps} />
</Wallet>
</>
);
}
export default SwapUI;
Start the dev server. For now you should see jupiter and raydium page with only plain text and one wallet connecting button:
$ yarn dev

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:
import style from "../../styles/swap.module.sass";
import {
Tooltip,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
PopoverArrow
} from "@chakra-ui/react";
import { SettingsIcon, InfoOutlineIcon } from "@chakra-ui/icons";
import { useState, useEffect, FunctionComponent } from "react";
import { TokenData, ITokenInfo } from ".";
interface ITitleProps {
toggleSlippageSetting: Function;
fromData: TokenData;
toData: TokenData;
updateSwapOutAmount: Function;
}
interface IAddressInfoProps {
type: string;
}
const TitleRow: FunctionComponent<ITitleProps> = (props): JSX.Element => {
const [second, setSecond] = useState<number>(0);
const [percentage, setPercentage] = useState<number>(0);
useEffect(() => {
let id = setInterval(() => {
setSecond(second + 1);
setPercentage((second * 100) / 60);
if (second === 60) {
setSecond(0);
props.updateSwapOutAmount();
}
}, 1000);
return () => clearInterval(id);
});
const AddressInfo: FunctionComponent<IAddressInfoProps> = (
addressProps
): JSX.Element => {
let fromToData = {} as ITokenInfo;
if (addressProps.type === "From") {
fromToData = props.fromData.tokenInfo;
} else {
fromToData = props.toData.tokenInfo;
}
return (
<>
<span className={style.symbol}>{fromToData?.symbol}</span>
<span className={style.address}>
<span>{fromToData?.mintAddress.substring(0, 14)}</span>
<span>{fromToData?.mintAddress ? "..." : ""}</span>
{fromToData?.mintAddress.substr(-14)}
</span>
</>
);
};
return (
<div className={style.titleContainer}>
<div className={style.title}>Swap</div>
<div className={style.iconContainer}>
<Tooltip
hasArrow
label={`Displayed data will auto-refresh after ${
60 - second
} seconds. Click this circle to update manually.`}
color="white"
bg="brand.100"
padding="3"
>
<svg
viewBox="0 0 36 36"
className={`${style.percentageCircle} ${style.icon}`}
>
<path
className={style.circleBg}
d="M18 2.0845
a 15.9155 15.9155 0 0 1 0 31.831
a 15.9155 15.9155 0 0 1 0 -31.831"
/>
<path
d="M18 2.0845
a 15.9155 15.9155 0 0 1 0 31.831
a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="rgb(20, 120, 227)"
strokeWidth="3"
// @ts-ignore
strokeDasharray={[percentage, 100]}
/>
</svg>
</Tooltip>
<Popover trigger="hover">
<PopoverTrigger>
<div className={style.icon}>
<InfoOutlineIcon w={18} h={18} />
</div>
</PopoverTrigger>
<PopoverContent
color="white"
bg="brand.100"
border="none"
w="auto"
className={style.popover}
>
<PopoverArrow bg="brand.100" className={style.popover} />
<PopoverBody>
<div className={style.selectTokenAddressTitle}>
Program Addresses (DO NOT DEPOSIT)
</div>
<div className={style.selectTokenAddress}>
{props.fromData.tokenInfo?.symbol ? (
<AddressInfo type="From" />
) : (
""
)}
</div>
<div className={style.selectTokenAddress}>
{props.toData.tokenInfo?.symbol ? (
<AddressInfo type="To" />
) : (
""
)}
</div>
</PopoverBody>
</PopoverContent>
</Popover>
<div
className={style.icon}
onClick={() => props.toggleSlippageSetting()}
>
<SettingsIcon w={18} h={18} />
</div>
</div>
</div>
);
};
export default TitleRow;

TokenList.tsx

Add the following code in ./views/raydium/TokenList.tsx:
import { FunctionComponent, useEffect, useRef, useState } from "react";
import { CloseIcon } from "@chakra-ui/icons";
import SPLTokenRegistrySource from "../../utils/tokenList";
import { TOKENS } from "../../utils/tokens";
import { ITokenInfo } from ".";
import style from "../../styles/swap.module.sass";
interface TokenListProps {
showTokenList: boolean;
toggleTokenList: (event?: React.MouseEvent<HTMLDivElement>) => void;
getTokenInfo: Function;
}
const TokenList: FunctionComponent<TokenListProps> = props => {
const [initialList, setList] = useState<ITokenInfo[]>([]);
const [searchedList, setSearchList] = useState<ITokenInfo[]>([]);
const searchRef = useRef<any>();
useEffect(() => {
SPLTokenRegistrySource().then((res: any) => {
let list: ITokenInfo[] = [];
res.map((item: any) => {
let token = {} as ITokenInfo;
if (
TOKENS[item.symbol] &&
!list.find(
(t: ITokenInfo) => t.mintAddress === TOKENS[item.symbol].mintAddress
)
) {
token = TOKENS[item.symbol];
token["logoURI"] = item.logoURI;
list.push(token);
}
});
setList(() => list);
props.getTokenInfo(
list.find((item: ITokenInfo) => item.symbol === "SOL")
);
});
}, []);
useEffect(() => {
setSearchList(() => initialList);
}, [initialList]);
const setTokenInfo = (item: ITokenInfo) => {
props.getTokenInfo(item);
props.toggleTokenList();
};
useEffect(() => {
if (!props.showTokenList) {
setSearchList(initialList);
searchRef.current.value = "";
}
}, [props.showTokenList]);
const listItems = (data: ITokenInfo[]) => {
return data.map((item: ITokenInfo) => {
return (
<div
className={style.tokenRow}
key={item.mintAddress}
onClick={() => setTokenInfo(item)}
>
<img src={item.logoURI} alt="" className={style.tokenLogo} />
<div>{item.symbol}</div>
</div>
);
});
};
const searchToken = (e: any) => {
let key = e.target.value.toUpperCase();
let newList: ITokenInfo[] = [];
initialList.map((item: ITokenInfo) => {
if (item.symbol.includes(key)) {
newList.push(item);
}
});
setSearchList(() => newList);
};
let tokeListComponentStyle;
if (!props.showTokenList) {
tokeListComponentStyle = {
display: "none"
};
} else {
tokeListComponentStyle = {
display: "block"
};
}
return (
<div className={style.tokeListComponent} style={tokeListComponentStyle}>
<div className={style.tokeListContainer}>
<div className={style.header}>
<div>Select a token</div>
<div className={style.closeIcon} onClick={props.toggleTokenList}>
<CloseIcon w={5} h={5} />
</div>
</div>
<div className={style.inputBlock}>
<input
type="text"
placeholder="Search name or mint address"
ref={searchRef}
className={style.searchTokenInput}
onChange={searchToken}
/>
<div className={style.tokenListTitleRow}>
<div>Token name</div>
</div>
</div>
<div className={style.list}>{listItems(searchedList)}</div>
<div className={style.tokenListSetting}>View Token List</div>
</div>
</div>
);
};
export default TokenList;

SlippageSetting.tsx

Add the following code in ./views/raydium/SlippageSetting.tsx:
import { useState, useEffect, FunctionComponent } from "react";
import { CloseIcon } from "@chakra-ui/icons";
import style from "../../styles/swap.module.sass";
interface SlippageSettingProps {
showSlippageSetting: boolean;
toggleSlippageSetting: Function;
getSlippageValue: Function;
slippageValue: number;
}
const SlippageSetting: FunctionComponent<SlippageSettingProps> = props => {
const rate = [0.1, 0.5, 1];
const [warningText, setWarningText] = useState("");
const setSlippageBtn = (item: number) => {
props.getSlippageValue(item);
};
useEffect(() => {
Options();
if (props.slippageValue < 0) {
setWarningText("Please enter a valid slippage percentage");
} else if (props.slippageValue < 1) {
setWarningText("Your transaction may fail");
} else {
setWarningText("");
}
}, [props.slippageValue]);
const Options = (): JSX.Element => {
return (
<>
{rate.map(item => {
return (
<button
className={`${style.optionBtn} ${
item === props.slippageValue
? style.selectedSlippageRateBtn
: ""
}`}
key={item}
onClick={() => setSlippageBtn(item)}
>
{item}%
</button>
);
})}
</>
);
};
const updateInputRate = (e: React.FormEvent<HTMLInputElement>) => {
props.getSlippageValue(e.currentTarget.value);
};
const close = () => {
if (props.slippageValue < 0) {
return;
}
props.toggleSlippageSetting();
};
if (!props.showSlippageSetting) {
return null;
}
return (
<div className={style.slippageSettingComponent}>
<div className={style.slippageSettingContainer}>
<div className={style.header}>
<div>Setting</div>
<div className={style.closeIcon} onClick={close}>
<CloseIcon w={5} h={5} />
</div>
</div>
<div className={style.settingSelectBlock}>
<div className={style.title}>Slippage tolerance</div>
<div className={style.optionsBlock}>
<Options />
<button className={`${style.optionBtn} ${style.inputBtn}`}>
<input
type="number"
placeholder="0%"
className={style.input}
value={props.slippageValue}
onChange={updateInputRate}
/>
%
</button>
</div>
<div className={style.warning}>{warningText}</div>
</div>
</div>
</div>
);
};
export default SlippageSetting;

TokenSelect.tsx

Add the following code in ./views/raydium/TokenSelect.tsx:
import { FunctionComponent, useEffect, useState } from "react";
import { ArrowDownIcon } from "@chakra-ui/icons";
import { useWallet } from "@solana/wallet-adapter-react";
import { TokenData } from "./index";
import { ISplToken } from "../../utils/web3";
import style from "../../styles/swap.module.sass";
interface TokenSelectProps {
type: string;
toggleTokenList: Function;
tokenData: TokenData;
updateAmount: Function;
wallet: Object;
splTokenData: ISplToken[];
}
export interface IUpdateAmountData {
type: string;
amount: number;
}
interface SelectTokenProps {
propsData: {
tokenData: TokenData;
};
}
const TokenSelect: FunctionComponent<TokenSelectProps> = props => {
let wallet = useWallet();
const [tokenBalance, setTokenBalance] = useState<number | null>(null);
const updateAmount = (e: any) => {
e.preventDefault();
const amountData: IUpdateAmountData = {
amount: e.target.value,
type: props.type
};
props.updateAmount(amountData);
};
const selectToken = () => {
props.toggleTokenList(props.type);
};
useEffect(() => {
const getTokenBalance = () => {
let data: ISplToken | undefined = props.splTokenData.find(
(t: ISplToken) =>
t.parsedInfo.mint === props.tokenData.tokenInfo?.mintAddress
);
if (data) {
//@ts-ignore
setTokenBalance(data.amount);
}
};
getTokenBalance();
}, [props.splTokenData]);
const SelectTokenBtn: FunctionComponent<
SelectTokenProps
> = selectTokenProps => {
if (selectTokenProps.propsData.tokenData.tokenInfo?.symbol) {
return (
<>
<img
src={selectTokenProps.propsData.tokenData.tokenInfo?.logoURI}
alt="logo"
className={style.img}
/>
<div className={style.coinNameBlock}>
<span className={style.coinName}>
{selectTokenProps.propsData.tokenData.tokenInfo?.symbol}
</span>
<ArrowDownIcon w={5} h={5} />
</div>
</>
);
}
return (
<>
<span>Select a token</span>
<ArrowDownIcon w={5} h={5} />
</>
);
};
return (
<div className={style.coinSelect}>
<div className={style.noteText}>
<div>
{props.type === "To" ? `${props.type} (Estimate)` : props.type}
</div>
<div>
{wallet.connected && tokenBalance
? `Balance: ${tokenBalance.toFixed(4)}`
: ""}
</div>
</div>
<div className={style.coinAmountRow}>
{props.type !== "From" ? (
<div className={style.input}>
{props.tokenData.amount ? props.tokenData.amount : "-"}
</div>
) : (
<input
type="number"
className={style.input}
placeholder="0.00"
onChange={updateAmount}
disabled={props.type !== "From"}
/>
)}
<div className={style.selectTokenBtn} onClick={selectToken}>
<SelectTokenBtn propsData={props} />
</div>
</div>
</div>
);
};
export default TokenSelect;

SwapOperateContainer.tsx

Add the following code in ./views/raydium/SwapOperateContainer.tsx:
import { FunctionComponent } from "react";
import { ArrowUpDownIcon, QuestionOutlineIcon } from "@chakra-ui/icons";
import { Tooltip } from "@chakra-ui/react";
import { useWallet } from "@solana/wallet-adapter-react";
import {
WalletModalProvider,
WalletMultiButton
} from "@solana/wallet-adapter-react-ui";
import { TokenData } from ".";
import TokenSelect from "./TokenSelect";
import { ISplToken } from "../../utils/web3";
import style from "../../styles/swap.module.sass";
interface SwapOperateContainerProps {
toggleTokenList: Function;
fromData: TokenData;
toData: TokenData;
updateAmount: Function;
switchFromAndTo: (event?: React.MouseEvent<HTMLDivElement>) => void;
slippageValue: number;
sendSwapTransaction: (event?: React.MouseEvent<HTMLButtonElement>) => void;
splTokenData: ISplToken[];
}
interface SwapDetailProps {
title: string;
tooltipContent: string;
value: string;
}
const SwapOperateContainer: FunctionComponent<
SwapOperateContainerProps
> = props => {
let wallet = useWallet();
const SwapBtn = (swapProps: any) => {
if (wallet.connected) {
if (
!swapProps.props.fromData.tokenInfo?.symbol ||
!swapProps.props.toData.tokenInfo?.symbol
) {
return (
<button
className={`${style.operateBtn} ${style.disabledBtn}`}
disabled
>
Select a token
</button>
);
}
if (
swapProps.props.fromData.tokenInfo?.symbol &&
swapProps.props.toData.tokenInfo?.symbol
) {
if (
!swapProps.props.fromData.amount ||
!swapProps.props.toData.amount
) {
return (
<button
className={`${style.operateBtn} ${style.disabledBtn}`}
disabled
>
Enter an amount
</button>
);
}
}
return (
<button
className={style.operateBtn}
onClick={props.sendSwapTransaction}
>
Swap
</button>
);
} else {
return (
<div className={style.selectWallet}>
<WalletModalProvider>
<WalletMultiButton />
</WalletModalProvider>
</div>
);
}
};
const SwapDetailPreview: FunctionComponent<SwapDetailProps> = props => {
return (
<div className={style.slippageRow}>
<div className={style.slippageTooltipBlock}>
<div>{props.title}</div>
<Tooltip
hasArrow
label={props.tooltipContent}
color="white"
bg="brand.100"
padding="3"
>
<QuestionOutlineIcon
w={5}
h={5}
className={`${style.icon} ${style.icon}`}
/>
</Tooltip>
</div>
<div>{props.value}</div>
</div>
);
};
const SwapDetailPreviewList = (): JSX.Element => {
return (
<>
<SwapDetailPreview
title="Swapping Through"
tooltipContent="This venue gave the best price for your trade"
value={`${props.fromData.tokenInfo.symbol} > ${props.toData.tokenInfo.symbol}`}
/>
</>
);
};
return (
<div className={style.swapCard}>
<div className={style.cardBody}>
<TokenSelect
type="From"
toggleTokenList={props.toggleTokenList}
tokenData={props.fromData}
updateAmount={props.updateAmount}
wallet={wallet}
splTokenData={props.splTokenData}
/>
<div
className={`${style.switchIcon} ${style.icon}`}
onClick={props.switchFromAndTo}
>
<ArrowUpDownIcon w={5} h={5} />
</div>
<TokenSelect
type="To"
toggleTokenList={props.toggleTokenList}
tokenData={props.toData}
updateAmount={props.updateAmount}
wallet={wallet}
splTokenData={props.splTokenData}
/>
<div className={style.slippageRow}>
<div className={style.slippageTooltipBlock}>
<div>Slippage Tolerance </div>
<Tooltip
hasArrow
label="The maximum difference between your estimated price and execution price."
color="white"
bg="brand.100"
padding="3"
>
<QuestionOutlineIcon
w={5}
h={5}
className={`${style.icon} ${style.icon}`}
/>
</Tooltip>
</div>
<div>{props.slippageValue}%</div>
</div>
{props.fromData.amount! > 0 &&
props.fromData.tokenInfo.symbol &&
props.toData.amount! > 0 &&
props.toData.tokenInfo.symbol ? (
<SwapDetailPreviewList />
) : (
""
)}
<SwapBtn props={props} />
</div>
</div>
);
};
export default SwapOperateContainer;

index

Add the following code in ./views/raydium/index.tsx:
import { useState, useEffect, FunctionComponent } from "react";
import TokenList from "./TokenList";
import TitleRow from "./TitleRow";
import SlippageSetting from "./SlippageSetting";
import SwapOperateContainer from "./SwapOperateContainer";
import { Connection } from "@solana/web3.js";
import { Spinner } from "@chakra-ui/react";
import { useWallet, WalletContextState } from "@solana/wallet-adapter-react";
import { getPoolByTokenMintAddresses } from "../../utils/pools";
import { swap, getSwapOutAmount, setupPools } from "../../utils/swap";
import { getSPLTokenData } from "../../utils/web3";
import Notify from "../commons/Notify";
import { INotify } from "../commons/Notify";
import SplTokenList from "../commons/SplTokenList";
import { ISplToken } from "../../utils/web3";
import { IUpdateAmountData } from "./TokenSelect";
import style from "../../styles/swap.module.sass";
export interface ITokenInfo {
symbol: string;
mintAddress: string;
logoURI: string;
}
export interface TokenData {
amount: number | null;
tokenInfo: ITokenInfo;
}
const SwapPage: FunctionComponent = () => {
const [showTokenList, setShowTokenList] = useState(false);
const [showSlippageSetting, setShowSlippageSetting] = useState(false);
const [selectType, setSelectType] = useState<string>("From");
const [fromData, setFromData] = useState<TokenData>({} as TokenData);
const [toData, setToData] = useState<TokenData>({} as TokenData);
const [slippageValue, setSlippageValue] = useState(1);
const [splTokenData, setSplTokenData] = useState<ISplToken[]>([]);
const [liquidityPools, setLiquidityPools] = useState<any>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [notify, setNotify] = useState<INotify>({
status: "info",
title: "",
description: "",
link: ""
});
const [showNotify, toggleNotify] = useState<Boolean>(false);
let wallet: WalletContextState = useWallet();
const connection = new Connection("https://rpc-mainnet-fork.dappio.xyz", {
wsEndpoint: "wss://rpc-mainnet-fork.dappio.xyz/ws",
commitment: "processed"
});
useEffect(() => {
setIsLoading(true);
setupPools(connection).then(data => {
setLiquidityPools(data);
setIsLoading(false);
});
return () => {
setLiquidityPools("");
};
}, []);
useEffect(() => {
if (wallet.connected) {
getSPLTokenData(wallet, connection).then((tokenList: ISplToken[]) => {
if (tokenList) {
setSplTokenData(() => tokenList.filter(t => t !== undefined));
}
});
}
}, [wallet.connected]);
const updateAmount = (e: IUpdateAmountData) => {
if (e.type === "From") {
setFromData((old: TokenData) => ({
...old,
amount: e.amount
}));
if (!e.amount) {
setToData((old: TokenData) => ({
...old,
amount: 0
}));
}
}
};
const updateSwapOutAmount = () => {
if (
fromData.amount! > 0 &&
fromData.tokenInfo?.symbol &&
toData.tokenInfo?.symbol
) {
let poolInfo = getPoolByTokenMintAddresses(
fromData.tokenInfo.mintAddress,
toData.tokenInfo.mintAddress
);
if (!poolInfo) {
setNotify((old: INotify) => ({
...old,
status: "error",
title: "AMM error",
description: "Current token pair pool not found"
}));
toggleNotify(true);
return;
}
let parsedPoolsData = liquidityPools;
let parsedPoolInfo = parsedPoolsData[poolInfo?.lp.mintAddress];
// //@ts-ignore
const { amountOutWithSlippage } = getSwapOutAmount(
parsedPoolInfo,
fromData.tokenInfo.mintAddress,
toData.tokenInfo.mintAddress,
fromData.amount!.toString(),
slippageValue
);
setToData((old: TokenData) => ({
...old,
amount: parseFloat(amountOutWithSlippage.fixed())
}));
}
};
useEffect(() => {
updateSwapOutAmount();
}, [fromData]);
useEffect(() => {
updateSwapOutAmount();
}, [toData.tokenInfo?.symbol]);
useEffect(() => {
updateSwapOutAmount();
}, [slippageValue]);
const toggleTokenList = (e: any) => {
setShowTokenList(() => !showTokenList);
setSelectType(() => e);
};
const toggleSlippageSetting = () => {
setShowSlippageSetting(() => !showSlippageSetting);
};
const getSlippageValue = (e: number) => {
if (!e) {
setSlippageValue(() => e);
} else {
setSlippageValue(() => e);
}
};
const switchFromAndTo = () => {
const fromToken = fromData.tokenInfo;
const toToken = toData.tokenInfo;
setFromData((old: TokenData) => ({
...old,
tokenInfo: toToken,
amount: null
}));
setToData((old: TokenData) => ({
...old,
tokenInfo: fromToken,
amount: null
}));
};
const getTokenInfo = (e: any) => {
if (selectType === "From") {
if (toData.tokenInfo?.symbol === e?.symbol) {
setToData((old: TokenData) => ({
...old,
tokenInfo: {
symbol: "",
mintAddress: "",
logoURI: ""
}
}));
}
setFromData((old: TokenData) => ({
...old,
tokenInfo: e
}));
} else {
if (fromData.tokenInfo?.symbol === e.symbol) {
setFromData((old: TokenData) => ({
...old,