├── .env.local ├── .gitignore ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── assets │ └── img │ │ ├── SOL.png │ │ ├── cmfi.png │ │ ├── cmfi_ticker.png │ │ ├── compendex2.png │ │ ├── sol.png │ │ └── solscan.png ├── favicon.ico ├── next.svg ├── static │ └── imgs │ │ └── favicon.ico ├── thirteen.svg └── vercel.svg ├── src ├── components │ ├── AppSearch.tsx │ ├── ConvertForm.tsx │ ├── CustomClusterEndpointDialog.tsx │ ├── CustomMarketDialog.jsx │ ├── DepositDialog.jsx │ ├── DeprecatedMarketsInstructions.js │ ├── ErrorBoundary.jsx │ ├── Footer.tsx │ ├── Header.jsx │ ├── HelpUrls.ts │ ├── LinkAddress.tsx │ ├── MainLayout.tsx │ ├── MintName.tsx │ ├── OrderComp.jsx │ ├── Orderbook.jsx │ ├── Settings.jsx │ ├── StandaloneBalancesDisplay.tsx │ ├── StandaloneTokenAccountSelect.tsx │ ├── TopBar.tsx │ ├── TradeForm.tsx │ ├── TradeHistoryChart.jsx │ ├── TradesTable.tsx │ ├── TradingView │ │ ├── index.css │ │ ├── index.tsx │ │ └── saveLoadAdapter.tsx │ ├── UserInfoTable │ │ ├── BalancesTable.jsx │ │ ├── FeesTable.js │ │ ├── FillsTable.jsx │ │ ├── OpenOrderTable.tsx │ │ ├── UserInfoTableDisabled.jsx │ │ ├── WalletBalancesTable.tsx │ │ ├── datatable.css │ │ ├── index.jsx │ │ └── styles.css │ ├── WalletConnect.tsx │ ├── global_style.ts │ ├── layout │ │ ├── DataTable.jsx │ │ ├── FloatingElement.jsx │ │ ├── FloatingElementBlock.jsx │ │ └── FloatingNavbar.jsx │ ├── styles.css │ ├── tradeform.css │ ├── useMintInput.tsx │ └── walletbutton.css ├── context │ └── tokenList.tsx ├── pages │ ├── BalancesPage.tsx │ ├── ConvertPage.tsx │ ├── ListNewMarketPage.jsx │ ├── OpenOrdersPage.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── hello.ts │ ├── index.tsx │ ├── market │ │ └── [address].tsx │ ├── pools │ │ ├── NewPoolPage.tsx │ │ ├── PoolListPage.tsx │ │ └── PoolPage │ │ │ ├── PoolAdminPanel.tsx │ │ │ ├── PoolBalancesPanel.tsx │ │ │ ├── PoolBasketDisplay.tsx │ │ │ ├── PoolCreateRedeemPanel.tsx │ │ │ ├── PoolInfoPanel.tsx │ │ │ └── index.tsx │ └── tradepage.css ├── styles │ ├── App.less │ ├── Home.module.css │ ├── MainHeader.module.css │ └── globals.css └── utils │ ├── ChartdatapApi.ts │ ├── Datafeed.ts │ ├── MainContextProvider.tsx │ ├── bonfidaConnector.tsx │ ├── connection.tsx │ ├── customMarkets.ts │ ├── fetch-loop.tsx │ ├── markets.tsx │ ├── notifications.tsx │ ├── preferences.tsx │ ├── referrer.tsx │ ├── send.tsx │ ├── tokenApi.ts │ ├── tokens.tsx │ ├── types.tsx │ ├── useInterval.tsx │ ├── usePrevious.tsx │ ├── utils.tsx │ └── wallet.tsx ├── tsconfig.json └── yarn.lock /.env.local: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SOLANA_RPC_ENDPOINT="place your solana rpc here " 2 | NEXT_PUBLIC_USDT_REFERRAL="ZcHd98AKDwFkB72X5r77FUusCHf41NeJwZUub94tUWu" 3 | NEXT_PUBLIC_USDC_REFERRAL="92Q3GzZ2JkGoiQzUSgpS6YDv9ZUG161GpNHPf2TW8vkE" 4 | NEXT_PUBLIC_DATA_FEED_ENDPOINT="place your data feed for trading view here" 5 | NEXT_PUBLIC_WEBSITE_TITLE="Compendex Open Book Dex" 6 | NEXT_PUBLIC_DISPLAY_TRADING_VIEW=2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compendex Openbook GUI Example 2 | 3 | This is an open source implementation of the new Openbook protocol for Solana (forked from Project Serum) provided by the [Compendium](https://docs.compendium.finance) team. The purpose of this repository is to provide a new stepping stone for the greater community to further build new tools and expand on. A version of this interface is publicly available for use and integrated into our [Compendex](https://sol.compendex.xyz) platform. Pull requests and Issue submissions are welcome and will be reviewed. 4 | 5 | ![image](https://user-images.githubusercontent.com/36686278/227060628-ee55ab72-513d-40af-84d6-dc755e839659.png) 6 | 7 | ## What is Openbook? 8 | 9 | OpenBook is a cutting-edge decentralized exchange (DEX) built on Solana, a high-performance blockchain network. Leveraging the Continuous Limit Order Book (CLOB) model, OpenBook offers a seamless trading experience to its users. In this segment, we will delve into the technicalities of a DEX and elucidate how users interact with the platform through various order types. 10 | 11 | At its core, the trade lifecycle on OpenBook can be broadly classified into four stages. Firstly, users transfer their funds from their SPL token account, i.e., their digital wallet, to an intermediary account termed "Open Orders" account. Next, they initiate an order placement request that gets added to the Request Queue. Subsequently, the request is extracted from the Request Queue, it is matched with other orders. Upon successful matching, the trade is executed, and the trade details are added to the Orderbook. The entire process is seamlessly reported in the Event Queue. 12 | 13 | In the next stage, users can consume the trade events from the Event Queue and have their Open Orders account balances updated. Finally, the trade settlement stage enables users to retrieve any residual funds from their Open Orders account back to their SPL token account. OpenBook's superior technology ensures that the trade settlement is quick and hassle-free, ensuring users can focus on their trading strategies as they would with a traditional centralized exchange. 14 | 15 | OpenBook's implementation of the CLOB model ensures efficient price discovery, higher liquidity, and reduced slippage, making it a highly sought-after DEX model. The platform's intuitive user interface, lightning-fast trade execution, and robust security features make it an excellent choice for both novice and seasoned traders alike. 16 | 17 | Further Documentation for usage of the Openbook interface from a user perspective can be found [here](https://docs.compendium.finance/decentralized-trading-tools/solana-integrations/openbook-spot-markets). 18 | 19 | #### Protocol Management Legal Notice 20 | 21 | Compendium & Compendex do not manage the OpenBook protocol nor are directly associated with any smart contract deployments. This interface example simply provides a way to access and initiate on-chain events. 22 | 23 | ## How to run the project 24 | 25 | 1. Clone the repository 26 | 2. Navigate to the source directory 27 | 3. Run `npm ci` or `yarn` to install dependencies 28 | 4. Copy TradingView chart library folders (charting_library,datafeed) into public/static folder 29 | 5. Create an `.env.local` file in the project directory following this example: 30 | ``` 31 | NEXT_PUBLIC_SOLANA_RPC_ENDPOINT="place your solana rpc here " 32 | NEXT_PUBLIC_USDT_REFERRAL="place your USDT referral address here" 33 | NEXT_PUBLIC_USDC_REFERRAL="place your USDC referral address here" 34 | NEXT_PUBLIC_DATA_FEED_ENDPOINT="place your data feed for trading view here" 35 | NEXT_PUBLIC_WEBSITE_TITLE="Compendex Open Book Dex" 36 | NEXT_PUBLIC_DISPLAY_TRADING_VIEW=1 //One to display TV by default, 2 Displays simple history chart 37 | ``` 38 | 6. Run `npm run dev` or `yarn dev` to start the development server. 39 | 40 | Note: You will need to have Node.js and npm or yarn installed on your machine to run this project. 41 | 42 | ## Collecting Referral Fees 43 | 44 | If you are hosting a public UI using this codebase, you can collect referral fees when your users trade through your site. 45 | 46 | To do so, set the `NEXT_PUBLIC_USDT_REFERRAL` and `NEXT_PUBLIC_USDC_REFERRAL` environment variables to the addresses of your USDT and USDC SPL token accounts. 47 | 48 | ## TradingView Chart Data Insertion 49 | 50 | Use in public deployment of an interface may require the deployer to digest on-chain data and reformat into a compatible OHLCV format in order to work with the TradingView integration. 51 | 52 | ![image](https://user-images.githubusercontent.com/36686278/227062566-81ab8804-c696-4b74-8310-03100869ec1b.png) 53 | 54 | ### Compendex TradingView WebSocket 55 | 56 | If you are unable to retrieve and host your own TradingView data feeds we may be able to help. Reach out to our team via our official Discord server to initiate conversation. 57 | 58 | ### CoinGecko "Simple Charts" Fallback 59 | 60 | The example user interface also includes the [CoinGecko API](https://www.coingecko.com/en/api) to provide a backup pricing chart for every supported market. Users can select the "Simple Chart" option to display a line area graph. The deployer can easily make this the default if they wish to substitute it for TradingView by setting `NEXT_PUBLIC_DISPLAY_TRADING_VIEW=2` in the `ENV` file. The deployer can also upgrade the CoinGecko API from the free version to a PRO version in order to handle more frequent calls and traffic if needed. 61 | 62 | ![image](https://user-images.githubusercontent.com/36686278/227062603-fce438ad-d57b-42b9-96c5-f595bec787e0.png) 63 | 64 | ## Official Links & Support 65 | 66 | For further support, please join our [Discord server](https://discord.gg/compendium-pendax-846967032288509953) or email us at support@compendium.finance. 67 | 68 | - Compendex Twitter: https://twitter.com/compendexyz 69 | - Compendium Twitter: https://twitter.com/CompendiumFi 70 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const withLess = require('next-with-less'); 3 | const withPlugins = require('next-compose-plugins'); 4 | const nextConfig = { 5 | reactStrictMode: true, webpack: (config, options) => { 6 | // Important: return the modified config 7 | if (!options.isServer) { 8 | config.resolve.fallback.fs = false 9 | } 10 | return config 11 | } 12 | } 13 | 14 | const plugins = [ 15 | [ 16 | withLess, 17 | { 18 | lessLoaderOptions: {}, 19 | }, 20 | ], 21 | ]; 22 | 23 | module.exports = withPlugins(plugins, nextConfig); 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compendex-dex-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "@ant-design/icons": "^4.2.1", 11 | "@craco/craco": "^5.6.4", 12 | "@ledgerhq/hw-transport-webusb": "^5.41.0", 13 | "@project-serum/associated-token": "0.1.0", 14 | "@project-serum/awesome-serum": "1.0.1", 15 | "@project-serum/pool": "0.2.2", 16 | "@project-serum/serum": "^0.13.58", 17 | "@project-serum/sol-wallet-adapter": "^0.2.0", 18 | "@solana/spl-name-service": "^0.1.2", 19 | "@solana/spl-token": "^0.1.6", 20 | "@solana/wallet-adapter-ledger": "^0.9.20", 21 | "@solana/wallet-adapter-mathwallet": "^0.9.13", 22 | "@solana/wallet-adapter-phantom": "^0.9.17", 23 | "@solana/wallet-adapter-react": "^0.15.21-rc.3", 24 | "@solana/wallet-adapter-solflare": "^0.6.18", 25 | "@solana/wallet-adapter-sollet": "^0.11.12", 26 | "@solana/wallet-adapter-solong": "^0.9.13", 27 | "@solana/web3.js": "^1.66.2", 28 | "@testing-library/jest-dom": "^4.2.4", 29 | "@testing-library/react": "^9.3.2", 30 | "@testing-library/user-event": "^7.1.2", 31 | "@tsconfig/node12": "^1.0.7", 32 | "@types/bn.js": "^4.11.6", 33 | "@types/jest": "^26.0.14", 34 | "@types/node": "^14.11.4", 35 | "@types/react": "^16.9.51", 36 | "@types/react-dom": "^16.9.8", 37 | "antd": "^4.6.0", 38 | "bn.js": "^5.1.3", 39 | "craco-less": "^1.17.0", 40 | "immutable-tuple": "^0.4.10", 41 | "nanoid": "^3.1.22", 42 | "qrcode.react": "^1.0.0", 43 | "query-string": "^7.0.1", 44 | "react-copy-to-clipboard": "^5.0.2", 45 | "styled-components": "^5.1.1", 46 | "superstruct": "0.8.3", 47 | "typescript": "^3.9.7", 48 | "@solana/spl-token-v1": "npm:@solana/spl-token@0.1.8" 49 | }, 50 | "dependencies": { 51 | "@emotion/react": "^11.10.6", 52 | "@emotion/styled": "^11.10.6", 53 | "@mantine/core": "^6.0.2", 54 | "@mantine/hooks": "^6.0.2", 55 | "@mantine/notifications": "^6.0.2", 56 | "@material-ui/core": "^4.12.4", 57 | "@material-ui/icons": "^4.11.3", 58 | "@mui/icons-material": "^5.11.11", 59 | "@mui/lab": "^5.0.0-alpha.123", 60 | "@mui/material": "^5.11.13", 61 | "@project-serum/pool": "^0.2.3", 62 | "@project-serum/serum": "^0.13.65", 63 | "@react-aria/ssr": "^3.5.0", 64 | "@solana/spl-name-service": "^0.1.4", 65 | "@solana/wallet-adapter-bitpie": "^0.5.17", 66 | "@solana/wallet-adapter-blocto": "^0.5.21", 67 | "@solana/wallet-adapter-exodus": "^0.1.17", 68 | "@solana/wallet-adapter-ledger": "^0.9.24", 69 | "@solana/wallet-adapter-mathwallet": "^0.9.17", 70 | "@solana/wallet-adapter-phantom": "^0.9.22", 71 | "@solana/wallet-adapter-react": "^0.15.31", 72 | "@solana/wallet-adapter-react-ui": "^0.9.30", 73 | "@solana/wallet-adapter-solflare": "^0.6.24", 74 | "@solana/wallet-adapter-sollet": "^0.11.16", 75 | "@solana/web3.js": "^1.74.0", 76 | "@types/node": "18.15.3", 77 | "@types/react": "18.0.28", 78 | "@types/react-dom": "18.0.11", 79 | "antd": "4.16.11", 80 | "apexcharts": "^3.37.1", 81 | "axios": "^1.3.4", 82 | "immutable-tuple": "0.4.10", 83 | "less": "^4.1.3", 84 | "less-loader": "^11.1.0", 85 | "next": "13.2.4", 86 | "next-compose-plugins": "^2.2.1", 87 | "next-with-less": "^2.0.5", 88 | "notistack": "^3.0.1", 89 | "numeral": "^2.0.6", 90 | "react": "18.2.0", 91 | "react-apexcharts": "^1.4.0", 92 | "react-dom": "18.2.0", 93 | "styled-components": "^5.3.9", 94 | "superstruct": "0.8.3", 95 | "typescript": "5.0.2" 96 | }, 97 | "devDependencies": { 98 | "@types/numeral": "^2.0.2", 99 | "@types/styled-components": "^5.1.26" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /public/assets/img/SOL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Compendium-Fi/Compendex-Openbook-UI/ee4b3426cfbda93010e39515645009cb4c3ee490/public/assets/img/SOL.png -------------------------------------------------------------------------------- /public/assets/img/cmfi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Compendium-Fi/Compendex-Openbook-UI/ee4b3426cfbda93010e39515645009cb4c3ee490/public/assets/img/cmfi.png -------------------------------------------------------------------------------- /public/assets/img/cmfi_ticker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Compendium-Fi/Compendex-Openbook-UI/ee4b3426cfbda93010e39515645009cb4c3ee490/public/assets/img/cmfi_ticker.png -------------------------------------------------------------------------------- /public/assets/img/compendex2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Compendium-Fi/Compendex-Openbook-UI/ee4b3426cfbda93010e39515645009cb4c3ee490/public/assets/img/compendex2.png -------------------------------------------------------------------------------- /public/assets/img/sol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Compendium-Fi/Compendex-Openbook-UI/ee4b3426cfbda93010e39515645009cb4c3ee490/public/assets/img/sol.png -------------------------------------------------------------------------------- /public/assets/img/solscan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Compendium-Fi/Compendex-Openbook-UI/ee4b3426cfbda93010e39515645009cb4c3ee490/public/assets/img/solscan.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Compendium-Fi/Compendex-Openbook-UI/ee4b3426cfbda93010e39515645009cb4c3ee490/public/favicon.ico -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/imgs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Compendium-Fi/Compendex-Openbook-UI/ee4b3426cfbda93010e39515645009cb4c3ee490/public/static/imgs/favicon.ico -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AppSearch.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Select, Typography } from 'antd'; 3 | import { SearchOutlined } from '@ant-design/icons'; 4 | import { TOKEN_MINTS } from '@project-serum/serum'; 5 | import apps from '@project-serum/awesome-serum'; 6 | 7 | const { Option } = Select; 8 | 9 | const appsAndTokens = apps.concat( 10 | TOKEN_MINTS.map((mint) => { 11 | return { 12 | name: `${mint.name} SPL`, 13 | url: `https://solscan.io/address/${mint.address.toBase58()}`, 14 | description: `${mint.name} SPL token`, 15 | icon: '', 16 | tags: [ 17 | 'token', 18 | 'blockchain', 19 | 'solana', 20 | 'spl', 21 | 'solana', 22 | mint.address.toBase58(), 23 | ], 24 | }; 25 | }), 26 | ); 27 | 28 | interface App { 29 | name: string; 30 | url: string; 31 | description: string; 32 | tags: string[]; 33 | } 34 | 35 | export default function AppSearch(props) { 36 | const [searchMatches, setSearchMatches] = useState([]); 37 | const [searchValue, setSearchValue] = useState(undefined); 38 | 39 | const matchApp = (searchString: string, app: App) => { 40 | const lowerSearchStr = searchString.toLowerCase(); 41 | return ( 42 | app.name.toLowerCase().includes(lowerSearchStr) || 43 | app.tags.some( 44 | (tag) => 45 | tag.toLowerCase().includes(lowerSearchStr) || 46 | lowerSearchStr.includes(tag.toLowerCase()), 47 | ) 48 | ); 49 | }; 50 | 51 | const handleSearch = (value) => { 52 | setSearchValue(value === '' ? undefined : value); 53 | const filteredApps = appsAndTokens.filter((app) => matchApp(value, app)); 54 | setSearchMatches(filteredApps); 55 | }; 56 | 57 | const handleSelect = (value, option) => { 58 | window.open(option.href, '_blank'); 59 | handleClear(); 60 | }; 61 | 62 | const handleClear = () => { 63 | setSearchMatches([]); 64 | setSearchValue(undefined); 65 | }; 66 | 67 | const options = searchMatches.map((d) => ( 68 | 74 | )); 75 | 76 | const ref = useRef(); 77 | return ( 78 | 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /src/components/CustomClusterEndpointDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Col, Input, Modal, Row } from 'antd'; 3 | import { EndpointInfo } from '../utils/types'; 4 | 5 | export default function CustomClusterEndpointDialog({ 6 | visible, 7 | testingConnection, 8 | onAddCustomEndpoint, 9 | onClose, 10 | }: { 11 | visible: boolean; 12 | testingConnection: boolean; 13 | onAddCustomEndpoint: (info: EndpointInfo) => void; 14 | onClose?: () => void; 15 | }) { 16 | const [customEndpoint, setCustomEndpoint] = useState(''); 17 | const [customEndpointName, setCustomEndpointName] = useState(''); 18 | 19 | const onSubmit = () => { 20 | const fullEndpoint = 'https://' + customEndpoint; 21 | const params = { 22 | name: customEndpointName, 23 | endpoint: fullEndpoint, 24 | custom: true, 25 | }; 26 | onAddCustomEndpoint(params); 27 | onDoClose(); 28 | }; 29 | const onDoClose = () => { 30 | setCustomEndpoint(''); 31 | setCustomEndpointName(''); 32 | onClose && onClose(); 33 | }; 34 | const canSubmit = customEndpoint !== '' && customEndpointName !== ''; 35 | 36 | return ( 37 | 45 | 46 | 47 | setCustomEndpointName(e.target.value)} 51 | /> 52 | 53 | 54 | 55 | 56 | setCustomEndpoint(e.target.value)} 61 | /> 62 | 63 | 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/components/CustomMarketDialog.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Col, Input, Modal, Row, Typography } from 'antd'; 3 | import { notify } from '../utils/notifications'; 4 | import { isValidPublicKey } from '../utils/utils'; 5 | import { PublicKey } from '@solana/web3.js'; 6 | import { Market, MARKETS, TOKEN_MINTS } from '@project-serum/serum'; 7 | import { useAccountInfo, useConnection } from '../utils/connection'; 8 | import { LoadingOutlined } from '@ant-design/icons'; 9 | 10 | const { Text } = Typography; 11 | 12 | export default function CustomMarketDialog({ 13 | visible, 14 | onAddCustomMarket, 15 | onClose, 16 | }) { 17 | const connection = useConnection(); 18 | 19 | const [marketId, setMarketId] = useState(''); 20 | 21 | const [marketLabel, setMarketLabel] = useState(''); 22 | const [baseLabel, setBaseLabel] = useState(''); 23 | const [quoteLabel, setQuoteLabel] = useState(''); 24 | 25 | const [market, setMarket] = useState(null); 26 | const [loadingMarket, setLoadingMarket] = useState(false); 27 | 28 | const wellFormedMarketId = isValidPublicKey(marketId); 29 | 30 | const [marketAccountInfo] = useAccountInfo( 31 | wellFormedMarketId ? new PublicKey(marketId) : null, 32 | ); 33 | const programId = marketAccountInfo 34 | ? marketAccountInfo.owner.toBase58() 35 | : MARKETS.find(({ deprecated }) => !deprecated).programId.toBase58(); 36 | 37 | useEffect(() => { 38 | if (!wellFormedMarketId || !programId) { 39 | resetLabels(); 40 | return; 41 | } 42 | setLoadingMarket(true); 43 | Market.load( 44 | connection, 45 | new PublicKey(marketId), 46 | {}, 47 | new PublicKey(programId), 48 | ) 49 | .then((market) => { 50 | setMarket(market); 51 | }) 52 | .catch(() => { 53 | resetLabels(); 54 | setMarket(null); 55 | }) 56 | .finally(() => setLoadingMarket(false)); 57 | // eslint-disable-next-line react-hooks/exhaustive-deps 58 | }, [connection, marketId, programId]); 59 | 60 | const resetLabels = () => { 61 | setMarketLabel(null); 62 | setBaseLabel(null); 63 | setQuoteLabel(null); 64 | }; 65 | 66 | const knownMarket = MARKETS.find( 67 | (m) => 68 | m.address.toBase58() === marketId && m.programId.toBase58() === programId, 69 | ); 70 | const knownProgram = MARKETS.find( 71 | (m) => m.programId.toBase58() === programId, 72 | ); 73 | const knownBaseCurrency = 74 | market?.baseMintAddress && 75 | TOKEN_MINTS.find((token) => token.address.equals(market.baseMintAddress)) 76 | ?.name; 77 | 78 | const knownQuoteCurrency = 79 | market?.quoteMintAddress && 80 | TOKEN_MINTS.find((token) => token.address.equals(market.quoteMintAddress)) 81 | ?.name; 82 | 83 | const canSubmit = 84 | !loadingMarket && 85 | !!market && 86 | market.publicKey.toBase58() === marketId && 87 | marketId && 88 | programId && 89 | marketLabel && 90 | (knownBaseCurrency || baseLabel) && 91 | (knownQuoteCurrency || quoteLabel) && 92 | wellFormedMarketId; 93 | 94 | const onSubmit = () => { 95 | if (!canSubmit) { 96 | notify({ 97 | message: 'Please fill in all fields with valid values', 98 | type: 'error', 99 | }); 100 | return; 101 | } 102 | 103 | let params = { 104 | address: marketId, 105 | programId, 106 | name: marketLabel, 107 | }; 108 | if (!knownBaseCurrency) { 109 | params.baseLabel = baseLabel; 110 | } 111 | if (!knownQuoteCurrency) { 112 | params.quoteLabel = quoteLabel; 113 | } 114 | onAddCustomMarket(params); 115 | onDoClose(); 116 | }; 117 | 118 | const onDoClose = () => { 119 | resetLabels(); 120 | setMarket(null); 121 | setMarketId(null); 122 | onClose(); 123 | }; 124 | 125 | return ( 126 | 134 |
135 | {wellFormedMarketId ? ( 136 | <> 137 | {!market && !loadingMarket && ( 138 | 139 | Not a valid market 140 | 141 | )} 142 | {market && knownMarket && ( 143 | 144 | Market known: {knownMarket.name} 145 | 146 | )} 147 | {market && !knownProgram && ( 148 | 149 | Warning: unknown DEX program 150 | 151 | )} 152 | {market && knownProgram && knownProgram.deprecated && ( 153 | 154 | Warning: deprecated DEX program 155 | 156 | )} 157 | 158 | ) : ( 159 | <> 160 | {marketId && !wellFormedMarketId && ( 161 | 162 | Invalid market ID 163 | 164 | )} 165 | 166 | )} 167 | 168 | 169 | setMarketId(e.target.value)} 173 | suffix={loadingMarket ? : null} 174 | /> 175 | 176 | 177 | 178 | 179 | 180 | setMarketLabel(e.target.value)} 185 | /> 186 | 187 | 188 | 189 | 190 | setBaseLabel(e.target.value)} 195 | /> 196 | {market && !knownBaseCurrency && ( 197 |
198 | Warning: unknown token 199 |
200 | )} 201 | 202 | 203 | setQuoteLabel(e.target.value)} 208 | /> 209 | {market && !knownQuoteCurrency && ( 210 |
211 | Warning: unknown token 212 |
213 | )} 214 | 215 |
216 |
217 |
218 | ); 219 | } 220 | -------------------------------------------------------------------------------- /src/components/DepositDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Modal } from 'antd'; 3 | import { 4 | useSelectedBaseCurrencyAccount, 5 | useMarket, 6 | useSelectedQuoteCurrencyAccount, 7 | } from '../utils/markets'; 8 | //import { useWallet } from '../utils/wallet'; 9 | //import Link from './Link'; 10 | import Link from 'next/link'; 11 | import { useWallet } from '@solana/wallet-adapter-react'; 12 | export default function DepositDialog({ onClose, baseOrQuote }) { 13 | const { market, baseCurrency, quoteCurrency } = useMarket(); 14 | 15 | const { providerName, providerUrl } = useWallet(); 16 | const baseCurrencyAccount = useSelectedBaseCurrencyAccount(); 17 | const quoteCurrencyAccount = useSelectedQuoteCurrencyAccount(); 18 | let coinMint; 19 | let account; 20 | let depositCoin; 21 | if (baseOrQuote === 'base') { 22 | coinMint = market?.baseMintAddress; 23 | account = baseCurrencyAccount; 24 | depositCoin = baseCurrency; 25 | } else if (baseOrQuote === 'quote') { 26 | coinMint = market?.quoteMintAddress; 27 | account = quoteCurrencyAccount; 28 | depositCoin = quoteCurrency; 29 | } else { 30 | account = null; 31 | } 32 | if (!coinMint) { 33 | return null; 34 | } 35 | return ( 36 | 42 |
43 |

