├── .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 | 
4 |
5 | [](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 |
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 |
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 |
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 |
8 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-up.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/assets/icons/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/cog.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/dropdown-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/error.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/mint-icon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/pending-mint.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/pending-swap.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/assets/icons/success.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/swap-direction.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/assets/icons/swap-icon.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/assets/icons/swap.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/assets/menu/Block.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/menu/Document.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/menu/Download.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/menu/Github.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/menu/Star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/menu/Starkware.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/assets/starknet.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/src/assets/tokens/placeholder.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/assets/tokens/token.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/assets/tokens/token2.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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:
,
72 | success:
,
73 | error:
,
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 |
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 |
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 ;
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/common/SplashLoader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Backdrop from '@material-ui/core/Backdrop';
3 | import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
4 | import { NotificationsContext } from '../../context/notifications';
5 | import Logo from 'assets/logo.svg';
6 |
7 | const useStyles = makeStyles((theme: Theme) =>
8 | createStyles({
9 | backdrop: {
10 | zIndex: theme.zIndex.drawer + 1,
11 | color: '#fff',
12 | },
13 | spinningLogo: {
14 | height: 128,
15 | animation: '$spin 2s linear 0s infinite',
16 | position: 'absolute',
17 | left: 'calc(50% + 70px)',
18 | [theme.breakpoints.down('sm')]: {
19 | left: 'unset',
20 | },
21 | },
22 | '@keyframes spin': {
23 | from: {
24 | transform: 'rotate(0deg)',
25 | },
26 | to: {
27 | transform: 'rotate(360deg)',
28 | },
29 | },
30 | }),
31 | );
32 |
33 | export const SplashLoader = () => {
34 | const {
35 | state: { loading },
36 | } = React.useContext(NotificationsContext);
37 | const classes = useStyles();
38 |
39 | return (
40 |
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/components/common/StarknetLogo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Starknet from 'assets/starknet.svg';
3 | import { Box, styled, Typography } from '@material-ui/core';
4 |
5 | const Subtitle = styled(Typography)({
6 | display: 'block',
7 | boxSizing: 'border-box',
8 | textAlign: 'end',
9 | marginTop: '-10px',
10 | paddingRight: '9px',
11 | });
12 |
13 | const Container = styled(Box)({
14 | width: 196,
15 | height: 56,
16 | });
17 |
18 | const StarkNetLogo = styled('img')({
19 | width: 196,
20 | height: 39,
21 | margin: 'auto',
22 | });
23 |
24 | export const StarknetLogo: React.FC = () => {
25 | return (
26 |
27 |
28 | Live on Testnet
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/common/TokenIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Avatar, AvatarProps, styled } from '@material-ui/core';
3 |
4 | export type LogoSize = 'default' | 'medium' | 'large';
5 |
6 | export interface TokenIconProps extends AvatarProps {
7 | icon: string;
8 | size?: LogoSize;
9 | }
10 |
11 | const TokenLogoContainer = styled(Avatar)((props: { dimension: number }) => ({
12 | width: props.dimension,
13 | height: props.dimension,
14 | borderRadius: '100%',
15 | '& > *': {
16 | width: '100%',
17 | height: '100%',
18 | },
19 | }));
20 |
21 | export const TokenIcon: React.FC = React.memo(function component({
22 | icon,
23 | size = 'default',
24 | ...props
25 | }) {
26 | let iconSize: number;
27 |
28 | switch (size) {
29 | case 'default':
30 | iconSize = 32;
31 | break;
32 | case 'medium':
33 | iconSize = 48;
34 | break;
35 | case 'large':
36 | iconSize = 66;
37 | break;
38 | }
39 |
40 | return (
41 |
42 |
43 |
44 | );
45 | });
46 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import TokenSVG from './assets/tokens/token.svg';
2 | import Token2SVG from './assets/tokens/token2.svg';
3 |
4 | export const tokens = [
5 | {
6 | id: '1',
7 | name: 'Token 1',
8 | symbol: 'TK1',
9 | icon: TokenSVG,
10 | price: '1',
11 | color: '#FE9493',
12 | },
13 | {
14 | id: '2',
15 | name: 'Token 2',
16 | symbol: 'TK2',
17 | icon: Token2SVG,
18 | price: '0.8',
19 | color: '#48C8FF',
20 | },
21 | ];
22 |
23 | const API_BASE_URL = process.env.REACT_APP_API_BASE_URL as string;
24 |
25 | if (!API_BASE_URL) {
26 | throw new Error(`Missing API_BASE_URL env var`);
27 | }
28 |
29 | const EXPLORER_URL = process.env.REACT_APP_EXPLORER_URL as string;
30 |
31 | if (!EXPLORER_URL) {
32 | throw new Error(`Missing EXPLORER_URL env var`);
33 | }
34 |
35 | const GET_ACCOUNT_TOKEN_BALANCE_ENTRYPOINT = process.env
36 | .REACT_APP_GET_ACCOUNT_TOKEN_BALANCE_ENTRYPOINT as string;
37 |
38 | if (!GET_ACCOUNT_TOKEN_BALANCE_ENTRYPOINT) {
39 | throw new Error(`Missing GET_ACCOUNT_TOKEN_BALANCE_ENTRYPOINT env var`);
40 | }
41 |
42 | const GET_POOL_TOKEN_BALANCE_ENTRYPOINT = process.env
43 | .REACT_APP_GET_POOL_TOKEN_BALANCE_ENTRYPOINT as string;
44 |
45 | if (!GET_POOL_TOKEN_BALANCE_ENTRYPOINT) {
46 | throw new Error(`Missing GET_POOL_TOKEN_BALANCE_ENTRYPOINT env var`);
47 | }
48 | const ADD_DEMO_TOKEN_ENTRYPOINT = process.env.REACT_APP_ADD_DEMO_TOKEN_ENTRYPOINT as string;
49 |
50 | if (!ADD_DEMO_TOKEN_ENTRYPOINT) {
51 | throw new Error(`Missing ADD_DEMO_TOKEN_ENTRYPOINT env var`);
52 | }
53 | const SWAP_ENTRYPOINT = process.env.REACT_APP_SWAP_ENTRYPOINT as string;
54 |
55 | if (!SWAP_ENTRYPOINT) {
56 | throw new Error(`Missing SWAP_ENTRYPOINT env var`);
57 | }
58 | const INIT_POOL_ENTRYPOINT = process.env.REACT_APP_INIT_POOL_ENTRYPOINT as string;
59 |
60 | if (!INIT_POOL_ENTRYPOINT) {
61 | throw new Error(`Missing INIT_POOL_ENTRYPOINT env var`);
62 | }
63 | const CONTRACT_ADDRESS = process.env.REACT_APP_CONTRACT_ADDRESS as string;
64 |
65 | if (!CONTRACT_ADDRESS) {
66 | throw new Error(`Missing CONTRACT_ADDRESS env var`);
67 | }
68 |
69 | export {
70 | GET_ACCOUNT_TOKEN_BALANCE_ENTRYPOINT,
71 | GET_POOL_TOKEN_BALANCE_ENTRYPOINT,
72 | ADD_DEMO_TOKEN_ENTRYPOINT,
73 | SWAP_ENTRYPOINT,
74 | INIT_POOL_ENTRYPOINT,
75 | CONTRACT_ADDRESS,
76 | EXPLORER_URL,
77 | API_BASE_URL,
78 | };
79 |
--------------------------------------------------------------------------------
/src/context/notifications.tsx:
--------------------------------------------------------------------------------
1 | import { MultiTokenSuccessDialog } from 'components/MultiTokenSuccessDialog';
2 | import { SuccessDialog } from 'components/SuccessDialog';
3 | import React, { createContext, Dispatch, useReducer } from 'react';
4 | import { useCallback } from 'react';
5 | import { SplashLoader } from '../components/common/SplashLoader';
6 |
7 | export enum ActionTypes {
8 | OPEN_SUCCESS = 'OPEN_SUCCESS',
9 | OPEN_MULTI_TOKEN_SUCCESS = 'OPEN_MULTI_TOKEN_SUCCESS',
10 | CLOSE_SUCCESS = 'CLOSE_SUCCESS',
11 | CLOSE_MULTI_TOKEN_SUCCESS = 'CLOSE_MULTI_TOKEN_SUCCESS',
12 | SHOW_LOADING = 'SHOW_LOADING',
13 | HIDE_LOADING = 'HIDE_LOADING',
14 | }
15 |
16 | export type CloseSuccessAction = {
17 | type: ActionTypes.CLOSE_SUCCESS;
18 | };
19 |
20 | export type CloseMultiTokenSuccessAction = {
21 | type: ActionTypes.CLOSE_MULTI_TOKEN_SUCCESS;
22 | };
23 |
24 | export type OpenSuccessAction = {
25 | type: ActionTypes.OPEN_SUCCESS;
26 | payload: {
27 | title: string;
28 | icon: string;
29 | text: string;
30 | txId: string;
31 | buttonText: string;
32 | };
33 | };
34 |
35 | export type OpenMultiTokenSuccessAction = {
36 | type: ActionTypes.OPEN_MULTI_TOKEN_SUCCESS;
37 | payload: {
38 | title: string;
39 | icons: string[];
40 | text: string;
41 | txIds: string[];
42 | buttonText: string;
43 | };
44 | };
45 |
46 | export type ShowLoading = {
47 | type: ActionTypes.SHOW_LOADING;
48 | };
49 |
50 | export type HideLoading = {
51 | type: ActionTypes.HIDE_LOADING;
52 | };
53 |
54 | type NotificationsContextAction =
55 | | OpenSuccessAction
56 | | CloseSuccessAction
57 | | ShowLoading
58 | | HideLoading
59 | | OpenMultiTokenSuccessAction
60 | | CloseMultiTokenSuccessAction;
61 |
62 | export interface NotificationContextState {
63 | success: {
64 | open: boolean;
65 | title: string;
66 | icon: string;
67 | text: string;
68 | txId: string;
69 | buttonText: string;
70 | };
71 | multiTokenSuccess: {
72 | open: boolean;
73 | title: string;
74 | icons: string[];
75 | text: string;
76 | txIds: string[];
77 | buttonText: string;
78 | };
79 | loading: boolean;
80 | }
81 |
82 | interface Context {
83 | state: NotificationContextState;
84 | dispatch: Dispatch;
85 | close: () => void;
86 | }
87 |
88 | const INITIAL_STATE: NotificationContextState = {
89 | success: {
90 | open: false,
91 | title: '',
92 | icon: '',
93 | text: '',
94 | txId: '',
95 | buttonText: '',
96 | },
97 | multiTokenSuccess: {
98 | open: false,
99 | title: '',
100 | icons: [],
101 | text: '',
102 | txIds: [],
103 | buttonText: '',
104 | },
105 | loading: false,
106 | };
107 |
108 | const reducer = (
109 | state: NotificationContextState,
110 | action: NotificationsContextAction,
111 | ): NotificationContextState => {
112 | switch (action.type) {
113 | case ActionTypes.OPEN_SUCCESS:
114 | return {
115 | ...state,
116 | success: {
117 | open: true,
118 | ...action.payload,
119 | },
120 | };
121 | case ActionTypes.CLOSE_SUCCESS:
122 | return {
123 | ...state,
124 | success: INITIAL_STATE.success,
125 | };
126 | case ActionTypes.SHOW_LOADING:
127 | return {
128 | ...state,
129 | loading: true,
130 | };
131 | case ActionTypes.HIDE_LOADING:
132 | return {
133 | ...state,
134 | loading: false,
135 | };
136 | case ActionTypes.OPEN_MULTI_TOKEN_SUCCESS:
137 | return {
138 | ...state,
139 | multiTokenSuccess: {
140 | open: true,
141 | ...action.payload,
142 | },
143 | };
144 | case ActionTypes.CLOSE_MULTI_TOKEN_SUCCESS:
145 | return {
146 | ...state,
147 | multiTokenSuccess: INITIAL_STATE.multiTokenSuccess,
148 | };
149 | default:
150 | throw new Error(`Unrecognized action in Notifications Provider`);
151 | }
152 | };
153 |
154 | export const NotificationsContext = createContext({
155 | state: INITIAL_STATE,
156 | dispatch: () => null,
157 | close: () => undefined,
158 | });
159 |
160 | export const NotificationsProvider: React.FC = ({ children }) => {
161 | const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
162 |
163 | const handleClose = useCallback(() => {
164 | dispatch({
165 | type: ActionTypes.CLOSE_SUCCESS,
166 | });
167 | }, [dispatch]);
168 |
169 | return (
170 |
171 | {children}
172 |
173 |
174 |
175 |
176 | );
177 | };
178 |
--------------------------------------------------------------------------------
/src/context/user.tsx:
--------------------------------------------------------------------------------
1 | import { useTxNotifications } from 'hooks/notifications';
2 | import React, { createContext, Dispatch, useContext, useReducer, useState } from 'react';
3 | import { useQueryClient, useQuery } from 'react-query';
4 | import { MintArgs } from 'services/API/mutations/useMint';
5 | import { SwapArgs } from 'services/API/mutations/useSwap';
6 | import { TransactionStatus } from 'services/API/types';
7 | import { httpClient } from 'services/API/utils/http';
8 | import { randomUserId } from '../utils/random';
9 | import { ActionTypes as NotificationsActionTypes, NotificationsContext } from './notifications';
10 |
11 | const STORAGE_PREFIX = `starkware`;
12 | const STORAGE_KEY = `${STORAGE_PREFIX}:user`;
13 |
14 | interface UserData {
15 | activity: Transaction[];
16 | userId: string;
17 | }
18 |
19 | const getUserData = (): UserData => {
20 | const userDataString = localStorage.getItem(STORAGE_KEY);
21 |
22 | if (userDataString) {
23 | return JSON.parse(userDataString);
24 | }
25 |
26 | const userId = randomUserId();
27 | const userData = {
28 | userId,
29 | activity: [],
30 | };
31 |
32 | localStorage.setItem(STORAGE_KEY, JSON.stringify(userData));
33 |
34 | return userData;
35 | };
36 |
37 | export enum TransactionType {
38 | MINT = 'MINT',
39 | SWAP = 'SWAP',
40 | }
41 |
42 | export enum ActionTypes {
43 | ADD_TRANSACTION = 'ADD_TRANSACTION',
44 | UPDATE_USER_ID = 'UPDATE_USER_ID',
45 | SET_ACTIVE_TRANSACTION = 'SET_ACTIVE_TRANSACTION',
46 | UNSET_ACTIVE_TRANSACTION = 'UNSET_ACTIVE_TRANSACTION',
47 | }
48 |
49 | export type Transaction =
50 | | {
51 | id: string;
52 | type: TransactionType.MINT;
53 | args: MintArgs;
54 | timestamp: string;
55 | }
56 | | {
57 | id: string;
58 | type: TransactionType.SWAP;
59 | args: SwapArgs;
60 | timestamp: string;
61 | };
62 |
63 | export type updateUserIdAction = {
64 | type: ActionTypes.UPDATE_USER_ID;
65 | payload: {
66 | userId: string;
67 | };
68 | };
69 |
70 | export type addTransactionAction = {
71 | type: ActionTypes.ADD_TRANSACTION;
72 | payload: Transaction;
73 | };
74 |
75 | export type addActiveAction = {
76 | type: ActionTypes.SET_ACTIVE_TRANSACTION;
77 | payload: Transaction;
78 | };
79 |
80 | export type removeActiveAction = {
81 | type: ActionTypes.UNSET_ACTIVE_TRANSACTION;
82 | };
83 |
84 | export type UserContextState = {
85 | userId: string;
86 | activity: Transaction[];
87 | activeTransaction: Transaction | null;
88 | };
89 |
90 | const INITIAL_USER_DATA = getUserData();
91 |
92 | const INITIAL_STATE: UserContextState = {
93 | userId: INITIAL_USER_DATA.userId,
94 | activeTransaction: null,
95 | activity: INITIAL_USER_DATA.activity,
96 | };
97 | type UserContextAction =
98 | | updateUserIdAction
99 | | addActiveAction
100 | | removeActiveAction
101 | | addTransactionAction;
102 |
103 | const reducer = (state: UserContextState, action: UserContextAction): UserContextState => {
104 | switch (action.type) {
105 | case ActionTypes.UPDATE_USER_ID:
106 | return {
107 | ...state,
108 | userId: action.payload.userId,
109 | activity: [],
110 | };
111 | case ActionTypes.SET_ACTIVE_TRANSACTION:
112 | return {
113 | ...state,
114 | activeTransaction: action.payload,
115 | };
116 | case ActionTypes.ADD_TRANSACTION:
117 | localStorage.setItem(
118 | STORAGE_KEY,
119 | JSON.stringify({
120 | userId: state.userId,
121 | activity: [...state.activity, action.payload],
122 | }),
123 | );
124 |
125 | return {
126 | ...state,
127 | activity: [...state.activity, action.payload],
128 | };
129 | case ActionTypes.UNSET_ACTIVE_TRANSACTION:
130 | return {
131 | ...state,
132 | activeTransaction: null,
133 | };
134 | default:
135 | throw new Error(`Unrecognized action in User Context Provider`);
136 | }
137 | };
138 |
139 | interface Context {
140 | state: UserContextState;
141 | dispatch: Dispatch;
142 | }
143 |
144 | export const UserContext = createContext({
145 | state: INITIAL_STATE,
146 | dispatch: () => null,
147 | });
148 |
149 | export const UserProvider: React.FC = ({ children }) => {
150 | const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
151 | const [stop, setStop] = useState(false);
152 |
153 | const queryClient = useQueryClient();
154 | const { showSuccess, showError, closeSnackbar } = useTxNotifications();
155 | const { dispatch: notificationDispatch } = useContext(NotificationsContext);
156 |
157 | const displayNotifications = () => {
158 | if (!state.activeTransaction) return;
159 |
160 | if (state.activeTransaction.type === TransactionType.MINT) {
161 | const { mint1, mint2 } = state.activeTransaction.args;
162 |
163 | if (!mint2) {
164 | notificationDispatch({
165 | type: NotificationsActionTypes.OPEN_SUCCESS,
166 | payload: {
167 | title: `Success!`,
168 | icon: mint1.token.icon,
169 | text: `Received ${mint1.amount} ${mint1.token.symbol}`,
170 | txId: state.activeTransaction.id,
171 | buttonText: 'Go Back',
172 | },
173 | });
174 | return;
175 | }
176 |
177 | notificationDispatch({
178 | type: NotificationsActionTypes.OPEN_MULTI_TOKEN_SUCCESS,
179 | payload: {
180 | title: `Success!`,
181 | icons: [mint1.token.icon, mint2.token.icon],
182 | text: `Minted ${mint1.amount} ${mint1.token.symbol} & ${mint2.amount} ${mint2.token.symbol}`,
183 | txIds: [state.activeTransaction.id],
184 | buttonText: 'Go Back',
185 | },
186 | });
187 | }
188 |
189 | if (state.activeTransaction.type === TransactionType.SWAP) {
190 | const { to } = state.activeTransaction.args;
191 | notificationDispatch({
192 | type: NotificationsActionTypes.OPEN_SUCCESS,
193 | payload: {
194 | title: `Success!`,
195 | icon: to.token.icon,
196 | text: `Received ${to.amount} ${to.token.symbol}`,
197 | txId: state.activeTransaction.id,
198 | buttonText: 'Go Back',
199 | },
200 | });
201 | }
202 | };
203 |
204 | const handleTxSuccess = (data: TransactionStatus) => {
205 | //TODO: remove execution on rejected
206 | if (data.tx_status === 'PENDING' && data.block_id) {
207 | queryClient.resetQueries('accountBalance');
208 | setStop(true);
209 | closeSnackbar();
210 | showSuccess();
211 | dispatch({
212 | type: ActionTypes.ADD_TRANSACTION,
213 | payload: state.activeTransaction as Transaction,
214 | });
215 | dispatch({
216 | type: ActionTypes.UNSET_ACTIVE_TRANSACTION,
217 | });
218 | displayNotifications();
219 | }
220 | };
221 |
222 | const handleTxError = (error: unknown) => {
223 | console.error(`Error while querying for Tx ID ${state.activeTransaction?.id}: ${error}`);
224 | setStop(true);
225 | closeSnackbar();
226 | showError();
227 | dispatch({
228 | type: ActionTypes.UNSET_ACTIVE_TRANSACTION,
229 | });
230 | };
231 |
232 | useQuery(
233 | ['txStatus', state.activeTransaction],
234 | async () => {
235 | setStop(false);
236 | const { data } = await httpClient.get(
237 | `feeder_gateway/get_transaction_status?transactionHash=${state.activeTransaction?.id}`,
238 | );
239 |
240 | return data;
241 | },
242 | {
243 | onSuccess: handleTxSuccess,
244 | onError: handleTxError,
245 | enabled: !!state.activeTransaction,
246 | refetchInterval: stop ? false : 10000,
247 | refetchIntervalInBackground: true,
248 | refetchOnWindowFocus: false,
249 | },
250 | );
251 |
252 | return {children};
253 | };
254 |
--------------------------------------------------------------------------------
/src/hooks/amounts.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 | import { ConversionRate, Token } from 'models/token';
3 | import { usePoolBalance } from '../services/API/queries/usePoolBalance';
4 | import { calculateSwapValue } from '../utils/swap';
5 |
6 | export const useConversionError = (
7 | token?: Token,
8 | amount?: string,
9 | limit?: string,
10 | ): string | undefined => {
11 | const inputAmount = new BigNumber(amount || '');
12 |
13 | if (!token) return 'Select a token';
14 | if (inputAmount.isNaN()) return 'Enter an amount';
15 | if (inputAmount.isZero()) return 'Enter an amount';
16 |
17 | if (limit && inputAmount.gt(limit)) {
18 | return `Insufficient ${token.symbol}`;
19 | }
20 |
21 | return undefined;
22 | };
23 |
24 | export const useConversionRates = (
25 | fromToken?: Token,
26 | toToken?: Token,
27 | ): ConversionRate | undefined => {
28 | const { data: poolBalance } = usePoolBalance();
29 | const poolFromBalance = fromToken ? poolBalance?.get(fromToken.id) : undefined;
30 | const poolToBalance = toToken ? poolBalance?.get(toToken.id) : undefined;
31 |
32 | if (!fromToken || !toToken || !poolFromBalance || !poolToBalance) return;
33 |
34 | return {
35 | from: calculateSwapValue(poolFromBalance, poolToBalance, '1'),
36 | to: calculateSwapValue(poolToBalance, poolFromBalance, '1'),
37 | };
38 | };
39 |
40 | export const useMintError = (token?: Token, amount?: string): string | undefined => {
41 | const inputAmount = new BigNumber(amount || '');
42 |
43 | if (!token) return 'Select a token';
44 | if (inputAmount.isNaN()) return 'Enter an amount';
45 | if (inputAmount.isZero()) return 'Enter an amount';
46 | if (inputAmount.gt(1000)) {
47 | return `You can mint up to 1000 ${token.symbol}`;
48 | }
49 |
50 | return undefined;
51 | };
52 |
--------------------------------------------------------------------------------
/src/hooks/notifications.ts:
--------------------------------------------------------------------------------
1 | import { useSnackbar } from 'notistack';
2 |
3 | export const useTxNotifications = () => {
4 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
5 |
6 | return {
7 | closeSnackbar,
8 | showPendingTransaction: () => {
9 | enqueueSnackbar('Transaction Pending...', { variant: 'info', persist: true });
10 | },
11 | showSuccess: () => {
12 | enqueueSnackbar('Transaction Successful!', { variant: 'success' });
13 | },
14 | showError: () => {
15 | enqueueSnackbar('Transaction Failed.', { variant: 'error' });
16 | },
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/src/hooks/tokens.ts:
--------------------------------------------------------------------------------
1 | import { Token } from '../models/token';
2 | import { useAccountBalance } from '../services/API/queries/useAccountBalance';
3 | import { tokens } from '../constants';
4 |
5 | export const useTokenBalance = (token?: Token): string | undefined => {
6 | const { data: tokenBalances } = useAccountBalance();
7 | if (!token) return;
8 | return tokenBalances?.get(token.id);
9 | };
10 |
11 | export const useFilteredTokens = (toFilter?: Token) => {
12 | if (!toFilter) return tokens;
13 | return tokens.filter(({ id }) => id !== toFilter.id);
14 | };
15 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { ThemeProvider } from '@material-ui/core';
4 | import { ReactQueryDevtools } from 'react-query/devtools';
5 | import CssBaseline from '@material-ui/core/CssBaseline';
6 |
7 | import App from 'App';
8 | import LocalizedFormat from 'dayjs/plugin/localizedFormat';
9 | import dayjs from 'dayjs';
10 | import { BrowserRouter as Router } from 'react-router-dom';
11 | import { theme } from 'theme/theme';
12 | import reportWebVitals from './reportWebVitals';
13 | import './assets/css/body.css';
14 | import { NotificationsProvider } from 'context/notifications';
15 | import { UserProvider } from 'context/user';
16 | import { QueryClient, QueryClientProvider } from 'react-query';
17 | import { SnackbarProvider } from './components/SnackbarProvider';
18 | const queryClient = new QueryClient();
19 | dayjs.extend(LocalizedFormat);
20 |
21 | ReactDOM.render(
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ,
40 | document.getElementById('root'),
41 | );
42 |
43 | // If you want to start measuring performance in your app, pass a function
44 | // to log results (for example: reportWebVitals(console.log))
45 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
46 | reportWebVitals();
47 |
--------------------------------------------------------------------------------
/src/models/mint.ts:
--------------------------------------------------------------------------------
1 | import { Token } from './token';
2 |
3 | export interface MintInformation {
4 | token: Token;
5 | amount: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/models/swap.ts:
--------------------------------------------------------------------------------
1 | import { Token } from './token';
2 |
3 | export interface SwapInformation {
4 | token: Token;
5 | amount: string;
6 | }
7 |
8 | export interface SwapReceipt {
9 | from: SwapInformation;
10 | to: SwapInformation;
11 | }
12 |
--------------------------------------------------------------------------------
/src/models/token.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from 'bignumber.js';
2 |
3 | export interface Token {
4 | id: string;
5 | name: string;
6 | symbol: string;
7 | icon: string;
8 | price: string;
9 | color: string;
10 | }
11 |
12 | export interface TokenBalance {
13 | token: Token;
14 | amount: string;
15 | }
16 |
17 | export interface ConversionRate {
18 | from: BigNumber;
19 | to: BigNumber;
20 | }
21 |
--------------------------------------------------------------------------------
/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useMemo } from 'react';
2 | import { styled, Container, Box, Tabs, createStyles, makeStyles, Tab } from '@material-ui/core';
3 | import { Redirect, Route, Switch, useHistory, useLocation } from 'react-router-dom';
4 | import { Mint } from 'components/Mint';
5 | import { Swap } from 'components/Swap';
6 | import { Activity } from 'components/Activity';
7 |
8 | const CardContainer = styled(Box)(({ theme }) => ({
9 | margin: 'auto',
10 | width: '100%',
11 | maxWidth: '440px',
12 | padding: '26px 33px 31px 33px',
13 | marginTop: 76,
14 | background: '#28286E',
15 | border: '1.5px solid rgba(83, 83, 135, 0.3)',
16 | boxSizing: 'border-box',
17 | boxShadow: '0px 8px 15px 3px rgba(7, 7, 7, 0.13)',
18 | borderRadius: '8px',
19 | [theme.breakpoints.only('xs')]: {
20 | marginBottom: theme.spacing(5),
21 | },
22 | }));
23 |
24 | const CardContent = styled(Box)({
25 | paddingTop: 22,
26 | boxSizing: 'border-box',
27 | });
28 |
29 | const useTabsStyles = makeStyles((theme) =>
30 | createStyles({
31 | root: {
32 | minHeight: 'unset',
33 | },
34 | indicator: {
35 | display: 'flex',
36 | backgroundColor: 'transparent',
37 | '& > span': {
38 | width: '75%',
39 | margin: '0 auto 0 0',
40 | height: 1.5,
41 | backgroundColor: theme.palette.secondary.main,
42 | },
43 | },
44 | }),
45 | );
46 |
47 | const useTabStyles = makeStyles(() =>
48 | createStyles({
49 | root: {
50 | minHeight: 'unset',
51 | fontSize: 20,
52 | textTransform: 'none',
53 | color: 'white',
54 | padding: '0 0 5px 0',
55 | '&:focus': {
56 | opacity: 1,
57 | },
58 | minWidth: 80,
59 | },
60 | wrapper: {
61 | flexDirection: 'row',
62 | justifyContent: 'start',
63 | },
64 | }),
65 | );
66 |
67 | const TABS = [
68 | {
69 | label: 'Mint',
70 | value: 'mint',
71 | },
72 | {
73 | label: 'Swap',
74 | value: 'swap',
75 | },
76 | {
77 | label: 'Activity',
78 | value: 'activity',
79 | },
80 | ];
81 |
82 | const tabsUnderlyingBorderWidths: Record = {
83 | mint: 43,
84 | swap: 52,
85 | activity: 70,
86 | };
87 |
88 | export const Home = (): JSX.Element => {
89 | const tabsStyles = useTabsStyles();
90 | const tabStyles = useTabStyles();
91 | const location = useLocation();
92 | const initialTab = useMemo(() => location.pathname.split('/').slice(-1)[0] || 'swap', [location]);
93 |
94 | const [selectedTab, setSelectedTab] = useState(initialTab);
95 | const history = useHistory();
96 |
97 | const handleTabSelected = useCallback(
98 | (_: unknown, tab: string) => {
99 | setSelectedTab(tab);
100 | history.push(tab);
101 | },
102 | [history],
103 | );
104 |
105 | return (
106 |
107 |
108 | ,
111 | }}
112 | classes={tabsStyles}
113 | value={selectedTab}
114 | onChange={handleTabSelected}
115 | >
116 | {TABS.map(({ label, value }, i) => (
117 |
118 | ))}
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | );
137 | };
138 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/services/API/mutations/useInitPool.ts:
--------------------------------------------------------------------------------
1 | import { INIT_POOL_ENTRYPOINT, CONTRACT_ADDRESS } from '../../../constants';
2 | import { useMutation } from 'react-query';
3 | import { callContract } from '../utils/callContract';
4 | interface InitPool {
5 | tokensId: string[];
6 | blockId?: string;
7 | }
8 |
9 | export const useInitPool = ({ tokensId, blockId }: InitPool) => {
10 | return useMutation(['invoke', tokensId, blockId], () =>
11 | callContract({
12 | blockId: blockId || null,
13 | contract_address: CONTRACT_ADDRESS,
14 | calldata: tokensId,
15 | entry_point_selector: INIT_POOL_ENTRYPOINT,
16 | }),
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/services/API/mutations/useMint.ts:
--------------------------------------------------------------------------------
1 | import { ADD_DEMO_TOKEN_ENTRYPOINT, CONTRACT_ADDRESS, tokens } from '../../../constants';
2 | import { useMutation } from 'react-query';
3 | import dayjs from 'dayjs';
4 | import { APITransactionType, TransactionResponse } from '../types';
5 | import { sendTransaction } from '../utils/sendTransaction';
6 | import { useContext } from 'react';
7 | import { ActionTypes, TransactionType, UserContext } from 'context/user';
8 | import { MintInformation } from '../../../models/mint';
9 | import { useTxNotifications } from '../../../hooks/notifications';
10 |
11 | export interface MintArgs {
12 | mint1: MintInformation;
13 | mint2?: MintInformation;
14 | }
15 |
16 | export const useMint = () => {
17 | const {
18 | dispatch,
19 | state: { userId },
20 | } = useContext(UserContext);
21 | const { showPendingTransaction } = useTxNotifications();
22 |
23 | return useMutation(async (args) => {
24 | let token1Amount: string;
25 | let token2Amount: string;
26 |
27 | if (args.mint1.token.id === tokens[0].id) {
28 | token1Amount = args.mint1.amount;
29 | token2Amount = args.mint2?.amount || '0';
30 | } else {
31 | token2Amount = args.mint1.amount;
32 | token1Amount = args.mint2?.amount || '0';
33 | }
34 |
35 | const result = await sendTransaction({
36 | contract_address: CONTRACT_ADDRESS,
37 | entry_point_selector: ADD_DEMO_TOKEN_ENTRYPOINT,
38 | type: APITransactionType.INVOKE_FUNCTION,
39 | calldata: [userId, token1Amount, token2Amount],
40 | });
41 |
42 | showPendingTransaction();
43 | dispatch({
44 | type: ActionTypes.SET_ACTIVE_TRANSACTION,
45 | payload: {
46 | id: result.transaction_hash.toString(),
47 | type: TransactionType.MINT,
48 | args,
49 | timestamp: dayjs().toString(),
50 | },
51 | });
52 |
53 | return result;
54 | });
55 | };
56 |
--------------------------------------------------------------------------------
/src/services/API/mutations/useSwap.ts:
--------------------------------------------------------------------------------
1 | import { ActionTypes, TransactionType, UserContext } from 'context/user';
2 | import { useContext } from 'react';
3 | import dayjs from 'dayjs';
4 | import { useMutation } from 'react-query';
5 | import { APITransactionType, TransactionResponse } from '../types';
6 | import { sendTransaction } from '../utils/sendTransaction';
7 | import { SwapInformation } from '../../../models/swap';
8 | import { CONTRACT_ADDRESS, SWAP_ENTRYPOINT } from '../../../constants';
9 | import { useTxNotifications } from '../../../hooks/notifications';
10 |
11 | export interface SwapArgs {
12 | from: SwapInformation;
13 | to: SwapInformation;
14 | }
15 |
16 | export const useSwap = () => {
17 | const {
18 | dispatch,
19 | state: { userId },
20 | } = useContext(UserContext);
21 | const { showPendingTransaction } = useTxNotifications();
22 |
23 | return useMutation(async ({ from, to }) => {
24 | const result = await sendTransaction({
25 | contract_address: CONTRACT_ADDRESS,
26 | entry_point_selector: SWAP_ENTRYPOINT,
27 | type: APITransactionType.INVOKE_FUNCTION,
28 | calldata: [userId, from.token.id, from.amount],
29 | });
30 |
31 | showPendingTransaction();
32 | dispatch({
33 | type: ActionTypes.SET_ACTIVE_TRANSACTION,
34 | payload: {
35 | id: result.transaction_hash.toString(),
36 | type: TransactionType.SWAP,
37 | args: {
38 | from,
39 | to,
40 | },
41 | timestamp: dayjs().toString(),
42 | },
43 | });
44 |
45 | return result;
46 | });
47 | };
48 |
--------------------------------------------------------------------------------
/src/services/API/queries/useAccountBalance.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useQuery } from 'react-query';
3 | import { callContract } from '../utils/callContract';
4 | import { UserContext } from 'context/user';
5 | import { CONTRACT_ADDRESS, GET_ACCOUNT_TOKEN_BALANCE_ENTRYPOINT, tokens } from '../../../constants';
6 | import { useSnackbar } from 'notistack';
7 | import BigNumber from 'bignumber.js';
8 |
9 | interface BalancesFetchResult {
10 | result: number[];
11 | }
12 |
13 | export const useAccountBalance = (blockId?: string) => {
14 | const {
15 | state: { userId },
16 | } = React.useContext(UserContext);
17 | const { enqueueSnackbar } = useSnackbar();
18 |
19 | return useQuery(
20 | ['accountBalance', userId, blockId],
21 | async () => {
22 | try {
23 | const balances = new Map();
24 |
25 | const balancesInformation = await Promise.all(
26 | tokens.map(({ id }) =>
27 | callContract({
28 | contract_address: CONTRACT_ADDRESS,
29 | blockId: blockId || null,
30 | calldata: [userId, id],
31 | entry_point_selector: GET_ACCOUNT_TOKEN_BALANCE_ENTRYPOINT,
32 | }),
33 | ),
34 | );
35 |
36 | for (let index = 0; index < balancesInformation.length; index++) {
37 | const token = tokens[index];
38 | const { data: balanceInformation } = balancesInformation[index];
39 | balances.set(token.id, new BigNumber(balanceInformation.result[0], 16).toString());
40 | }
41 |
42 | return balances;
43 | } catch (error) {
44 | enqueueSnackbar('There was a problem getting account information', { variant: 'error' });
45 | throw error;
46 | }
47 | },
48 | {
49 | retry: false,
50 | retryOnMount: false,
51 | refetchOnMount: false,
52 | refetchOnWindowFocus: false,
53 | staleTime: 10000,
54 | },
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/services/API/queries/useBlock.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from 'react-query';
2 | import { httpClient } from '../utils/http';
3 | import { TransactionStatus, APITransactionType } from '../types';
4 |
5 | interface BlockArgs {
6 | blockId: string;
7 | }
8 |
9 | interface DeployTransaction {
10 | contract_address: string;
11 | type: APITransactionType.DEPLOY;
12 | }
13 |
14 | interface InvokeTransaction {
15 | entry_point_selector: string;
16 | contract_address: string;
17 | calldata: string[];
18 | type: APITransactionType.INVOKE_FUNCTION;
19 | }
20 |
21 | interface BlockResponse {
22 | previous_block_id: number;
23 | txs: {
24 | [blockNumber: string]: DeployTransaction | InvokeTransaction;
25 | };
26 | status: TransactionStatus;
27 | state_root: string;
28 | sequence_number: number;
29 | block_id: number;
30 | timestamp: number;
31 | }
32 |
33 | export const useBlock = (args: BlockArgs) => {
34 | return useQuery(['block', args.blockId], async () => {
35 | const { data } = await httpClient.get(
36 | `feeder_gateway/get_block?blockId=${args.blockId}`,
37 | );
38 | return data;
39 | });
40 | };
41 |
--------------------------------------------------------------------------------
/src/services/API/queries/usePoolBalance.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from 'react-query';
2 | import { useSnackbar } from 'notistack';
3 |
4 | import { callContract } from '../utils/callContract';
5 | import { CONTRACT_ADDRESS, GET_POOL_TOKEN_BALANCE_ENTRYPOINT, tokens } from '../../../constants';
6 | import BigNumber from 'bignumber.js';
7 |
8 | interface PoolBalanceFetchResult {
9 | result: number[];
10 | }
11 |
12 | export const usePoolBalance = (blockId?: string) => {
13 | const { enqueueSnackbar } = useSnackbar();
14 |
15 | return useQuery(
16 | ['poolBalance', blockId],
17 | async () => {
18 | try {
19 | const balances = new Map();
20 |
21 | const balancesInformation = await Promise.all(
22 | tokens.map(({ id }) =>
23 | callContract({
24 | contract_address: CONTRACT_ADDRESS,
25 | blockId: blockId || null,
26 | calldata: [id],
27 | entry_point_selector: GET_POOL_TOKEN_BALANCE_ENTRYPOINT,
28 | }),
29 | ),
30 | );
31 |
32 | for (let index = 0; index < balancesInformation.length; index++) {
33 | const token = tokens[index];
34 | const { data: balanceInformation } = balancesInformation[index];
35 | balances.set(token.id, new BigNumber(balanceInformation.result[0], 16).toString());
36 | }
37 |
38 | return balances;
39 | } catch (error) {
40 | enqueueSnackbar('There was a problem getting pool information', { variant: 'error' });
41 | throw error;
42 | }
43 | },
44 | {
45 | retry: false,
46 | retryOnMount: false,
47 | refetchInterval: 10000,
48 | refetchOnMount: false,
49 | refetchOnWindowFocus: false,
50 | staleTime: 10000,
51 | },
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/services/API/queries/useStorageAt.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from 'react-query';
2 | import BigNumber from 'bignumber.js';
3 | import { httpClient } from '../utils/http';
4 |
5 | export interface StorageArgs {
6 | contractAddress: string;
7 | key: string;
8 | blockId: string | null;
9 | }
10 |
11 | export const useStorageAt = (args: StorageArgs) => {
12 | return useQuery(
13 | ['storageAt', args.contractAddress, args.key, args.blockId],
14 | async () => {
15 | const contractNumber = new BigNumber(args.contractAddress).toNumber();
16 | const { data } = await httpClient.get(
17 | `feeder_gateway/get_storage_at?contractAddress=${contractNumber}&key=${args.key}&blockId=${args.blockId}`,
18 | );
19 | return data;
20 | },
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/services/API/types.ts:
--------------------------------------------------------------------------------
1 | export enum TransactionResponseCode {
2 | TRANSACTION_RECEIVED = 'TRANSACTION_RECEIVED',
3 | }
4 |
5 | export enum APITransactionType {
6 | INVOKE_FUNCTION = 'INVOKE_FUNCTION',
7 | DEPLOY = 'DEPLOY',
8 | }
9 |
10 | export type TransactionStatus =
11 | | {
12 | tx_status: 'RECEIVED';
13 | }
14 | | {
15 | tx_status: 'PENDING';
16 | block_id: string;
17 | }
18 | | {
19 | tx_status: 'REJECTED';
20 | }
21 | | {
22 | tx_status: 'ACCEPTED_ONCHAIN';
23 | };
24 |
25 | export interface TransactionArgs {
26 | calldata: string[];
27 | contract_address: string;
28 | entry_point_selector: string;
29 | type: APITransactionType;
30 | }
31 |
32 | export interface CallArgs {
33 | calldata: string[];
34 | contract_address: string;
35 | entry_point_selector: string;
36 | blockId: string | null;
37 | }
38 |
39 | export interface TransactionResponse {
40 | code: TransactionResponseCode;
41 | tx_id: number;
42 | }
43 |
--------------------------------------------------------------------------------
/src/services/API/utils/callContract.ts:
--------------------------------------------------------------------------------
1 | import { httpClient } from './http';
2 | import { CallArgs } from '../types';
3 |
4 | export async function callContract>({
5 | contract_address,
6 | calldata,
7 | entry_point_selector,
8 | blockId,
9 | }: CallArgs) {
10 | return httpClient.post(`feeder_gateway/call_contract?blockId=${blockId}`, {
11 | calldata,
12 | contract_address,
13 | entry_point_selector,
14 | signature: [],
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/src/services/API/utils/http.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { API_BASE_URL } from '../../../constants';
3 |
4 | export const httpClient = axios.create({
5 | baseURL: API_BASE_URL,
6 | });
7 |
--------------------------------------------------------------------------------
/src/services/API/utils/sendTransaction.ts:
--------------------------------------------------------------------------------
1 | import { TransactionArgs } from '../types';
2 | import { httpClient } from './http';
3 |
4 | export const sendTransaction = async ({
5 | contract_address,
6 | calldata,
7 | entry_point_selector,
8 | type,
9 | }: TransactionArgs) => {
10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 | const { data } = await httpClient.post(`gateway/add_transaction`, {
12 | calldata,
13 | contract_address,
14 | entry_point_selector,
15 | type,
16 | signature: [],
17 | });
18 |
19 | return data;
20 | };
21 |
--------------------------------------------------------------------------------
/src/services/API/utils/toShortAddress.ts:
--------------------------------------------------------------------------------
1 | export const toShortAddress = (address: string, limit = 4): string => {
2 | return address
3 | .slice(0, limit)
4 | .concat('...')
5 | .concat(address.slice(address.length - limit, address.length));
6 | };
7 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | import { setLogger } from 'react-query';
2 |
3 | jest.mock('notistack', () => ({
4 | ...jest.requireActual('notistack'),
5 | useSnackbar: () => {
6 | return {
7 | enqueueSnackbar: jest.fn(),
8 | };
9 | },
10 | }));
11 |
12 | setLogger({
13 | log: console.log,
14 | warn: console.warn,
15 | // eslint-disable-next-line @typescript-eslint/no-empty-function
16 | error: () => {},
17 | });
18 |
--------------------------------------------------------------------------------
/src/tests/NumericInput.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { customRender, fireEvent, screen } from './utils';
4 | import { NumericInput } from '../components/NumericInput';
5 |
6 | describe('NumericInput', () => {
7 | it('displays placeholder', () => {
8 | const { container } = customRender(
9 | ,
10 | );
11 | expect(container).toMatchSnapshot();
12 | });
13 |
14 | it('handles valid change', () => {
15 | const handleChange = jest.fn();
16 | customRender();
17 | fireEvent.change(screen.getByRole('textbox'), { target: { value: '123' } });
18 | expect(handleChange).toHaveBeenNthCalledWith(1, '123');
19 | });
20 |
21 | it('skips invalid change', () => {
22 | const handleChange = jest.fn();
23 | customRender();
24 | fireEvent.change(screen.getByRole('textbox'), { target: { value: 'asd' } });
25 | expect(handleChange).not.toHaveBeenCalled();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/tests/TokenSelectorDialog.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 |
4 | import { customRender, screen, fireEvent } from './utils';
5 | import TokenSVG from '../assets/tokens/token.svg';
6 | import Token2SVG from '../assets/tokens/token2.svg';
7 | import { TokenSelectDialog } from '../components/TokenSelectDialog';
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('TokenSelectorDialog', () => {
29 | it('displays list of tokens', () => {
30 | const { baseElement } = customRender(
31 | ,
32 | );
33 | expect(baseElement).toMatchSnapshot();
34 | });
35 |
36 | it('triggers close handler', () => {
37 | const closeHandlerMock = jest.fn();
38 | customRender(
39 | ,
45 | );
46 | fireEvent.click(screen.getByLabelText('close'));
47 | expect(closeHandlerMock).toHaveBeenCalledTimes(1);
48 | });
49 |
50 | it('triggers select handler', () => {
51 | const selectHandlerMock = jest.fn();
52 | customRender(
53 | ,
59 | );
60 | fireEvent.click(screen.getByText(options[0].symbol));
61 | expect(selectHandlerMock).toHaveBeenNthCalledWith(1, options[0]);
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/src/tests/__snapshots__/NumericInput.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`NumericInput displays placeholder 1`] = `
4 |
21 | `;
22 |
--------------------------------------------------------------------------------
/src/tests/__snapshots__/TokenSelectorDialog.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`TokenSelectorDialog displays list of tokens 1`] = `
4 |
7 |
10 |
15 |
20 |
24 |
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 |

44 |
45 |
46 |
59 |
62 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
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 |

193 |
194 |
195 |
198 |
201 |
204 | TK1
205 |
206 |
207 |
208 |
211 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
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 |
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 |

61 |
62 |
63 |
76 |
79 |
80 |
81 |
82 |
83 |
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 |

192 |
193 |
194 |
197 |
200 |
203 | TK2
204 |
205 |
206 |
207 |
210 |
211 |
212 |
213 |
214 |
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 |

335 |
336 |
337 |
340 |
343 |
346 | TK2
347 |
348 |
349 |
350 |
353 |
354 |
355 |
356 |
357 |
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 |

466 |
467 |
468 |
471 |
474 |
477 | TK1
478 |
479 |
480 |
481 |
484 |
485 |
486 |
487 |
488 |
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 |
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 |

66 |
67 |
68 |
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 |
--------------------------------------------------------------------------------