├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc ├── README.md ├── app.json ├── netlify.toml ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo.ico ├── logo.svg ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt ├── screenshot.png └── starknet.svg ├── src ├── App.tsx ├── assets │ ├── css │ │ └── body.css │ ├── disabled-logo.svg │ ├── gifs │ │ └── loading_tx.gif │ ├── icons │ │ ├── arrow-down.svg │ │ ├── arrow-up.svg │ │ ├── close.svg │ │ ├── cog.svg │ │ ├── dropdown-arrow.svg │ │ ├── error.svg │ │ ├── mint-icon.svg │ │ ├── pending-mint.svg │ │ ├── pending-swap.svg │ │ ├── success.svg │ │ ├── swap-direction.svg │ │ ├── swap-icon.svg │ │ └── swap.svg │ ├── logo.svg │ ├── menu │ │ ├── Block.svg │ │ ├── Collaboration.svg │ │ ├── Document.svg │ │ ├── Download.svg │ │ ├── Github.svg │ │ ├── Star.svg │ │ └── Starkware.svg │ ├── starknet.svg │ └── tokens │ │ ├── placeholder.svg │ │ ├── token.svg │ │ └── token2.svg ├── components │ ├── Activity.tsx │ ├── ActivityTransactionItem.tsx │ ├── ConfirmSwapDialog.tsx │ ├── Mint.tsx │ ├── MultiTokenSuccessDialog.tsx │ ├── Navbar.tsx │ ├── NumericInput.tsx │ ├── Sidemenu.tsx │ ├── SnackbarProvider.tsx │ ├── SuccessDialog.tsx │ ├── Swap.tsx │ ├── TokenBalances.tsx │ ├── TokenInput.tsx │ ├── TokenSelectDialog.tsx │ ├── TokenSelector.tsx │ └── common │ │ ├── BouncingDots.tsx │ │ ├── DarkBox.tsx │ │ ├── ExternalLink.tsx │ │ ├── RoundedButton.tsx │ │ ├── SplashLoader.tsx │ │ ├── StarknetLogo.tsx │ │ └── TokenIcon.tsx ├── constants.ts ├── context │ ├── notifications.tsx │ └── user.tsx ├── hooks │ ├── amounts.ts │ ├── notifications.ts │ └── tokens.ts ├── index.tsx ├── models │ ├── mint.ts │ ├── swap.ts │ └── token.ts ├── pages │ └── Home.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts ├── services │ └── API │ │ ├── mutations │ │ ├── useInitPool.ts │ │ ├── useMint.ts │ │ └── useSwap.ts │ │ ├── queries │ │ ├── useAccountBalance.ts │ │ ├── useBlock.ts │ │ ├── usePoolBalance.ts │ │ └── useStorageAt.ts │ │ ├── types.ts │ │ └── utils │ │ ├── callContract.ts │ │ ├── http.ts │ │ ├── sendTransaction.ts │ │ └── toShortAddress.ts ├── setupTests.ts ├── tests │ ├── NumericInput.test.tsx │ ├── TokenSelectorDialog.test.tsx │ ├── __snapshots__ │ │ ├── NumericInput.test.tsx.snap │ │ └── TokenSelectorDialog.test.tsx.snap │ ├── integration │ │ ├── Mint.spec.tsx │ │ ├── Swap.spec.tsx │ │ ├── TokenSelector.spec.tsx │ │ └── __snapshots__ │ │ │ ├── Mint.spec.tsx.snap │ │ │ ├── Swap.spec.tsx.snap │ │ │ └── TokenSelector.spec.tsx.snap │ └── utils.tsx ├── theme │ └── theme.ts └── utils │ ├── random.ts │ └── swap.ts ├── travis.yml ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | public 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | // Allows for the parsing of JSX 8 | "jsx": true 9 | } 10 | }, 11 | "ignorePatterns": ["node_modules/**/*"], 12 | "settings": { 13 | "react": { 14 | "version": "detect" 15 | } 16 | }, 17 | "extends": [ 18 | "eslint:recommended", 19 | "plugin:react/recommended", 20 | "plugin:prettier/recommended", 21 | "plugin:@typescript-eslint/recommended", 22 | "plugin:react-hooks/recommended" 23 | ], 24 | "plugins": ["react", "@typescript-eslint"], 25 | "rules": { 26 | "@typescript-eslint/no-explicit-any": 2, 27 | "react/prop-types": "off", 28 | "@typescript-eslint/no-var-requires": 2, 29 | "@typescript-eslint/explicit-function-return-type": "off", 30 | "@typescript-eslint/explicit-module-boundary-types": "off", 31 | "react-hooks/exhaustive-deps": "error", 32 | "no-var": "error", 33 | "brace-style": "error", 34 | "prefer-template": "error", 35 | "space-before-blocks": "error", 36 | "import/prefer-default-export": "off" 37 | }, 38 | "overrides": [ 39 | { 40 | "files": [" ,**/*.test.tsx", " ,**/*.spec.tsx"], 41 | "env": { 42 | "jest": true 43 | } 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | .eslintcache 26 | .vscode 27 | .idea -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | public 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "useTabs": true, 8 | "arrowParens": "always" 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starknet AMM Demo 2 | 3 | ![starknet logo](public/starknet.svg) 4 | 5 | [![Netlify Status](https://api.netlify.com/api/v1/badges/c8bf575a-b8a1-4bef-b855-b84e2265ec29/deploy-status)](https://app.netlify.com/sites/starkware-amm-demo/deploys) 6 | 7 | Automated Market Maker demo running on Starknet. 8 | 9 | Part of [StarkNet Planets Demo - Alpha on Ropsten](https://starkware.medium.com/starknet-planets-alpha-on-ropsten-e7494929cb95) 10 | 11 | 12 | 13 | ## Prequisites 14 | 15 | Node v14.x 16 | 17 | ## Installation 18 | 19 | - `git clone git@github.com:dOrgTech/starkware-demo.git` 20 | - `cd starkware-demo` 21 | - `yarn` 22 | - Create a `.env` file at root level and ask one of the maintainers for the required API keys and URLs 23 | - `yarn dev` 24 | 25 | ## Contributors 26 | 27 | Starkware & dOrg. 28 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starkware-demo", 3 | "env": {}, 4 | "formation": { 5 | "web": { 6 | "quantity": 1 7 | } 8 | }, 9 | "buildpacks": [ 10 | { 11 | "url": "heroku/nodejs" 12 | } 13 | ], 14 | "stack": "heroku-20" 15 | } 16 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/api/*" 3 | to = "https://alpha3.starknet.io/:splat" 4 | status = 200 5 | 6 | [[redirects]] 7 | from = "/*" 8 | to = "/index.html" 9 | status = 200 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starkware-demo-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.4", 7 | "@material-ui/icons": "^4.11.2", 8 | "@material-ui/lab": "^4.0.0-alpha.58", 9 | "@testing-library/jest-dom": "^5.11.4", 10 | "@testing-library/react": "^11.2.7", 11 | "@types/jest": "^26.0.15", 12 | "@types/node": "^12.0.0", 13 | "@types/react": "^17.0.0", 14 | "@types/react-dom": "^17.0.0", 15 | "@types/react-router-dom": "^5.1.7", 16 | "axios": "^0.21.1", 17 | "bignumber.js": "^9.0.1", 18 | "dayjs": "^1.10.5", 19 | "hex-to-rgba": "^2.0.1", 20 | "notistack": "^1.0.9", 21 | "react": "^17.0.2", 22 | "react-dom": "^17.0.2", 23 | "react-query": "^3.15.2", 24 | "react-router-dom": "^5.2.0", 25 | "react-scripts": "4.0.3", 26 | "serve": "^11.3.2", 27 | "typescript": "^4.1.2", 28 | "web-vitals": "^1.0.1" 29 | }, 30 | "devDependencies": { 31 | "@testing-library/dom": "^7.31.0", 32 | "@typescript-eslint/eslint-plugin": "^4.18.0", 33 | "@typescript-eslint/parser": "^4.18.0", 34 | "eslint": "^7.22.0", 35 | "eslint-config-prettier": "^8.1.0", 36 | "eslint-plugin-prettier": "^3.4.0", 37 | "eslint-plugin-react": "^7.21.5", 38 | "eslint-plugin-react-hooks": "^4.2.0", 39 | "husky": ">=6", 40 | "lint-staged": "^10.5.4", 41 | "prettier": "^2.3.0" 42 | }, 43 | "scripts": { 44 | "start": "serve -s build", 45 | "dev": "react-scripts start", 46 | "build": "react-scripts build", 47 | "test": "react-scripts test --watchAll=false", 48 | "eject": "react-scripts eject", 49 | "lint:ci": "eslint --color 'src/**/*.{js,jsx,ts,tsx}'", 50 | "lint:check": "eslint --quiet 'src/**/*.{js,jsx,ts,tsx}'", 51 | "lint:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx}'", 52 | "format": "prettier --write \"**/*.+(js|jsx|ts|tsx|json|css|md)\"", 53 | "prepare": "husky install" 54 | }, 55 | "eslintConfig": { 56 | "extends": [ 57 | "react-app", 58 | "react-app/jest" 59 | ] 60 | }, 61 | "browserslist": { 62 | "production": [ 63 | ">0.2%", 64 | "not dead", 65 | "not op_mini all" 66 | ], 67 | "development": [ 68 | "last 1 chrome version", 69 | "last 1 firefox version", 70 | "last 1 safari version" 71 | ] 72 | }, 73 | "lint-staged": { 74 | "*.{js,ts,tsx}": [ 75 | "yarn format", 76 | "yarn lint:fix" 77 | ] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dOrgTech/starkware-demo/fcdf8c2bd860dc7f76a84ed8953a04d6beafd021/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 19 | 20 | 24 | 25 | 34 | 35 | 36 | 43 | StarkNet Planets Demo - Alpha 44 | 45 | 46 | 47 |
48 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /public/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dOrgTech/starkware-demo/fcdf8c2bd860dc7f76a84ed8953a04d6beafd021/public/logo.ico -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dOrgTech/starkware-demo/fcdf8c2bd860dc7f76a84ed8953a04d6beafd021/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dOrgTech/starkware-demo/fcdf8c2bd860dc7f76a84ed8953a04d6beafd021/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dOrgTech/starkware-demo/fcdf8c2bd860dc7f76a84ed8953a04d6beafd021/public/screenshot.png -------------------------------------------------------------------------------- /public/starknet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, styled, useTheme } from '@material-ui/core'; 3 | import { Home } from 'pages/Home'; 4 | import { Navbar } from 'components/Navbar'; 5 | import { ActionTypes, NotificationsContext } from './context/notifications'; 6 | import { Sidemenu } from 'components/Sidemenu'; 7 | import { useMediaQuery } from '@material-ui/core'; 8 | import { useAccountBalance } from './services/API/queries/useAccountBalance'; 9 | 10 | const MainContainer = styled(Grid)({ 11 | background: 12 | 'linear-gradient(360deg, #2D2F82 -0.49%, #2B2C78 11.89%, #2A2B76 18.71%, #26276F 38.09%, #202167 70.91%, #16175F 99.22%);', 13 | width: '100%', 14 | minHeight: '100vh', 15 | }); 16 | 17 | function App() { 18 | const { dispatch } = React.useContext(NotificationsContext); 19 | const { isLoading: isLoadingAccount } = useAccountBalance(); 20 | const theme = useTheme(); 21 | const isMobile = useMediaQuery(theme.breakpoints.down('sm')); 22 | const isFirstRender = React.useRef(true); 23 | 24 | React.useEffect(() => { 25 | if (isLoadingAccount && isFirstRender.current) { 26 | isFirstRender.current = false; 27 | dispatch({ type: ActionTypes.SHOW_LOADING }); 28 | } else { 29 | dispatch({ type: ActionTypes.HIDE_LOADING }); 30 | } 31 | }, [dispatch, isLoadingAccount]); 32 | 33 | return ( 34 | 35 | {!isMobile && ( 36 | 37 | 38 | 39 | )} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | export default App; 55 | -------------------------------------------------------------------------------- /src/assets/css/body.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/disabled-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/gifs/loading_tx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dOrgTech/starkware-demo/fcdf8c2bd860dc7f76a84ed8953a04d6beafd021/src/assets/gifs/loading_tx.gif -------------------------------------------------------------------------------- /src/assets/icons/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/cog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/dropdown-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/mint-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/pending-mint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/pending-swap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/icons/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/swap-direction.svg: -------------------------------------------------------------------------------- 1 | 2 | swap direction 3 | invert the tokens swap operation direction 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/icons/swap-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/icons/swap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/menu/Block.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/menu/Document.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/menu/Download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/menu/Github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/menu/Star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/menu/Starkware.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/starknet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/tokens/placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/tokens/token.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/tokens/token2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/Activity.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useMemo } from 'react'; 2 | import { Grid, styled, Typography } from '@material-ui/core'; 3 | import { UserContext, TransactionType } from 'context/user'; 4 | import { ActivityTransactionItem } from 'components/ActivityTransactionItem'; 5 | import { ReactComponent as DisabledLogo } from 'assets/disabled-logo.svg'; 6 | 7 | const ActivityListWrapper = styled('div')(() => ({ 8 | maxHeight: '431px', 9 | height: 'fit-content', 10 | margin: '0 -33px -30px -33px', 11 | overflowY: 'auto', 12 | })); 13 | 14 | const PlaceholderActivityContent = styled(Grid)({ 15 | height: 294, 16 | textAlign: 'center', 17 | marginTop: -22, 18 | }); 19 | 20 | const NoTransactionsText = styled(Typography)({ 21 | fontWeight: 500, 22 | }); 23 | 24 | export const Activity = (): JSX.Element => { 25 | const { state: userState } = useContext(UserContext); 26 | 27 | const formattedActivity = useMemo(() => { 28 | const activity = userState.activeTransaction 29 | ? [userState.activeTransaction, ...userState.activity] 30 | : userState.activity; 31 | 32 | return activity 33 | .map((transaction) => { 34 | const pending = userState.activeTransaction?.id === transaction.id; 35 | 36 | if (transaction.type === TransactionType.MINT) { 37 | if (transaction.args.mint2) { 38 | return [ 39 | { 40 | type: transaction.type, 41 | timestamp: transaction.timestamp, 42 | amount: transaction.args.mint1.amount, 43 | symbol: transaction.args.mint1.token.symbol, 44 | id: transaction.id, 45 | pending, 46 | }, 47 | { 48 | type: transaction.type, 49 | timestamp: transaction.timestamp, 50 | amount: transaction.args.mint2.amount, 51 | symbol: transaction.args.mint2.token.symbol, 52 | id: transaction.id, 53 | pending, 54 | }, 55 | ]; 56 | } 57 | 58 | return { 59 | type: transaction.type, 60 | timestamp: transaction.timestamp, 61 | amount: transaction.args.mint1.amount, 62 | symbol: transaction.args.mint1.token.symbol, 63 | id: transaction.id, 64 | pending, 65 | }; 66 | } 67 | 68 | return { 69 | type: transaction.type, 70 | timestamp: transaction.timestamp, 71 | amount: transaction.args.to.amount, 72 | symbol: transaction.args.to.token.symbol, 73 | id: transaction.id, 74 | pending, 75 | }; 76 | }) 77 | .flat() 78 | .sort((a, b) => Number(b.id) - Number(a.id)); 79 | }, [userState.activeTransaction, userState.activity]); 80 | 81 | const ActivityList = () => { 82 | if (formattedActivity.length === 0) { 83 | return ( 84 | <> 85 | 92 | 93 | 94 | 95 | 96 | 97 | No transactions 98 | 99 | 100 | 101 | 102 | ); 103 | } 104 | 105 | return ( 106 | 107 | {formattedActivity.map((transaction, i) => ( 108 | 109 | ))} 110 | 111 | ); 112 | }; 113 | 114 | return ( 115 | 116 | 117 | 118 | 119 | 120 | ); 121 | }; 122 | -------------------------------------------------------------------------------- /src/components/ActivityTransactionItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import dayjs from 'dayjs'; 3 | import { styled, Typography, Link } from '@material-ui/core'; 4 | import { TransactionType } from 'context/user'; 5 | import mintIcon from 'assets/icons/mint-icon.svg'; 6 | import swapIcon from 'assets/icons/swap-icon.svg'; 7 | import pendingMintIcon from 'assets/icons/pending-mint.svg'; 8 | import pendingSwapIcon from 'assets/icons/pending-swap.svg'; 9 | import { EXPLORER_URL } from '../constants'; 10 | 11 | const ActivityListItem = styled('div')({ 12 | height: '81px', 13 | width: '100%', 14 | padding: '0 40px 0 35px', 15 | display: 'flex', 16 | justifyContent: 'space-between', 17 | boxSizing: 'border-box', 18 | borderTop: '1px solid rgb(193,193,255,0.2)!important', 19 | '& div': { 20 | fontFamily: 'IBM Plex Sans', 21 | fontStyle: 'normal', 22 | }, 23 | }); 24 | const ActivityDescription = styled('div')({ 25 | height: '80px', 26 | width: 'fit-content', 27 | justifyContent: 'flex-start', 28 | display: 'flex', 29 | flexDirection: 'row', 30 | }); 31 | const ActivityValue = styled(Typography)({ 32 | fontSize: 18, 33 | }); 34 | const ActivityIcon = styled('img')({ 35 | height: '32px', 36 | width: '32px', 37 | }); 38 | const ActivityType = styled('div')({ 39 | height: 'fit-content', 40 | marginBottom: '4px', 41 | 42 | fontWeight: 'normal', 43 | fontSize: '18px', 44 | color: '#FAFAF5', 45 | textTransform: 'capitalize', 46 | }); 47 | const ActivityDate = styled('div')({ 48 | fontSize: '14px', 49 | color: '#FAFAF5', 50 | opacity: 0.6, 51 | }); 52 | const Flex = styled('div')({ 53 | display: 'flex', 54 | justifyContent: 'start', 55 | alignItems: 'center', 56 | marginRight: '16px', 57 | }); 58 | const ColumnFlex = styled('div')({ 59 | display: 'flex', 60 | justifyContent: 'center', 61 | flexDirection: 'column', 62 | height: '80px', 63 | }); 64 | 65 | interface ActivityTransactionProps { 66 | type: TransactionType; 67 | amount: string; 68 | symbol: string; 69 | timestamp: string; 70 | pending: boolean; 71 | id: string; 72 | } 73 | export const ActivityTransactionItem: React.FC = ({ 74 | type, 75 | amount, 76 | symbol, 77 | timestamp, 78 | pending, 79 | id, 80 | }) => { 81 | const iconSrc = useMemo(() => { 82 | if (type === TransactionType.MINT) { 83 | if (pending) { 84 | return pendingMintIcon; 85 | } 86 | 87 | return mintIcon; 88 | } 89 | 90 | if (pending) { 91 | return pendingSwapIcon; 92 | } 93 | 94 | return swapIcon; 95 | }, [pending, type]); 96 | return ( 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | {type.toLowerCase()} 105 | 106 | {dayjs(timestamp).format('MMM DD')} 107 | {pending ? ' • Pending' : ''} 108 | 109 | 110 | 111 | 112 | {`+${amount} ${symbol}`} 113 | 114 | 115 | 116 | ); 117 | }; 118 | -------------------------------------------------------------------------------- /src/components/ConfirmSwapDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | Button, 4 | Dialog, 5 | DialogContent, 6 | DialogProps, 7 | DialogTitle, 8 | Grid, 9 | makeStyles, 10 | styled, 11 | Theme, 12 | Typography, 13 | } from '@material-ui/core'; 14 | import IconButton from '@material-ui/core/IconButton'; 15 | 16 | import { ReactComponent as CloseIcon } from '../assets/icons/close.svg'; 17 | import { ReactComponent as ArrowDownIcon } from '../assets/icons/arrow-down.svg'; 18 | import { ReactComponent as SwapIcon } from '../assets/icons/swap.svg'; 19 | import { TokenIcon } from './common/TokenIcon'; 20 | import { SwapInformation, SwapReceipt } from '../models/swap'; 21 | 22 | const StyledCloseButton = styled(IconButton)(({ theme }) => ({ 23 | position: 'absolute', 24 | right: theme.spacing(3), 25 | top: theme.spacing(4), 26 | color: theme.palette.grey[500], 27 | padding: theme.spacing(1), 28 | })); 29 | 30 | const StyledDialogTitle = styled(Typography)({ 31 | fontWeight: 600, 32 | fontSize: 20, 33 | }); 34 | 35 | const StyledSummaryText = styled(Typography)({ 36 | fontSize: 16, 37 | }); 38 | 39 | const StyledPrice = styled(StyledSummaryText)({ 40 | opacity: 0.6, 41 | }); 42 | 43 | const StyledArrowIcon = styled(ArrowDownIcon)({ 44 | paddingLeft: 9, 45 | width: 25, 46 | }); 47 | 48 | const StyledEndAlignText = styled(Typography)({ 49 | textAlign: 'end', 50 | }); 51 | 52 | const StyledRow = styled(Grid)({ 53 | marginBottom: 22, 54 | }); 55 | 56 | const useStyles = makeStyles((theme: Theme) => ({ 57 | root: { 58 | width: '100%', 59 | padding: 0, 60 | }, 61 | scrollPaper: { 62 | alignItems: 'baseline', 63 | }, 64 | gutters: { 65 | paddingRight: 33, 66 | paddingLeft: 33, 67 | }, 68 | 69 | dialogTitle: { 70 | padding: '30px 33px 35px 33px', 71 | }, 72 | dialogContent: { 73 | padding: '0px 33px 33px 33px', 74 | }, 75 | amount: { 76 | marginLeft: 13, 77 | textAlign: 'center', 78 | }, 79 | rateSummary: { 80 | justifyContent: 'flex-end', 81 | [theme.breakpoints.only('xs')]: { 82 | justifyContent: 'start', 83 | }, 84 | }, 85 | })); 86 | 87 | interface Props extends DialogProps { 88 | from?: SwapInformation; 89 | to?: SwapInformation; 90 | onClose: () => void; 91 | onSwap: (receipt: SwapReceipt) => void; 92 | } 93 | 94 | export const ConfirmSwapDialog = ({ open, from, to, onClose, onSwap }: Props) => { 95 | const classes = useStyles(); 96 | const [sent, isSent] = useState(false); 97 | 98 | useEffect(() => { 99 | if (!open) { 100 | isSent(false); 101 | } 102 | }, [open]); 103 | 104 | return ( 105 | 113 | 114 | Confirm Swap 115 | 116 | 117 | 118 | 119 | 120 | {from && to && ( 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | {from.amount} 130 | 131 | 132 | 133 | 134 | 135 | {from.token.symbol} 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | {to.amount} 150 | 151 | 152 | 153 | 154 | 155 | {to.token.symbol} 156 | 157 | 158 | 159 | 160 | 161 | 162 | Price 163 | 164 | 165 | 174 | 175 | 176 | {`${to.amount} ${to.token.symbol} / ${from.token.symbol}`} 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 199 | 200 | 201 | )} 202 | 203 | 204 | ); 205 | }; 206 | -------------------------------------------------------------------------------- /src/components/Mint.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import { Button, Grid, makeStyles, styled } from '@material-ui/core'; 3 | 4 | import { Token } from '../models/token'; 5 | import { DarkBox } from './common/DarkBox'; 6 | import { SelectedToken, TokenSelector } from './TokenSelector'; 7 | import { NumericInput } from './NumericInput'; 8 | import { useMintError } from '../hooks/amounts'; 9 | import { BouncingDots } from './common/BouncingDots'; 10 | import { useFilteredTokens } from '../hooks/tokens'; 11 | import { useMint } from 'services/API/mutations/useMint'; 12 | import { UserContext } from '../context/user'; 13 | 14 | const StyledInputContainer = styled(Grid)(({ theme }) => ({ 15 | [theme.breakpoints.up('sm')]: { 16 | paddingLeft: 12, 17 | }, 18 | })); 19 | 20 | const StyledMint2Container = styled(Grid)({ 21 | marginTop: 16, 22 | }); 23 | 24 | const StyledAddButtonContainer = styled(Grid)({ 25 | textAlign: 'center', 26 | marginTop: 18, 27 | }); 28 | 29 | const StyledAddTokenButton = styled(Button)(({ theme }) => ({ 30 | fontSize: theme.spacing(2), 31 | })); 32 | 33 | const useButtonStyles = (isSingleMint: boolean) => { 34 | return makeStyles({ 35 | actionButton: { 36 | marginTop: isSingleMint ? 18 : 32, 37 | }, 38 | }); 39 | }; 40 | 41 | export const Mint = (): JSX.Element => { 42 | const { 43 | state: { activeTransaction }, 44 | } = useContext(UserContext); 45 | const { mutate, isLoading } = useMint(); 46 | 47 | const [mintToken1, setMintToken1] = useState(); 48 | const [mintToken2, setMintToken2] = useState(); 49 | const [mintAmount1, setMintAmount1] = useState('1000'); 50 | const [mintAmount2, setMintAmount2] = useState('1000'); 51 | 52 | const buttonClasses = useButtonStyles(!!mintToken1 && !mintToken2)(); 53 | const options = useFilteredTokens(mintToken1); 54 | const mint1Error = useMintError(mintToken1, mintAmount1); 55 | const mint2Error = useMintError(mintToken2, mintAmount2); 56 | const error = mint1Error || (mintToken2 && mint2Error); 57 | const actionButtonText = error || 'Mint'; 58 | 59 | const handleAddToken = () => { 60 | if (!options) return; 61 | setMintToken2(options[0]); 62 | }; 63 | 64 | const handleMint = () => { 65 | if (!mintToken1) return; 66 | mutate({ 67 | mint1: { 68 | token: mintToken1, 69 | amount: mintAmount1, 70 | }, 71 | mint2: mintToken2 72 | ? { 73 | token: mintToken2, 74 | amount: mintAmount2, 75 | } 76 | : undefined, 77 | }); 78 | }; 79 | 80 | const MintToken1 = () => { 81 | if (mintToken1 && mintToken2) { 82 | return ; 83 | } 84 | 85 | return ( 86 | setMintToken1(token)} 90 | /> 91 | ); 92 | }; 93 | 94 | return ( 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | {mintToken1 && ( 103 | 104 | 105 | setMintAmount1(change)} 111 | /> 112 | 113 | 114 | )} 115 | 116 | 117 | 118 | {mintToken1 && !mintToken2 && ( 119 | 120 | 121 | + Add Token 122 | 123 | 124 | )} 125 | {mintToken2 && ( 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | setMintAmount2(change)} 140 | /> 141 | 142 | 143 | 144 | 145 | 146 | )} 147 | 148 | 159 | 160 | 161 | ); 162 | }; 163 | -------------------------------------------------------------------------------- /src/components/MultiTokenSuccessDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext } from 'react'; 2 | import { Box, Button, Dialog, Grid, makeStyles, styled, Typography } from '@material-ui/core'; 3 | import { ActionTypes, NotificationsContext } from 'context/notifications'; 4 | import { TokenIcon } from './common/TokenIcon'; 5 | import { ExternalLink } from './common/ExternalLink'; 6 | 7 | const StyledContainer = styled(Box)({ 8 | maxWidth: '100%', 9 | width: 434, 10 | minHeight: 359, 11 | maxHeight: '100%', 12 | '& > *': { 13 | height: '100%', 14 | }, 15 | padding: 32, 16 | boxSizing: 'border-box', 17 | }); 18 | 19 | const StyledIconContainer = styled(Box)({ 20 | width: '100%', 21 | 22 | '& > *': { 23 | margin: 'auto', 24 | }, 25 | }); 26 | 27 | const StyledTitle = styled(Typography)({ 28 | textAlign: 'center', 29 | padding: '0 32px 32px 32px', 30 | }); 31 | 32 | const StyledText = styled(Typography)({ 33 | textAlign: 'center', 34 | padding: 22, 35 | }); 36 | 37 | const StyledLinkContainer = styled(Grid)({ 38 | margin: 'auto', 39 | textAlign: 'center', 40 | marginBottom: 35, 41 | display: 'block', 42 | }); 43 | 44 | const useStyles = makeStyles({ 45 | dialog: { 46 | top: 100, 47 | }, 48 | scrollPaper: { 49 | alignItems: 'baseline', 50 | }, 51 | }); 52 | 53 | export const MultiTokenSuccessDialog: React.FC = () => { 54 | const { 55 | state: { multiTokenSuccess }, 56 | dispatch, 57 | } = useContext(NotificationsContext); 58 | const classes = useStyles(); 59 | 60 | const handleClose = useCallback(() => { 61 | dispatch({ 62 | type: ActionTypes.CLOSE_MULTI_TOKEN_SUCCESS, 63 | }); 64 | }, [dispatch]); 65 | 66 | return ( 67 | 75 | 76 | 77 | 78 | 79 | {multiTokenSuccess.title} 80 | 81 | 82 | {multiTokenSuccess.icons.map((icon, i) => ( 83 | 84 | 85 | 86 | 87 | 88 | ))} 89 | 90 | 91 | 92 | {multiTokenSuccess.text} 93 | 94 | 95 | {multiTokenSuccess.txIds.map((txId, i) => ( 96 | 97 | 98 | 99 | 100 | 101 | ))} 102 | 103 | 104 | 105 | 114 | 115 | 116 | 117 | 118 | ); 119 | }; 120 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AppBar, 3 | styled, 4 | Grid, 5 | SwipeableDrawer, 6 | IconButton, 7 | useMediaQuery, 8 | useTheme, 9 | Typography, 10 | } from '@material-ui/core'; 11 | import React, { useState } from 'react'; 12 | import { Sidemenu } from './Sidemenu'; 13 | import { TokenBalances } from './TokenBalances'; 14 | import MenuIcon from '@material-ui/icons/Menu'; 15 | import { StarknetLogo } from './common/StarknetLogo'; 16 | 17 | const StyledAppBar = styled(AppBar)({ 18 | boxShadow: 'unset', 19 | padding: '32px 50px 0 44px', 20 | alignItems: 'center', 21 | }); 22 | 23 | const MenuBar = styled(Grid)({ 24 | width: 'calc(100% + 40px)', 25 | margin: '-10px -10px 30px -10px', 26 | alignItems: 'center', 27 | }); 28 | 29 | export const Navbar: React.FC = () => { 30 | const theme = useTheme(); 31 | const isMobile = useMediaQuery(theme.breakpoints.down('sm')); 32 | const [open, setOpen] = useState(false); 33 | 34 | return ( 35 | <> 36 | 37 | setOpen(false)} 41 | onOpen={() => setOpen(true)} 42 | > 43 | 44 | 45 | {isMobile && ( 46 | 47 | 48 | 49 | 50 | 51 | setOpen(!open)}> 52 | 53 | 54 | 55 | 56 | )} 57 | 63 | 64 | Example Contract - Simple AMM 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/NumericInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { InputBase, InputBaseComponentProps, InputBaseProps, makeStyles } from '@material-ui/core'; 3 | 4 | export interface NumericInputProps extends InputBaseProps { 5 | value?: string; 6 | placeholder?: string; 7 | disabled?: boolean; 8 | className?: string; 9 | inputProps?: InputBaseComponentProps; 10 | handleChange: (amount: string) => void; 11 | } 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | input: { 15 | color: theme.palette.text.primary, 16 | fontWeight: 'normal', 17 | fontSize: 22, 18 | }, 19 | })); 20 | 21 | function isValidChange(input: string): boolean { 22 | // matches one or many digits followed by an optional single "." appearance that's followed by one or more digits 23 | const inputRegex = RegExp(`^\\d*(?:\\\\[])?\\d*$`); 24 | // remove any non-numeric invalid characters 25 | const cleanInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 26 | return inputRegex.test(cleanInput); 27 | } 28 | 29 | export const NumericInput = ({ 30 | value = '', 31 | placeholder = '0', 32 | disabled = false, 33 | className = '', 34 | inputProps = {}, 35 | handleChange, 36 | ...props 37 | }: NumericInputProps): JSX.Element => { 38 | const classes = useStyles(); 39 | 40 | const handleAmountChange = (event: React.ChangeEvent<{ value: unknown }>) => { 41 | // replace commas with periods 42 | const input = (event.target.value as string).replace(/,/g, '.'); 43 | 44 | if (input === '' || isValidChange(input)) { 45 | handleChange(input); 46 | } 47 | }; 48 | 49 | const roundedValue = Math.floor(Number(value)); 50 | 51 | return ( 52 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/Sidemenu.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Link, styled, Typography } from '@material-ui/core'; 2 | import Star from 'assets/menu/Star.svg'; 3 | import Block from 'assets/menu/Block.svg'; 4 | import Document from 'assets/menu/Document.svg'; 5 | import Github from 'assets/menu/Github.svg'; 6 | import Starkware from 'assets/menu/Starkware.svg'; 7 | import { ReactComponent as Collaboration } from 'assets/menu/Collaboration.svg'; 8 | 9 | import hexToRgba from 'hex-to-rgba'; 10 | import React from 'react'; 11 | import { StarknetLogo } from './common/StarknetLogo'; 12 | import { EXPLORER_URL } from '../constants'; 13 | 14 | const StyledContainer = styled(Grid)({ 15 | height: '100%', 16 | width: 277, 17 | background: hexToRgba('#535387', 0.28), 18 | borderRight: '1px solid rgba(145, 145, 183, 0.26)', 19 | }); 20 | 21 | interface MenuLink { 22 | label: string; 23 | icon: string; 24 | url: string; 25 | } 26 | 27 | const MenuItem = styled(Grid)({ 28 | width: 230, 29 | }); 30 | 31 | const MenuItemText = styled(Typography)({ 32 | display: 'inline-block', 33 | }); 34 | 35 | const MenuIcon = styled('img')({ 36 | width: 15, 37 | height: 15, 38 | }); 39 | 40 | const links: MenuLink[] = [ 41 | { label: 'Documentation', icon: Document, url: 'http://cairo-lang.org/docs/hello_starknet/' }, 42 | { 43 | label: 'See the Code', 44 | icon: Github, 45 | url: 'https://github.com/starkware-libs/cairo-lang/tree/master/src/starkware/starknet/apps/amm_sample/amm_sample.cairo', 46 | }, 47 | { label: 'Block Explorer', icon: Block, url: EXPLORER_URL }, 48 | { label: 'What is StarkNet?', icon: Star, url: 'https://starkware.co/product/starknet/' }, 49 | { 50 | label: 'StarkNet Planets Alpha', 51 | icon: Starkware, 52 | url: 'https://starkware.medium.com/starknet-planets-alpha-on-ropsten-e7494929cb95', 53 | }, 54 | ]; 55 | 56 | const MenuItems = styled(Grid)({ 57 | maxWidth: '100%', 58 | }); 59 | 60 | const LogoContainer = styled(Grid)({ 61 | width: '100%', 62 | height: 115, 63 | }); 64 | 65 | const CollaborationContainer = styled(MenuItem)({ 66 | marginBottom: 24, 67 | }); 68 | 69 | export const Sidemenu: React.FC = () => { 70 | return ( 71 | 72 | 73 | 74 | 75 | 76 | 77 | {links.map(({ label, icon, url }, i) => ( 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | {label} 86 | 87 | 88 | 89 | 90 | ))} 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /src/components/SnackbarProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SnackbarKey, SnackbarProvider as MaterialSnackbarProvider, useSnackbar } from 'notistack'; 3 | import { IconButton, Theme } from '@material-ui/core'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import { Close as IconClose } from '@material-ui/icons'; 6 | 7 | import TxLoader from 'assets/gifs/loading_tx.gif'; 8 | import SuccessIcon from 'assets/icons/success.svg'; 9 | import ErrorIcon from 'assets/icons/error.svg'; 10 | 11 | const useStyles = makeStyles((theme: Theme) => ({ 12 | root: { 13 | [theme.breakpoints.up('sm')]: { 14 | minWidth: 350, 15 | }, 16 | }, 17 | success: { 18 | backgroundColor: theme.palette.success.main, 19 | fontSize: `18px !important`, 20 | }, 21 | error: { 22 | backgroundColor: theme.palette.error.main, 23 | fontSize: `18px !important`, 24 | }, 25 | info: { 26 | backgroundColor: theme.palette.info.main, 27 | fontSize: `18px !important`, 28 | }, 29 | loader: { 30 | width: 22, 31 | height: 22, 32 | marginRight: 9, 33 | }, 34 | icon: { 35 | width: 16, 36 | height: 16, 37 | marginRight: 9, 38 | }, 39 | close: { 40 | width: 18, 41 | height: 18, 42 | color: theme.palette.text.primary, 43 | }, 44 | })); 45 | 46 | const SnackbarCloseButton = ({ snackbarKey }: { snackbarKey: SnackbarKey }): JSX.Element => { 47 | const classes = useStyles(); 48 | const { closeSnackbar } = useSnackbar(); 49 | 50 | return ( 51 | closeSnackbar(snackbarKey)}> 52 | 53 | 54 | ); 55 | }; 56 | 57 | export const SnackbarProvider: React.FC = ({ children }) => { 58 | const classes = useStyles(); 59 | 60 | return ( 61 | } 63 | anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} 64 | classes={{ 65 | root: classes.root, 66 | variantSuccess: classes.success, 67 | variantError: classes.error, 68 | variantInfo: classes.info, 69 | }} 70 | iconVariant={{ 71 | info: success tx icon, 72 | success: success tx icon, 73 | error: success tx icon, 74 | }} 75 | maxSnack={3} 76 | > 77 | {children} 78 | 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /src/components/SuccessDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext } from 'react'; 2 | import { 3 | Box, 4 | Button, 5 | Dialog, 6 | DialogContent, 7 | Grid, 8 | makeStyles, 9 | styled, 10 | Typography, 11 | } from '@material-ui/core'; 12 | import { ActionTypes, NotificationsContext } from 'context/notifications'; 13 | import { TokenIcon } from './common/TokenIcon'; 14 | import { ExternalLink } from './common/ExternalLink'; 15 | 16 | const StyledContainer = styled(Box)({ 17 | maxWidth: '100%', 18 | width: 434, 19 | minHeight: 359, 20 | maxHeight: '100%', 21 | '& > *': { 22 | height: '100%', 23 | }, 24 | padding: 32, 25 | boxSizing: 'border-box', 26 | }); 27 | 28 | const StyledIconContainer = styled(Box)({ 29 | width: '100%', 30 | 31 | '& > *': { 32 | margin: 'auto', 33 | }, 34 | }); 35 | 36 | const StyledTitle = styled(Typography)({ 37 | textAlign: 'center', 38 | padding: '0 32px 32px 32px', 39 | }); 40 | 41 | const StyledText = styled(Typography)({ 42 | textAlign: 'center', 43 | padding: 22, 44 | }); 45 | 46 | const StyledLinkContainer = styled(Grid)({ 47 | margin: 'auto', 48 | textAlign: 'center', 49 | marginBottom: 35, 50 | display: 'block', 51 | }); 52 | 53 | const useStyles = makeStyles({ 54 | scrollPaper: { 55 | alignItems: 'baseline', 56 | }, 57 | }); 58 | 59 | export const SuccessDialog: React.FC = () => { 60 | const { 61 | state: { success }, 62 | dispatch, 63 | } = useContext(NotificationsContext); 64 | const classes = useStyles(); 65 | 66 | const handleClose = useCallback(() => { 67 | dispatch({ 68 | type: ActionTypes.CLOSE_SUCCESS, 69 | }); 70 | }, [dispatch]); 71 | 72 | return ( 73 | 82 | 83 | 84 | 85 | 86 | 87 | {success.title} 88 | 89 | 90 | 91 | 92 | 93 | {success.text} 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 112 | 113 | 114 | 115 | 116 | 117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /src/components/Swap.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import { Button, Grid, IconButton, styled, Typography } from '@material-ui/core'; 3 | import { ReactComponent as SwapDirection } from 'assets/icons/swap-direction.svg'; 4 | import { SelectedToken } from './TokenSelector'; 5 | import { DarkBox } from './common/DarkBox'; 6 | import { NumericInput } from './NumericInput'; 7 | import { Token } from 'models/token'; 8 | import { RoundedButton } from './common/RoundedButton'; 9 | import { ConfirmSwapDialog } from './ConfirmSwapDialog'; 10 | import { useConversionError, useConversionRates } from '../hooks/amounts'; 11 | import { calculateSwapValue } from '../utils/swap'; 12 | import { SwapReceipt } from '../models/swap'; 13 | import { useFilteredTokens, useTokenBalance } from '../hooks/tokens'; 14 | import { useSwap } from '../services/API/mutations/useSwap'; 15 | import { BouncingDots } from './common/BouncingDots'; 16 | import { usePoolBalance } from '../services/API/queries/usePoolBalance'; 17 | import { UserContext } from '../context/user'; 18 | import { Skeleton } from '@material-ui/lab'; 19 | 20 | const StyledArrowsContainer = styled(Grid)({ 21 | width: '100%', 22 | margin: '0 auto -23px auto', 23 | height: 65, 24 | }); 25 | 26 | const StyledSwapButton = styled(Button)({ 27 | marginTop: 20, 28 | }); 29 | 30 | const StyledLeftLabelText = styled(Typography)({ 31 | fontWeight: 600, 32 | }); 33 | 34 | const StyledRightLabelText = styled(Typography)({ 35 | fontWeight: 400, 36 | }); 37 | 38 | const StyledLabelsContainer = styled(Grid)({ 39 | paddingBottom: '5px', 40 | }); 41 | 42 | const StyledConversionContainer = styled(Grid)({ 43 | marginTop: 16, 44 | }); 45 | 46 | const StyledEndText = styled(StyledRightLabelText)({ 47 | textAlign: 'end', 48 | }); 49 | 50 | const ConversionSkeleton = styled(Skeleton)({ 51 | display: 'inline-block', 52 | }); 53 | 54 | const Labels = ({ leftText, rightText }: { leftText: string; rightText: string }): JSX.Element => ( 55 | 56 | 57 | 58 | {leftText} 59 | 60 | 61 | 62 | 63 | {rightText} 64 | 65 | 66 | 67 | ); 68 | 69 | export const Swap = (): JSX.Element => { 70 | const { 71 | state: { activeTransaction }, 72 | } = useContext(UserContext); 73 | const { mutate: makeSwap, isLoading } = useSwap(); 74 | const options = useFilteredTokens(); 75 | 76 | const [showConfirm, setShowConfirm] = useState(false); 77 | const [fromToken, setFromToken] = useState(options[0]); 78 | const [fromAmount, setFromAmount] = useState(''); 79 | const [toToken, setToToken] = useState(options[1]); 80 | const [toAmount, setToAmount] = useState(''); 81 | 82 | const { data: poolBalance } = usePoolBalance(); 83 | const fromBalance = useTokenBalance(fromToken); 84 | const toBalance = useTokenBalance(toToken); 85 | const conversionRates = useConversionRates(fromToken, toToken); 86 | 87 | const fromError = useConversionError(fromToken, fromAmount, fromBalance); 88 | const toError = useConversionError(toToken, toAmount); 89 | 90 | const poolFromBalance = fromToken ? poolBalance?.get(fromToken.id) : undefined; 91 | const poolToBalance = toToken ? poolBalance?.get(toToken.id) : undefined; 92 | const maxAmountError = Number(fromAmount) > 1000 ? 'Max swap limit is 1000' : undefined; 93 | const error = maxAmountError || fromError || toError; 94 | const actionButtonText = error || 'Swap'; 95 | 96 | const handleSwitch = () => { 97 | if (!fromToken || !toToken || !poolFromBalance || !poolToBalance) return; 98 | 99 | setFromToken(toToken); 100 | setToAmount( 101 | Math.floor( 102 | calculateSwapValue(poolToBalance, poolFromBalance, toAmount).toNumber(), 103 | ).toString(), 104 | ); 105 | setFromAmount(toAmount); 106 | setToToken(fromToken); 107 | }; 108 | 109 | const handleFromAmountChange = (amount: string) => { 110 | setFromAmount(amount); 111 | 112 | if (!toToken) return; 113 | 114 | if (!amount) { 115 | setToAmount(''); 116 | return; 117 | } 118 | 119 | if (!poolFromBalance || !poolToBalance) return; 120 | 121 | setToAmount( 122 | Math.floor(calculateSwapValue(poolFromBalance, poolToBalance, amount).toNumber()).toString(), 123 | ); 124 | }; 125 | 126 | const handleToAmountChange = (amount: string) => { 127 | setToAmount(amount); 128 | 129 | if (!amount) { 130 | setFromAmount(''); 131 | return; 132 | } 133 | 134 | if (!poolFromBalance || !poolToBalance) return; 135 | 136 | setFromAmount( 137 | Math.floor(calculateSwapValue(poolFromBalance, poolToBalance, amount).toNumber()).toString(), 138 | ); 139 | }; 140 | 141 | const handleSwap = (receipt: SwapReceipt) => { 142 | setShowConfirm(false); 143 | makeSwap(receipt); 144 | }; 145 | 146 | return ( 147 | <> 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | handleFromAmountChange(change)} 164 | /> 165 | 166 | {fromBalance && ( 167 | 168 | { 170 | if (!fromBalance) return; 171 | handleFromAmountChange(fromBalance); 172 | }} 173 | > 174 | Max 175 | 176 | 177 | )} 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | handleToAmountChange(change)} 205 | /> 206 | 207 | 208 | 209 | 210 | 211 | 212 | {`1 ${fromToken.symbol} = `} 213 | 214 | {conversionRates ? ( 215 | conversionRates.from.toString() 216 | ) : ( 217 | 218 | )} 219 | 220 | {` ${toToken.symbol}`} 221 | 222 | 223 | 224 | setShowConfirm(true)} 231 | > 232 | {activeTransaction || isLoading ? : actionButtonText} 233 | 234 | 235 | 236 | setShowConfirm(false)} 241 | onSwap={handleSwap} 242 | /> 243 | 244 | ); 245 | }; 246 | -------------------------------------------------------------------------------- /src/components/TokenBalances.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, GridProps, makeStyles, Typography } from '@material-ui/core'; 3 | import { Skeleton } from '@material-ui/lab'; 4 | import hexToRgba from 'hex-to-rgba'; 5 | import { useAccountBalance } from '../services/API/queries/useAccountBalance'; 6 | import { tokens } from '../constants'; 7 | 8 | interface StyleProps { 9 | tokenColor: string; 10 | } 11 | 12 | const useStyles = ({ tokenColor }: StyleProps) => { 13 | return makeStyles((theme) => ({ 14 | box: { 15 | height: 42, 16 | minWidth: 164, 17 | background: hexToRgba(tokenColor, 0.1), 18 | borderRadius: 6, 19 | '& > div': { 20 | height: '100%', 21 | }, 22 | margin: '8px', 23 | }, 24 | text: { 25 | fontWeight: 400, 26 | color: tokenColor, 27 | fontSize: 18, 28 | display: 'inline-block', 29 | }, 30 | loader: { 31 | width: theme.spacing(4), 32 | fontSize: 18, 33 | }, 34 | amountContainer: { 35 | marginRight: 6, 36 | }, 37 | })); 38 | }; 39 | 40 | interface Props extends GridProps { 41 | symbol: string; 42 | color: string; 43 | amount?: string; 44 | } 45 | 46 | const TokenBalance = ({ symbol, color, amount, className, ...props }: Props): JSX.Element => { 47 | const classes = useStyles({ tokenColor: color })(); 48 | 49 | return ( 50 | 51 | 52 | 53 | {amount ? ( 54 | {amount} 55 | ) : ( 56 | 57 | )} 58 | 59 | 60 | {symbol} 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | export const TokenBalances = (): JSX.Element => { 68 | const { data: balances } = useAccountBalance(); 69 | 70 | return ( 71 | 72 | {tokens.map(({ id, symbol, color }, index) => { 73 | return ( 74 | 82 | ); 83 | })} 84 | 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /src/components/TokenInput.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, styled, Typography } from '@material-ui/core'; 2 | import React from 'react'; 3 | import { DarkBox } from './common/DarkBox'; 4 | import { TokenIcon } from './common/TokenIcon'; 5 | import { NumericInput, NumericInputProps } from './NumericInput'; 6 | 7 | const InputsContainer = styled(Grid)({ 8 | padding: '0 0 0 12px', 9 | }); 10 | 11 | export interface TokenInputProps { 12 | tokenProps: { 13 | symbol: string; 14 | icon: string; 15 | }; 16 | inputProps: NumericInputProps; 17 | } 18 | 19 | export const TokenInput: React.FC = ({ tokenProps, inputProps }) => { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {tokenProps.symbol} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/TokenSelectDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | DialogProps, 4 | styled, 5 | makeStyles, 6 | Dialog, 7 | ListItemAvatar, 8 | ListItemText, 9 | DialogTitle, 10 | DialogContent, 11 | List, 12 | ListItem, 13 | Typography, 14 | } from '@material-ui/core'; 15 | import IconButton from '@material-ui/core/IconButton'; 16 | 17 | import { ReactComponent as CloseIcon } from 'assets/icons/close.svg'; 18 | import { TokenIcon } from './common/TokenIcon'; 19 | import { Token } from 'models/token'; 20 | import { useAccountBalance } from '../services/API/queries/useAccountBalance'; 21 | 22 | const useStyles = makeStyles(() => ({ 23 | root: { 24 | width: '100%', 25 | padding: 0, 26 | }, 27 | scrollPaper: { 28 | alignItems: 'baseline', 29 | }, 30 | gutters: { 31 | paddingRight: 33, 32 | paddingLeft: 33, 33 | }, 34 | dialogTitle: { 35 | padding: '30px 33px 0px 33px', 36 | }, 37 | dialogContent: { 38 | padding: '14px 0px 25px 0px', 39 | }, 40 | })); 41 | 42 | const StyledListItemText = styled(ListItemText)(({ theme }) => ({ 43 | color: theme.palette.text.primary, 44 | })); 45 | 46 | const StyledDialogTitle = styled(Typography)({ 47 | fontWeight: 600, 48 | fontSize: 20, 49 | }); 50 | 51 | const StyledCloseButton = styled(IconButton)(({ theme }) => ({ 52 | position: 'absolute', 53 | right: theme.spacing(3), 54 | top: theme.spacing(4), 55 | color: theme.palette.grey[500], 56 | padding: theme.spacing(1), 57 | })); 58 | 59 | interface Props extends DialogProps { 60 | tokens: Token[]; 61 | handleSelect: (token: Token) => void; 62 | onClose: () => void; 63 | } 64 | 65 | export const TokenSelectDialog = ({ 66 | tokens, 67 | onClose, 68 | handleSelect, 69 | ...props 70 | }: Props): JSX.Element => { 71 | const { data: tokenBalances } = useAccountBalance(); 72 | const classes = useStyles(); 73 | 74 | return ( 75 | 83 | 84 | Select a token 85 | 86 | 87 | 88 | 89 | 90 | 91 | {tokens.map((token, index) => ( 92 | 93 | handleSelect(token)} 96 | classes={{ gutters: classes.gutters }} 97 | > 98 | 99 | 100 | 101 | 102 | 103 | {tokenBalances?.get(token.id) || '0.0'} 104 | 105 | 106 | 107 | ))} 108 | 109 | 110 | 111 | ); 112 | }; 113 | -------------------------------------------------------------------------------- /src/components/TokenSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { Box, Grid, IconButton, styled, Typography } from '@material-ui/core'; 3 | 4 | import { Token } from 'models/token'; 5 | import { TokenIcon } from './common/TokenIcon'; 6 | import { TokenSelectDialog } from './TokenSelectDialog'; 7 | import { RoundedButton } from './common/RoundedButton'; 8 | import { ReactComponent as DropdownArrow } from 'assets/icons/dropdown-arrow.svg'; 9 | import { ReactComponent as PlaceholderToken } from 'assets/tokens/placeholder.svg'; 10 | 11 | const StyledTokenContainer = styled(Box)({ 12 | width: '100%', 13 | height: '100%', 14 | }); 15 | 16 | const StyledTokenSymbol = styled(Box)(({ theme }) => ({ 17 | boxSizing: 'border-box', 18 | [theme.breakpoints.up('md')]: { 19 | paddingLeft: 12, 20 | }, 21 | })); 22 | 23 | const StyledTypography = styled(Typography)({ 24 | fontWeight: 600, 25 | }); 26 | 27 | interface Props { 28 | value?: Token; 29 | options: Token[]; 30 | onChange: (token: Token) => void; 31 | } 32 | 33 | export const SelectedToken: React.FC<{ token: Token }> = ({ token, children }) => ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {token.symbol} 43 | 44 | 45 | {children} 46 | 47 | 48 | 49 | ); 50 | 51 | export const TokenSelector = ({ value: token, onChange, options }: Props): JSX.Element => { 52 | const [open, setOpen] = useState(false); 53 | 54 | const handleClick = useCallback(() => { 55 | setOpen(true); 56 | }, []); 57 | 58 | const Selector = () => ( 59 | 60 | 61 | 62 | 63 | 64 | Select a Token 65 | 66 | 67 | ); 68 | 69 | return ( 70 | <> 71 | 72 | {token ? ( 73 | 74 | 75 | 76 | 77 | 78 | ) : ( 79 | 80 | )} 81 | 82 | setOpen(false)} 85 | tokens={options} 86 | handleSelect={(e) => { 87 | onChange(e); 88 | setOpen(false); 89 | }} 90 | /> 91 | 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /src/components/common/BouncingDots.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps, makeStyles } from '@material-ui/core'; 2 | import React from 'react'; 3 | 4 | const useStyles = makeStyles(() => ({ 5 | bouncingDotContainer: { 6 | display: 'flex', 7 | justifyContent: 'center', 8 | alignItems: 'center', 9 | }, 10 | bouncingDot: { 11 | width: 14, 12 | height: 14, 13 | borderRadius: '50%', 14 | margin: 6, 15 | background: '#78B1FC', 16 | animation: '$bounce 0.4s infinite alternate', 17 | '&:nth-child(2)': { 18 | background: '#FB514F', 19 | animationDelay: '0.1s', 20 | }, 21 | '&:nth-child(3)': { 22 | background: '#F9F7F5', 23 | animationDelay: '0.2s', 24 | }, 25 | }, 26 | '@keyframes bounce': { 27 | to: { 28 | transform: 'translate(0, -6px)', 29 | }, 30 | }, 31 | })); 32 | 33 | export const BouncingDots = ({ className, ...props }: BoxProps) => { 34 | const classes = useStyles(); 35 | 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/common/DarkBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box, styled } from '@material-ui/core'; 2 | 3 | export const DarkBox = styled(Box)({ 4 | width: '100%', 5 | minHeight: 94.5, 6 | padding: '23px 17px', 7 | background: 'rgba(0, 0, 0, 0.09)', 8 | borderRadius: 8, 9 | boxSizing: 'border-box', 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/common/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ContentCopyIcon from '@material-ui/icons/FileCopyOutlined'; 3 | import { Grid, Link, styled, Tooltip, Typography } from '@material-ui/core'; 4 | import { EXPLORER_URL } from '../../constants'; 5 | import { toShortAddress } from 'services/API/utils/toShortAddress'; 6 | 7 | const ExplorerText = styled(Typography)({ 8 | fontSize: '14px', 9 | }); 10 | 11 | const CopyIcon = styled(ContentCopyIcon)(({ theme }) => ({ 12 | fontSize: '14px', 13 | color: theme.palette.secondary.main, 14 | cursor: 'pointer', 15 | })); 16 | 17 | const StyledContainer = styled(Grid)({ 18 | width: 'unset', 19 | }); 20 | 21 | interface Props { 22 | txId: string; 23 | } 24 | 25 | const StyledLinkContainer = styled(Grid)({ 26 | marginTop: '-5px', 27 | }); 28 | 29 | export const ExternalLink = ({ txId }: Props): JSX.Element => { 30 | const [isCopied, setIsCopied] = React.useState(false); 31 | 32 | const handleCopy = () => { 33 | setIsCopied(true); 34 | navigator.clipboard.writeText(txId); 35 | }; 36 | 37 | return ( 38 | 39 | 40 | View on Block Explorer: 41 | 42 | 43 | 50 | {toShortAddress(txId, 4)} 51 | 52 | 53 | 54 | { 60 | setTimeout(() => { 61 | setIsCopied(false); 62 | }, 500); 63 | }} 64 | > 65 | 66 | 67 | 68 | 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/common/RoundedButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, ButtonProps, createStyles, makeStyles } from '@material-ui/core'; 3 | 4 | const useButtonStyles = makeStyles(() => 5 | createStyles({ 6 | root: { 7 | borderRadius: 50, 8 | }, 9 | label: { 10 | fontSize: 12, 11 | maxWidth: 95, 12 | height: 16, 13 | }, 14 | }), 15 | ); 16 | 17 | export const RoundedButton = (props: ButtonProps) => { 18 | const buttonStyles = useButtonStyles(); 19 | 20 | return 59 | 60 |
63 |
    66 |
    72 |
    75 |
    79 | token icon 83 |
    84 |
    85 |
    88 | 91 | TK1 92 | 93 |
    94 |

    97 | 0.0 98 |

    99 | 102 |
    103 |
    109 |
    112 |
    116 | token icon 120 |
    121 |
    122 |
    125 | 128 | TK2 129 | 130 |
    131 |

    134 | 0.0 135 |

    136 | 139 |
    140 |
141 |
142 | 143 | 144 |
148 |
149 | 150 | `; 151 | -------------------------------------------------------------------------------- /src/tests/integration/Mint.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/jest-dom'; 3 | 4 | import { customRender, screen, fireEvent, within } from '../utils'; 5 | import { Mint } from '../../components/Mint'; 6 | 7 | describe('Mint', () => { 8 | const setupMintToken = async () => { 9 | const { container } = customRender(); 10 | fireEvent.click(screen.getAllByRole('button', { name: /Select a Token/i })[0]); 11 | const dialog = screen.getByRole('presentation'); 12 | fireEvent.click(within(dialog).getByText(/TK1/i)); 13 | return container; 14 | }; 15 | 16 | it('displays empty token selector initially', () => { 17 | const { container } = customRender(); 18 | expect(container).toMatchSnapshot(); 19 | }); 20 | 21 | it('can select mint token', async () => { 22 | const container = await setupMintToken(); 23 | expect(container).toMatchSnapshot(); 24 | }); 25 | 26 | it('can change mint amount', async () => { 27 | const container = await setupMintToken(); 28 | fireEvent.change(screen.getByDisplayValue('1000'), { 29 | target: { value: '500' }, 30 | }); 31 | expect(container).toMatchSnapshot(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/tests/integration/Swap.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/jest-dom'; 3 | 4 | import { customRender, screen, fireEvent } from '../utils'; 5 | import { Swap } from '../../components/Swap'; 6 | 7 | describe('Swap', () => { 8 | it('can swap tokens direction', async () => { 9 | const { container } = customRender(); 10 | fireEvent.click(screen.getByRole('button', { name: /invert tokens swap direction/i })); 11 | expect(container).toMatchSnapshot(); 12 | }); 13 | 14 | it('can change tokens amounts', async () => { 15 | const { container } = customRender(); 16 | fireEvent.change(screen.getByLabelText(/amount of token to swap/i), { 17 | target: { value: '100' }, 18 | }); 19 | expect(container).toMatchSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/tests/integration/TokenSelector.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/jest-dom'; 3 | 4 | import { customRender, screen, fireEvent } from '../utils'; 5 | import { TokenSelector } from '../../components/TokenSelector'; 6 | import TokenSVG from '../../assets/tokens/token.svg'; 7 | import Token2SVG from '../../assets/tokens/token2.svg'; 8 | 9 | const options = [ 10 | { 11 | id: '1', 12 | name: 'Token 1', 13 | symbol: 'TK1', 14 | icon: TokenSVG, 15 | price: '2', 16 | color: '#FE9493', 17 | }, 18 | { 19 | id: '2', 20 | name: 'Token 2', 21 | symbol: 'TK2', 22 | icon: Token2SVG, 23 | price: '1', 24 | color: '#48C8FF', 25 | }, 26 | ]; 27 | 28 | describe('TokenSelector', () => { 29 | it('displays placeholder selector', () => { 30 | const { container } = customRender(); 31 | expect(container).toMatchSnapshot(); 32 | }); 33 | 34 | it('displays selected token information', () => { 35 | const { container } = customRender( 36 | , 37 | ); 38 | expect(container).toMatchSnapshot(); 39 | }); 40 | 41 | it('triggers change handler', () => { 42 | const handlerMock = jest.fn(); 43 | customRender(); 44 | fireEvent.click(screen.getByRole('button'), { name: 'Select a Token' }); 45 | fireEvent.click(screen.getByText(options[0].symbol)); 46 | expect(handlerMock).toHaveBeenNthCalledWith(1, options[0]); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/tests/integration/__snapshots__/Mint.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Mint can change mint amount 1`] = ` 4 |
5 |
8 |
11 |
14 |
17 |
21 |
24 |
27 |
30 |
33 |
36 |
40 | token icon 44 |
45 |
46 |
49 |
52 |

55 | TK1 56 |

57 |
58 |
59 |
62 | 79 |
80 |
81 |
82 |
83 |
84 |
85 |
88 |
91 |
97 | 106 |
107 |
108 |
109 |
110 |
111 |
112 |
115 | 129 |
130 |
133 | 147 |
148 |
149 |
150 | `; 151 | 152 | exports[`Mint can select mint token 1`] = ` 153 |
154 |
157 |
160 |
163 |
166 |
170 |
173 |
176 |
179 |
182 |
185 |
189 | token icon 193 |
194 |
195 |
198 |
201 |

204 | TK1 205 |

206 |
207 |
208 |
211 | 228 |
229 |
230 |
231 |
232 |
233 |
234 |
237 |
240 |
246 | 255 |
256 |
257 |
258 |
259 |
260 |
261 |
264 | 278 |
279 |
282 | 296 |
297 |
298 |
299 | `; 300 | 301 | exports[`Mint displays empty token selector initially 1`] = ` 302 |
303 |
306 |
309 |
312 |
315 |
319 |
322 |
325 |
328 | 329 | placeholder.svg 330 | 331 |
332 |
335 | 349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
359 | 371 |
372 |
373 |
374 | `; 375 | -------------------------------------------------------------------------------- /src/tests/integration/__snapshots__/Swap.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Swap can change tokens amounts 1`] = ` 4 |
5 |
8 |
11 |
14 |
17 |
20 | From 21 |
22 |
23 |
26 |
29 |
30 |
31 |
34 |
37 |
41 |
44 |
47 |
50 |
53 |
57 | token icon 61 |
62 |
63 |
66 |
69 |

