#4 - BUIDL a Swap UI on Solana
[Updated at 2022.3.31]
- Use Next.js + React.js + @solana/web3.js
- Raydium AMM swap
- Jupiter SDK swap
- 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.
- web3.js library
- Solana tx
- Solana ix
├── 📂 pages
│ │
│ ├── 📂 api
│ │
│ ├── 📄 _app.tsx
│ │
│ ├── 📄 index.tsx
│ │
│ ├── 📄 jupiter.tsx
│ │
│ └── 📄 raydium.tsx
│
└── 📂 views
│ │
│ ├── 📂 commons
│ │
│ ├── 📂 jupiter
│ │
│ └── 📂 raydium
│
├── 📂 utils
│
├── 📂 styles
│
├── 📂 chakra
│ │
│ └── 📄 style.js
│
├── 📂 public
│
│── 📄 next.config.js
│
└── ...
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
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 ofbuffer-layout
is locked at^3.0.0
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
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;
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;
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;
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 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;
Theere are 3 style sheets to be updated:
globals.css
navigator.module.sass
color.module.sass
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
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
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
- 1.Token list
- 2.Slippage setting
- 3.Price out
- 4.Amm pools info
- 5.Interact with on-chain program
Let's update each component one by one:
We will update the following components:
SlippageSetting
SwapOperateContainer
TitleRow
TokenList
TokenSelect
index
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;
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;
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;
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;
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;
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
}));