├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── empty-token.png
├── UpCarret.svg
├── DownCarret.svg
├── circle.svg
├── swap.svg
├── dropdown.svg
├── info.svg
├── info-general-notification.svg
├── WarningSign.svg
├── manifest.json
├── pebbles-pad.svg
├── index.html
└── arrow-bottom.svg
├── src
├── assets
│ ├── fonts
│ │ ├── Asap-Bold.ttf
│ │ ├── Asap-Italic.ttf
│ │ ├── Asap-Medium.ttf
│ │ ├── Asap-Regular.ttf
│ │ ├── DJB-Digital.ttf
│ │ ├── Asap-SemiBold.ttf
│ │ ├── Asap-BoldItalic.ttf
│ │ ├── Asap-MediumItalic.ttf
│ │ └── Asap-SemiBoldItalic.ttf
│ ├── images
│ │ ├── metamask.png
│ │ ├── dropdown.svg
│ │ ├── dropup.svg
│ │ ├── arrow-right.svg
│ │ ├── x.svg
│ │ └── circle.svg
│ └── pngs
│ │ └── balancer_logo.png
├── configs
│ ├── index.ts
│ ├── config-main.ts
│ └── addresses.ts
├── provider
│ ├── Web3Window.ts
│ ├── NetworkConnector.js
│ ├── UncheckedJsonRpcSigner.ts
│ └── connectors.ts
├── utils
│ ├── bignumber.ts
│ ├── fathom.ts
│ ├── CostCalculator.ts
│ ├── helperHooks.jsx
│ ├── web3.js
│ ├── subGraph.ts
│ └── balancerCalcs.ts
├── stores
│ ├── errors
│ │ └── errors.ts
│ ├── TokenPanel.ts
│ ├── AppSettings.ts
│ ├── Dropdown.ts
│ ├── actions
│ │ ├── fetch.ts
│ │ ├── validators.ts
│ │ └── actions.ts
│ ├── Error.ts
│ ├── AssetOptions.ts
│ ├── Root.ts
│ ├── Transaction.ts
│ ├── BlockchainFetch.ts
│ ├── Pool.ts
│ └── ContractMetadata.ts
├── contexts
│ └── storesContext.tsx
├── index.css
├── index.tsx
├── components
│ ├── Web3ReactManager
│ │ └── index.jsx
│ ├── Web3PillBox.tsx
│ ├── ErrorDisplay.tsx
│ ├── Identicon
│ │ └── index.js
│ ├── Switch.tsx
│ ├── Button.tsx
│ ├── BuyToken.tsx
│ ├── SellToken.tsx
│ ├── Header
│ │ └── index.tsx
│ ├── GeneralNotification.tsx
│ ├── WalletDropdown
│ │ ├── TransactionPanel.tsx
│ │ ├── Transaction.js
│ │ └── index.js
│ ├── SlippageInfo.tsx
│ ├── Wallet
│ │ └── index.jsx
│ ├── AssetSelector.tsx
│ ├── SlippageSelector.tsx
│ └── AssetOptions.tsx
├── types.ts
├── styles.scss
├── App.css
├── theme
│ ├── components.js
│ └── index.js
├── App.tsx
├── test
│ └── onchain.test.js
├── abi
│ ├── Multicall.json
│ └── BFactory.json
├── serviceWorker.js
└── logo.svg
├── tslint.json
├── .prettierrc
├── config
├── jest
│ ├── cssTransform.js
│ └── fileTransform.js
├── pnpTs.js
├── paths.js
├── env.js
├── modules.js
└── webpackDevServer.config.js
├── tsconfig.json
├── .gitignore
├── README.md
├── .env.example
├── scripts
├── test.js
├── start.js
└── build.js
└── package.json
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/empty-token.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/public/empty-token.png
--------------------------------------------------------------------------------
/src/assets/fonts/Asap-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/src/assets/fonts/Asap-Bold.ttf
--------------------------------------------------------------------------------
/src/assets/images/metamask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/src/assets/images/metamask.png
--------------------------------------------------------------------------------
/src/assets/fonts/Asap-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/src/assets/fonts/Asap-Italic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Asap-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/src/assets/fonts/Asap-Medium.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Asap-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/src/assets/fonts/Asap-Regular.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/DJB-Digital.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/src/assets/fonts/DJB-Digital.ttf
--------------------------------------------------------------------------------
/src/assets/pngs/balancer_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/src/assets/pngs/balancer_logo.png
--------------------------------------------------------------------------------
/src/assets/fonts/Asap-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/src/assets/fonts/Asap-SemiBold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Asap-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/src/assets/fonts/Asap-BoldItalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Asap-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/src/assets/fonts/Asap-MediumItalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Asap-SemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/balancer/balancer-exchange/HEAD/src/assets/fonts/Asap-SemiBoldItalic.ttf
--------------------------------------------------------------------------------
/src/configs/index.ts:
--------------------------------------------------------------------------------
1 | import { appConfig } from './config-main';
2 | import { contracts, assets } from './addresses';
3 |
4 | export { appConfig, contracts, assets };
5 |
--------------------------------------------------------------------------------
/src/assets/images/dropdown.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/dropup.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/provider/Web3Window.ts:
--------------------------------------------------------------------------------
1 | interface Web3Window extends Window {
2 | readonly web3: any;
3 | readonly ethereum: any;
4 | }
5 |
6 | const web3Window = window as Web3Window;
7 | export { web3Window };
8 |
--------------------------------------------------------------------------------
/src/configs/config-main.ts:
--------------------------------------------------------------------------------
1 | /* Main app configs go here */
2 | export const appConfig = {
3 | name: 'Balancer',
4 | shortName: 'Balancer',
5 | description: '',
6 | splashScreenBackground: '#ffffff',
7 | };
8 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
3 | "rulesDirectory": ["tslint-plugin-prettier"],
4 | "rules": {
5 | "prettier": true,
6 | "interface-name": false
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/bignumber.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'bignumber.js';
2 |
3 | BigNumber.config({
4 | EXPONENTIAL_AT: [-100, 100],
5 | ROUNDING_MODE: BigNumber.ROUND_HALF_EVEN,
6 | DECIMAL_PLACES: 18,
7 | });
8 |
9 | export { BigNumber };
10 |
--------------------------------------------------------------------------------
/src/assets/images/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/stores/errors/errors.ts:
--------------------------------------------------------------------------------
1 | export enum ERROR_CODES {
2 | GENERIC_TX_FAILURE,
3 | BALANCER_MAX_RATIO_IN,
4 | }
5 |
6 | export const ERRORS = {
7 | GENERIC_TX_FAILURE: {
8 | code: ERROR_CODES.GENERIC_TX_FAILURE,
9 | message: 'Transaction Failed',
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/public/UpCarret.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/DownCarret.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/contexts/storesContext.tsx:
--------------------------------------------------------------------------------
1 | // src/contexts/index.tsx
2 | import React from 'react';
3 | import RootStore from 'stores/Root';
4 |
5 | export const storesContext = React.createContext({
6 | root: new RootStore(),
7 | });
8 |
9 | export const useStores = () => React.useContext(storesContext);
10 |
--------------------------------------------------------------------------------
/src/assets/images/x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 4,
4 | "singleQuote": true,
5 | "bracketSpacing": true,
6 | "trailingComma": "es5",
7 | "semi": true,
8 | "newline-before-return": true,
9 | "no-duplicate-variable": [true, "check-parameters"],
10 | "no-var-keyword": true
11 | }
12 |
--------------------------------------------------------------------------------
/public/circle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/circle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/swap.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | // This is a custom Jest transformer turning style imports into empty objects.
2 | // http://facebook.github.io/jest/docs/en/webpack.html
3 |
4 | module.exports = {
5 | process() {
6 | return 'module.exports = {};';
7 | },
8 | getCacheKey() {
9 | // The output is always the same.
10 | return 'cssTransform';
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
4 | 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
5 | 'Helvetica Neue', sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/provider/NetworkConnector.js:
--------------------------------------------------------------------------------
1 | import { NetworkConnector as NetworkConnectorCore } from '@web3-react/network-connector';
2 |
3 | export class NetworkConnector extends NetworkConnectorCore {
4 | pause() {
5 | if (this.active) {
6 | this.providers[this.currentChainId].stop();
7 | }
8 | }
9 |
10 | resume() {
11 | if (this.active) {
12 | this.providers[this.currentChainId].start();
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "noImplicitAny": false,
5 | "module": "es6",
6 | "moduleResolution": "node",
7 | "target": "es5",
8 | "jsx": "react",
9 | "baseUrl": "src",
10 | "esModuleInterop": true,
11 | "resolveJsonModule": true,
12 | "allowJs": true,
13 | "experimentalDecorators": true,
14 | "strictNullChecks": false
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/stores/TokenPanel.ts:
--------------------------------------------------------------------------------
1 | import { action, observable } from 'mobx';
2 | import RootStore from 'stores/Root';
3 |
4 | export default class TokenPanelStore {
5 | @observable isFocus: boolean;
6 | rootStore: RootStore;
7 |
8 | constructor(rootStore) {
9 | this.rootStore = rootStore;
10 | this.isFocus = false;
11 | }
12 |
13 | @action setFocus(visible: boolean) {
14 | this.isFocus = visible;
15 | }
16 |
17 | isFocused(): boolean {
18 | return this.isFocus;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/public/dropdown.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/.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 |
26 | # Editor directories and files
27 | .idea
28 | .vscode
29 | *.suo
30 | *.ntvs*
31 | *.njsproj
32 | *.sln
33 | *.sw?
34 |
--------------------------------------------------------------------------------
/src/stores/AppSettings.ts:
--------------------------------------------------------------------------------
1 | import { action, observable } from 'mobx';
2 | import RootStore from 'stores/Root';
3 |
4 | export default class AppSettingsStore {
5 | @observable darkMode: boolean;
6 | rootStore: RootStore;
7 |
8 | constructor(rootStore) {
9 | this.rootStore = rootStore;
10 | this.darkMode = false;
11 | }
12 |
13 | @action toggleDarkMode() {
14 | this.darkMode = !this.darkMode;
15 | }
16 |
17 | @action setDarkMode(visible: boolean) {
18 | this.darkMode = visible;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/public/info.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import 'index.css';
4 | import App from 'App';
5 | import * as serviceWorker from './serviceWorker';
6 | import './utils/fathom.ts';
7 |
8 | const Root = (
9 | <>
10 |
11 | >
12 | );
13 | ReactDOM.render(Root, document.getElementById('root'));
14 |
15 | // If you want your app to work offline and load faster, you can change
16 | // unregister() to register() below. Note this comes with some pitfalls.
17 | // Learn more about service workers: https://bit.ly/CRA-PWA
18 | serviceWorker.unregister();
19 |
--------------------------------------------------------------------------------
/public/info-general-notification.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/stores/Dropdown.ts:
--------------------------------------------------------------------------------
1 | import { action, observable } from 'mobx';
2 | import RootStore from 'stores/Root';
3 |
4 | export default class DropdownStore {
5 | @observable walletDropdownVisible: boolean;
6 | rootStore: RootStore;
7 |
8 | constructor(rootStore) {
9 | this.rootStore = rootStore;
10 | this.walletDropdownVisible = false;
11 | }
12 |
13 | @action toggleWalletDropdown() {
14 | this.walletDropdownVisible = !this.walletDropdownVisible;
15 | }
16 |
17 | @action setWalletDropdownVisible(visible: boolean) {
18 | this.walletDropdownVisible = visible;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/public/WarningSign.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Balancer Exchange",
3 | "name": "Balancer Exchange Dapp",
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 |
--------------------------------------------------------------------------------
/src/components/Web3ReactManager/index.jsx:
--------------------------------------------------------------------------------
1 | import { useStores } from 'contexts/storesContext';
2 | import { useInterval } from 'utils/helperHooks';
3 | import { observer } from 'mobx-react';
4 |
5 | const Web3Manager = observer(({ children }) => {
6 | const {
7 | root: { blockchainFetchStore, poolStore },
8 | } = useStores();
9 |
10 | //Fetch user blockchain data on an interval using current params
11 | blockchainFetchStore.blockchainFetch(false);
12 | useInterval(() => blockchainFetchStore.blockchainFetch(false), 2000);
13 | useInterval(() => poolStore.fetchPools(), 300000);
14 | useInterval(() => poolStore.fetchOnChainBalances(), 70000);
15 |
16 | return children;
17 | });
18 |
19 | export default Web3Manager;
20 |
--------------------------------------------------------------------------------
/src/components/Web3PillBox.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const Pill = styled.div`
5 | background: var(--panel-background);
6 | border: 1px solid var(--panel-border);
7 | border-radius: 4px;
8 | color: var(--button-text);
9 |
10 | display: flex;
11 | justify-content: space-evenly;
12 | align-items: center;
13 | text-align: center;
14 |
15 | font-family: var(--roboto);
16 | font-style: normal;
17 | font-weight: 500;
18 | font-size: 14px;
19 | line-height: 16px;
20 | cursor: pointer;
21 |
22 | width: 158px;
23 | height: 40px;
24 | `;
25 |
26 | const Web3PillBox = ({ children, onClick }) => {
27 | return {children};
28 | };
29 |
30 | export default Web3PillBox;
31 |
--------------------------------------------------------------------------------
/config/pnpTs.js:
--------------------------------------------------------------------------------
1 | const { resolveModuleName } = require('ts-pnp');
2 |
3 | exports.resolveModuleName = (
4 | typescript,
5 | moduleName,
6 | containingFile,
7 | compilerOptions,
8 | resolutionHost
9 | ) => {
10 | return resolveModuleName(
11 | moduleName,
12 | containingFile,
13 | compilerOptions,
14 | resolutionHost,
15 | typescript.resolveModuleName
16 | );
17 | };
18 |
19 | exports.resolveTypeReferenceDirective = (
20 | typescript,
21 | moduleName,
22 | containingFile,
23 | compilerOptions,
24 | resolutionHost
25 | ) => {
26 | return resolveModuleName(
27 | moduleName,
28 | containingFile,
29 | compilerOptions,
30 | resolutionHost,
31 | typescript.resolveTypeReferenceDirective
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/utils/fathom.ts:
--------------------------------------------------------------------------------
1 | import { supportedChainId } from '../provider/connectors';
2 |
3 | const actionToGoalId = {
4 | multihopBatchSwapExactIn: 'OAXZQIBH',
5 | multihopBatchSwapExactOut: 'ZT892CNN',
6 | approve: 'ST3CSJFO',
7 | };
8 |
9 | export function setGoal(action, value = 0) {
10 | const id = actionToGoalId[action];
11 | if (window['fathom'] && supportedChainId === 1 && id)
12 | window['fathom'].trackGoal(id, value);
13 | }
14 |
15 | if (supportedChainId === 1) {
16 | const script = document.createElement('script');
17 | script.setAttribute('src', 'https://cdn.usefathom.com/script.js');
18 | script.setAttribute('data-spa', 'auto');
19 | script.setAttribute('data-site', 'YRAWPOKJ');
20 | script.setAttribute('honor-dnt', 'true');
21 | script.setAttribute('defer', '');
22 | document.head.appendChild(script);
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/CostCalculator.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'utils/bignumber';
2 |
3 | export default class CostCalculator {
4 | gasPrice: BigNumber;
5 | gasPerTrade: BigNumber;
6 | outTokenEthPrice: BigNumber;
7 | costPerTrade: BigNumber;
8 | costOutputToken: BigNumber;
9 |
10 | constructor(params: {
11 | gasPrice: BigNumber;
12 | gasPerTrade: BigNumber;
13 | outTokenEthPrice: BigNumber;
14 | }) {
15 | const { gasPrice, gasPerTrade, outTokenEthPrice } = params;
16 | this.gasPrice = gasPrice;
17 | this.gasPerTrade = gasPerTrade;
18 | this.outTokenEthPrice = outTokenEthPrice;
19 | this.costPerTrade = gasPrice.times(gasPerTrade);
20 | this.costOutputToken = this.costPerTrade.times(outTokenEthPrice);
21 | }
22 |
23 | getCostOutputToken(): BigNumber {
24 | return this.costOutputToken;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/ErrorDisplay.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const ErrorTextContainer = styled.div`
5 | font-family: var(--roboto);
6 | font-size: 14px;
7 | line-height: 16px;
8 | display: flex;
9 | align-items: center;
10 | text-align: center;
11 | color: var(--error-color);
12 | margin-top: 6px;
13 | margin-bottom: 36px;
14 | `;
15 |
16 | const ErrorTextContainerPlaceholder = styled.div`
17 | height: 58px;
18 | `;
19 |
20 | const ErrorDisplay = ({ errorText }) => {
21 | const ErrorTextElement = ({ errorText }) => {
22 | if (errorText) {
23 | return {errorText};
24 | } else {
25 | return ;
26 | }
27 | };
28 |
29 | return ;
30 | };
31 |
32 | export default ErrorDisplay;
33 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'utils/bignumber';
2 | import { ValidationStatus } from './stores/actions/validators';
3 |
4 | export interface BigNumberMap {
5 | [index: string]: BigNumber;
6 | }
7 |
8 | export interface StringMap {
9 | [index: string]: string;
10 | }
11 |
12 | export interface NumberMap {
13 | [index: string]: number;
14 | }
15 |
16 | // Token Address -> checked
17 | export interface CheckboxMap {
18 | [index: string]: Checkbox;
19 | }
20 |
21 | // Token -> amount
22 | export interface InputMap {
23 | [index: string]: Input;
24 | }
25 |
26 | export interface Input {
27 | value: string;
28 | touched: boolean;
29 | validation: ValidationStatus;
30 | }
31 |
32 | export interface Checkbox {
33 | checked: boolean;
34 | touched: boolean;
35 | }
36 |
37 | export interface Swap {
38 | tokenIn;
39 | tokenInSym;
40 | tokenAmountIn;
41 | tokenOut;
42 | tokenOutSym;
43 | tokenAmountOut;
44 | }
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Balancer Exchange
2 |
3 | ## Development
4 |
5 | - Note
6 |
7 | This is Exchange Proxy for first generation SOR/Front end. An updated version can be found at [balancer-registry](https://github.com/balancer-labs/balancer-registry).
8 |
9 | - Environment Config
10 |
11 | - Copy .env.example -> .env
12 | - Configure backup node urls
13 |
14 | ```
15 | # Backup node url
16 | REACT_APP_RPC_URL_1="https://mainnet.infura.io/v3/{apiKey}"
17 | REACT_APP_RPC_URL_3="https://ropsten.infura.io/v3/{apiKey}"
18 | REACT_APP_RPC_URL_42="https://kovan.infura.io/v3/{apiKey}"
19 | REACT_APP_RPC_URL_LOCAL="http://localhost:8545"
20 | ```
21 |
22 | - Configure supported network
23 |
24 | ```
25 | # Supported Network ID (e.g. mainnet = 1, rinkeby = 4, kovan = 42)
26 | REACT_APP_SUPPORTED_NETWORK_ID="42"
27 | ```
28 |
29 | - Build & run locally
30 |
31 | ```
32 | yarn build
33 | yarn start
34 | ```
35 |
--------------------------------------------------------------------------------
/src/components/Identicon/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 |
3 | import styled from 'styled-components';
4 |
5 | import Jazzicon from 'jazzicon';
6 | import { useStores } from '../../contexts/storesContext';
7 |
8 | const StyledIdenticon = styled.div`
9 | height: 1rem;
10 | width: 1rem;
11 | border-radius: 1.125rem;
12 | background-color: ${({ theme }) => theme.silverGray};
13 | `;
14 |
15 | export default function Identicon() {
16 | const ref = useRef();
17 |
18 | const {
19 | root: { providerStore },
20 | } = useStores();
21 | const account = providerStore.providerStatus.account;
22 |
23 | useEffect(() => {
24 | if (account && ref.current) {
25 | ref.current.innerHTML = '';
26 | ref.current.appendChild(
27 | Jazzicon(16, parseInt(account.slice(2, 10), 16))
28 | );
29 | }
30 | });
31 |
32 | return ;
33 | }
34 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @import './styles/third-party/normalize';
2 | @import '../../configs/theme/config-styles';
3 | @import './styles/typography';
4 | @import './styles/links';
5 | @import './styles/animations';
6 |
7 | /* global styles go here */
8 | :global(body) {
9 | background: $white;
10 | color: $darkGrey35;
11 | font-family: 'Asap-Regular', sans-serif;
12 |
13 | .container {
14 | max-width: 1000px;
15 | padding: 20px;
16 | margin: 0 auto;
17 | }
18 |
19 | ul {
20 | margin: 0;
21 | padding: 0;
22 | }
23 |
24 | li {
25 | list-style-type: none;
26 | list-style-position: outside;
27 | }
28 |
29 | .app-shell {
30 | margin-top: 140px;
31 | margin-bottom: 100px;
32 | }
33 |
34 | @media (min-width: 1100px) {
35 | .app-shell {
36 | width: 1000px;
37 | padding: 20px;
38 | margin: 0 auto;
39 | margin-top: 70px;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/stores/actions/fetch.ts:
--------------------------------------------------------------------------------
1 | import { TokenBalance, UserAllowance } from '../Token';
2 |
3 | export enum AsyncStatus {
4 | SUCCESS,
5 | STALE,
6 | TIMEOUT,
7 | FAILURE,
8 | }
9 |
10 | export interface TokenBalanceFetchRequest {
11 | chainId: number;
12 | tokenAddress: string;
13 | account: string;
14 | fetchBlock: number;
15 | }
16 |
17 | export class TokenBalanceFetch {
18 | status: AsyncStatus;
19 | request: TokenBalanceFetchRequest;
20 | payload: TokenBalance | undefined;
21 |
22 | constructor({ status, request, payload }) {
23 | this.status = status;
24 | this.request = request;
25 | this.payload = payload;
26 | }
27 | }
28 |
29 | export interface UserAllowanceFetchRequest {
30 | chainId: number;
31 | tokenAddress: string;
32 | owner: string;
33 | spender: string;
34 | fetchBlock: number;
35 | }
36 |
37 | export class UserAllowanceFetch {
38 | status: AsyncStatus;
39 | request: UserAllowanceFetchRequest;
40 | payload: UserAllowance | undefined;
41 |
42 | constructor({ status, request, payload }) {
43 | this.status = status;
44 | this.request = request;
45 | this.payload = payload;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 |
2 | # Subgraph URL
3 | REACT_APP_SUBGRAPH_URL_1=https://api.thegraph.com/subgraphs/name/balancer-labs/balancer
4 | REACT_APP_SUBGRAPH_URL_3=https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-ropsten
5 | REACT_APP_SUBGRAPH_URL_4=https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-rinkeby
6 | REACT_APP_SUBGRAPH_URL_42=https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-kovan
7 |
8 | # Backup node url
9 | REACT_APP_RPC_URL_1="https://mainnet.infura.io/v3/{apiKey}"
10 | REACT_APP_RPC_URL_3="https://ropsten.infura.io/v3/{apiKey}"
11 | REACT_APP_RPC_URL_42="https://kovan.infura.io/v3/{apiKey}"
12 | REACT_APP_RPC_URL_LOCAL="http://localhost:8545"
13 |
14 | # Backup websocket node url
15 | REACT_APP_WS_URL_1="wss://eth-mainnet.ws.alchemyapi.io/v2/{apiKey}"
16 | REACT_APP_WS_URL_3="wss://eth-reopsten.ws.alchemyapi.io/v2/{apiKey}"
17 | REACT_APP_WS_URL_42="wss://eth-kovan.ws.alchemyapi.io/v2/{apiKey}"
18 | REACT_APP_WS_URL_LOCAL="wss://eth-mainnet.ws.alchemyapi.io/v2/{apiKey}"
19 |
20 | # Supported Network ID (e.g. mainnet = 1, ropsten = 3, rinkeby = 4, kovan = 42)
21 | REACT_APP_SUPPORTED_NETWORK_ID="42"
22 |
23 | # Infura ID, used for WalletConnect
24 | REACT_APP_INFURA_ID=your_key
25 |
26 | REACT_APP_GAS_PRICE={current-gas-price}
27 | # Cost to swap with a pool, recommend 100000
28 | REACT_APP_SWAP_COST=100000
29 | # Max pools to swap against
30 | REACT_APP_MAX_POOLS=4
31 |
--------------------------------------------------------------------------------
/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const camelcase = require('camelcase');
3 |
4 | // This is a custom Jest transformer turning file imports into filenames.
5 | // http://facebook.github.io/jest/docs/en/webpack.html
6 |
7 | module.exports = {
8 | process(src, filename) {
9 | const assetFilename = JSON.stringify(path.basename(filename));
10 |
11 | if (filename.match(/\.svg$/)) {
12 | // Based on how SVGR generates a component name:
13 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
14 | const pascalCaseFilename = camelcase(path.parse(filename).name, {
15 | pascalCase: true,
16 | });
17 | const componentName = `Svg${pascalCaseFilename}`;
18 | return `const React = require('react');
19 | module.exports = {
20 | __esModule: true,
21 | default: ${assetFilename},
22 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
23 | return {
24 | $$typeof: Symbol.for('react.element'),
25 | type: 'svg',
26 | ref: ref,
27 | key: null,
28 | props: Object.assign({}, props, {
29 | children: ${assetFilename}
30 | })
31 | };
32 | }),
33 | };`;
34 | }
35 |
36 | return `module.exports = ${assetFilename};`;
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/src/stores/Error.ts:
--------------------------------------------------------------------------------
1 | import { action, observable } from 'mobx';
2 | import RootStore from 'stores/Root';
3 |
4 | export enum ErrorIds {
5 | SWAP_FORM_STORE,
6 | }
7 |
8 | export enum ErrorCodes {
9 | NO_ERROR,
10 | GENERIC_TX_FAILURE,
11 | BALANCER_MAX_RATIO_IN,
12 | NO_WALLET_FOUND,
13 | INSUFFICIENT_BALANCE_FOR_SWAP,
14 | INSUFFICIENT_APPROVAL_FOR_SWAP,
15 | }
16 |
17 | export const ERROR_MESSAGES = [
18 | 'No Error',
19 | 'Transaction Failed',
20 | 'Balancer Max in Ratio',
21 | 'No Ethereum wallet found',
22 | 'Insufficient Balance',
23 | 'Enable Input Token',
24 | ];
25 |
26 | export interface BalancerError {
27 | code: ErrorCodes;
28 | message: string;
29 | }
30 |
31 | interface BalancerErrorMap {
32 | [index: number]: BalancerError | undefined;
33 | }
34 |
35 | export default class ErrorStore {
36 | rootStore: RootStore;
37 | @observable activeErrors: BalancerErrorMap;
38 |
39 | constructor(rootStore) {
40 | this.activeErrors = {} as BalancerErrorMap;
41 | this.rootStore = rootStore;
42 | }
43 |
44 | getActiveError(id: ErrorIds): BalancerError | undefined {
45 | return this.activeErrors[id];
46 | }
47 |
48 | @action setActiveError(id: ErrorIds, code: ErrorCodes) {
49 | if (code === ErrorCodes.NO_ERROR) {
50 | this.activeErrors[id] = undefined;
51 | } else {
52 | this.activeErrors[id] = {
53 | code,
54 | message: ERROR_MESSAGES[code],
55 | };
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/scripts/test.js:
--------------------------------------------------------------------------------
1 | // Do this as the first thing so that any code reading it knows the right env.
2 | process.env.BABEL_ENV = 'test';
3 | process.env.NODE_ENV = 'test';
4 | process.env.PUBLIC_URL = '';
5 |
6 | // Makes the script crash on unhandled rejections instead of silently
7 | // ignoring them. In the future, promise rejections that are not handled will
8 | // terminate the Node.js process with a non-zero exit code.
9 | process.on('unhandledRejection', err => {
10 | throw err;
11 | });
12 |
13 | // Ensure environment variables are read.
14 | require('../config/env');
15 |
16 | const jest = require('jest');
17 | const execSync = require('child_process').execSync;
18 | let argv = process.argv.slice(2);
19 |
20 | function isInGitRepository() {
21 | try {
22 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
23 | return true;
24 | } catch (e) {
25 | return false;
26 | }
27 | }
28 |
29 | function isInMercurialRepository() {
30 | try {
31 | execSync('hg --cwd . root', { stdio: 'ignore' });
32 | return true;
33 | } catch (e) {
34 | return false;
35 | }
36 | }
37 |
38 | // Watch unless on CI or explicitly running all tests
39 | if (
40 | !process.env.CI &&
41 | argv.indexOf('--watchAll') === -1 &&
42 | argv.indexOf('--watchAll=false') === -1
43 | ) {
44 | // https://github.com/facebook/create-react-app/issues/5210
45 | const hasSourceControl = isInGitRepository() || isInMercurialRepository();
46 | argv.push(hasSourceControl ? '--watch' : '--watchAll');
47 | }
48 |
49 | jest.run(argv);
50 |
--------------------------------------------------------------------------------
/src/components/Switch.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer } from 'mobx-react';
3 | import styled, { keyframes } from 'styled-components';
4 | import { useStores } from '../contexts/storesContext';
5 |
6 | const Container = styled.div`
7 | display: flex;
8 | justify-content: center;
9 | align-items: center
10 | width: 148px;
11 | `;
12 |
13 | const SwapIcon = styled.img`
14 | width: 24px;
15 | height: 24px;
16 | cursor: pointer;
17 | `;
18 |
19 | const rotate = keyframes`
20 | from {
21 | transform: rotate(0deg);
22 | }
23 | to {
24 | transform: rotate(360deg);
25 | }
26 | `;
27 |
28 | const Spinner = styled.img`
29 | animation: 2s ${rotate} linear infinite;
30 | width: 80px;
31 | height: 80px;
32 | `;
33 |
34 | const Switch = observer(() => {
35 | const {
36 | root: { swapFormStore, sorStore, tokenPanelStore },
37 | } = useStores();
38 |
39 | const switchAssets = () => {
40 | swapFormStore.switchInputOutputValues();
41 | };
42 |
43 | const showLoader =
44 | (sorStore.isPathsLoading() && tokenPanelStore.isFocused()) ||
45 | swapFormStore.showLoader;
46 |
47 | return (
48 |
49 |
53 | switchAssets()}
56 | style={{ display: showLoader ? 'none' : 'block' }}
57 | />
58 |
59 | );
60 | });
61 |
62 | export default Switch;
63 |
--------------------------------------------------------------------------------
/src/stores/actions/validators.ts:
--------------------------------------------------------------------------------
1 | import { ValidationRules } from 'react-form-validator-core';
2 |
3 | export enum ValidationStatus {
4 | VALID = 'Valid',
5 | EMPTY = 'Empty',
6 | ZERO = 'Zero',
7 | NOT_FLOAT = 'Not Float',
8 | NEGATIVE = 'Negative',
9 | INSUFFICIENT_BALANCE = 'Insufficient Balance',
10 | NO_POOLS = 'There are no Pools with selected tokens',
11 | MAX_DIGITS_EXCEEDED = 'Maximum Digits Exceeded',
12 | MAX_VALUE_EXCEEDED = 'Maximum Value Exceeded',
13 | }
14 |
15 | export const validateTokenValue = (
16 | value: string,
17 | options?: {
18 | limitDigits?: boolean;
19 | }
20 | ): ValidationStatus => {
21 | if (value.substr(0, 1) === '.') {
22 | value = '0' + value;
23 | }
24 |
25 | if (ValidationRules.isEmpty(value)) {
26 | return ValidationStatus.EMPTY;
27 | }
28 |
29 | if (!ValidationRules.isFloat(value)) {
30 | return ValidationStatus.NOT_FLOAT;
31 | }
32 |
33 | if (parseFloat(value).toString() === '0') {
34 | return ValidationStatus.ZERO;
35 | }
36 |
37 | if (!ValidationRules.isPositive(value)) {
38 | return ValidationStatus.NEGATIVE;
39 | }
40 |
41 | if (options && options.limitDigits) {
42 | // restrict to 2 decimal places
43 | const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/];
44 | // if its within accepted decimal limit, update the input state
45 | if (!acceptableValues.some(a => a.test(value))) {
46 | return ValidationStatus.MAX_DIGITS_EXCEEDED;
47 | }
48 | }
49 |
50 | return ValidationStatus.VALID;
51 | };
52 |
--------------------------------------------------------------------------------
/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const Container = styled.div`
5 | font-family: var(--roboto);
6 | display: flex;
7 | flex-direction: row;
8 | align-items: center;
9 | justify-content: center;
10 | `;
11 |
12 | const ButtonBase = styled.div`
13 | border-radius: 4px;
14 | width: 163px;
15 | height: 40px;
16 |
17 | font-family: var(--roboto);
18 | font-style: normal;
19 | font-weight: 500;
20 | font-size: 14px;
21 | line-height: 16px;
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | text-align: center;
26 | cursor: pointer;
27 | `;
28 |
29 | const ActiveButton = styled(ButtonBase)`
30 | background: var(--button-background);
31 | border: 1px solid var(--button-border);
32 | color: var(--button-text);
33 | `;
34 |
35 | const InactiveButton = styled(ButtonBase)`
36 | background: var(--background);
37 | border: 1px solid var(--inactive-button-border);
38 | color: var(--inactive-button-text);
39 | `;
40 |
41 | const Button = ({ buttonText, active, onClick }) => {
42 | const ButtonDisplay = ({ activeButton, children }) => {
43 | if (activeButton) {
44 | return {children};
45 | } else {
46 | return {children};
47 | }
48 | };
49 |
50 | return (
51 |
52 | {buttonText}
53 |
54 | );
55 | };
56 |
57 | export default Button;
58 |
--------------------------------------------------------------------------------
/src/configs/addresses.ts:
--------------------------------------------------------------------------------
1 | import registry from '@balancer-labs/assets/generated/dex/registry.homestead.json';
2 | import registryKovan from '@balancer-labs/assets/generated/dex/registry.kovan.json';
3 | import { getSupportedChainName } from '../provider/connectors';
4 |
5 | function getContracts(chainName: string) {
6 | if (chainName === 'mainnet') {
7 | return {
8 | bFactory: '0x9424B1412450D0f8Fc2255FAf6046b98213B76Bd',
9 | proxy: '0x3E66B66Fd1d0b02fDa6C811Da9E0547970DB2f21',
10 | weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
11 | multicall: '0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441',
12 | sorMulticall: '0x514053aCEC7177e277B947b1EBb5C08AB4C4580E',
13 | };
14 | }
15 | if (chainName === 'kovan') {
16 | return {
17 | bFactory: '0x8f7F78080219d4066A8036ccD30D588B416a40DB',
18 | proxy: '0x2641f150669739986CDa3ED6860DeD44BC3Cda5d',
19 | weth: '0xd0A1E359811322d97991E03f863a0C30C2cF029C',
20 | multicall: '0x2cc8688C5f75E365aaEEb4ea8D6a480405A48D2A',
21 | sorMulticall: '0x71c7f1086aFca7Aa1B0D4d73cfa77979d10D3210',
22 | };
23 | }
24 | return {};
25 | }
26 |
27 | function getAssets(chainName: string) {
28 | if (chainName === 'mainnet') {
29 | return registry;
30 | }
31 | if (chainName === 'kovan') {
32 | return registryKovan;
33 | }
34 | return {
35 | tokens: {},
36 | untrusted: [],
37 | };
38 | }
39 |
40 | const chainName = getSupportedChainName();
41 | const contracts = getContracts(chainName);
42 | const assets = getAssets(chainName);
43 |
44 | export { contracts, assets };
45 |
--------------------------------------------------------------------------------
/src/provider/UncheckedJsonRpcSigner.ts:
--------------------------------------------------------------------------------
1 | import * as ethers from 'ethers';
2 |
3 | class UncheckedJsonRpcSigner extends ethers.Signer {
4 | signer: any;
5 |
6 | constructor(signer) {
7 | super();
8 | ethers.utils.defineReadOnly(this, 'signer', signer);
9 | ethers.utils.defineReadOnly(this, 'provider', signer.provider);
10 | }
11 |
12 | getAddress() {
13 | return this.signer.getAddress();
14 | }
15 |
16 | sendTransaction(transaction) {
17 | return this.signer.sendUncheckedTransaction(transaction).then(hash => {
18 | return {
19 | hash: hash,
20 | nonce: null,
21 | gasLimit: null,
22 | gasPrice: null,
23 | data: null,
24 | value: null,
25 | chainId: null,
26 | confirmations: 0,
27 | from: null,
28 | wait: confirmations => {
29 | return this.signer.provider.waitForTransaction(
30 | hash,
31 | confirmations
32 | );
33 | },
34 | };
35 | });
36 | }
37 |
38 | signMessage(message) {
39 | return this.signer.signMessage(message);
40 | }
41 |
42 | signTransaction(
43 | transaction: ethers.ethers.utils.Deferrable<
44 | ethers.ethers.providers.TransactionRequest
45 | >
46 | ): Promise {
47 | return this.signer.signTransaction(transaction);
48 | }
49 |
50 | connect(provider: ethers.ethers.providers.Provider): ethers.ethers.Signer {
51 | return this.signer.connect(provider);
52 | }
53 | }
54 |
55 | export default UncheckedJsonRpcSigner;
56 |
--------------------------------------------------------------------------------
/public/pebbles-pad.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/stores/AssetOptions.ts:
--------------------------------------------------------------------------------
1 | import { action, observable } from 'mobx';
2 | import { BigNumber } from 'utils/bignumber';
3 | import RootStore from 'stores/Root';
4 |
5 | interface Asset {
6 | address: string;
7 | symbol: string;
8 | name: string;
9 | hasIcon: boolean;
10 | userBalance: string;
11 | isTradable: boolean;
12 | decimals: number;
13 | precision: number;
14 | allowance: BigNumber;
15 | balanceBn: BigNumber;
16 | }
17 |
18 | export default class AssetOptions {
19 | @observable tokenAssetData: Asset;
20 | rootStore: RootStore;
21 |
22 | constructor(rootStore) {
23 | this.rootStore = rootStore;
24 | this.tokenAssetData = undefined;
25 | }
26 |
27 | @action fetchTokenAssetData = async (address: string, account: string) => {
28 | console.log(`[AssetOptions] fetchTokenAssetData: ${address}`);
29 |
30 | const { tokenStore } = this.rootStore;
31 |
32 | try {
33 | const tokenMetadata = await tokenStore.fetchOnChainTokenMetadata(
34 | address,
35 | account
36 | );
37 |
38 | this.tokenAssetData = {
39 | address: tokenMetadata.address,
40 | symbol: tokenMetadata.symbol,
41 | name: tokenMetadata.name,
42 | hasIcon: tokenMetadata.hasIcon,
43 | userBalance: tokenMetadata.balanceFormatted,
44 | isTradable: true,
45 | decimals: tokenMetadata.decimals,
46 | precision: tokenMetadata.precision,
47 | allowance: tokenMetadata.allowance,
48 | balanceBn: tokenMetadata.balanceBn,
49 | };
50 | } catch (err) {
51 | this.tokenAssetData = undefined;
52 | }
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | :root {
2 | /* General Styles */
3 | --background: #21222c;
4 | --body-text: #90a4ae;
5 | --exit-modal-color: #dfe3e9;
6 | --error-color: #ff8a80;
7 | --header-text: #fafafa;
8 | --info-border: #ffffff;
9 | --link-text: #8c9eff;
10 | --pie-chart-color: #b388ff;
11 | --token-balance-text: #90a4ae;
12 | --warning: #ff8a80;
13 | --address-color: #82b1ff;
14 | --info: #ffc780;
15 |
16 | /* Input Fields */
17 | --input-text: #fafafa;
18 | --input-placeholder-text: #575860;
19 | --input-hover-background: #31313b;
20 | --input-hover-placeholder-text: #818187;
21 | --input-hover-border: #4c5480;
22 |
23 | /* Button Styles */
24 | --button-text: #fafafa;
25 | --button-background: #536dfe;
26 | --button-border: #3d5afe;
27 | --inactive-button-border: rgba(140, 158, 255, 0.2);
28 | --inactive-button-text: rgba(255, 255, 255, 0.6);
29 |
30 | /* Fonts */
31 | --roboto: 'Roboto', sans-serif;
32 |
33 | /* Panel Styles */
34 | --panel-border: #41476b;
35 | --panel-background: #2e2f39;
36 | --panel-hover-border: #4c5480;
37 | --panel-hover-background: #31313b;
38 | --panel-header-background: #282932;
39 |
40 | /* Selector Styles */
41 | --highlighted-selector-background: #41476b;
42 | --highlighted-selector-border: #7785d5;
43 | --highlighted-selector-text: #ffffff;
44 | --selector-background: #2e2f39;
45 | --selector-border: #41476b;
46 | --selector-text: #fafafa;
47 | }
48 |
49 | body {
50 | background: var(--background);
51 | }
52 |
53 | .app-shell {
54 | margin-top: 60px;
55 | }
56 |
57 | #root::-webkit-scrollbar {
58 | display: none;
59 | }
60 |
61 | .popup-content {
62 | color: var(--background);
63 | border-radius: 4px;
64 | display: flex;
65 | align-items: center;
66 | justify-content: center;
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/BuyToken.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TokenPanel from './TokenPanel';
3 | import { observer } from 'mobx-react';
4 | import { useStores } from '../contexts/storesContext';
5 |
6 | const BuyToken = observer(
7 | ({
8 | inputName,
9 | tokenSymbol,
10 | tokenName,
11 | tokenBalance,
12 | truncatedTokenBalance,
13 | tokenAddress,
14 | tokenHasIcon,
15 | errorMessage,
16 | showMax,
17 | }) => {
18 | const {
19 | root: { swapFormStore },
20 | } = useStores();
21 |
22 | const onChange = async event => {
23 | const { value } = event.target;
24 | updateSwapFormData(value);
25 | };
26 |
27 | /* To protect against race conditions in this async method we check
28 | for staleness of inputAmount after getting preview and before making updates */
29 | const updateSwapFormData = async value => {
30 | await swapFormStore.refreshSwapFormPreviewEAO(value);
31 | };
32 |
33 | const { inputs } = swapFormStore;
34 | const { outputAmount } = inputs;
35 |
36 | return (
37 | onChange(e)}
41 | updateSwapFormData={updateSwapFormData}
42 | inputName={inputName}
43 | tokenSymbol={tokenSymbol}
44 | tokenName={tokenName}
45 | tokenBalance={tokenBalance}
46 | truncatedTokenBalance={truncatedTokenBalance}
47 | tokenAddress={tokenAddress}
48 | tokenHasIcon={tokenHasIcon}
49 | errorMessage={errorMessage}
50 | showMax={false}
51 | />
52 | );
53 | }
54 | );
55 |
56 | export default BuyToken;
57 |
--------------------------------------------------------------------------------
/src/utils/helperHooks.jsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef, useState } from 'react';
2 | import copy from 'copy-to-clipboard';
3 |
4 | export function useInterval(callback, delay) {
5 | const savedCallback = useRef();
6 |
7 | // Remember the latest function.
8 | useEffect(() => {
9 | savedCallback.current = callback;
10 | }, [callback]);
11 |
12 | // Set up the interval.
13 | useEffect(() => {
14 | function tick() {
15 | savedCallback.current();
16 | }
17 | if (delay !== null) {
18 | let id = setInterval(tick, delay);
19 | return () => clearInterval(id);
20 | }
21 | }, [delay]);
22 | }
23 |
24 | export function useCopyClipboard(timeout = 500) {
25 | const [isCopied, setIsCopied] = useState(false);
26 |
27 | const staticCopy = useCallback(text => {
28 | const didCopy = copy(text);
29 | setIsCopied(didCopy);
30 | }, []);
31 |
32 | useEffect(() => {
33 | if (isCopied) {
34 | const hide = setTimeout(() => {
35 | setIsCopied(false);
36 | }, timeout);
37 |
38 | return () => {
39 | clearTimeout(hide);
40 | };
41 | }
42 | }, [isCopied, setIsCopied, timeout]);
43 |
44 | return [isCopied, staticCopy];
45 | }
46 |
47 | // modified from https://usehooks.com/usePrevious/
48 | export function usePrevious(value) {
49 | // The ref object is a generic container whose current property is mutable ...
50 | // ... and can hold any value, similar to an instance property on a class
51 | const ref = useRef();
52 |
53 | // Store current value in ref
54 | useEffect(() => {
55 | ref.current = value;
56 | }, [value]); // Only re-run if value changes
57 |
58 | // Return previous value (happens before update in useEffect above)
59 | return ref.current;
60 | }
61 |
--------------------------------------------------------------------------------
/src/utils/web3.js:
--------------------------------------------------------------------------------
1 | // Libraries
2 | import Web3 from 'web3';
3 |
4 | class Web3Extended extends Web3 {
5 | stop = () => {
6 | // this.reset(true);
7 | if (
8 | this.currentProvider &&
9 | typeof this.currentProvider.stop === 'function'
10 | ) {
11 | this.currentProvider.stop();
12 | }
13 | };
14 |
15 | bindProvider = provider => {
16 | this.setProvider(provider);
17 | };
18 |
19 | setWebClientProvider = () => {
20 | this.stop();
21 | return new Promise(async (resolve, reject) => {
22 | try {
23 | // Checking if the the provider is compliant with the new EIP1102 Standard.
24 | if (window.ethereum) {
25 | //following the new EIP1102 standard
26 | window.ethereum.enable().then(
27 | () => {
28 | this.bindProvider(window.ethereum);
29 | resolve();
30 | },
31 | () => {
32 | reject();
33 | }
34 | );
35 |
36 | return;
37 | }
38 |
39 | if (window.web3) {
40 | // This is the case for Provider Injectors which don't follow EIP1102 ( parity-extension ? )
41 | this.bindProvider(window.web3.currentProvider);
42 | resolve();
43 |
44 | return;
45 | }
46 |
47 | reject();
48 | } catch (e) {
49 | reject(e);
50 | }
51 | });
52 | };
53 | }
54 |
55 | const web3 = new Web3Extended();
56 | //TODO: What was this for?
57 | // web3.utils.BN.config({ EXPONENTIAL_AT: [-18, 21] });
58 | window.web3Provider = web3;
59 |
60 | export default web3;
61 |
--------------------------------------------------------------------------------
/src/components/SellToken.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TokenPanel from './TokenPanel';
3 | import { observer } from 'mobx-react';
4 | import { useStores } from '../contexts/storesContext';
5 |
6 | const SellToken = observer(
7 | ({
8 | inputName,
9 | tokenSymbol,
10 | tokenName,
11 | tokenBalance,
12 | truncatedTokenBalance,
13 | tokenAddress,
14 | tokenHasIcon,
15 | errorMessage,
16 | showMax,
17 | }) => {
18 | const {
19 | root: { swapFormStore },
20 | } = useStores();
21 |
22 | const onChange = async event => {
23 | const { value } = event.target;
24 | updateSwapFormData(value);
25 | };
26 |
27 | /* To protect against race conditions in this async method we check
28 | for staleness of inputAmount after getting preview and before making updates */
29 | const updateSwapFormData = async value => {
30 | await swapFormStore.refreshSwapFormPreviewEAI(value);
31 | };
32 |
33 | const { inputs } = swapFormStore;
34 | const { inputAmount } = inputs;
35 |
36 | return (
37 | onChange(e)}
41 | updateSwapFormData={updateSwapFormData}
42 | inputName={inputName}
43 | tokenSymbol={tokenSymbol}
44 | tokenName={tokenName}
45 | tokenBalance={tokenBalance}
46 | truncatedTokenBalance={truncatedTokenBalance}
47 | tokenAddress={tokenAddress}
48 | tokenHasIcon={tokenHasIcon}
49 | errorMessage={errorMessage}
50 | showMax={showMax}
51 | />
52 | );
53 | }
54 | );
55 |
56 | export default SellToken;
57 |
--------------------------------------------------------------------------------
/src/stores/actions/actions.ts:
--------------------------------------------------------------------------------
1 | import { Contract } from 'ethers';
2 | import { providers } from 'ethers';
3 | import { setGoal } from '../../utils/fathom';
4 |
5 | interface ActionRequest {
6 | contract: Contract;
7 | action: string;
8 | sender: string;
9 | data: any[];
10 | overrides: any;
11 | }
12 |
13 | export interface ActionResponse {
14 | contract: Contract;
15 | action: string;
16 | sender: string;
17 | data: object;
18 | txResponse: providers.TransactionResponse | undefined;
19 | error: any | undefined;
20 | }
21 |
22 | const preLog = (params: ActionRequest) => {
23 | console.log(`[@action start: ${params.action}]`, {
24 | contract: params.contract,
25 | action: params.action,
26 | sender: params.sender,
27 | data: params.data,
28 | overrides: params.overrides,
29 | });
30 | };
31 |
32 | const postLog = (result: ActionResponse) => {
33 | console.log(`[@action end: ${result.action}]`, {
34 | contract: result.contract,
35 | action: result.action,
36 | sender: result.sender,
37 | data: result.data,
38 | result: result.txResponse,
39 | error: result.error,
40 | });
41 | };
42 |
43 | export const sendAction = async (
44 | params: ActionRequest
45 | ): Promise => {
46 | const { contract, action, sender, data, overrides } = params;
47 | preLog(params);
48 |
49 | const actionResponse: ActionResponse = {
50 | contract,
51 | action,
52 | sender,
53 | data,
54 | txResponse: undefined,
55 | error: undefined,
56 | };
57 |
58 | try {
59 | actionResponse.txResponse = await contract[action](...data, overrides);
60 | actionResponse.txResponse.wait().then(() => setGoal(action));
61 | } catch (e) {
62 | actionResponse.error = e;
63 | }
64 |
65 | postLog(actionResponse);
66 | return actionResponse;
67 | };
68 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Balancer Exchange
28 |
32 |
33 |
34 |
35 |
36 |
46 |
47 |
--------------------------------------------------------------------------------
/src/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | //@ts-ignore
2 | import React from 'react';
3 | import { appConfig } from 'configs';
4 | import styled from 'styled-components';
5 | import Wallet from '../Wallet';
6 |
7 | const HeaderFrame = styled.div`
8 | display: flex;
9 | align-items: center;
10 | justify-content: space-between;
11 | width: 100%;
12 | `;
13 |
14 | const HeaderElement = styled.div`
15 | margin: 1.25rem;
16 | display: flex;
17 | min-width: 0;
18 | display: flex;
19 | align-items: center;
20 | `;
21 |
22 | const Title = styled.div`
23 | display: flex;
24 | align-items: center;
25 | cursor: pointer;
26 | a {
27 | display: inline;
28 | font-size: 1rem;
29 | font-weight: 500;
30 | text-decoration: none;
31 | img {
32 | height: 32px;
33 | width: 32px;
34 | }
35 | }
36 | `;
37 |
38 | const AppName = styled.div`
39 | font-family: Roboto;
40 | font-style: normal;
41 | font-weight: 500;
42 | font-size: 15px;
43 | line-height: 18px;
44 | letter-spacing: 1px;
45 | color: var(--header-text);
46 | margin-left: 12px;
47 | `;
48 |
49 | const Link = styled.a`
50 | font-family: Roboto;
51 | margin-right: 24px;
52 | font-size: 15px;
53 | display: flex;
54 | align-items: center;
55 | color: var(--header-text);
56 | text-decoration: none;
57 | @media screen and (max-width: 767px) {
58 | display: none;
59 | }
60 | `;
61 |
62 | const Header = () => {
63 | return (
64 |
65 |
66 |
67 |
68 |
69 |
70 | {appConfig.name}
71 |
72 |
73 |
74 |
75 | Add Liquidity
76 |
77 |
78 |
79 |
80 | );
81 | };
82 |
83 | export default Header;
84 |
--------------------------------------------------------------------------------
/src/components/GeneralNotification.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const Wrapper = styled.div``;
5 |
6 | const Warning = styled.div`
7 | display: flex;
8 | flex-direction: row;
9 | align-items: center;
10 | color: var(--info);
11 | width: 50%;
12 | margin: 20px auto;
13 | border: 1px solid var(--info);
14 | border-radius: 4px;
15 | padding: 20px;
16 | @media screen and (max-width: 1024px) {
17 | width: 80%;
18 | }
19 | `;
20 |
21 | const Message = styled.div`
22 | display: inline;
23 | font-family: Roboto;
24 | font-style: normal;
25 | font-weight: normal;
26 | font-size: 14px;
27 | line-height: 16px;
28 | letter-spacing: 0.2px;
29 | `;
30 |
31 | const WarningIcon = styled.img`
32 | width: 22px;
33 | height: 26px;
34 | margin-right: 20px;
35 | color: var(--info);
36 | `;
37 |
38 | const Link = styled.a`
39 | color: color: var(--info);
40 | `;
41 |
42 | const GeneralNotification = () => {
43 | return (
44 |
45 |
46 |
47 |
48 | The exchange has been upgraded to use multi-path order
49 | routing which improves overall pricing and gas usage. You
50 | will need to unlock tokens again for the new{' '}
51 |
56 | proxy contract
57 |
58 | . To use the old exchange proxy visit:{' '}
59 |
64 | https://legacy.balancer.exchange
65 |
66 |
67 |
68 |
69 | );
70 | };
71 |
72 | export default GeneralNotification;
73 |
--------------------------------------------------------------------------------
/src/utils/subGraph.ts:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-fetch';
2 | import * as allPools from 'allPublicPools.json';
3 | import { SUBGRAPH_URL } from 'provider/connectors';
4 |
5 | // Returns all public pools
6 | export async function getAllPublicSwapPools() {
7 | let pools = { pools: [] };
8 | try {
9 | pools = await getSubgraphPools();
10 | if (pools.pools.length === 0) {
11 | console.log(
12 | `[SubGraph] Load Error - No Pools Returned. Defaulting To Backup List.`
13 | );
14 | pools.pools = allPools.pools;
15 | }
16 | } catch (error) {
17 | console.log(`[SubGraph] Load Error. Defaulting To Backup List.`);
18 | console.log(`[SubGraph] Error: ${error.message}`);
19 | pools.pools = allPools.pools;
20 | }
21 |
22 | return pools;
23 | }
24 |
25 | async function getSubgraphPools() {
26 | const query = `
27 | {
28 | pools0: pools (first: 1000, where: {publicSwap: true, active: true}) {
29 | id
30 | swapFee
31 | totalWeight
32 | publicSwap
33 | tokens {
34 | id
35 | address
36 | balance
37 | decimals
38 | symbol
39 | denormWeight
40 | }
41 | tokensList
42 | },
43 | pools1000: pools (first: 1000, skip: 1000, where: {publicSwap: true, active: true}) {
44 | id
45 | swapFee
46 | totalWeight
47 | publicSwap
48 | tokens {
49 | id
50 | address
51 | balance
52 | decimals
53 | symbol
54 | denormWeight
55 | }
56 | tokensList
57 | }
58 | }
59 | `;
60 |
61 | const response = await fetch(SUBGRAPH_URL, {
62 | method: 'POST',
63 | headers: {
64 | Accept: 'application/json',
65 | 'Content-Type': 'application/json',
66 | },
67 | body: JSON.stringify({
68 | query,
69 | }),
70 | });
71 |
72 | const { data } = await response.json();
73 | let pools = data.pools0.concat(data.pools1000);
74 | console.log(`[SubGraph] Number Of Pools: ${pools.length}`);
75 | return { pools: pools };
76 | }
77 |
--------------------------------------------------------------------------------
/src/theme/components.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components';
2 | import { darken } from 'polished';
3 |
4 | export const Button = styled.button.attrs(({ warning, theme }) => ({
5 | backgroundColor: warning ? theme.salmonRed : theme.royalBlue,
6 | }))`
7 | padding: 1rem 2rem 1rem 2rem;
8 | border-radius: 3rem;
9 | cursor: pointer;
10 | user-select: none;
11 | font-size: 1rem;
12 | border: none;
13 | outline: none;
14 | background-color: ${({ backgroundColor }) => backgroundColor};
15 | color: ${({ theme }) => theme.white};
16 | width: 100%;
17 |
18 | :hover,
19 | :focus {
20 | background-color: ${({ backgroundColor }) =>
21 | darken(0.05, backgroundColor)};
22 | }
23 |
24 | :active {
25 | background-color: ${({ backgroundColor }) =>
26 | darken(0.1, backgroundColor)};
27 | }
28 |
29 | :disabled {
30 | background-color: ${({ theme }) => theme.concreteGray};
31 | color: ${({ theme }) => theme.silverGray};
32 | cursor: auto;
33 | }
34 | `;
35 |
36 | export const Link = styled.a.attrs({
37 | target: '_blank',
38 | rel: 'noopener noreferrer',
39 | })`
40 | text-decoration: none;
41 | cursor: pointer;
42 | color: ${({ theme }) => theme.royalBlue};
43 |
44 | :focus {
45 | outline: none;
46 | text-decoration: underline;
47 | }
48 |
49 | :active {
50 | text-decoration: none;
51 | }
52 | `;
53 |
54 | export const BorderlessInput = styled.input`
55 | color: ${({ theme }) => theme.textColor};
56 | font-size: 1rem;
57 | outline: none;
58 | border: none;
59 | flex: 1 1 auto;
60 | width: 0;
61 | background-color: ${({ theme }) => theme.inputBackground};
62 |
63 | [type='number'] {
64 | -moz-appearance: textfield;
65 | }
66 |
67 | ::-webkit-outer-spin-button,
68 | ::-webkit-inner-spin-button {
69 | -webkit-appearance: none;
70 | }
71 |
72 | ::placeholder {
73 | color: ${({ theme }) => theme.chaliceGray};
74 | }
75 | `;
76 |
77 | const rotate = keyframes`
78 | from {
79 | transform: rotate(0deg);
80 | }
81 | to {
82 | transform: rotate(360deg);
83 | }
84 | `;
85 |
86 | export const Spinner = styled.img`
87 | animation: 2s ${rotate} linear infinite;
88 | width: 16px;
89 | height: 16px;
90 | `;
91 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { HashRouter, Redirect, Route, Switch } from 'react-router-dom';
3 | import styled from 'styled-components';
4 | import Web3ReactManager from 'components/Web3ReactManager';
5 | import Header from 'components/Header';
6 | import SwapForm from 'components/SwapForm';
7 | import { isAddress, toChecksum } from 'utils/helpers';
8 | import './App.css';
9 |
10 | const BuildVersion = styled.div`
11 | display: flex;
12 | flex-direction: row;
13 | text-align: center;
14 | margin: 20px;
15 | font-size: 10px;
16 | color: var(--body-text);
17 | position: fixed;
18 | bottom: 0px;
19 | @media screen and (max-width: 1024px) {
20 | display: none;
21 | }
22 | `;
23 |
24 | const BuildLink = styled.a`
25 | color: var(--body-text);
26 | text-decoration: none;
27 | margin-left: 5px;
28 | `;
29 |
30 | const App = () => {
31 | const PoolSwapView = props => {
32 | let { tokenIn, tokenOut } = props.match.params;
33 | if (isAddress(tokenIn)) {
34 | tokenIn = toChecksum(tokenIn);
35 | }
36 | if (isAddress(tokenOut)) {
37 | tokenOut = toChecksum(tokenOut);
38 | }
39 |
40 | return ;
41 | };
42 |
43 | const buildId = process.env.REACT_APP_COMMIT_REF || '';
44 |
45 | const renderViews = () => {
46 | return (
47 |
48 |
49 |
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | return (
60 |
61 |
62 |
63 | {renderViews()}
64 |
65 | BUILD ID:{' '}
66 |
70 | {buildId.substring(0, 12)}
71 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default App;
79 |
--------------------------------------------------------------------------------
/src/stores/Root.ts:
--------------------------------------------------------------------------------
1 | // Stores
2 | import ProxyStore from 'stores/Proxy';
3 | import ProviderStore from 'stores/Provider';
4 | import BlockchainFetchStore from 'stores/BlockchainFetch';
5 | import SwapFormStore from 'stores/SwapForm';
6 | import TokenStore from 'stores/Token';
7 | import DropdownStore from './Dropdown';
8 | import ErrorStore from './Error';
9 | import ContractMetadataStore from './ContractMetadata';
10 | import TransactionStore from './Transaction';
11 | import AppSettingsStore from './AppSettings';
12 | import PoolStore from './Pool';
13 | import AssetOptionsStore from './AssetOptions';
14 | import SorStore from './Sor';
15 | import TokenPanelStore from './TokenPanel';
16 |
17 | export default class RootStore {
18 | proxyStore: ProxyStore;
19 | providerStore: ProviderStore;
20 | blockchainFetchStore: BlockchainFetchStore;
21 | swapFormStore: SwapFormStore;
22 | tokenStore: TokenStore;
23 | poolStore: PoolStore;
24 | dropdownStore: DropdownStore;
25 | contractMetadataStore: ContractMetadataStore;
26 | transactionStore: TransactionStore;
27 | appSettingsStore: AppSettingsStore;
28 | assetOptionsStore: AssetOptionsStore;
29 | tokenPanelStore: TokenPanelStore;
30 | sorStore: SorStore;
31 | errorStore: ErrorStore;
32 |
33 | constructor() {
34 | this.proxyStore = new ProxyStore(this);
35 | this.poolStore = new PoolStore(this);
36 | this.providerStore = new ProviderStore(this);
37 | this.blockchainFetchStore = new BlockchainFetchStore(this);
38 | this.contractMetadataStore = new ContractMetadataStore(this);
39 | this.swapFormStore = new SwapFormStore(this);
40 | this.tokenStore = new TokenStore(this);
41 | this.dropdownStore = new DropdownStore(this);
42 | this.transactionStore = new TransactionStore(this);
43 | this.appSettingsStore = new AppSettingsStore(this);
44 | this.assetOptionsStore = new AssetOptionsStore(this);
45 | this.tokenPanelStore = new TokenPanelStore(this);
46 | this.sorStore = new SorStore(this);
47 | this.errorStore = new ErrorStore(this);
48 |
49 | this.asyncSetup().catch(e => {
50 | //TODO: Add retry on these fetches
51 | throw new Error('Async Setup Failed ' + e);
52 | });
53 | }
54 |
55 | async asyncSetup() {
56 | await this.providerStore.loadWeb3();
57 | this.poolStore.fetchPools(true); // Loads SubGraph pools and onChain balances
58 | this.blockchainFetchStore.blockchainFetch(false);
59 | // Load on-chain data as soon as a provider is available
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/provider/connectors.ts:
--------------------------------------------------------------------------------
1 | import Web3Modal from 'web3modal';
2 | import WalletConnectProvider from '@walletconnect/web3-provider';
3 | import Portis from '@portis/web3';
4 |
5 | const providerOptions = {
6 | walletconnect: {
7 | package: WalletConnectProvider,
8 | options: {
9 | infuraId: process.env.REACT_APP_INFURA_ID,
10 | },
11 | },
12 | portis: {
13 | package: Portis,
14 | options: {
15 | id: '3f1c3cfc-7dd5-4e8a-aa03-71ff7396d9fe',
16 | },
17 | },
18 | };
19 |
20 | export const web3Modal = new Web3Modal({
21 | providerOptions: providerOptions,
22 | theme: {
23 | background: '#282932',
24 | main: '#282932',
25 | secondary: '#90a4ae',
26 | border: '#41476b',
27 | hover: '#21222c',
28 | },
29 | });
30 |
31 | export const supportedChainId = Number(
32 | process.env.REACT_APP_SUPPORTED_NETWORK_ID
33 | );
34 |
35 | export const getSupportedChainId = () => {
36 | return supportedChainId;
37 | };
38 |
39 | export const getSupportedChainName = () => {
40 | return chainNameById[supportedChainId];
41 | };
42 |
43 | export const chainNameById = {
44 | '1': 'mainnet',
45 | '3': 'ropsten',
46 | '42': 'kovan',
47 | };
48 |
49 | export const isChainIdSupported = (chainId: number): boolean => {
50 | return supportedChainId === chainId;
51 | };
52 |
53 | const RPC_URLS: { [chainId: number]: string } = {
54 | 1: process.env.REACT_APP_RPC_URL_1 as string,
55 | 3: process.env.REACT_APP_RPC_URL_3 as string,
56 | 42: process.env.REACT_APP_RPC_URL_42 as string,
57 | };
58 |
59 | export const SUBGRAPH_URLS: { [chainId: number]: string } = {
60 | 1: process.env.REACT_APP_SUBGRAPH_URL_1 as string,
61 | 3: process.env.REACT_APP_SUBGRAPH_URL_3 as string,
62 | 42: process.env.REACT_APP_SUBGRAPH_URL_42 as string,
63 | };
64 |
65 | export const SUBGRAPH_URL =
66 | SUBGRAPH_URLS[process.env.REACT_APP_SUPPORTED_NETWORK_ID];
67 |
68 | export const backupUrls = {};
69 | backupUrls[supportedChainId] = RPC_URLS[supportedChainId];
70 |
71 | export const SUPPORTED_WALLETS = {
72 | INJECTED: {
73 | isInjected: true,
74 | name: 'Injected',
75 | iconName: 'arrow-right.svg',
76 | description: 'Injected web3 provider.',
77 | href: null,
78 | color: '#010101',
79 | primary: true,
80 | },
81 | METAMASK: {
82 | isInjected: true,
83 | name: 'MetaMask',
84 | iconName: 'metamask.png',
85 | description: 'Easy-to-use browser extension.',
86 | href: null,
87 | color: '#E8831D',
88 | },
89 | };
90 |
--------------------------------------------------------------------------------
/public/arrow-bottom.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/components/WalletDropdown/TransactionPanel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { observer } from 'mobx-react';
4 | import { useStores } from 'contexts/storesContext';
5 | import Transaction from './Transaction';
6 | import { TransactionRecord } from 'stores/Transaction';
7 | import { isChainIdSupported } from '../../provider/connectors';
8 |
9 | const TransactionListWrapper = styled.div`
10 | display: flex;
11 | flex-flow: column nowrap;
12 | `;
13 |
14 | const Panel = styled.div`
15 | text-align: left;
16 | display: flex;
17 | flex-flow: column nowrap;
18 | padding-top: 2rem;
19 | flex-grow: 1;
20 | overflow: auto;
21 | background-color: var(--panel-background);
22 | `;
23 |
24 | const TransactionHeader = styled.div`
25 | border-top: 1px solid var(--panel-border);
26 | align-items: left;
27 | font-family: Roboto;
28 | font-style: normal;
29 | font-weight: 500;
30 | font-size: 14px;
31 | line-height: 18px;
32 | padding-top: 14px;
33 | color: var(--token-balance-text);
34 | text-transform: uppercase;
35 | `;
36 |
37 | const TransactionPanel = observer(() => {
38 | const {
39 | root: { transactionStore, providerStore },
40 | } = useStores();
41 |
42 | const account = providerStore.providerStatus.account;
43 | const activeChainId = providerStore.providerStatus.activeChainId;
44 |
45 | let pending = undefined;
46 | let confirmed = undefined;
47 |
48 | if (account && isChainIdSupported(activeChainId)) {
49 | pending = transactionStore.getPendingTransactions(account);
50 | confirmed = transactionStore.getConfirmedTransactions(account);
51 | }
52 |
53 | function renderTransactions(transactions: TransactionRecord[], pending) {
54 | return (
55 |
56 | {transactions.map((value, i) => {
57 | return (
58 |
63 | );
64 | })}
65 |
66 | );
67 | }
68 |
69 | let hasTx = !!pending.length || !!confirmed.length;
70 |
71 | if (hasTx) {
72 | return (
73 |
74 | Recent Transactions
75 | {renderTransactions(pending, true)}
76 | {renderTransactions(confirmed, false)}
77 |
78 | );
79 | }
80 |
81 | return <>>;
82 | });
83 |
84 | export default TransactionPanel;
85 |
--------------------------------------------------------------------------------
/src/components/WalletDropdown/Transaction.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 | import { Check } from 'react-feather';
4 | import { getEtherscanLink } from 'utils/helpers';
5 | import Circle from '../../assets/images/circle.svg';
6 | import { useStores } from '../../contexts/storesContext';
7 |
8 | const TransactionStatusWrapper = styled.div`
9 | display: flex;
10 | align-items: center;
11 | min-width: 12px;
12 | overflow: hidden;
13 | text-overflow: ellipsis;
14 | white-space: nowrap;
15 | a {
16 | color: var(--link-text);
17 | font-weight: 500;
18 | font-size: 14px;
19 | }
20 | `;
21 |
22 | const TransactionWrapper = styled.div`
23 | display: flex;
24 | flex-flow: row nowrap;
25 | justify-content: space-between;
26 | width: 100%;
27 | margin-top: 0.75rem;
28 | a {
29 | /* flex: 1 1 auto; */
30 | overflow: hidden;
31 | text-overflow: ellipsis;
32 | white-space: nowrap;
33 | min-width: 0;
34 | max-width: 250px;
35 | }
36 | `;
37 |
38 | const rotate = keyframes`
39 | from {
40 | transform: rotate(0deg);
41 | }
42 | to {
43 | transform: rotate(360deg);
44 | }
45 | `;
46 |
47 | const Spinner = styled.img`
48 | animation: 2s ${rotate} linear infinite;
49 | width: 16px;
50 | height: 16px;
51 | `;
52 |
53 | const TransactionState = styled.div`
54 | display: flex;
55 | color: ${({ pending, theme }) => (pending ? '#DC6BE5' : '#27AE60')};
56 | padding: 0.5rem 0.75rem;
57 | font-weight: 500;
58 | font-size: 0.75rem;
59 | #pending {
60 | animation: 2s ${rotate} linear infinite;
61 | }
62 | `;
63 |
64 | export default function Transaction({ hash, pending }) {
65 | const {
66 | root: { providerStore },
67 | } = useStores();
68 |
69 | const chainId = providerStore.providerStatus.activeChainId;
70 |
71 | return (
72 |
73 | {pending ? (
74 |
75 |
76 |
77 | ) : (
78 |
79 |
80 |
81 | )}
82 |
83 |
88 | {hash} ↗{' '}
89 |
90 |
91 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const url = require('url');
4 |
5 | // Make sure any symlinks in the project folder are resolved:
6 | // https://github.com/facebook/create-react-app/issues/637
7 | const appDirectory = fs.realpathSync(process.cwd());
8 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
9 |
10 | const envPublicUrl = process.env.PUBLIC_URL;
11 |
12 | function ensureSlash(inputPath, needsSlash) {
13 | const hasSlash = inputPath.endsWith('/');
14 | if (hasSlash && !needsSlash) {
15 | return inputPath.substr(0, inputPath.length - 1);
16 | } else if (!hasSlash && needsSlash) {
17 | return `${inputPath}/`;
18 | } else {
19 | return inputPath;
20 | }
21 | }
22 |
23 | const getPublicUrl = appPackageJson =>
24 | envPublicUrl || require(appPackageJson).homepage;
25 |
26 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
27 | // "public path" at which the app is served.
28 | // Webpack needs to know it to put the right