Mint address:

44 |

{coinMint.toBase58()}

45 |
46 |

SPL Deposit address:

47 |

48 | {account ? ( 49 | account.pubkey.toBase58() 50 | ) : ( 51 | <> 52 | Visit{' '} 53 | 54 | {providerName} 55 | {' '} 56 | to create an account for this mint 57 | 58 | )} 59 |

60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/components/DeprecatedMarketsInstructions.js: -------------------------------------------------------------------------------- 1 | import { Button, Divider, Spin, Typography } from 'antd'; 2 | import React from 'react'; 3 | import { 4 | useGetOpenOrdersForDeprecatedMarkets, 5 | useBalancesForDeprecatedMarkets, 6 | useUnmigratedOpenOrdersAccounts, 7 | } from '../utils/markets'; 8 | import FloatingElement from './layout/FloatingElement'; 9 | import CheckOutlined from '@ant-design/icons/lib/icons/CheckOutlined'; 10 | import BalancesTable from './UserInfoTable/BalancesTable'; 11 | import OpenOrderTable from './UserInfoTable/OpenOrderTable'; 12 | import SyncOutlined from '@ant-design/icons/lib/icons/SyncOutlined'; 13 | 14 | const { Title } = Typography; 15 | 16 | export default function DeprecatedMarketsInstructions({ switchToLiveMarkets }) { 17 | const balances = useBalancesForDeprecatedMarkets(); 18 | const { 19 | openOrders, 20 | loaded, 21 | refreshOpenOrders, 22 | } = useGetOpenOrdersForDeprecatedMarkets(); 23 | 24 | const { refresh } = useUnmigratedOpenOrdersAccounts(); 25 | const needToCancelOrders = !openOrders || openOrders.length > 0; 26 | const filteredBalances = 27 | balances && 28 | balances.filter(({ orders, unsettled }) => orders > 0 || unsettled > 0); 29 | const needToSettleFunds = filteredBalances && filteredBalances.length > 0; 30 | return ( 31 | 32 | 33 | Migrate new markets 34 | 35 | 36 | Markets on older versions of the DEX or using Wrapped USDT are now deprecated. To migrate over 37 | to the new markets, please cancel your orders and settle your funds on old markets. To convert 38 | from Wrapped USDT to Native USDT use sollet.io. 39 | 40 |
41 | 44 |
45 | {!balances ? ( 46 | 47 | ) : ( 48 | <> 49 | 50 | {!needToCancelOrders && } Cancel your orders 51 | 52 | {needToCancelOrders ? ( 53 | loaded ? ( 54 | { 57 | setTimeout(() => { 58 | refresh(); 59 | refreshOpenOrders(); 60 | }, 1000); // Wait so that on-chain account state reflects the update 61 | }} 62 | /> 63 | ) : ( 64 | 65 | ) 66 | ) : null} 67 | 68 | {!needToSettleFunds && } Settle your funds 69 | 70 | {needToSettleFunds && ( 71 | { 76 | setTimeout(refresh, 1000); // Wait so that on-chain account state reflects the update 77 | }} 78 | /> 79 | )} 80 | Switch to upgraded markets 81 |
82 | 88 |
89 | 90 | )} 91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Typography } from 'antd'; 3 | 4 | const { Title } = Typography; 5 | 6 | export default class ErrorBoundary extends Component { 7 | state = { 8 | hasError: false, 9 | }; 10 | 11 | static getDerivedStateFromError(error) { 12 | return { hasError: true }; 13 | } 14 | 15 | render() { 16 | if (this.state.hasError) { 17 | return ( 18 |
19 |
20 | Something went wrong. 21 | Please try again later. 22 |
23 |
24 | ); 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Layout, Row, Col, Grid } from 'antd'; 3 | import Link from './Link'; 4 | import { helpUrls } from './HelpUrls'; 5 | import { useReferrer } from '../utils/referrer'; 6 | const { Footer } = Layout; 7 | const { useBreakpoint } = Grid; 8 | 9 | const footerElements = [ 10 | { 11 | description: 'Serum Developer Resources', 12 | link: helpUrls.developerResources, 13 | }, 14 | { description: 'Discord', link: helpUrls.discord }, 15 | { description: 'Telegram', link: helpUrls.telegram }, 16 | { description: 'GitHub', link: helpUrls.github }, 17 | { description: 'Project Serum', link: helpUrls.projectSerum }, 18 | { description: 'Solana Network', link: helpUrls.solanaBeach }, 19 | ]; 20 | 21 | export const CustomFooter = () => { 22 | const smallScreen = !useBreakpoint().lg; 23 | const { refCode, allowRefLink } = useReferrer(); 24 | return ( 25 |
32 | {refCode && allowRefLink && ( 33 | Your referrer is {refCode} 34 | )} 35 | 36 | {!smallScreen && ( 37 | <> 38 | 39 | {footerElements.map((elem, index) => { 40 | return ( 41 | 42 | 43 | {elem.description} 44 | 45 | 46 | ); 47 | })} 48 | 49 | )} 50 | {/* */} 51 | 52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { useEffect, useLayoutEffect, useState } from "react"; 3 | 4 | import WalletConnect from "./WalletConnect"; 5 | 6 | function useWindowSize() { 7 | const [size, setSize] = useState([0, 0]); 8 | 9 | useLayoutEffect(() => { 10 | function updateSize() { 11 | setSize([window.innerWidth, window.innerHeight]); 12 | } 13 | window.addEventListener("resize", updateSize); 14 | updateSize(); 15 | return () => window.removeEventListener("resize", updateSize); 16 | }, []); 17 | return size; 18 | } 19 | const MainHeader = () => { 20 | const [width] = useWindowSize(); 21 | const [tokenPrice, setTokenPrice] = useState(null); 22 | 23 | // const history = useHistory(); 24 | // const location = useLocation(); 25 | 26 | const getTokenPrice = async () => { 27 | try { 28 | let result = await axios.get( 29 | "https://coins.llama.fi/prices/current/solana:5Wsd311hY8NXQhkt9cWHwTnqafk7BGEbLu8Py3DSnPAr" 30 | ); 31 | if (result.data) { 32 | setTokenPrice( 33 | result.data.coins[ 34 | "solana:5Wsd311hY8NXQhkt9cWHwTnqafk7BGEbLu8Py3DSnPAr" 35 | ].price 36 | ); 37 | } 38 | } catch (error) { } 39 | }; 40 | 41 | useEffect(() => { 42 | getTokenPrice(); 43 | }, []); 44 | 45 | return ( 46 |
47 |
48 |
49 | 67 |
68 |
69 | 94 |
95 |
102 |
112 |
113 | 114 | SOL 115 |
116 |
117 |
118 | 119 | ${tokenPrice && tokenPrice.toFixed(4)} 120 |
121 | 125 | 126 | 127 | 128 |
129 |
130 |
131 | ); 132 | }; 133 | MainHeader.ssr = false; 134 | export default MainHeader; 135 | -------------------------------------------------------------------------------- /src/components/HelpUrls.ts: -------------------------------------------------------------------------------- 1 | export const helpUrls = { 2 | customerSupport: 'https://t.me/ProjectSerum', 3 | customerSupportZh: 'https://t.me/ProjectSerum_Chinese', 4 | contactEmail: 'mailto:contact@projectserum.com', 5 | discord: 'https://discord.gg/EDvudv6', 6 | telegram: 'https://t.me/ProjectSerum', 7 | github: 'https://github.com/project-serum', 8 | projectSerum: 'https://projectserum.com/', 9 | developerResources: 'https://projectserum.com/developer-resources', 10 | solanaBeach: 'https://solanabeach.io', 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/LinkAddress.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'antd'; 3 | import { LinkOutlined } from '@ant-design/icons'; 4 | 5 | export default function LinkAddress({ 6 | title, 7 | address, 8 | shorten = false, 9 | }: { 10 | title?: undefined | any; 11 | address: string; 12 | shorten?: boolean; 13 | }) { 14 | return ( 15 |
16 | {title &&

{title}

} 17 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { MantineProvider } from "@mantine/core"; 4 | import { Notifications } from "@mantine/notifications"; 5 | import { SnackbarProvider } from "notistack"; 6 | import { useMemo } from "react"; 7 | 8 | import { 9 | ConnectionProvider, 10 | useConnectionConfig 11 | } from "../utils/connection"; 12 | import { ReferrerProvider } from "../utils/referrer"; 13 | 14 | import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; 15 | import { BitpieWalletAdapter } from "@solana/wallet-adapter-bitpie"; 16 | import { BloctoWalletAdapter } from "@solana/wallet-adapter-blocto"; 17 | import { ExodusWalletAdapter } from "@solana/wallet-adapter-exodus"; 18 | import { LedgerWalletAdapter } from "@solana/wallet-adapter-ledger"; 19 | import { MathWalletAdapter } from "@solana/wallet-adapter-mathwallet"; 20 | import { PhantomWalletAdapter } from "@solana/wallet-adapter-phantom"; 21 | import { WalletProvider } from "@solana/wallet-adapter-react"; 22 | import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"; 23 | import { SolflareWalletAdapter } from "@solana/wallet-adapter-solflare"; 24 | import { 25 | SolletExtensionWalletAdapter, 26 | SolletWalletAdapter 27 | } from "@solana/wallet-adapter-sollet"; 28 | 29 | import SplTokenProvider from "../context/tokenList"; 30 | require("@solana/wallet-adapter-react-ui/styles.css"); 31 | 32 | function AppImpl({ children }: { children: any }) { 33 | const { endpoint } = useConnectionConfig(); 34 | const network = useMemo(() => endpoint as WalletAdapterNetwork, [endpoint]); 35 | const wallets = useMemo( 36 | () => [ 37 | new SolletWalletAdapter({ network }), 38 | new SolletExtensionWalletAdapter({ network }), 39 | new LedgerWalletAdapter(), 40 | new SolflareWalletAdapter({ network }), 41 | new PhantomWalletAdapter(), 42 | new MathWalletAdapter(), 43 | new ExodusWalletAdapter(), 44 | new BloctoWalletAdapter(), 45 | new BitpieWalletAdapter() 46 | ], 47 | [network] 48 | ); 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {/*@ts-ignore*/} 59 | 60 | 61 |
62 | {children} 63 |
64 | 65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 |
73 | 74 | ); 75 | } 76 | 77 | const MainLayout = ({ children }: { children: any }) => { 78 | return ( 79 | 80 | 81 | 82 | {children} 83 | 84 | 85 | 86 | 87 | ); 88 | } 89 | MainLayout.ssr = false; 90 | export default MainLayout 91 | -------------------------------------------------------------------------------- /src/components/MintName.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import { abbreviateAddress } from '../utils/utils'; 4 | import { useMintToTickers } from '../utils/tokens'; 5 | import { Popover } from 'antd'; 6 | import LinkAddress from './LinkAddress'; 7 | import { InfoCircleOutlined } from '@ant-design/icons'; 8 | 9 | export function MintName({ 10 | mint, 11 | showAddress = false, 12 | }: { 13 | mint: string | PublicKey | null | undefined; 14 | showAddress?: boolean; 15 | }) { 16 | const mintToTickers = useMintToTickers(); 17 | if (!mint) { 18 | return null; 19 | } 20 | const mintKey = typeof mint === 'string' ? new PublicKey(mint) : mint; 21 | const mintAddress = typeof mint === 'string' ? mint : mint.toBase58(); 22 | const ticker = mintToTickers[mintAddress] ?? abbreviateAddress(mintKey); 23 | 24 | return ( 25 | <> 26 | {ticker} 27 | {showAddress ? ( 28 | <> 29 | {' '} 30 | } 32 | placement="bottomRight" 33 | title="Token mint" 34 | trigger="hover" 35 | > 36 | 37 | 38 | 39 | ) : null} 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/OrderComp.jsx: -------------------------------------------------------------------------------- 1 | import { Col, Row } from "antd"; 2 | import React from "react"; 3 | import styled, { css } from "styled-components"; 4 | 5 | import FloatingElementBlock from "../Serum/components/layout/FloatingElementBlock"; 6 | 7 | const SizeTitle = styled(Row)` 8 | /* padding: 0px 0 14px; */ 9 | color: #acacad; 10 | `; 11 | 12 | const Line = styled.div` 13 | text-align: left; 14 | float: left; 15 | height: 100%; 16 | ${(props) => 17 | props["data-width"] && 18 | css` 19 | width: ${props["data-width"]}; 20 | `} 21 | ${(props) => 22 | props["data-bgcolor"] && 23 | css` 24 | background-color: ${props["data-bgcolor"]}; 25 | `} 26 | `; 27 | 28 | const Price = styled.div` 29 | position: absolute; 30 | right: 5px; 31 | color: white; 32 | top: 5px; 33 | `; 34 | 35 | export default function OrderComp({ smallScreen, depth = 7, onPrice, onSize }) { 36 | return ( 37 | 65 | 73 | 85 | PRICE 86 | 87 | 99 | AMOUNT 100 | 101 | 113 | {/* TOTAL ({quoteCurrency}) */} 114 | 115 | 116 |
117 | {orderbookData?.asks.map(({ price, size, sizePercent }) => ( 118 | onPrice(price)} 125 | onSizeClick={() => onSize(size)} 126 | /> 127 | ))} 128 | 132 | {orderbookData?.bids.map(({ price, size, sizePercent }) => ( 133 | onPrice(price)} 140 | onSizeClick={() => onSize(size)} 141 | /> 142 | ))} 143 |
144 |
145 | ); 146 | } 147 | 148 | const OrderbookRow = React.memo( 149 | ({ side, price, size, sizePercent, onSizeClick, onPriceClick }) => { 150 | return ( 151 | 159 | 166 | 180 | {/* {formattedPrice} */} 181 | 182 | 190 | 191 | 203 | {/* {formattedSize} */} 204 | 205 | 217 | 218 | ); 219 | } 220 | ); 221 | -------------------------------------------------------------------------------- /src/components/Settings.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Typography } from 'antd'; 3 | import { usePreferences } from '../utils/preferences'; 4 | 5 | const { Paragraph } = Typography; 6 | 7 | export default function Settings({ autoApprove }) { 8 | const { autoSettleEnabled, setAutoSettleEnabled } = usePreferences(); 9 | 10 | return ( 11 |
12 | {' '} 18 | Auto settle 19 | {!autoApprove && ( 20 | 21 | To use auto settle, first enable auto approval in your wallet 22 | 23 | )} 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/StandaloneTokenAccountSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TokenAccount } from "../utils/types"; 3 | import { useSelectedTokenAccounts } from "../utils/markets"; 4 | import { Button, Col, Select, Typography } from "antd"; 5 | import { CopyOutlined } from "@ant-design/icons"; 6 | import { abbreviateAddress } from "../utils/utils"; 7 | import { notify } from "../utils/notifications"; 8 | 9 | export default function StandaloneTokenAccountsSelect({ 10 | accounts, 11 | mint, 12 | label, 13 | }: { 14 | accounts: TokenAccount[] | null | undefined; 15 | mint: string | undefined; 16 | label?: boolean; 17 | }) { 18 | const [selectedTokenAccounts, setSelectedTokenAccounts] = 19 | useSelectedTokenAccounts(); 20 | 21 | let selectedValue: string | undefined; 22 | if (mint && mint in selectedTokenAccounts) { 23 | selectedValue = selectedTokenAccounts[mint]; 24 | } else if (accounts && accounts?.length > 0) { 25 | selectedValue = accounts[0].pubkey.toBase58(); 26 | } else { 27 | selectedValue = undefined; 28 | } 29 | 30 | const setTokenAccountForCoin = (value) => { 31 | if (!mint) { 32 | notify({ 33 | message: "Error selecting token account", 34 | description: "Mint is undefined", 35 | type: "error", 36 | }); 37 | return; 38 | } 39 | const newSelectedTokenAccounts = { ...selectedTokenAccounts }; 40 | newSelectedTokenAccounts[mint] = value; 41 | setSelectedTokenAccounts(newSelectedTokenAccounts); 42 | }; 43 | 44 | return ( 45 | 46 | {label && Token account:} 47 | 48 | 73 | 74 | 75 | 95 | 96 | ), 97 | }, 98 | ].filter((x) => x); 99 | return ( 100 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/components/UserInfoTable/FeesTable.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Row, Col, Typography, Tag } from "antd"; 3 | import { useFeeDiscountKeys } from "../../utils/markets"; 4 | import DataTable from "../layout/DataTable"; 5 | import { TokenInstructions, getFeeRates } from "@project-serum/serum"; 6 | import { percentFormat } from "../../utils/utils"; 7 | 8 | export default function FeesTable() { 9 | const [feeAccounts] = useFeeDiscountKeys(); 10 | 11 | const columns = [ 12 | { 13 | title: "Fee Tier", 14 | dataIndex: "feeTier", 15 | key: "feeTier", 16 | render: (feeTier, row) => ( 17 |
18 | {feeTier} 19 | {row.index === 0 ? ( 20 |
21 | 22 | Selected 23 | 24 |
25 | ) : null} 26 |
27 | ), 28 | }, 29 | { 30 | title: "Taker", 31 | dataIndex: "taker", 32 | key: "taker", 33 | render: (feeTier, row) => 34 | percentFormat.format(getFeeRates(row.feeTier).taker), 35 | }, 36 | { 37 | title: "Maker", 38 | dataIndex: "maker", 39 | key: "maker", 40 | render: (feeTier, row) => 41 | percentFormat.format(getFeeRates(row.feeTier).maker), 42 | }, 43 | { 44 | title: `Public Key`, 45 | dataIndex: "pubkey", 46 | key: "pubkey", 47 | render: (pubkey) => pubkey.toBase58(), 48 | }, 49 | { 50 | title: `Balance`, 51 | dataIndex: "balance", 52 | key: "balance", 53 | }, 54 | { 55 | title: `Mint`, 56 | dataIndex: "mint", 57 | key: "mint", 58 | render: (_, row) => 59 | row.mint.equals(TokenInstructions.SRM_MINT) 60 | ? "SRM" 61 | : row.mint.equals(TokenInstructions.MSRM_MINT) 62 | ? "MSRM" 63 | : "UNKNOWN", 64 | }, 65 | ]; 66 | 67 | const dataSource = (feeAccounts || []).map((account, index) => ({ 68 | ...account, 69 | index, 70 | key: account.pubkey.toBase58(), 71 | })); 72 | 73 | return ( 74 | <> 75 | 76 | 77 | 84 | 85 | 86 | 87 | 88 | 89 | Holding SRM or MSRM makes you eligible for fee discounts: 90 | 91 | 92 | 93 | 94 | 95 | ); 96 | } 97 | 98 | function FeeScheduleTable() { 99 | // Representation of serum-js/src/fees.ts 100 | const dataSource = [ 101 | { feeTier: 0, taker: 0.0022, maker: -0.0003, token: "", balance: "" }, 102 | { feeTier: 1, taker: 0.002, maker: -0.0003, token: "SRM", balance: 100 }, 103 | { feeTier: 2, taker: 0.0018, maker: -0.0003, token: "SRM", balance: 1000 }, 104 | { feeTier: 3, taker: 0.0016, maker: -0.0003, token: "SRM", balance: 10000 }, 105 | { 106 | feeTier: 4, 107 | taker: 0.0014, 108 | maker: -0.0003, 109 | token: "SRM", 110 | balance: 100000, 111 | }, 112 | { 113 | feeTier: 5, 114 | taker: 0.0012, 115 | maker: -0.0003, 116 | token: "SRM", 117 | balance: 1000000, 118 | }, 119 | { feeTier: 6, taker: 0.001, maker: -0.0005, token: "MSRM", balance: 1 }, 120 | ]; 121 | const columns = [ 122 | { 123 | title: "Fee Tier", 124 | dataIndex: "feeTier", 125 | key: "feeTier", 126 | }, 127 | { 128 | title: "Taker", 129 | dataIndex: "taker", 130 | key: "taker", 131 | render: (feeTier, row) => 132 | percentFormat.format(getFeeRates(row.feeTier).taker), 133 | }, 134 | { 135 | title: "Maker", 136 | dataIndex: "maker", 137 | key: "maker", 138 | render: (feeTier, row) => 139 | percentFormat.format(getFeeRates(row.feeTier).maker), 140 | }, 141 | { 142 | title: "Requirements", 143 | dataIndex: "requirements", 144 | key: "requirements", 145 | render: (_, row) => ( 146 | 147 | {!row.balance ? "None" : `≥ ${row.balance} ${row.token}`} 148 | 149 | ), 150 | }, 151 | ]; 152 | return ( 153 | ({ ...info, key: info.feeTier }))} 155 | columns={columns} 156 | /> 157 | ); 158 | } 159 | -------------------------------------------------------------------------------- /src/components/UserInfoTable/FillsTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col, Tag } from 'antd'; 3 | import { useFills, useMarket } from '../../utils/markets'; 4 | import DataTable from '../layout/DataTable'; 5 | 6 | export default function FillsTable() { 7 | const fills = useFills(); 8 | 9 | const { quoteCurrency } = useMarket(); 10 | 11 | const columns = [ 12 | { 13 | title: 'Market', 14 | dataIndex: 'marketName', 15 | key: 'marketName', 16 | }, 17 | { 18 | title: 'Side', 19 | dataIndex: 'side', 20 | key: 'side', 21 | render: (side) => ( 22 | 26 | {side.charAt(0).toUpperCase() + side.slice(1)} 27 | 28 | ), 29 | }, 30 | { 31 | title: `Size`, 32 | dataIndex: 'size', 33 | key: 'size', 34 | }, 35 | { 36 | title: `Price`, 37 | dataIndex: 'price', 38 | key: 'price', 39 | }, 40 | { 41 | title: `Liquidity`, 42 | dataIndex: 'liquidity', 43 | key: 'liquidity', 44 | }, 45 | { 46 | title: quoteCurrency ? `Fees (${quoteCurrency})` : 'Fees', 47 | dataIndex: 'feeCost', 48 | key: 'feeCost', 49 | }, 50 | ]; 51 | 52 | const dataSource = (fills || []).map((fill) => ({ 53 | ...fill, 54 | key: `${fill.orderId}${fill.side}`, 55 | liquidity: fill.eventFlags.maker ? 'Maker' : 'Taker', 56 | })); 57 | 58 | return ( 59 | <> 60 | 61 | 62 | 69 | 70 | 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/components/UserInfoTable/OpenOrderTable.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import DataTable from "../layout/DataTable"; 3 | 4 | import { DeleteOutlined } from "@ant-design/icons"; 5 | import { Button, Col, Row, Tag } from "antd"; 6 | import styled from "styled-components"; 7 | import { useSendConnection } from "../../utils/connection"; 8 | import { notify } from "../../utils/notifications"; 9 | import { cancelOrder } from "../../utils/send"; 10 | import { OrderWithMarketAndMarketName } from "../../utils/types"; 11 | //import { useWallet } from "../../utils/wallet"; 12 | import { useWallet } from "@solana/wallet-adapter-react"; 13 | import { BaseSignerWalletAdapter } from "@solana/wallet-adapter-base"; 14 | // import "./datatable.css"; 15 | const CancelButton = styled(Button)` 16 | color: #f23b69; 17 | border: 1px solid #f23b69; 18 | `; 19 | 20 | export default function OpenOrderTable({ 21 | openOrders, 22 | onCancelSuccess, 23 | pageSize, 24 | loading, 25 | marketFilter 26 | }: { 27 | openOrders: OrderWithMarketAndMarketName[] | null | undefined; 28 | onCancelSuccess?: () => void; 29 | pageSize?: number; 30 | loading?: boolean; 31 | marketFilter?: boolean; 32 | }) { 33 | let { wallet } = useWallet(); 34 | let connection = useSendConnection(); 35 | 36 | const [cancelId, setCancelId] = useState(null); 37 | 38 | async function cancel(order) { 39 | setCancelId(order?.orderId); 40 | try { 41 | if (!wallet) { 42 | return null; 43 | } 44 | 45 | await cancelOrder({ 46 | order, 47 | market: order.market, 48 | connection, 49 | wallet: wallet.adapter as BaseSignerWalletAdapter 50 | }); 51 | } catch (e: any) { 52 | notify({ 53 | message: "Error cancelling order", 54 | description: e.message, 55 | type: "error" 56 | }); 57 | return; 58 | } finally { 59 | setCancelId(null); 60 | } 61 | onCancelSuccess && onCancelSuccess(); 62 | } 63 | 64 | const marketFilters = [ 65 | ...new Set((openOrders || []).map((orderInfos) => orderInfos.marketName)) 66 | ].map((marketName) => { 67 | return { text: marketName, value: marketName }; 68 | }); 69 | 70 | const columns = [ 71 | { 72 | title: "Market", 73 | dataIndex: "marketName", 74 | key: "marketName", 75 | filters: marketFilter ? marketFilters : undefined, 76 | onFilter: (value, record) => record.marketName.indexOf(value) === 0 77 | }, 78 | { 79 | title: "Side", 80 | dataIndex: "side", 81 | key: "side", 82 | render: (side) => ( 83 | 87 | {side.charAt(0).toUpperCase() + side.slice(1)} 88 | 89 | ), 90 | sorter: (a, b) => { 91 | if (a.side === b.side) { 92 | return 0; 93 | } else if (a.side === "buy") { 94 | return 1; 95 | } else { 96 | return -1; 97 | } 98 | }, 99 | showSorterTooltip: false 100 | }, 101 | { 102 | title: "Size", 103 | dataIndex: "size", 104 | key: "size", 105 | sorter: (a, b) => b.size - a.size, 106 | showSorterTooltip: false 107 | }, 108 | { 109 | title: "Price", 110 | dataIndex: "price", 111 | key: "price", 112 | sorter: (a, b) => b.price - a.price, 113 | showSorterTooltip: false 114 | }, 115 | { 116 | key: "orderId", 117 | render: (order) => ( 118 |
119 | } 121 | onClick={() => cancel(order)} 122 | loading={cancelId + "" === order?.orderId + ""} 123 | > 124 | Cancel 125 | 126 |
127 | ) 128 | } 129 | ]; 130 | const dataSource = (openOrders || []).map((order) => ({ 131 | ...order, 132 | key: order.orderId 133 | })); 134 | 135 | return ( 136 | 137 | 138 | 146 | 147 | 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /src/components/UserInfoTable/UserInfoTableDisabled.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import BalancesTable from "./BalancesTable"; 3 | import OpenOrderTable from "./OpenOrderTable"; 4 | import { Box, makeStyles, Tab } from "@material-ui/core"; 5 | import { TabContext, TabList, TabPanel } from "@mui/lab"; 6 | import styled from "styled-components"; 7 | import { useBalances, useMarket, useOpenOrders } from "../../utils/markets"; 8 | import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet"; 9 | import WalletConnect from "../WalletConnect"; 10 | 11 | const Wrapper = styled.div` 12 | margin: 0px; 13 | padding: 0px; 14 | // background-color: #1a2029; 15 | `; 16 | function FloatingElement({ 17 | style = {}, 18 | children, 19 | stretchVertical = false, 20 | className = "" 21 | }) { 22 | return ( 23 | 30 | {children} 31 | 32 | ); 33 | } 34 | //const { TabPane } = Tabs; 35 | const useStyles = makeStyles({ 36 | root: { 37 | padding: 0, 38 | minHeight: 0, 39 | "& .Mui-selected": { 40 | backgroundColor: "#132235", 41 | color: "#E2E8F0", 42 | fontFamily: "Poppins", 43 | fontStyle: "normal", 44 | fontWeight: "400", 45 | fontSize: "11px", 46 | lineHeight: "12px", 47 | textTransform: "capitalize", 48 | border: "1px solid #132235", 49 | borderTopRadius: "5px", 50 | borderBottom: "none" 51 | }, 52 | "& .MuiTab-root": { 53 | color: "#E2E8F0", 54 | fontFamily: "Poppins", 55 | fontStyle: "normal", 56 | fontWeight: "400", 57 | fontSize: "11px", 58 | lineHeight: "12px", 59 | textTransform: "capitalize", 60 | maxHeight: "30px", 61 | borderBottom: "none", 62 | minHeight: "30px", 63 | minWidth: "125px" 64 | }, 65 | "& .MuiTabs-indicator": { 66 | backgroundColor: "transparent" 67 | }, 68 | "& .MuiTabs-flexContainer": { 69 | borderBottom: "1px solid #132235" 70 | }, 71 | "& .iuIFQc": { 72 | minHeight: "10px !important" 73 | } 74 | } 75 | }); 76 | export default function UserInfoDisabled() { 77 | const [value, setValue] = useState("1"); 78 | const classes = useStyles(); 79 | const handleChange = (event, newValue) => { 80 | setValue(newValue); 81 | }; 82 | 83 | const { market } = useMarket(); 84 | return ( 85 | 101 | 102 | 103 | 109 | 110 | 111 | 112 | {market && market.supportsSrmFeeDiscounts && ( 113 | 114 | )} 115 | 116 | 117 | 118 | 119 |
120 |
121 |
122 | 125 |
126 | Please connect your wallet to utilize Openbook functions , 127 | view balances, or access your open orders. 128 |
129 |
130 |
131 | 132 |
133 |
134 |
135 |
136 | 137 |
138 | 139 | 140 |
141 | ); 142 | } 143 | 144 | const OpenOrdersTab = () => { 145 | const openOrders = useOpenOrders(); 146 | 147 | return ; 148 | }; 149 | 150 | const BalancesTab = () => { 151 | const balances = useBalances(); 152 | 153 | return ; 154 | }; 155 | -------------------------------------------------------------------------------- /src/components/UserInfoTable/WalletBalancesTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import DataTable from "../layout/DataTable"; 3 | import { Button, Row } from "antd"; 4 | import { settleAllFunds } from "../../utils/send"; 5 | import { notify } from "../../utils/notifications"; 6 | import { useConnection } from "../../utils/connection"; 7 | //import { useWallet } from "../../utils/wallet"; 8 | import { 9 | useAllMarkets, 10 | useSelectedTokenAccounts, 11 | useTokenAccounts 12 | } from "../../utils/markets"; 13 | import StandaloneTokenAccountsSelect from "../StandaloneTokenAccountSelect"; 14 | import { abbreviateAddress } from "../../utils/utils"; 15 | import { PublicKey } from "@solana/web3.js"; 16 | import { useWallet } from "@solana/wallet-adapter-react"; 17 | import { BaseSignerWalletAdapter } from "@solana/wallet-adapter-base"; 18 | export default function WalletBalancesTable({ 19 | walletBalances 20 | }: { 21 | walletBalances: { 22 | coin: string; 23 | mint: string; 24 | walletBalance: number; 25 | openOrdersFree: number; 26 | openOrdersTotal: number; 27 | }[]; 28 | }) { 29 | const connection = useConnection(); 30 | const { wallet, connected } = useWallet(); 31 | const [selectedTokenAccounts] = useSelectedTokenAccounts(); 32 | const [tokenAccounts, tokenAccountsConnected] = useTokenAccounts(); 33 | const [allMarkets, allMarketsConnected] = useAllMarkets(); 34 | const [settlingFunds, setSettlingFunds] = useState(false); 35 | 36 | async function onSettleFunds() { 37 | setSettlingFunds(true); 38 | try { 39 | if (!wallet) { 40 | notify({ 41 | message: "Wallet not connected", 42 | description: "Wallet not connected", 43 | type: "error" 44 | }); 45 | return; 46 | } 47 | 48 | if (!tokenAccounts || !tokenAccountsConnected) { 49 | notify({ 50 | message: "Error settling funds", 51 | description: "TokenAccounts not connected", 52 | type: "error" 53 | }); 54 | return; 55 | } 56 | if (!allMarkets || !allMarketsConnected) { 57 | notify({ 58 | message: "Error settling funds", 59 | description: "Markets not connected", 60 | type: "error" 61 | }); 62 | return; 63 | } 64 | await settleAllFunds({ 65 | connection, 66 | tokenAccounts, 67 | selectedTokenAccounts, 68 | wallet: wallet.adapter as BaseSignerWalletAdapter, 69 | markets: allMarkets.map((marketInfo) => marketInfo.market) 70 | }); 71 | } catch (e: any) { 72 | notify({ 73 | message: "Error settling funds", 74 | description: e.message, 75 | type: "error" 76 | }); 77 | } finally { 78 | setSettlingFunds(false); 79 | } 80 | } 81 | 82 | const columns = [ 83 | { 84 | title: "Coin", 85 | key: "coin", 86 | width: "20%", 87 | render: (walletBalance) => ( 88 | 89 | 94 | {walletBalance.coin || 95 | abbreviateAddress(new PublicKey(walletBalance.mint))} 96 | 97 | 98 | ) 99 | }, 100 | { 101 | title: "Wallet Balance", 102 | dataIndex: "walletBalance", 103 | key: "walletBalance", 104 | width: "20%" 105 | }, 106 | { 107 | title: "Open orders total balances", 108 | dataIndex: "openOrdersTotal", 109 | key: "openOrdersTotal", 110 | width: "20%" 111 | }, 112 | { 113 | title: "Unsettled balances", 114 | dataIndex: "openOrdersFree", 115 | key: "openOrdersFree", 116 | width: "20%" 117 | }, 118 | { 119 | title: "Selected token account", 120 | key: "selectTokenAccount", 121 | width: "20%", 122 | render: (walletBalance) => ( 123 | 124 | t.effectiveMint.toBase58() === walletBalance.mint 127 | )} 128 | mint={walletBalance.mint} 129 | /> 130 | 131 | ) 132 | } 133 | ]; 134 | return ( 135 | 136 | 142 | {connected && ( 143 | 146 | )} 147 | 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /src/components/UserInfoTable/datatable.css: -------------------------------------------------------------------------------- 1 | .ant-table table { 2 | width: 100%; 3 | text-align: left; 4 | border-radius: 2px 2px 0 0; 5 | border-collapse: separate; 6 | border-spacing: 0; 7 | background: transparent !important; 8 | } 9 | .ant-table-wrapper { 10 | background: transparent !important; 11 | 12 | min-width: 100% !important; 13 | /* height: 463px !important; */ 14 | /* padding-bottom: 10em; */ 15 | } 16 | 17 | .ant-table-thead > tr > th { 18 | /* background: transparent !important; */ 19 | /* color: #000 !important; */ 20 | } 21 | 22 | .ant-table-tbody > tr > td { 23 | background: transparent !important; 24 | /* color: #000 !important; */ 25 | } 26 | .ant-typography { 27 | /* color: #000 !important; */ 28 | } 29 | .ant-table-container { 30 | border-top-left-radius: 2px; 31 | border-top-right-radius: 2px; 32 | /* margin-top: 20px; */ 33 | /* margin-left: -55px !important; */ 34 | } 35 | .ant-table { 36 | background-color: transparent !important; 37 | } 38 | .ant-table-tbody > tr > td { 39 | background: transparent !important; 40 | border-bottom: solid 1px rgba(12, 12, 12, 0.2) !important; 41 | } 42 | .active-table { 43 | margin-left: -55px !important; 44 | } 45 | .ant-modal-title { 46 | /* color: #000 !important; */ 47 | } 48 | .ant-btn-ghost { 49 | /* color: rgba(12, 12, 12, 0.7) !important; */ 50 | } 51 | .ant-modal-header { 52 | /* border-bottom: rgba(12, 12, 12, 0.8) !important; */ 53 | } 54 | .ant-btn > span { 55 | /* color: #000 !important; */ 56 | } 57 | .ant-table-pagination.ant-pagination { 58 | margin: 20px 15px !important; 59 | } 60 | -------------------------------------------------------------------------------- /src/components/UserInfoTable/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import BalancesTable from "./BalancesTable"; 3 | import OpenOrderTable from "./OpenOrderTable"; 4 | //import { Tabs, } from "antd"; 5 | import FillsTable from "./FillsTable"; 6 | 7 | import { Box, makeStyles, Tab } from "@material-ui/core"; 8 | import { TabContext, TabList, TabPanel } from "@mui/lab"; 9 | import styled from "styled-components"; 10 | import { useBalances, useMarket, useOpenOrders } from "../../utils/markets"; 11 | import FeesTable from "./FeesTable"; 12 | //import "./styles.css"; 13 | 14 | const Wrapper = styled.div` 15 | margin: 0px; 16 | padding: 0px; 17 | // background-color: #1a2029; 18 | `; 19 | function FloatingElement({ 20 | style = {}, 21 | children, 22 | stretchVertical = false, 23 | className = "" 24 | }) { 25 | return ( 26 | 33 | {children} 34 | 35 | ); 36 | } 37 | //const { TabPane } = Tabs; 38 | const useStyles = makeStyles({ 39 | root: { 40 | padding: 0, 41 | minHeight: 0, 42 | "& .Mui-selected": { 43 | backgroundColor: "#132235", 44 | color: "#E2E8F0", 45 | fontFamily: "Poppins", 46 | fontStyle: "normal", 47 | fontWeight: "400", 48 | fontSize: "11px", 49 | lineHeight: "12px", 50 | textTransform: "capitalize", 51 | border: "1px solid #132235", 52 | borderTopRadius: "5px", 53 | borderBottom: "none" 54 | }, 55 | "& .MuiTab-root": { 56 | color: "#E2E8F0", 57 | fontFamily: "Poppins", 58 | fontStyle: "normal", 59 | fontWeight: "400", 60 | fontSize: "11px", 61 | lineHeight: "12px", 62 | textTransform: "capitalize", 63 | maxHeight: "30px", 64 | borderBottom: "none", 65 | minHeight: "30px", 66 | minWidth: "125px" 67 | }, 68 | "& .MuiTabs-indicator": { 69 | backgroundColor: "transparent" 70 | }, 71 | "& .MuiTabs-flexContainer": { 72 | borderBottom: "1px solid #132235" 73 | }, 74 | "& .iuIFQc": { 75 | minHeight: "10px !important" 76 | } 77 | } 78 | }); 79 | export default function Index() { 80 | const [value, setValue] = useState("1"); 81 | const classes = useStyles(); 82 | const handleChange = (event, newValue) => { 83 | setValue(newValue); 84 | }; 85 | 86 | const { market } = useMarket(); 87 | return ( 88 | 104 | 105 | 106 | 112 | 113 | 114 | 115 | {market && market.supportsSrmFeeDiscounts && ( 116 | 117 | )} 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | {market && market.supportsSrmFeeDiscounts ? ( 131 | 132 | 133 | 134 | ) : null} 135 | 136 | 137 | 138 | 139 | ); 140 | } 141 | 142 | const OpenOrdersTab = () => { 143 | const openOrders = useOpenOrders(); 144 | 145 | return ; 146 | }; 147 | 148 | const BalancesTab = () => { 149 | const balances = useBalances(); 150 | 151 | return ; 152 | }; 153 | -------------------------------------------------------------------------------- /src/components/UserInfoTable/styles.css: -------------------------------------------------------------------------------- 1 | .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn { 2 | color: #fff !important ; 3 | text-shadow: 0 0 0.25px currentColor; 4 | } 5 | .ant-tabs-tab { 6 | margin-bottom: 10px !important; 7 | font-weight: bold !important; 8 | } 9 | .ant-tabs-tab-active { 10 | /* background: #191b20 !important; */ 11 | border-radius: 10px !important; 12 | padding: 8px 16px; 13 | background: rgb(50, 205, 153) none repeat scroll 0% 0%; 14 | 15 | color: #fff !important; 16 | border-radius: 5px !important; 17 | } 18 | .ant-tabs-tab { 19 | width: 251px !important ; 20 | height: 45px !important; 21 | text-align: center !important; 22 | display: flex; 23 | justify-content: center !important; 24 | /* color: #000 !important; */ 25 | border-color: transparent !important; 26 | } 27 | .ant-tabs-ink-bar { 28 | height: 5px; 29 | background: none !important; 30 | } 31 | /* div { 32 | color: #fff; 33 | } */ 34 | @media screen and (max-width: 1849px) { 35 | .info-table { 36 | scrollbar-width: none !important; 37 | } 38 | .trade-info { 39 | /* margin-top: -3em !important; */ 40 | } 41 | } 42 | @media screen and (min-width: 1886px) and (max-width: 1920px) { 43 | .info-table { 44 | scrollbar-width: none !important; 45 | /* margin-top: -8em !important; */ 46 | } 47 | } 48 | @media screen and (min-width: 1921px) { 49 | .info-table { 50 | scrollbar-width: none !important; 51 | } 52 | } 53 | .trade-info { 54 | /* margin-top: 5em; */ 55 | } 56 | @media screen and (min-width: 1770px) { 57 | .trade-info { 58 | width: 64% !important; 59 | scrollbar-width: none !important; 60 | scrollbar-color: transaprent !important; 61 | } 62 | } 63 | 64 | @media screen and (max-width: 1769px) { 65 | .trade-info { 66 | width: 100% !important; 67 | scrollbar-width: none !important; 68 | scrollbar-color: transaprent !important; 69 | } 70 | } 71 | .info-table::-webkit-scrollbar { 72 | display: none !important; 73 | background-color: transparent !important; 74 | } 75 | 76 | 77 | .ant-table table { 78 | background: transparent !important ; 79 | } 80 | .ant-tabs > .ant-tabs-nav .ant-tabs-nav-wrap, 81 | .ant-tabs > div > .ant-tabs-nav .ant-tabs-nav-wrap { 82 | border-color: transparent !important; 83 | border: none !important; 84 | } 85 | .ant-input::placeholder { 86 | /* color: #181818 !important; */ 87 | font-weight: lighter !important; 88 | } 89 | .ant-tabs-top > .ant-tabs-nav::before, 90 | .ant-tabs-bottom > .ant-tabs-nav::before, 91 | .ant-tabs-top > div > .ant-tabs-nav::before, 92 | .ant-tabs-bottom > div > .ant-tabs-nav::before { 93 | position: absolute; 94 | right: 0; 95 | left: 0; 96 | border-bottom: none !important; 97 | content: ""; 98 | } 99 | -------------------------------------------------------------------------------- /src/components/WalletConnect.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from "@mantine/core"; 2 | import { Button, Dropdown, Menu } from "antd"; 3 | import { useLayoutEffect, useState } from "react"; 4 | 5 | import { useWallet } from "@solana/wallet-adapter-react"; 6 | import LinkAddress from "./LinkAddress"; 7 | 8 | 9 | const WalletConnect = () => { 10 | 11 | const { connected, publicKey, connect, disconnect, select, wallet, wallets } = 12 | useWallet(); 13 | const [isModalVisible, setIsModalVisible] = useState(false); 14 | const publicKeyString = publicKey?.toBase58() || ""; 15 | 16 | 17 | const menu = ( 18 | 23 | {connected && } 24 | { 27 | setIsModalVisible((v) => !v); 28 | }} 29 | 30 | 31 | > 32 | Change Wallet 33 | 34 | 35 | ); 36 | 37 | return ( 38 | <> 39 | { 48 | setIsModalVisible(true); 49 | } 50 | } 51 | overlay={menu} 52 | > 53 | {connected ? "Disconnect" : "Connect"} 54 | 55 | setIsModalVisible(false)} 61 | transitionProps={{ transition: 'fade', duration: 600, timingFunction: 'linear' }} 62 | 63 | 64 | styles={{ 65 | content: { 66 | backgroundColor: "rgb(3, 10, 19)", 67 | borderStyle: "solid", 68 | borderWidth: "2px", 69 | borderColor: "rgb(19, 34, 53)", 70 | borderRadius: "3px", 71 | 72 | 73 | display: "flex", 74 | justifyContent: "center", 75 | alignItems: "center", 76 | flexDirection: "column", 77 | gap: "20px", 78 | marginTop: "25px" 79 | }, 80 | body: { width: "100%" } 81 | }} 82 | > 83 | {wallets.map(({ adapter }, ind) => { 84 | const onClick = function () { 85 | select(adapter.name); 86 | setIsModalVisible(false); 87 | }; 88 | 89 | return ( 90 | 113 | ); 114 | })} 115 | 116 | 117 | ); 118 | } 119 | WalletConnect.ssr = false; 120 | export default WalletConnect; -------------------------------------------------------------------------------- /src/components/global_style.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | export const GlobalStyle = createGlobalStyle` 4 | html,body{ 5 | 6 | } 7 | input[type=number]::-webkit-inner-spin-button { 8 | opacity: 0; 9 | } 10 | input[type=number]:hover::-webkit-inner-spin-button, 11 | input[type=number]:focus::-webkit-inner-spin-button { 12 | opacity: 0.25; 13 | } 14 | /* width */ 15 | ::-webkit-scrollbar { 16 | width: 15px; 17 | } 18 | /* Track */ 19 | ::-webkit-scrollbar-track { 20 | background: #fff; 21 | } 22 | /* Handle */ 23 | ::-webkit-scrollbar-thumb { 24 | background: #5b5f67; 25 | } 26 | /* Handle on hover */ 27 | ::-webkit-scrollbar-thumb:hover { 28 | background: #5b5f67; 29 | } 30 | .ant-slider-track, .ant-slider:hover .ant-slider-track { 31 | background-color: #355DFF 32 | opacity: 0.75; 33 | } 34 | .ant-slider-track, 35 | .ant-slider ant-slider-track:hover { 36 | background-color: #355DFF 37 | opacity: 0.75; 38 | } 39 | .ant-slider-dot-active, 40 | .ant-slider-handle, 41 | .ant-slider-handle-click-focused, 42 | .ant-slider:hover .ant-slider-handle:not(.ant-tooltip-open) { 43 | border: 2px solid #355DFF 44 | } 45 | .ant-table-tbody > tr.ant-table-row:hover > td { 46 | background: #273043; 47 | } 48 | .ant-table-tbody > tr > td { 49 | /* border-bottom: 8px solid #1A2029; */ 50 | } 51 | .ant-table-container table > thead > tr:first-child th { 52 | border-bottom: none; 53 | } 54 | .ant-divider-horizontal.ant-divider-with-text::before, .ant-divider-horizontal.ant-divider-with-text::after { 55 | border-top: 1px solid #acacad !important; 56 | } 57 | .ant-layout { 58 | // background: #11161D 59 | } 60 | .ant-table { 61 | background: #212734; 62 | } 63 | .ant-table-thead > tr > th { 64 | // background: transparent !important; 65 | } 66 | .ant-select-item-option-content { 67 | img { 68 | margin-right: 4px; 69 | } 70 | } 71 | .ant-modal-content { 72 | 73 | } 74 | 75 | @-webkit-keyframes highlight { 76 | from { background-color:#d3b3f2} 77 | to {background-color: rgba(234, 228, 240,0.5);} 78 | } 79 | @-moz-keyframes highlight { 80 | from { background-color: #d3b3f2} 81 | to {background-color: rgba(234, 228, 240,0.5);} 82 | } 83 | @-keyframes highlight { 84 | from { background-color: #d3b3f2} 85 | to {background-color: rgba(234, 228, 240,0.5);} 86 | } 87 | .flash { 88 | -moz-animation: highlight 30.5s ease 0s 1 alternate ; 89 | -webkit-animation: highlight 30.5s ease 0s 1 alternate; 90 | animation: highlight 0.5s ease 0s 1 alternate; 91 | }`; 92 | -------------------------------------------------------------------------------- /src/components/layout/DataTable.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ConfigProvider, Table } from "antd"; 3 | 4 | export default function DataTable({ 5 | dataSource, 6 | columns, 7 | emptyLabel = "No data", 8 | pagination = false, 9 | loading = false, 10 | pageSize = 10 11 | }) { 12 | const customizeRenderEmpty = () => ( 13 |
21 | {emptyLabel} 22 |
23 | ); 24 | 25 | return ( 26 | 27 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/layout/FloatingElement.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Wrapper = styled.div` 5 | margin: 5px; 6 | padding: 5px; 7 | // background-color: #1a2029; 8 | `; 9 | 10 | export default function FloatingElement({ 11 | style = {}, 12 | children, 13 | stretchVertical = false, 14 | className = "" 15 | }) { 16 | return ( 17 | 24 | {children} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/layout/FloatingElementBlock.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Wrapper = styled.div` 5 | // margin: 5px; 6 | background-color: transparent; 7 | padding-bottom: 2.5em; 8 | padding-top: 1.1em; 9 | `; 10 | 11 | export default function FloatingElementBlock({ 12 | style = undefined, 13 | children, 14 | stretchVertical = false 15 | }) { 16 | return ( 17 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/layout/FloatingNavbar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Wrapper = styled.div` 5 | margin-top: 15px; 6 | // margin-left:20px; 7 | background-color: transparent; 8 | padding-top: 1.1em; 9 | `; 10 | 11 | export default function FloatingNavBar({ 12 | style = undefined, 13 | children, 14 | stretchVertical = false 15 | }) { 16 | return ( 17 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/tradeform.css: -------------------------------------------------------------------------------- 1 | .ant-select:not(.ant-select-customize-input) .ant-select-selector { 2 | position: relative; 3 | background-color: transparent; 4 | border: none !important; 5 | border-radius: 2px; 6 | transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 7 | } 8 | 9 | .ant-select:not(.ant-select-disabled):hover .ant-select-selector { 10 | border: none !important; 11 | } 12 | 13 | select option { 14 | background: rgba(25, 27, 32, 0.5); 15 | border: none !important; 16 | } 17 | select option:hover { 18 | background-color: red !important; 19 | } 20 | select { 21 | border: none !important; 22 | outline: none !important; 23 | } 24 | input { 25 | border: none !important; 26 | outline: none !important; 27 | background-color: transparent !important; 28 | font-weight: bold; 29 | } 30 | .input-row { 31 | width: 90% !important; 32 | display: flex; 33 | margin-left: 1.1em; 34 | margin-bottom: 0.5em; 35 | position: relative; 36 | } 37 | .input-row span { 38 | font-size: 14px; 39 | line-height: 24px; 40 | /* identical to box height, or 171% */ 41 | 42 | text-align: right; 43 | 44 | font-weight: bold; 45 | position: absolute; 46 | color: #4fbf67; 47 | right: 0; 48 | padding-right: 5%; 49 | } 50 | .srm-col { 51 | font-size: 14px; 52 | line-height: 24px; 53 | /* identical to box height, or 171% */ 54 | 55 | text-align: right; 56 | 57 | font-weight: bold; 58 | position: absolute; 59 | color: #ff9f38 !important; 60 | 61 | right: 0; 62 | } 63 | 64 | .input-col { 65 | border: 2px solid #0d39eb; 66 | box-sizing: border-box; 67 | border-radius: 12px; 68 | } 69 | .buy-btn { 70 | background: #355dff !important; 71 | border-radius: 12px !important; 72 | border: none !important; 73 | } 74 | 75 | .market-select { 76 | cursor: pointer !important; 77 | } 78 | body { 79 | /* color: rgb(17, 20, 45) !important; */ 80 | } 81 | .ant-input-affix-wrapper { 82 | border: none !important ; 83 | min-height: 40px; 84 | } 85 | .ant-input-affix-wrapper:focus { 86 | border-color: transparent !important ; 87 | } 88 | .ant-input-group-addon { 89 | background-color: transparent !important; 90 | border: none !important; 91 | } 92 | .ant-input-affix-wrapper:focus, 93 | .ant-input-affix-wrapper-focused { 94 | outline: 0; 95 | box-shadow: 0 0 0 1px transparent !important; 96 | border-radius: 12px; 97 | } 98 | .active-sell { 99 | border-radius: 5px !important; 100 | background-color: rgba(19, 34, 53, 0.5) !important; 101 | 102 | min-width: 78px !important; 103 | height: 35px !important; 104 | width: 100% !important; 105 | } 106 | .main-trade-btn { 107 | padding: 0px; 108 | cursor: inherit; 109 | background: rgb(28, 24, 46) none repeat scroll 0% 0% !important; 110 | border: medium none; 111 | text-align: center; 112 | 113 | box-sizing: border-box; 114 | 115 | font-family: "Poppins"; 116 | font-size: 14px; 117 | font-weight: 500; 118 | text-shadow: rgba(0, 0, 0, 0.33) 1px 1px 2px; 119 | line-height: 1; 120 | border-radius: 15px; 121 | 122 | transition: color 300ms ease 0s, border-color 300ms ease 0s, 123 | background 300ms ease 0s; 124 | text-decoration: none; 125 | font-style: normal; 126 | box-shadow: none; 127 | } 128 | .sell-trade-btn { 129 | padding: 0px; 130 | cursor: inherit; 131 | background: rgb(221, 80, 151) none repeat scroll 0% 0% !important; 132 | border: medium none; 133 | text-align: center; 134 | 135 | box-sizing: border-box; 136 | 137 | font-family: "Poppins"; 138 | font-size: 14px; 139 | font-weight: 500; 140 | text-shadow: rgb(221, 80, 151) 1px 1px 2px; 141 | line-height: 1; 142 | border-radius: 5px; 143 | 144 | transition: color 300ms ease 0s, border-color 300ms ease 0s, 145 | background 300ms ease 0s; 146 | text-decoration: none; 147 | font-style: normal; 148 | box-shadow: none; 149 | } 150 | .buy-trade-btn { 151 | padding: 0px; 152 | cursor: inherit; 153 | background: rgb(50, 205, 153) none repeat scroll 0% 0% !important; 154 | border: medium none; 155 | text-align: center; 156 | 157 | box-sizing: border-box; 158 | 159 | font-family: "Poppins"; 160 | font-size: 14px; 161 | font-weight: 500; 162 | text-shadow: rgb(50, 205, 153) 1px 1px 2px; 163 | line-height: 1; 164 | border-radius: 5px; 165 | 166 | transition: color 300ms ease 0s, border-color 300ms ease 0s, 167 | background 300ms ease 0s; 168 | text-decoration: none; 169 | font-style: normal; 170 | box-shadow: none; 171 | } 172 | .disabled-trade-btn { 173 | padding: 0px; 174 | cursor: inherit; 175 | background-color: rgb(241, 241, 241) !important; 176 | border: medium none; 177 | text-align: center; 178 | 179 | box-sizing: border-box; 180 | 181 | font-family: "Poppins"; 182 | font-size: 14px; 183 | font-weight: 500; 184 | 185 | line-height: 1; 186 | border-radius: 5px; 187 | 188 | transition: color 300ms ease 0s, border-color 300ms ease 0s, 189 | background 300ms ease 0s; 190 | text-decoration: none; 191 | font-style: normal; 192 | box-shadow: none; 193 | color: #000 !important; 194 | } 195 | 196 | .active-sell-buy { 197 | padding: 0px !important; 198 | cursor: inherit !important; 199 | border-radius: 5px !important; 200 | background-color: transparent !important; 201 | font-size: 14px !important; 202 | font-weight: 500 !important; 203 | color: rgba(50, 205, 153, 1) !important; 204 | height: 35px !important; 205 | 206 | text-align: center !important; 207 | border: 1px solid rgba(50, 205, 153, 1) !important; 208 | background-color: rgba(19, 34, 53, 0.5) !important; 209 | box-sizing: border-box !important; 210 | 211 | font-family: "Poppins" !important; 212 | font-size: 14px !important; 213 | font-weight: 500 !important; 214 | 215 | width: 100% !important; 216 | 217 | transition: color 300ms ease 0s, border-color 300ms ease 0s, 218 | background 300ms ease 0s !important; 219 | text-decoration: none !important; 220 | font-style: normal !important; 221 | box-shadow: none !important; 222 | height: 35px !important; 223 | margin-right: 5px !important; 224 | } 225 | .active-sell-sell { 226 | font-family: "Poppins" !important; 227 | font-size: 14px !important; 228 | font-weight: bold !important; 229 | color: rgba(221, 80, 151, 1) !important; 230 | box-sizing: border-box !important; 231 | font-family: "Poppins" !important; 232 | font-size: 14px !important; 233 | font-weight: 500 !important; 234 | width: 100% !important; 235 | transition: color 300ms ease 0s, border-color 300ms ease 0s, 236 | background 300ms ease 0s !important; 237 | text-decoration: none !important; 238 | font-style: normal !important; 239 | box-shadow: none !important; 240 | height: 35px !important; 241 | margin-right: 5px !important; 242 | border: 1px solid rgba(221, 80, 151, 1) !important; 243 | background-color: rgba(19, 34, 53, 0.5) !important; 244 | } 245 | 246 | .ant-divider-horizontal.ant-divider-with-text::before, 247 | .ant-divider-horizontal.ant-divider-with-text::after { 248 | position: relative; 249 | top: 50%; 250 | width: 50%; 251 | border-top: 1px solid transparent; 252 | border-top-color: inherit; 253 | border-bottom: 0; 254 | transform: translateY(10%) !important; 255 | content: ""; 256 | } 257 | .remove-padding { 258 | margin-top: -16px !important; 259 | padding: 2px; 260 | } 261 | input::-webkit-outer-spin-button, 262 | input::-webkit-inner-spin-button { 263 | -webkit-appearance: none; 264 | margin: 0; 265 | } 266 | 267 | /* Firefox */ 268 | input[type="number"] { 269 | -moz-appearance: textfield; 270 | } 271 | .ant-slider-step { 272 | position: absolute !important; 273 | width: 100% !important; 274 | background-color: #fff !important; 275 | border: solid 1px rgb(170, 170, 170) !important; 276 | height: 8px !important; 277 | } 278 | .ant-slider-handle { 279 | border-radius: 5px !important; 280 | margin-top: -2px !important; 281 | } 282 | .ant-btn { 283 | /* border: none !important; */ 284 | } 285 | -------------------------------------------------------------------------------- /src/components/useMintInput.tsx: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import React, { ReactElement, useMemo, useState } from 'react'; 3 | import { useAccountInfo } from '../utils/connection'; 4 | import { isValidPublicKey } from '../utils/utils'; 5 | import { ValidateStatus } from 'antd/lib/form/FormItem'; 6 | import { TokenInstructions } from '@project-serum/serum'; 7 | import { parseTokenMintData, useMintToTickers } from '../utils/tokens'; 8 | import { AutoComplete, Form, Tooltip } from 'antd'; 9 | import Link from './Link'; 10 | 11 | export interface MintInfo { 12 | address: PublicKey; 13 | decimals: number; 14 | } 15 | 16 | export function useMintInput( 17 | name, 18 | label: string | ReactElement, 19 | tooltip?: string | ReactElement, 20 | ): [ReactElement, MintInfo | null] { 21 | const [address, setAddress] = useState(''); 22 | const [accountInfo, loaded] = useAccountInfo( 23 | isValidPublicKey(address) ? new PublicKey(address) : null, 24 | ); 25 | 26 | const mintToTickers = useMintToTickers(); 27 | const options = useMemo(() => { 28 | return Object.entries(mintToTickers) 29 | .filter( 30 | ([mintAddress, ticker]) => 31 | mintAddress.includes(address) || 32 | ticker.toLowerCase().includes(address.toLowerCase()), 33 | ) 34 | .map(([mintAddress, ticker]) => ({ 35 | value: mintAddress, 36 | label: ( 37 | <> 38 | {ticker} ({mintAddress}) 39 | 40 | ), 41 | })); 42 | }, [mintToTickers, address]); 43 | 44 | const { validateStatus, hasFeedback, help, mintInfo } = useMemo(() => { 45 | let validateStatus: ValidateStatus = ''; 46 | let hasFeedback = false; 47 | let help: string | null = null; 48 | let mintInfo: MintInfo | null = null; 49 | if (address) { 50 | hasFeedback = true; 51 | if (accountInfo) { 52 | if ( 53 | accountInfo.owner.equals(TokenInstructions.TOKEN_PROGRAM_ID) && 54 | accountInfo.data.length === 82 55 | ) { 56 | let parsed = parseTokenMintData(accountInfo.data); 57 | if (parsed.initialized) { 58 | validateStatus = 'success'; 59 | mintInfo = { 60 | address: new PublicKey(address), 61 | decimals: parsed.decimals, 62 | }; 63 | } else { 64 | validateStatus = 'error'; 65 | help = 'Invalid SPL mint'; 66 | } 67 | } else { 68 | validateStatus = 'error'; 69 | help = 'Invalid SPL mint address'; 70 | } 71 | } else if (isValidPublicKey(address) && !loaded) { 72 | validateStatus = 'validating'; 73 | } else { 74 | validateStatus = 'error'; 75 | help = 'Invalid Solana address'; 76 | } 77 | } 78 | return { validateStatus, hasFeedback, help, mintInfo }; 79 | }, [address, accountInfo, loaded]); 80 | 81 | const input = ( 82 | 87 | {tooltip} You can look up token mint addresses on{' '} 88 | 89 | sollet.io 90 | 91 | . 92 | 93 | } 94 | > 95 | {label} 96 | 97 | } 98 | name={name} 99 | validateStatus={validateStatus} 100 | hasFeedback={hasFeedback} 101 | help={help} 102 | > 103 | setAddress(value)} 107 | /> 108 | 109 | ); 110 | 111 | return [input, mintInfo]; 112 | } 113 | -------------------------------------------------------------------------------- /src/components/walletbutton.css: -------------------------------------------------------------------------------- 1 | .btn-connect { 2 | display: inline-block; 3 | font-weight: 400; 4 | color: #fff; 5 | text-align: center; 6 | vertical-align: middle; 7 | -webkit-user-select: none; 8 | user-select: none; 9 | background: -webkit-linear-gradient( 10 | left, 11 | rgb(119, 145, 224) 0%, 12 | rgb(50, 205, 153) 100% 13 | ) !important; 14 | border: 1px solid transparent; 15 | padding: 0.5rem 0.75rem; 16 | font-size: 0.9375rem; 17 | line-height: 1.5; 18 | border-radius: 0.375rem; 19 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 20 | border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 21 | display: flex !important; 22 | justify-content: center !important; 23 | align-items: center !important; 24 | height: 40px !important; 25 | min-width: 110px !important; 26 | } 27 | .ant-btn { 28 | border-color: transparent !important; 29 | } 30 | 31 | @media screen and (max-width: 380px) { 32 | .btn-connect { 33 | margin-left: 1.2em !important; 34 | } 35 | } 36 | .ant-dropdown-menu { 37 | box-shadow: none !important; 38 | border-radius: 10px !important; 39 | border: 1px none rgb(206, 216, 222) !important; 40 | 41 | color: rgb(18, 18, 18) !important; 42 | } 43 | .ant-btn-primary, 44 | .ant-btn-primary:hover, 45 | .ant-btn-primary:focus { 46 | background-color: rgba(44, 69, 102, 1) !important; 47 | } 48 | -------------------------------------------------------------------------------- /src/context/tokenList.tsx: -------------------------------------------------------------------------------- 1 | 2 | import axios from "axios"; 3 | import React, { createContext, useState, useContext, useEffect } from "react"; 4 | 5 | export const TokenListContext = createContext({ 6 | splTokenList: [], 7 | setSplTokenList: (data: any) => { } 8 | }); 9 | 10 | export const useTokenList = () => { 11 | const { splTokenList, setSplTokenList } = useContext(TokenListContext); 12 | return { splTokenList, setSplTokenList }; 13 | }; 14 | 15 | const SplTokenProvider = ({ children }: { children: any }) => { 16 | const [splTokenList, setSplTokenList] = useState([]); 17 | 18 | const updateSplTokenList = (newSplTokenList: any) => { 19 | setSplTokenList(newSplTokenList); 20 | }; 21 | const initTokenList = async () => { 22 | try { 23 | let { data } = await axios.get("https://cache.jup.ag/tokens"); 24 | setSplTokenList(data); 25 | } catch (err) { } 26 | }; 27 | 28 | useEffect(() => { 29 | initTokenList(); 30 | }, []); 31 | 32 | return ( 33 | 37 | {children} 38 | 39 | ); 40 | }; 41 | 42 | export default SplTokenProvider; 43 | -------------------------------------------------------------------------------- /src/pages/BalancesPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tabs } from 'antd'; 3 | import { 4 | useAllOpenOrdersBalances, 5 | useWalletBalancesForAllMarkets, 6 | } from '../utils/markets'; 7 | import FloatingElement from '../components/layout/FloatingElement'; 8 | import WalletBalancesTable from '../components/UserInfoTable/WalletBalancesTable'; 9 | import { useMintToTickers } from '../utils/tokens'; 10 | 11 | const { TabPane } = Tabs; 12 | 13 | export default function BalancesPage() { 14 | const walletBalances = useWalletBalancesForAllMarkets(); 15 | const mintToTickers = useMintToTickers(); 16 | const openOrdersBalances = useAllOpenOrdersBalances(); 17 | 18 | const data = (walletBalances || []).map((balance) => { 19 | const balances = { 20 | coin: mintToTickers[balance.mint], 21 | mint: balance.mint, 22 | walletBalance: balance.balance, 23 | openOrdersFree: 0, 24 | openOrdersTotal: 0, 25 | }; 26 | for (let openOrdersAccount of openOrdersBalances[balance.mint] || []) { 27 | balances['openOrdersFree'] += openOrdersAccount.free; 28 | balances['openOrdersTotal'] += openOrdersAccount.total; 29 | } 30 | return balances; 31 | }); 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/ConvertPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import ConvertForm from '../components/ConvertForm'; 4 | import { Row, Col } from 'antd'; 5 | import { DEFAULT_MARKET, MarketProvider } from '../utils/markets'; 6 | import { useLocalStorageState } from '../utils/utils'; 7 | 8 | const Wrapper = styled.div` 9 | height: 100%; 10 | display: flex; 11 | flex-direction: column; 12 | padding: 16px 16px; 13 | .borderNone .ant-select-selector { 14 | border: none !important; 15 | } 16 | `; 17 | 18 | export default function ConvertPage() { 19 | const [marketAddress, setMarketAddress] = useLocalStorageState( 20 | 'marketAddress', 21 | DEFAULT_MARKET?.address.toBase58(), 22 | ); 23 | return ( 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/ListNewMarketPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, Form, Input, Tooltip, Typography } from 'antd'; 3 | import { notify } from '../utils/notifications'; 4 | import { MARKETS } from '@project-serum/serum'; 5 | import { useConnection } from '../utils/connection'; 6 | import FloatingElement from '../components/layout/FloatingElement'; 7 | import styled from 'styled-components'; 8 | //import { useWallet } from '../utils/wallet'; 9 | import { listMarket } from '../utils/send'; 10 | import { useMintInput } from '../components/useMintInput'; 11 | import { useWallet } from '@solana/wallet-adapter-react'; 12 | const { Text, Title } = Typography; 13 | 14 | const Wrapper = styled.div` 15 | max-width: 800px; 16 | margin-left: auto; 17 | margin-right: auto; 18 | margin-top: 24px; 19 | margin-bottom: 24px; 20 | `; 21 | 22 | export default function ListNewMarketPage() { 23 | const connection = useConnection(); 24 | const { wallet, connected } = useWallet(); 25 | const [baseMintInput, baseMintInfo] = useMintInput( 26 | 'baseMint', 27 | 28 | Base Token Mint Address{' '} 29 | 30 | (e.g. BTC solana address:{' '} 31 | { 32 | 33 | 9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E 34 | 35 | } 36 | ) 37 | 38 | , 39 | 'The base token is the token being traded. For example, the base token of a BTC/USDT market is BTC.', 40 | ); 41 | const [quoteMintInput, quoteMintInfo] = useMintInput( 42 | 'quoteMint', 43 | 44 | Quote Token Mint Address{' '} 45 | 46 | (e.g. USDT solana address:{' '} 47 | { 48 | 49 | BQcdHdAQW1hczDbBi9hiegXAR7A98Q9jx3X3iBBBDiq4 50 | 51 | } 52 | ) 53 | 54 | , 55 | 'The quote token is the token used to price trades. For example, the quote token of a BTC/USDT market is USDT.', 56 | ); 57 | const [lotSize, setLotSize] = useState('1'); 58 | const [tickSize, setTickSize] = useState('0.01'); 59 | const dexProgramId = MARKETS.find(({ deprecated }) => !deprecated).programId; 60 | const [submitting, setSubmitting] = useState(false); 61 | 62 | const [listedMarket, setListedMarket] = useState(null); 63 | 64 | let baseLotSize; 65 | let quoteLotSize; 66 | if (baseMintInfo && parseFloat(lotSize) > 0) { 67 | baseLotSize = Math.round(10 ** baseMintInfo.decimals * parseFloat(lotSize)); 68 | if (quoteMintInfo && parseFloat(tickSize) > 0) { 69 | quoteLotSize = Math.round( 70 | parseFloat(lotSize) * 71 | 10 ** quoteMintInfo.decimals * 72 | parseFloat(tickSize), 73 | ); 74 | } 75 | } 76 | 77 | const canSubmit = 78 | connected && 79 | !!baseMintInfo && 80 | !!quoteMintInfo && 81 | !!baseLotSize && 82 | !!quoteLotSize; 83 | 84 | async function onSubmit() { 85 | if (!canSubmit) { 86 | return; 87 | } 88 | setSubmitting(true); 89 | try { 90 | const marketAddress = await listMarket({ 91 | connection, 92 | wallet, 93 | baseMint: baseMintInfo.address, 94 | quoteMint: quoteMintInfo.address, 95 | baseLotSize, 96 | quoteLotSize, 97 | dexProgramId, 98 | }); 99 | setListedMarket(marketAddress); 100 | } catch (e) { 101 | console.warn(e); 102 | notify({ 103 | message: 'Error listing new market', 104 | description: e.message, 105 | type: 'error', 106 | }); 107 | } finally { 108 | setSubmitting(false); 109 | } 110 | } 111 | 112 | return ( 113 | 114 | 115 | List New Market 116 |
122 | {baseMintInput} 123 | {quoteMintInput} 124 | 127 | Minimum Order Size{' '} 128 | (Lot size in e.g. BTC) 129 | 130 | } 131 | name="lotSize" 132 | initialValue="1" 133 | validateStatus={ 134 | baseMintInfo && quoteMintInfo 135 | ? baseLotSize 136 | ? 'success' 137 | : 'error' 138 | : null 139 | } 140 | hasFeedback={baseMintInfo && quoteMintInfo} 141 | > 142 | setLotSize(e.target.value.trim())} 145 | type="number" 146 | min="0" 147 | step="any" 148 | /> 149 | 150 | 153 | Tick Size{' '} 154 | (Price increment in e.g. USDT) 155 | 156 | } 157 | name="tickSize" 158 | initialValue="0.01" 159 | validateStatus={ 160 | baseMintInfo && quoteMintInfo 161 | ? quoteLotSize 162 | ? 'success' 163 | : 'error' 164 | : null 165 | } 166 | hasFeedback={baseMintInfo && quoteMintInfo} 167 | > 168 | setTickSize(e.target.value.trim())} 171 | type="number" 172 | min="0" 173 | step="any" 174 | /> 175 | 176 | 177 | 185 | 186 | 187 |
188 | {listedMarket ? ( 189 | 190 | New market address: {listedMarket.toBase58()} 191 | 192 | ) : null} 193 |
194 | ); 195 | } 196 | -------------------------------------------------------------------------------- /src/pages/OpenOrdersPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FloatingElement from "../components/layout/FloatingElement"; 3 | import { 4 | useAllMarkets, 5 | useAllOpenOrders, 6 | useMarketInfos 7 | } from "../utils/markets"; 8 | import OpenOrderTable from "../components/UserInfoTable/OpenOrderTable"; 9 | import { Button, Row } from "antd"; 10 | import { OrderWithMarketAndMarketName } from "../utils/types"; 11 | //import { useWallet } from '../utils/wallet'; 12 | import WalletConnect from "../components/WalletConnect"; 13 | import { useWallet } from "@solana/wallet-adapter-react"; 14 | export default function OpenOrdersPage() { 15 | const { connected } = useWallet(); 16 | const { openOrders, loaded, refreshOpenOrders } = useAllOpenOrders(); 17 | let marketInfos = useMarketInfos(); 18 | let marketAddressesToNames = Object.fromEntries( 19 | marketInfos.map((info) => [info.address.toBase58(), info.name]) 20 | ); 21 | let [allMarkets] = useAllMarkets(); 22 | const marketsByAddress = Object.fromEntries( 23 | (allMarkets || []).map((marketInfo) => [ 24 | marketInfo.market.address.toBase58(), 25 | marketInfo.market 26 | ]) 27 | ); 28 | 29 | const dataSource: OrderWithMarketAndMarketName[] = (openOrders || []) 30 | .map((orderInfos) => 31 | orderInfos.orders.map((order) => { 32 | return { 33 | marketName: marketAddressesToNames[orderInfos.marketAddress], 34 | market: marketsByAddress[orderInfos.marketAddress], 35 | ...order 36 | }; 37 | }) 38 | ) 39 | .flat(); 40 | 41 | if (!connected) { 42 | return ( 43 | 49 | 50 | 51 | ); 52 | } 53 | 54 | return ( 55 | 56 | 59 | 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { GlobalStyle } from "@/components/global_style"; 2 | import MainLayout from "@/components/MainLayout"; 3 | import MainHeader from "@/components/Header"; 4 | import React from "react"; 5 | import "@/styles/App.less"; 6 | import "@/styles/globals.css"; 7 | import type { AppProps } from "next/app"; 8 | import Head from "next/head"; 9 | import Script from 'next/script' 10 | import { SSRProvider } from '@react-aria/ssr'; 11 | if (!process.browser) React.useLayoutEffect = React.useEffect; 12 | export default function App({ Component, pageProps }: AppProps) { 13 | return ( 14 | 15 | 16 | 17 |