72 | TK1 73 |

74 |
75 |
76 |
79 |
80 |
81 |
82 |
83 |
86 |
89 |
95 | 104 |
105 |
106 |
107 |
108 |
109 |
110 |
113 |
116 |
119 | 136 |
137 |
138 |
139 |
142 |
145 |
148 |
151 | To 152 |
153 |
154 |
157 |
160 |
161 |
162 |
165 |
168 |
172 |
175 |
178 |
181 |
184 |
188 | token icon 192 |
193 |
194 |
197 |
200 |

203 | TK2 204 |

205 |
206 |
207 |
210 |
211 |
212 |
213 |
214 |
217 |
223 | 232 |
233 |
234 |
235 |
236 |
237 |
240 |
243 | 244 | 1 TK1 = 245 | 246 | 247 | 251 | 252 | 253 | TK2 254 | 255 |
256 |
257 |
260 | 272 |
273 |
274 |
275 | `; 276 | 277 | exports[`Swap can swap tokens direction 1`] = ` 278 |
279 |
282 |
285 |
288 |
291 |
294 | From 295 |
296 |
297 |
300 |
303 |
304 |
305 |
308 |
311 |
315 |
318 |
321 |
324 |
327 |
331 | token icon 335 |
336 |
337 |
340 |
343 |

