├── 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/dropup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/DownCarret.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/swap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 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 | 2 | 3 | 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 | 2 | 3 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 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 | 3 | 4 | pebbles-pad 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | <a href="/"> 68 | <img src="pebbles-pad.svg" alt="pebbles" /> 69 | </a> 70 | <AppName>{appConfig.name}</AppName> 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 | 2 | 3 | 4 | 5 | 6 | 7 | 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