#4 - BUIDL a Swap UI on Solana
Authors: @SaiyanBs, @ironaddicteddog
[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 create-next-app@latest 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
Navigator
Navigator
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
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
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
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
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
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
}
navigator.module.sass
navigator.module.sass
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
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
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?
Token list
Slippage setting
Price out
Amm pools info
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
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
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
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
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
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
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,