346 | TK2 347 |

348 |
349 |
350 |
353 |
354 |
355 |
356 |
357 |
360 |
363 |
369 | 378 |
379 |
380 |
381 |
382 |
383 |
384 |
387 |
390 |
393 | 410 |
411 |
412 |
413 |
416 |
419 |
422 |
425 | To 426 |
427 |
428 |
431 |
434 |
435 |
436 |
439 |
442 |
446 |
449 |
452 |
455 |
458 |
462 | token icon 466 |
467 |
468 |
471 |
474 |

477 | TK1 478 |

479 |
480 |
481 |
484 |
485 |
486 |
487 |
488 |
491 |
497 | 506 |
507 |
508 |
509 |
510 |
511 |
514 |
517 | 518 | 1 TK2 = 519 | 520 | 521 | 525 | 526 | 527 | TK1 528 | 529 |
530 |
531 |
534 | 546 |
547 |
548 |
549 | `; 550 | -------------------------------------------------------------------------------- /src/tests/integration/__snapshots__/TokenSelector.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TokenSelector displays placeholder selector 1`] = ` 4 |
5 |
8 |
11 |
14 | 15 | placeholder.svg 16 | 17 |
18 |
21 | 35 |
36 |
37 |
38 |
39 | `; 40 | 41 | exports[`TokenSelector displays selected token information 1`] = ` 42 |
43 |
46 |
49 |
52 |
55 |
58 |
62 | token icon 66 |
67 |
68 |
71 |
74 |

77 | TK1 78 |

79 |
80 |
81 |
84 | 101 |
102 |
103 |
104 |
105 |
106 |
107 | `; 108 | -------------------------------------------------------------------------------- /src/tests/utils.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createGenerateClassName, StylesProvider } from '@material-ui/core/styles'; 3 | import { render as tlRender, RenderOptions } from '@testing-library/react'; 4 | import { QueryClient, QueryClientProvider } from 'react-query'; 5 | 6 | /** 7 | * Styles and query client wrapper for the tests 8 | * @param children UI element to render 9 | * @see https://javascript.plainenglish.io/snapshots-of-material-ui-styled-component-with-react-testing-library-and-typescript-d82d7d926d2c 10 | */ 11 | const AppWrapper: React.FC = ({ children }) => { 12 | const queryClient = new QueryClient({ 13 | defaultOptions: { 14 | queries: { 15 | retry: false, 16 | }, 17 | }, 18 | }); 19 | 20 | const generateClassName = createGenerateClassName({ 21 | disableGlobal: true, 22 | productionPrefix: 'test', 23 | }); 24 | 25 | return ( 26 | 27 | {children} 28 | 29 | ); 30 | }; 31 | 32 | const customRender = (ui: React.ReactElement, options?: Omit) => 33 | tlRender(ui, { wrapper: AppWrapper, ...options }); 34 | 35 | export * from '@testing-library/react'; 36 | 37 | export { customRender }; 38 | -------------------------------------------------------------------------------- /src/theme/theme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles'; 2 | import hexToRgba from 'hex-to-rgba'; 3 | 4 | const defaultTheme = createMuiTheme(); 5 | 6 | export const theme = createMuiTheme({ 7 | palette: { 8 | primary: { 9 | main: '#FAFAF5', 10 | }, 11 | secondary: { 12 | main: '#FB514F', 13 | dark: '#FE4A49', 14 | }, 15 | background: { 16 | paper: '#28286E', 17 | }, 18 | text: { 19 | primary: '#FAFAF5', 20 | secondary: hexToRgba('#FAFAF5', 0.6), 21 | }, 22 | action: { 23 | disabled: 'rgba(250, 250, 245, 0.25)', 24 | disabledBackground: 'rgba(145, 145, 183, 0.15)', 25 | }, 26 | success: { 27 | main: '#74B0FF', 28 | }, 29 | info: { 30 | main: '#733461', 31 | }, 32 | error: { 33 | main: '#9E3B5A', 34 | }, 35 | }, 36 | typography: { 37 | fontFamily: 'IBM Plex Sans', 38 | subtitle1: { 39 | fontSize: '14px', 40 | lineHeight: '18px', 41 | }, 42 | subtitle2: { 43 | fontWeight: 300, 44 | fontSize: '10px', 45 | lineHeight: '13px', 46 | }, 47 | body1: { 48 | fontStyle: 'normal', 49 | fontSize: '20px', 50 | lineHeight: '26px', 51 | }, 52 | body2: { 53 | fontSize: '22px', 54 | lineHeight: '29px', 55 | }, 56 | h4: { 57 | fontWeight: 600, 58 | fontSize: 24, 59 | }, 60 | h5: { 61 | fontSize: '18px', 62 | }, 63 | h6: { 64 | fontSize: '11px', 65 | }, 66 | }, 67 | overrides: { 68 | MuiCssBaseline: { 69 | '@global': { 70 | '*::-webkit-scrollbar': { 71 | width: 4, 72 | }, 73 | '*::-webkit-scrollbar-track': { 74 | '-webkit-box-shadow': 'inset 0 0 6px rgba(0,0,0,0.3)', 75 | borderRadius: 10, 76 | }, 77 | '*::-webkit-scrollbar-thumb': { 78 | backgroundColor: hexToRgba('#6C76CF', 0.3), 79 | borderRadius: 10, 80 | }, 81 | }, 82 | }, 83 | MuiButton: { 84 | root: { 85 | textTransform: 'unset', 86 | }, 87 | outlinedSecondary: { 88 | fontSize: 12, 89 | }, 90 | containedSecondary: { 91 | minHeight: 66, 92 | fontSize: 18, 93 | backgroundColor: hexToRgba('#FE4A49', 0.2), 94 | color: '#FB514F', 95 | '&:hover': { 96 | backgroundColor: hexToRgba('#FE4A49', 0.1), 97 | }, 98 | }, 99 | }, 100 | MuiDialog: { 101 | paper: { 102 | position: 'absolute', 103 | top: 100, 104 | borderRadius: defaultTheme.spacing(1), 105 | left: 'calc(50% - 116px)', 106 | [defaultTheme.breakpoints.down('sm')]: { 107 | left: 'unset', 108 | }, 109 | }, 110 | }, 111 | }, 112 | }); 113 | -------------------------------------------------------------------------------- /src/utils/random.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | export function randomUserId(): string { 4 | const max = new BigNumber(2).exponentiatedBy(250).minus(1); 5 | return BigNumber.random().multipliedBy(max).toFixed(0, BigNumber.ROUND_FLOOR); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/swap.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | /* 4 | Calculates the receiving swap value using the following formula: 5 | amount_to = (amm_to_balance * amount_from) / (amm_from_balance + amount_from) 6 | */ 7 | export const calculateSwapValue = ( 8 | poolFromBalance: string, 9 | poolToBalance: string, 10 | inputAmount: string, 11 | ): BigNumber => { 12 | const ammFrom = new BigNumber(poolToBalance).multipliedBy(inputAmount); 13 | const ammTo = new BigNumber(poolFromBalance).plus(inputAmount); 14 | 15 | return ammFrom.dividedBy(ammTo); 16 | }; 17 | -------------------------------------------------------------------------------- /travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: node_js 3 | node_js: 4 | - 12 5 | cache: yarn 6 | before_script: 7 | - yarn 8 | script: 9 | - yarn lint:check 10 | - yarn build 11 | deploy: 12 | provider: heroku 13 | repo: dOrgTech/starkware-demo 14 | api_key: cb0fde8c-74d4-439c-ac86-f7a2dbba0f46 15 | app: 16 | develop: starkware-demo-staging 17 | master: starkware-demo-production 18 | skip_cleanup: 'true' -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "baseUrl": "./src", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["src"] 21 | } 22 | --------------------------------------------------------------------------------