39 | ) => ReactElement | null;
40 |
41 | export interface BaseButtonProps extends LayoutProps, SpaceProps {
42 | as?: "a" | "button" | typeof Link;
43 | external?: boolean;
44 | isLoading?: boolean;
45 | scale?: Scale;
46 | variant?: Variant;
47 | disabled?: boolean;
48 | startIcon?: ReactNode;
49 | endIcon?: ReactNode;
50 | }
51 |
52 | export type ButtonProps = PolymorphicComponentProps
;
53 |
--------------------------------------------------------------------------------
/src/components/Skeleton/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled, { keyframes } from "styled-components"
3 | import { space, layout } from "styled-system"
4 | import { SkeletonProps, animation as ANIMATION, variant as VARIANT } from "./types"
5 |
6 | const waves = keyframes`
7 | from {
8 | left: -150px;
9 | }
10 | to {
11 | left: 100%;
12 | }
13 | `
14 |
15 | const pulse = keyframes`
16 | 0% {
17 | opacity: 1;
18 | }
19 | 50% {
20 | opacity: 0.4;
21 | }
22 | 100% {
23 | opacity: 1;
24 | }
25 | `
26 |
27 | const Root = styled.div`
28 | min-height: 20px;
29 | display: block;
30 | background-color: #353688;
31 | border-radius: ${({ variant, theme }) => (variant === VARIANT.CIRCLE ? theme.radii.circle : theme.radii.small)};
32 |
33 | ${layout}
34 | ${space}
35 | `
36 |
37 | const Pulse = styled(Root)`
38 | animation: ${pulse} 2s infinite ease-out;
39 | transform: translate3d(0, 0, 0);
40 | border-radius: 16px;
41 | `
42 |
43 | const Waves = styled(Root)`
44 | position: relative;
45 | overflow: hidden;
46 | transform: translate3d(0, 0, 0);
47 | &:before {
48 | content: "";
49 | position: absolute;
50 | background-image: #353688;
51 | top: 0;
52 | left: -150px;
53 | height: 100%;
54 | width: 150px;
55 | animation: ${waves} 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
56 | }
57 | `
58 |
59 | const Skeleton: React.FC = ({ variant = VARIANT.RECT, animation = ANIMATION.PULSE, ...props }) => {
60 | if (animation === ANIMATION.WAVES) {
61 | return
62 | }
63 |
64 | return
65 | }
66 |
67 | export default Skeleton
68 |
--------------------------------------------------------------------------------
/src/utils/getTokenList.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-continue */
2 | /* eslint-disable no-await-in-loop */
3 | import { TokenList } from '@uniswap/token-lists'
4 | import schema from '@uniswap/token-lists/src/tokenlist.schema.json'
5 | import Ajv from 'ajv'
6 | import uriToHttp from './uriToHttp'
7 |
8 | const tokenListValidator = new Ajv({ allErrors: true }).compile(schema)
9 |
10 | /**
11 | * Contains the logic for resolving a list URL to a validated token list
12 | * @param listUrl list url
13 | */
14 | export default async function getTokenList(listUrl: string): Promise {
15 | const urls: string[] = uriToHttp(listUrl)
16 |
17 | for (let i = 0; i < urls.length; i++) {
18 | const url = urls[i]
19 | const isLast = i === urls.length - 1
20 | let response
21 | try {
22 | response = await fetch(url)
23 | } catch (error) {
24 | console.error('Failed to fetch list', listUrl, error)
25 | if (isLast) throw new Error(`Failed to download list ${listUrl}`)
26 | continue
27 | }
28 |
29 | if (!response.ok) {
30 | if (isLast) throw new Error(`Failed to download list ${listUrl}`)
31 | continue
32 | }
33 |
34 | const json = await response.json()
35 | if (!tokenListValidator(json)) {
36 | const validationErrors: string =
37 | tokenListValidator.errors?.reduce((memo, error) => {
38 | const add = `${(error as any).dataPath} ${error.message ?? ''}`
39 | return memo.length > 0 ? `${memo}; ${add}` : `${add}`
40 | }, '') ?? 'unknown error'
41 | throw new Error(`Token list failed validation: ${validationErrors}`)
42 | }
43 | return json as TokenList
44 | }
45 | throw new Error('Unrecognized list URL protocol.')
46 | }
47 |
--------------------------------------------------------------------------------
/src/hooks/ENS/useENSName.ts:
--------------------------------------------------------------------------------
1 | import { namehash } from 'ethers/lib/utils'
2 | import { useMemo } from 'react'
3 | import { useSingleCallResult } from '../../state/multicall/hooks'
4 | import { isAddress } from '../../utils'
5 | import isZero from '../../utils/isZero'
6 | import { useENSRegistrarContract, useENSResolverContract } from '../useContract'
7 | import useDebounce from '../useDebounce'
8 |
9 | /**
10 | * Does a reverse lookup for an address to find its ENS name.
11 | * Note this is not the same as looking up an ENS name to find an address.
12 | */
13 | export default function useENSName(address?: string): { ENSName: string | null; loading: boolean } {
14 | const debouncedAddress = useDebounce(address, 200)
15 | const ensNodeArgument = useMemo(() => {
16 | if (!debouncedAddress || !isAddress(debouncedAddress)) return [undefined]
17 | try {
18 | return debouncedAddress ? [namehash(`${debouncedAddress.toLowerCase().substr(2)}.addr.reverse`)] : [undefined]
19 | } catch (error) {
20 | return [undefined]
21 | }
22 | }, [debouncedAddress])
23 | const registrarContract = useENSRegistrarContract(false)
24 | const resolverAddress = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument)
25 | const resolverAddressResult = resolverAddress.result?.[0]
26 | const resolverContract = useENSResolverContract(
27 | resolverAddressResult && !isZero(resolverAddressResult) ? resolverAddressResult : undefined,
28 | false,
29 | )
30 | const name = useSingleCallResult(resolverContract, 'name', ensNodeArgument)
31 |
32 | const changed = debouncedAddress !== address
33 | return {
34 | ENSName: changed ? null : name.result?.[0] ?? null,
35 | loading: changed || resolverAddress.loading || name.loading,
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
3 |
4 | import { cn } from 'lib/utils'
5 |
6 | const ScrollArea = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, children, ...props }, ref) => (
10 |
11 | {children}
12 |
13 |
14 |
15 | ))
16 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
17 |
18 | const ScrollBar = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, orientation = 'vertical', ...props }, ref) => (
22 |
33 |
34 |
35 | ))
36 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
37 |
38 | export { ScrollArea, ScrollBar }
39 |
--------------------------------------------------------------------------------
/src/components/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { motion } from 'framer-motion'
3 | import Link from 'next/link'
4 | import { useTranslation } from 'contexts/Localization'
5 | import { Button } from './ui/button'
6 |
7 | const NotFound = () => {
8 | const { t } = useTranslation()
9 |
10 | return (
11 |
12 |
13 |
14 |
21 | {404}
22 |
23 |
30 | Oops, page not found
31 |
32 |
33 |
34 | {t('Home')}
35 |
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | export default NotFound
44 |
--------------------------------------------------------------------------------
/src/pages/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | input[type="number"]::-webkit-inner-spin-button,
7 | input[type="number"]::-webkit-outer-spin-button {
8 | -webkit-appearance: none;
9 | margin: 0;
10 | }
11 |
12 | ::-webkit-scrollbar {
13 | @apply z-50 w-2;
14 | }
15 |
16 | ::-webkit-scrollbar-track {
17 | @apply z-50 rounded-full bg-white/10;
18 | }
19 |
20 | ::-webkit-scrollbar-thumb {
21 | @apply z-50 rounded-full bg-white/20;
22 | }
23 |
24 | ::-webkit-scrollbar-thumb:hover {
25 | @apply bg-white/40;
26 | }
27 | }
28 |
29 | ::-moz-selection {
30 | /* Code for Firefox */
31 | color: black;
32 | background: #7a87ff;
33 | }
34 |
35 | ::selection {
36 | color: black;
37 | background: #7a87ff;
38 | }
39 |
40 | .button-primary {
41 | background: radial-gradient(
42 | 231.94% 231.94% at 50% 100%,
43 | #8a6cff 0,
44 | rgba(53, 41, 128, 0) 25.24%
45 | ),
46 | linear-gradient(180deg, rgba(243, 238, 255, 0), rgba(243, 238, 255, 0.04)),
47 | rgba(147, 130, 255, 0.01);
48 | box-shadow:
49 | 0 0 0 0 rgba(16, 0, 51, 0.4),
50 | 0 2px 5px 0 rgba(16, 0, 51, 0.39),
51 | 0 8px 8px 0 rgba(16, 0, 51, 0.34),
52 | 0 19px 11px 0 rgba(16, 0, 51, 0.2),
53 | 0 34px 14px 0 rgba(16, 0, 51, 0.06),
54 | 0 53px 15px 0 rgba(16, 0, 51, 0.01),
55 | inset 0 0 12px 0 hsla(0, 0%, 100%, 0.08),
56 | inset 0 -8px 32px 0 #1e0d49;
57 | }
58 |
59 | .button-primary:hover {
60 | background: radial-gradient(
61 | 231.94% 231.94% at 50% 100%,
62 | #8a6cff 0,
63 | rgba(53, 41, 128, 0) 25.24%
64 | ),
65 | linear-gradient(180deg, rgba(243, 238, 255, 0), rgba(243, 238, 255, 0.04)),
66 | rgba(147, 130, 255, 0.6);
67 | }
68 |
--------------------------------------------------------------------------------
/src/__tests__/utils/prices.test.ts:
--------------------------------------------------------------------------------
1 | import { ChainId, JSBI, Pair, Route, Token, TokenAmount, Trade, TradeType } from '@scads/sdk'
2 | import { computeTradePriceBreakdown } from 'utils/prices'
3 |
4 | describe('prices', () => {
5 | const token1 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18)
6 | const token2 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000002', 18)
7 | const token3 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000003', 18)
8 |
9 | const pair12 = new Pair(new TokenAmount(token1, JSBI.BigInt(10000)), new TokenAmount(token2, JSBI.BigInt(20000)))
10 | const pair23 = new Pair(new TokenAmount(token2, JSBI.BigInt(20000)), new TokenAmount(token3, JSBI.BigInt(30000)))
11 |
12 | describe('computeTradePriceBreakdown', () => {
13 | it('returns undefined for undefined', () => {
14 | expect(computeTradePriceBreakdown(undefined)).toEqual({
15 | priceImpactWithoutFee: undefined,
16 | realizedLPFee: undefined,
17 | })
18 | })
19 |
20 | it('correct realized lp fee for single hop', () => {
21 | expect(
22 | computeTradePriceBreakdown(
23 | new Trade(new Route([pair12], token1), new TokenAmount(token1, JSBI.BigInt(1000)), TradeType.EXACT_INPUT),
24 | ).realizedLPFee,
25 | ).toEqual(new TokenAmount(token1, JSBI.BigInt(2)))
26 | })
27 |
28 | it('correct realized lp fee for double hop', () => {
29 | expect(
30 | computeTradePriceBreakdown(
31 | new Trade(
32 | new Route([pair12, pair23], token1),
33 | new TokenAmount(token1, JSBI.BigInt(1000)),
34 | TradeType.EXACT_INPUT,
35 | ),
36 | ).realizedLPFee,
37 | ).toEqual(new TokenAmount(token1, JSBI.BigInt(4)))
38 | })
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/src/pages/_components/outro.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Image from 'next/image'
3 | import SwapModal from 'components/swap/swap-modal'
4 | import { SparklesCore } from 'components/ui/sparkles'
5 | import { useTranslation } from 'contexts/Localization'
6 |
7 | const Outro = () => {
8 | const { t } = useTranslation()
9 |
10 | return (
11 |
12 |
13 |
14 | {t('The change is here')}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | export default Outro
38 |
--------------------------------------------------------------------------------
/src/components/WalletModal/TransactionRow.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { BlockIcon, CheckmarkCircleIcon, Flex, Link, OpenNewIcon, RefreshIcon } from '@scads-io/uikit'
3 | import styled from 'styled-components'
4 | import { TransactionDetails } from 'state/transactions/reducer'
5 | import useActiveWeb3React from 'hooks/useActiveWeb3React'
6 | import { getBscScanLink } from 'utils'
7 |
8 | interface TransactionRowProps {
9 | txn: TransactionDetails
10 | }
11 |
12 | const TxnIcon = styled(Flex)`
13 | align-items: center;
14 | flex: none;
15 | width: 24px;
16 | `
17 |
18 | const Summary = styled.div`
19 | flex: 1;
20 | padding: 0 8px;
21 | `
22 |
23 | const TxnLink = styled(Link)`
24 | align-items: center;
25 | color: ${({ theme }) => theme.colors.text};
26 | display: flex;
27 | margin-bottom: 16px;
28 | width: 100%;
29 |
30 | &:hover {
31 | text-decoration: none;
32 | }
33 | `
34 |
35 | const renderIcon = (txn: TransactionDetails) => {
36 | if (!txn.receipt) {
37 | return
38 | }
39 |
40 | return txn.receipt?.status === 1 || typeof txn.receipt?.status === 'undefined' ? (
41 |
42 | ) : (
43 |
44 | )
45 | }
46 |
47 | const TransactionRow: React.FC = ({ txn }) => {
48 | const { chainId } = useActiveWeb3React()
49 |
50 | if (!txn) {
51 | return null
52 | }
53 |
54 | return (
55 |
56 | {renderIcon(txn)}
57 | {txn.summary ?? txn.hash}
58 |
59 |
60 |
61 |
62 | )
63 | }
64 |
65 | export default TransactionRow
66 |
--------------------------------------------------------------------------------
/src/components/swap/NumericalInput.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useTranslation } from 'contexts/Localization'
3 | import { escapeRegExp } from '../../utils'
4 |
5 | const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped '.' characters via in a non-capturing group
6 | const integerinputRegex = RegExp(`^\\d*(?:\\\\[])?\\d*$`) // match escaped '.' characters via in a non-capturing group
7 |
8 | export const Input = React.memo(function InnerInput({
9 | value,
10 | onUserInput,
11 | placeholder,
12 | onlyInteger,
13 | ...rest
14 | }: {
15 | value: string | number
16 | onUserInput: (input: string) => void
17 | onlyInteger?: boolean
18 | error?: boolean
19 | fontSize?: string
20 | align?: 'right' | 'left'
21 | } & Omit, 'ref' | 'onChange' | 'as'>) {
22 | const enforcer = (nextUserInput: string) => {
23 | const regex = onlyInteger ? integerinputRegex : inputRegex
24 | if (nextUserInput === '' || regex.test(escapeRegExp(nextUserInput))) {
25 | onUserInput(nextUserInput)
26 | }
27 | }
28 |
29 | const { t } = useTranslation()
30 |
31 | return (
32 | {
36 | // replace commas with periods, because we exclusively uses period as the decimal separator
37 | enforcer(event.target.value.replace(/,/g, '.'))
38 | }}
39 | // universal input options
40 | inputMode='decimal'
41 | title={t('Token Amount')}
42 | autoComplete='off'
43 | autoCorrect='off'
44 | // text-specific options
45 | type='text'
46 | pattern='^[0-9]*[.,]?[0-9]*$'
47 | placeholder={placeholder || '0.0'}
48 | minLength={1}
49 | maxLength={79}
50 | spellCheck='false'
51 | />
52 | )
53 | })
54 |
55 | export default Input
56 |
--------------------------------------------------------------------------------
/src/pages/faq/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { motion } from 'framer-motion'
3 | import { IoEllipse } from 'react-icons/io5'
4 | import { useTranslation } from 'contexts/Localization'
5 | import FaqAccordion from './_components/faq-accordion'
6 | import SearchBar from './_components/search-bar'
7 |
8 | const FaqPage = () => {
9 | const [searchValue, setSearchValue] = useState('')
10 | const { t } = useTranslation()
11 |
12 | return (
13 | <>
14 |
20 | {t('Frequently asked questions')}
21 |
22 |
28 |
29 |
30 |
33 |
37 |
41 | >
42 | )
43 | }
44 |
45 | export default FaqPage
46 |
--------------------------------------------------------------------------------
/src/hooks/useAuth.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import toast from 'react-hot-toast'
3 | import { useAppDispatch } from 'state'
4 | import { useConnect, useDisconnect, useNetwork, ConnectorNotFoundError, UserRejectedRequestError } from 'wagmi'
5 | import { useTranslation } from 'contexts/Localization'
6 | import { connectorLocalStorageKey, ConnectorNames } from 'components/WalletModal'
7 | import { clearUserStates } from '../utils/clearUserStates'
8 | import useActiveWeb3React from './useActiveWeb3React'
9 |
10 | const useAuth = () => {
11 | const { t } = useTranslation()
12 | const { chainId } = useActiveWeb3React()
13 | const dispatch = useAppDispatch()
14 | const { connectAsync, connectors } = useConnect()
15 | const { chain } = useNetwork()
16 | const { disconnect } = useDisconnect()
17 |
18 | const login = useCallback(
19 | async (connectorID: ConnectorNames) => {
20 | const findConnector = connectors.find((c) => c.id === connectorID)
21 | try {
22 | await connectAsync({ connector: findConnector, chainId })
23 | } catch (error) {
24 | console.error(error)
25 | window?.localStorage?.removeItem(connectorLocalStorageKey)
26 | if (error instanceof ConnectorNotFoundError) {
27 | toast.error(t('Provider Error'))
28 | return
29 | }
30 | if (error instanceof UserRejectedRequestError) {
31 | return
32 | }
33 | if (error instanceof Error) {
34 | toast.error(t('Please authorize to access your account'))
35 | }
36 | }
37 | },
38 | [connectors, connectAsync, chainId, t],
39 | )
40 |
41 | const logout = useCallback(() => {
42 | disconnect()
43 | clearUserStates(dispatch, chain?.id)
44 | }, [disconnect, dispatch, chain?.id])
45 |
46 | return { login, logout }
47 | }
48 |
49 | export default useAuth
50 |
--------------------------------------------------------------------------------
/src/components/swap/sorting.ts:
--------------------------------------------------------------------------------
1 | import { Token, TokenAmount } from '@scads/sdk'
2 | import { useMemo } from 'react'
3 | import { useAllTokenBalances } from '../../state/wallet/hooks'
4 |
5 | // compare two token amounts with highest one coming first
6 | function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
7 | if (balanceA && balanceB) {
8 | return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1
9 | }
10 | if (balanceA && balanceA.greaterThan('0')) {
11 | return -1
12 | }
13 | if (balanceB && balanceB.greaterThan('0')) {
14 | return 1
15 | }
16 | return 0
17 | }
18 |
19 | function getTokenComparator(balances: {
20 | [tokenAddress: string]: TokenAmount | undefined
21 | }): (tokenA: Token, tokenB: Token) => number {
22 | return function sortTokens(tokenA: Token, tokenB: Token): number {
23 | // -1 = a is first
24 | // 1 = b is first
25 |
26 | // sort by balances
27 | const balanceA = balances[tokenA.address]
28 | const balanceB = balances[tokenB.address]
29 |
30 | const balanceComp = balanceComparator(balanceA, balanceB)
31 | if (balanceComp !== 0) return balanceComp
32 |
33 | if (tokenA.symbol && tokenB.symbol) {
34 | // sort by symbol
35 | return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
36 | }
37 | return tokenA.symbol ? -1 : tokenB.symbol ? -1 : 0
38 | }
39 | }
40 |
41 | function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
42 | const balances = useAllTokenBalances()
43 | const comparator = useMemo(() => getTokenComparator(balances ?? {}), [balances])
44 | return useMemo(() => {
45 | if (inverted) {
46 | return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1
47 | }
48 | return comparator
49 | }, [inverted, comparator])
50 | }
51 |
52 | export default useTokenComparator
53 |
--------------------------------------------------------------------------------
/packages/uikit/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@scads-io/uikit",
3 | "version": "0.4.4",
4 | "description": "Set of UI components for scads projects",
5 | "main": "dist/index.cjs.js",
6 | "module": "dist/index.esm.js",
7 | "types": "dist/index.d.ts",
8 | "files": [
9 | "dist"
10 | ],
11 | "repository": "https://github.com/scads-io/toolkit/tree/main/packages/scads-uikit",
12 | "license": "MIT",
13 | "scripts": {
14 | "start": "npm storybook",
15 | "build": "rollup -c && tsc -d --emitDeclarationOnly --declarationDir dist",
16 | "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
17 | "format:check": "prettier --check --loglevel error 'src/**/*.{js,jsx,ts,tsx}'",
18 | "format:write": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
19 | "storybook": "start-storybook -p 6006",
20 | "storybook:build": "build-storybook",
21 | "test": "jest",
22 | "prepublishOnly": "npm run build"
23 | },
24 | "jest": {
25 | "setupFilesAfterEnv": [
26 | "/src/setupTests.js"
27 | ]
28 | },
29 | "devDependencies": {
30 | "@testing-library/jest-dom": "^5.11.6",
31 | "@testing-library/react": "^11.2.5",
32 | "@types/react-dom": "^17.0.5",
33 | "jest-styled-components": "^7.2.0",
34 | "react": "^18.2.0",
35 | "react-dom": "^18.2.0",
36 | "react-router-dom": "^5.2.0",
37 | "styled-components": "^6.0.7"
38 | },
39 | "peerDependencies": {
40 | "react": "^18.2.0",
41 | "react-dom": "^18.2.0",
42 | "react-router-dom": "^5.2.0",
43 | "styled-components": "^6.0.7"
44 | },
45 | "dependencies": {
46 | "@popperjs/core": "^2.9.2",
47 | "@types/lodash": "^4.14.168",
48 | "@types/styled-system": "^5.1.10",
49 | "lodash": "^4.17.20",
50 | "prettier": "^3.2.5",
51 | "react-popper": "^2.2.5",
52 | "styled-system": "^5.1.5"
53 | },
54 | "publishConfig": {
55 | "access": "public"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------