#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

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. Token list

  2. Slippage setting

  3. Price out

  4. Amm pools info

  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,