├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .solhint.json ├── LICENSE ├── README.md ├── interface ├── .env.local ├── components │ ├── account-details │ │ ├── Copy.tsx │ │ ├── Transaction.tsx │ │ └── index.tsx │ ├── anchor.tsx │ ├── banner │ │ ├── common.tsx │ │ └── index.tsx │ ├── button.tsx │ ├── combine │ │ └── index.tsx │ ├── dropdown.tsx │ ├── footer │ │ ├── common.tsx │ │ └── index.tsx │ ├── header │ │ ├── common.tsx │ │ ├── hero.tsx │ │ └── index.tsx │ ├── icons │ │ ├── arrow-right.tsx │ │ ├── fortmatic.tsx │ │ ├── logo.tsx │ │ ├── metamask.tsx │ │ ├── social.tsx │ │ ├── tokens.tsx │ │ ├── triangle.tsx │ │ └── wallet-connect.tsx │ ├── input.tsx │ ├── manage │ │ └── index.tsx │ ├── modals │ │ ├── common.tsx │ │ ├── index.tsx │ │ └── wallet │ │ │ ├── Option.tsx │ │ │ ├── PendingView.tsx │ │ │ └── index.tsx │ ├── split │ │ └── index.tsx │ ├── tables │ │ ├── assets.tsx │ │ └── common.tsx │ ├── typography.tsx │ ├── web3-status.tsx │ └── widget.tsx ├── connectors │ ├── Fortmatic.ts │ ├── NetworkConnector.ts │ ├── fortmatic.d.ts │ └── index.ts ├── constants │ └── index.tsx ├── contexts │ ├── asset-allowances.tsx │ ├── asset-balances.tsx │ ├── banner.tsx │ ├── blockchain.tsx │ ├── full-token-prices.tsx │ ├── modal.tsx │ ├── split-addresses.tsx │ ├── tokens.tsx │ ├── transaction.tsx │ ├── web3-connection.tsx │ └── yield-balances.tsx ├── data │ ├── split_merge.json │ └── tokens.ts ├── hooks │ ├── contracts.ts │ ├── useEthToken.ts │ ├── useMounted.ts │ ├── useOnClickOutside.ts │ └── wallet.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── [...actionParams].tsx │ ├── _app.tsx │ ├── _document.tsx │ └── index.tsx ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico ├── theme │ └── index.tsx ├── tsconfig.json ├── types │ ├── app.ts │ ├── ethereum.ts │ └── split.ts ├── typings.d.ts ├── utils │ ├── address.ts │ ├── etherscan.ts │ ├── format.ts │ └── number.ts └── yarn-error.log ├── package.json ├── protocol ├── .env_example ├── contracts │ ├── CTokenPriceOracle.sol │ ├── CapitalComponentToken.sol │ ├── SplitPoolFactory.sol │ ├── SplitVault.sol │ ├── VaultControlled.sol │ ├── YieldComponentToken.sol │ ├── interfaces │ │ ├── CTokenInterface.sol │ │ └── PriceOracle.sol │ ├── lib │ │ ├── DSMath.sol │ │ ├── ERC20Base.sol │ │ └── balancer │ │ │ ├── configurable-rights-pool │ │ │ ├── contracts │ │ │ │ ├── CRPFactory.sol │ │ │ │ ├── ConfigurableRightsPool.sol │ │ │ │ ├── IBFactory.sol │ │ │ │ ├── Migrations.sol │ │ │ │ ├── PCToken.sol │ │ │ │ └── utils │ │ │ │ │ ├── BalancerOwnable.sol │ │ │ │ │ └── BalancerReentrancyGuard.sol │ │ │ ├── interfaces │ │ │ │ ├── BalancerIERC20.sol │ │ │ │ └── IConfigurableRightsPool.sol │ │ │ └── libraries │ │ │ │ ├── BalancerConstants.sol │ │ │ │ ├── BalancerSafeMath.sol │ │ │ │ ├── RightsManager.sol │ │ │ │ ├── SafeApprove.sol │ │ │ │ └── SmartPoolManager.sol │ │ │ └── core │ │ │ ├── BColor.sol │ │ │ ├── BConst.sol │ │ │ ├── BFactory.sol │ │ │ ├── BMath.sol │ │ │ ├── BNum.sol │ │ │ ├── BPool.sol │ │ │ └── BToken.sol │ └── mocks │ │ ├── ERC20Mock.sol │ │ ├── PriceOracleMock.sol │ │ └── SplitVaultMock.sol ├── deployments │ ├── index.ts │ └── types.ts ├── hardhat.config.ts ├── index.ts ├── package.json ├── tasks │ ├── add_component_set.ts │ ├── deploy_component_tokens.ts │ ├── deploy_oracle.ts │ ├── deploy_pool_factory.ts │ ├── deploy_split_pool.ts │ ├── deploy_vault.ts │ ├── index.ts │ ├── mint_test_token.ts │ └── swap_tokens.ts ├── test │ ├── capital_component_token.ts │ ├── constants.ts │ ├── split_pool_factory.ts │ ├── split_vault.ts │ ├── types.ts │ ├── utils.ts │ └── yield_component_token.ts ├── tsconfig.json ├── yarn-error.log └── yarn.lock ├── split-banner.png ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/interface" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "npm" 8 | directory: "/protocol" 9 | schedule: 10 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build and Test 5 | 6 | on: [push] 7 | 8 | jobs: 9 | build_and_test: 10 | name: Build and Test 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v2 28 | env: 29 | cache-name: cached-node-modules 30 | with: 31 | path: ~/work/split/split/node_modules 32 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('yarn.lock') }} 33 | 34 | - run: yarn install --frozen-lockfile 35 | - run: yarn prettier:check 36 | - run: yarn build 37 | - run: yarn test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | .vscode 3 | node_modules 4 | 5 | #Buidler files 6 | cache 7 | artifacts 8 | 9 | #Typechain files 10 | typechain 11 | 12 | .env 13 | #React + Next files 14 | **/.next 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | typechain/ 2 | node_modules/ 3 | artifacts/ 4 | cache/ 5 | .vscode/ 6 | **/.next -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "printWidth": 120, 5 | "singleQuote": false, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "overrides": [ 9 | { 10 | "files": "*.sol", 11 | "options": { 12 | "tabWidth": 2 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 7], 6 | "compiler-version": ["error", "^0.7.2"], 7 | "constructor-syntax": "error", 8 | "max-line-length": ["error", 120], 9 | "not-rely-on-time": "off", 10 | "prettier/prettier": "error", 11 | "reason-string": ["warn", { "maxLength": 64 }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt text](https://raw.githubusercontent.com/split-fi/split/main/split-banner.png "Split") 2 | -------------------------------------------------------------------------------- /interface/.env.local: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CHAIN_ID="4" 2 | NEXT_PUBLIC_NETWORK_URL="https://eth-rinkeby.alchemyapi.io/v2/U2ncZJ--7GNdQJY9GUTTMy9MYAJTj3OP" 3 | NEXT_PUBLIC_FORTMATIC_KEY="pk_test_C172CCBF3C04CFA8" -------------------------------------------------------------------------------- /interface/components/account-details/Copy.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react' 2 | // import styled from 'styled-components' 3 | // import useCopyClipboard from '../../hooks/useCopyClipboard' 4 | 5 | // import { LinkStyledButton } from '../../theme' 6 | // import { CheckCircle, Copy } from 'react-feather' 7 | 8 | // const CopyIcon = styled(LinkStyledButton)` 9 | // color: ${({ theme }) => theme.text3}; 10 | // flex-shrink: 0; 11 | // display: flex; 12 | // text-decoration: none; 13 | // font-size: 0.825rem; 14 | // :hover, 15 | // :active, 16 | // :focus { 17 | // text-decoration: none; 18 | // color: ${({ theme }) => theme.text2}; 19 | // } 20 | // ` 21 | // const TransactionStatusText = styled.span` 22 | // margin-left: 0.25rem; 23 | // font-size: 0.825rem; 24 | // ${({ theme }) => theme.flexRowNoWrap}; 25 | // align-items: center; 26 | // ` 27 | 28 | // export default function CopyHelper(props: { toCopy: string; children?: React.ReactNode }) { 29 | // const [isCopied, setCopied] = useCopyClipboard() 30 | 31 | // return ( 32 | // setCopied(props.toCopy)}> 33 | // {isCopied ? ( 34 | // 35 | // 36 | // Copied 37 | // 38 | // ) : ( 39 | // 40 | // 41 | // 42 | // )} 43 | // {isCopied ? '' : props.children} 44 | // 45 | // ) 46 | // } 47 | -------------------------------------------------------------------------------- /interface/components/account-details/Transaction.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react' 2 | // import styled from 'styled-components' 3 | // import { CheckCircle, Triangle } from 'react-feather' 4 | 5 | // import { useActiveWeb3React } from '../../hooks' 6 | // import { getEtherscanLink } from '../../utils' 7 | // import { ExternalLink } from '../../theme' 8 | // import { useAllTransactions } from '../../state/transactions/hooks' 9 | // import { RowFixed } from '../Row' 10 | // import Loader from '../Loader' 11 | 12 | // const TransactionWrapper = styled.div`` 13 | 14 | // const TransactionStatusText = styled.div` 15 | // margin-right: 0.5rem; 16 | // display: flex; 17 | // align-items: center; 18 | // :hover { 19 | // text-decoration: underline; 20 | // } 21 | // ` 22 | 23 | // const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>` 24 | // display: flex; 25 | // justify-content: space-between; 26 | // align-items: center; 27 | // text-decoration: none !important; 28 | // border-radius: 0.5rem; 29 | // padding: 0.25rem 0rem; 30 | // font-weight: 500; 31 | // font-size: 0.825rem; 32 | // color: ${({ theme }) => theme.primary1}; 33 | // ` 34 | 35 | // const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>` 36 | // color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)}; 37 | // ` 38 | 39 | // export default function Transaction({ hash }: { hash: string }) { 40 | // const { chainId } = useActiveWeb3React() 41 | // const allTransactions = useAllTransactions() 42 | 43 | // const tx = allTransactions?.[hash] 44 | // const summary = tx?.summary 45 | // const pending = !tx?.receipt 46 | // const success = !pending && tx && (tx.receipt?.status === 1 || typeof tx.receipt?.status === 'undefined') 47 | 48 | // if (!chainId) return null 49 | 50 | // return ( 51 | // 52 | // 53 | // 54 | // {summary ?? hash} ↗ 55 | // 56 | // 57 | // {pending ? : success ? : } 58 | // 59 | // 60 | // 61 | // ) 62 | // } 63 | -------------------------------------------------------------------------------- /interface/components/anchor.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const PrimaryAnchor = styled.a` 4 | font-size: 16px; 5 | color: white; 6 | text-decoration: none; 7 | font-weight: bold; 8 | &:focus { 9 | outline: none; 10 | } 11 | `; 12 | 13 | export const NoStyledAnchor = styled.a` 14 | text-decoration: none; 15 | &:focus { 16 | outline: none; 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /interface/components/banner/common.tsx: -------------------------------------------------------------------------------- 1 | import { useWeb3React } from "@web3-react/core"; 2 | import { FC } from "react"; 3 | import styled from "styled-components"; 4 | import { BannerMetadata, BannerType, TxBannerMetadata } from "../../types/app"; 5 | import { getEtherscanLink } from "../../utils/etherscan"; 6 | import { PrimaryAnchor } from "../anchor"; 7 | import { FOOTER_HEIGHT } from "../footer/common"; 8 | import { H3 } from "../typography"; 9 | 10 | export const BannersWrapper = styled.div` 11 | position: fixed; 12 | right: 0; 13 | left: 0; 14 | bottom: ${FOOTER_HEIGHT}px; 15 | `; 16 | 17 | const bannerTypeToBackgroundColor = (type: BannerType) => { 18 | if (type === "success") { 19 | return "#97CB93"; 20 | } 21 | if (type === "error") { 22 | return "#D36D6D"; 23 | } 24 | return "#FFFFFF"; 25 | }; 26 | 27 | const bannerTypeToColor = (type: BannerType) => { 28 | if (type === "success") { 29 | return "#0E2991"; 30 | } 31 | if (type === "error") { 32 | return "#0E2991"; 33 | } 34 | return "#0E2991"; 35 | }; 36 | 37 | export interface BannerTypeProps { 38 | type: BannerType; 39 | } 40 | 41 | export const BannerWrapper = styled.div` 42 | width: 100%; 43 | background-color: ${props => bannerTypeToBackgroundColor(props.type)}; 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | padding: 16px 0; 48 | `; 49 | 50 | const StyledH3 = styled(H3)` 51 | color: ${props => bannerTypeToColor(props.type)}; 52 | `; 53 | 54 | const StyledA = styled(PrimaryAnchor)` 55 | color: ${props => bannerTypeToColor(props.type)}; 56 | font-size: 28px; 57 | `; 58 | 59 | export const TxBanner: FC = ({ type, txHash, description }) => { 60 | const { chainId } = useWeb3React(); 61 | 62 | return ( 63 | 64 | {description} 65 | 71 | see on etherscan 72 | 73 | 74 | ); 75 | }; 76 | 77 | export const Banner: FC = ({ type, description }) => { 78 | return ( 79 | 80 | {description} 81 | 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /interface/components/banner/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useBannerActions, useBanners } from "../../contexts/banner"; 3 | import { useMounted } from "../../hooks/useMounted"; 4 | import { BannerMetadata, TxBannerMetadata } from "../../types/app"; 5 | import { BannersWrapper, Banner, TxBanner } from "./common"; 6 | 7 | export const Banners = () => { 8 | const banners = useBanners(); 9 | const undismissedBanners = banners.filter(b => !b.dismissed); 10 | 11 | return ( 12 | 13 | {undismissedBanners.map((b: BannerMetadata, i: number) => { 14 | if (!!(b as TxBannerMetadata).txHash) { 15 | return ; 16 | } 17 | return ; 18 | // TODO key is not that great, need to change 19 | })} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /interface/components/button.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const PrimaryButton = styled.button` 4 | border: 2px white solid; 5 | font-weight: 700; 6 | cursor: pointer; 7 | border-radius: 999px; 8 | padding: 12px 32px; 9 | color: white; 10 | letter-spacing: 0.05rem; 11 | font-size: 14px; 12 | font-weight: bold; 13 | background-color: rgba(0, 0, 0, 0); 14 | &:focus { 15 | outline: none; 16 | } 17 | &:hover:enabled, 18 | &:hover:enabled > * { 19 | background-color: #ffffff; 20 | color: #0e2991; 21 | font-style: italic; 22 | } 23 | &:disabled { 24 | opacity: 0.5; 25 | cursor: default; 26 | } 27 | `; 28 | 29 | export const SecondaryDarkButton = styled.button` 30 | border: 2px rgba(0, 0, 0, 0.05) solid; 31 | font-weight: 900; 32 | padding: 12px 32px; 33 | color: black; 34 | letter-spacing: 0.05rem; 35 | background-color: rgba(0, 0, 0, 0); 36 | font-size: 14px; 37 | &:focus:enabled { 38 | outline: none; 39 | } 40 | &:hover:enabled { 41 | color: #0e2991; 42 | border-color: #0e2991; 43 | font-style: italic; 44 | } 45 | &:disabled { 46 | opacity: 0.5; 47 | cursor: default; 48 | } 49 | `; 50 | -------------------------------------------------------------------------------- /interface/components/combine/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { useSplitVault } from "../../hooks/contracts"; 5 | import { useFullTokens } from "../../contexts/tokens"; 6 | import { useFullTokenPrice } from "../../contexts/full-token-prices"; 7 | import { useSplitProtocolAddresses } from "../../contexts/split-addresses"; 8 | 9 | import { 10 | componentTokenAmountToFullTokenAmount, 11 | convertToBaseAmount, 12 | fullTokenAmountToComponentTokenAmount, 13 | } from "../../utils/number"; 14 | 15 | import { H1 } from "../typography"; 16 | import { Dropdown } from "../dropdown"; 17 | import { ConfirmButton, InputContainer } from "../widget"; 18 | import { TokenInput } from "../input"; 19 | import { useTransactionActions } from "../../contexts/transaction"; 20 | 21 | const CombineButton = styled(ConfirmButton)` 22 | font-size: 32px; 23 | `; 24 | 25 | const CombineContainer = styled.div` 26 | display: flex; 27 | flex-direction: column; 28 | `; 29 | 30 | export interface CombineWidgetProps {} 31 | 32 | export const CombineWidget: React.FC = () => { 33 | const { splitVault } = useSplitVault(); 34 | const tokens = useFullTokens(); 35 | const [selectedTokenIndex, setSelectedTokenIndex] = useState(0); 36 | const { addTransaction } = useTransactionActions(); 37 | const [value, setValue] = useState(""); 38 | const selectedToken = tokens[selectedTokenIndex]; 39 | const price = useFullTokenPrice(selectedToken.tokenAddress); 40 | const deployment = useSplitProtocolAddresses(); 41 | const baseAmount = convertToBaseAmount(value || "0", selectedToken.componentTokens.yieldComponentToken.decimals); 42 | 43 | const onCombineClick = useCallback(async () => { 44 | // No allowance needed for combining 45 | const tx = await splitVault.combine(baseAmount.toString(), selectedToken.tokenAddress); 46 | addTransaction(tx.hash, { 47 | fullToken: selectedToken, 48 | componentTokenAmount: baseAmount, 49 | type: "combine", 50 | }); 51 | // TODO: clear input on success???? 52 | setValue(""); 53 | }, [value, splitVault, selectedToken, deployment]); 54 | 55 | if (!tokens || !tokens.length || !price) { 56 | return
Please connect your wallet.
; 57 | } 58 | 59 | const dropdownItems = tokens.map(asset => ({ 60 | id: asset.tokenAddress, 61 | displayName: asset.symbol, 62 | })); 63 | 64 | const fullTokenValue = componentTokenAmountToFullTokenAmount( 65 | baseAmount, 66 | price, 67 | selectedToken.userlyingAssetMetaData.decimals, 68 | ) 69 | .toDecimalPlaces(4) 70 | .toString(); 71 | return ( 72 | 73 | 74 |

combine

75 | 80 |

{selectedToken.componentTokens.capitalComponentToken.symbol}

81 |

and

82 | 87 |

{selectedToken.componentTokens.yieldComponentToken.symbol}

88 |

to get

89 |

{fullTokenValue}

90 | 91 |
92 | 93 | Combine 94 | 95 |
96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /interface/components/dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { H1 } from "./typography"; 5 | import { Triangle } from "./icons/triangle"; 6 | import { useIsOpenUntilOutside } from "../hooks/useOnClickOutside"; 7 | 8 | const DropdownContainer = styled.div` 9 | display: flex; 10 | align-items: center; 11 | cursor: pointer; 12 | `; 13 | 14 | const Selector = styled.div` 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | `; 19 | 20 | const ArrowContainer = styled.div` 21 | margin-left: 10px; 22 | `; 23 | 24 | const Select = styled.div` 25 | position: absolute; 26 | border-radius: 21px; 27 | border: 2px solid white; 28 | padding: 30px; 29 | background-color: #0c2ea0; 30 | `; 31 | 32 | const Option = styled.div` 33 | font-size: 40px; 34 | color: white; 35 | margin: 20px 0px; 36 | &:hover { 37 | font-style: italic; 38 | font-weight: bold; 39 | } 40 | `; 41 | 42 | export interface DropdownItem { 43 | id: string; 44 | displayName: string; 45 | } 46 | 47 | export interface DropdownProps { 48 | selectedId: string; 49 | items: DropdownItem[]; 50 | onSelect?: (itemId: string) => void; 51 | onSelectIndex?: (index: number) => void; 52 | className?: string; 53 | } 54 | 55 | const noop = () => {}; 56 | 57 | export const Dropdown: React.FC = ({ 58 | selectedId, 59 | className, 60 | onSelect = noop, 61 | onSelectIndex = noop, 62 | items = [], 63 | }) => { 64 | const [isOpen, setIsOpen, node] = useIsOpenUntilOutside(); 65 | const selectedItem = items.find(item => item.id === selectedId); 66 | if (!selectedItem) { 67 | return null; 68 | } 69 | return ( 70 | setIsOpen(true)} className={className}> 71 | 72 |

{selectedItem.displayName}

73 | 74 | 75 | 76 |
77 | {isOpen && ( 78 | 93 | )} 94 |
95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /interface/components/footer/common.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const FOOTER_HEIGHT = 90; 4 | 5 | export const FooterSpacer = styled.div` 6 | height: ${FOOTER_HEIGHT}px; 7 | width: 100%; 8 | `; 9 | 10 | export const FooterContentWrapper = styled.div` 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | height: ${FOOTER_HEIGHT}px; 15 | padding: 0 24px; 16 | width: 100%; 17 | `; 18 | 19 | export const FooterRightContentWrapper = styled.div``; 20 | 21 | export const FooterLeftContentWrapper = styled.div``; 22 | -------------------------------------------------------------------------------- /interface/components/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { P } from "../typography"; 5 | import { Discord, Github, Twitter } from "../icons/social"; 6 | import { FooterContentWrapper, FooterLeftContentWrapper, FooterRightContentWrapper } from "./common"; 7 | 8 | const LinksWrapper = styled.div` 9 | display: grid; 10 | grid-gap: 40px; 11 | grid-template-columns: repeat(3, 1fr); 12 | `; 13 | 14 | export const Footer: React.FC = () => { 15 | return ( 16 | 17 | 18 |

© SPLIT 2020 UNAUDITED BETA – Mainnet, Rinkeby

19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /interface/components/header/common.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const HEADER_HEIGHT = 90; 4 | 5 | export const HeaderSpacer = styled.div` 6 | height: ${HEADER_HEIGHT}px; 7 | width: 100%; 8 | `; 9 | 10 | export const HeaderContentWrapper = styled.div` 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | height: ${HEADER_HEIGHT}px; 15 | padding: 0 30px; 16 | width: 100%; 17 | position: relative; 18 | `; 19 | 20 | export const HeaderRightContentWrapper = styled.div` 21 | position: relative; 22 | z-index: 1000; 23 | `; 24 | 25 | export const HeaderLeftContentWrapper = styled.div` 26 | position: relative; 27 | z-index: 1000; 28 | `; 29 | 30 | export const HeaderCenterContentWrapper = styled.div` 31 | display: flex; 32 | align-items: center; 33 | justify-content: space-around; 34 | top: 0; 35 | left: 0; 36 | right: 0; 37 | height: ${HEADER_HEIGHT}px; 38 | z-index: 999; 39 | width: 100%; 40 | `; 41 | -------------------------------------------------------------------------------- /interface/components/header/hero.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | 5 | import { PATHS } from "../../constants"; 6 | import { PrimaryButton } from "../button"; 7 | import { LogoFull } from "../icons/logo"; 8 | 9 | import { HeaderContentWrapper, HeaderLeftContentWrapper, HeaderRightContentWrapper } from "./common"; 10 | 11 | const LogoWrapper = styled.div` 12 | cursor: pointer; 13 | `; 14 | 15 | export const HeroHeader: React.FC = () => { 16 | const router = useRouter(); 17 | const onGoToAppClick = () => { 18 | router.push(PATHS.SPLIT); 19 | }; 20 | const goToHome = () => { 21 | router.push(PATHS.ROOT); 22 | }; 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Go to app 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /interface/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRouter } from "next/router"; 3 | import styled from "styled-components"; 4 | import { LogoSmall } from "../icons/logo"; 5 | import { 6 | HeaderContentWrapper, 7 | HeaderLeftContentWrapper, 8 | HeaderCenterContentWrapper, 9 | HeaderRightContentWrapper, 10 | } from "./common"; 11 | import Web3Status from "../web3-status"; 12 | import { PATHS } from "../../constants"; 13 | import { NoStyledAnchor } from "../anchor"; 14 | import { AppAction } from "../../types/app"; 15 | 16 | interface HeaderProps { 17 | showTabs?: boolean; 18 | currentAppAction: AppAction; 19 | onTabClick: (appAction: AppAction) => void; 20 | } 21 | 22 | interface TabButtonProps { 23 | isActive?: boolean; 24 | } 25 | 26 | const TabButton = styled.button` 27 | font-weight: ${props => (props.isActive ? 700 : 300)}; 28 | font-style: ${props => (props.isActive ? "italic" : "normal")}; 29 | padding: 12px 0px; 30 | color: white; 31 | letter-spacing: 0.1rem; 32 | background-color: rgba(0, 0, 0, 0); 33 | font-size: 36px; 34 | cursor: pointer; 35 | border: 0px solid transparent; 36 | &:focus { 37 | outline: none; 38 | } 39 | &:hover { 40 | font-weight: 700; 41 | font-style: italic; 42 | } 43 | `; 44 | 45 | const LogoWrapper = styled.div` 46 | cursor: pointer; 47 | width: 200px; 48 | `; 49 | 50 | const APP_ACTION_TO_TAB_TITLE = { 51 | [AppAction.SPLIT]: "split", 52 | [AppAction.MANAGE]: "manage", 53 | [AppAction.COMBINE]: "combine", 54 | }; 55 | 56 | export const Header: React.FC = ({ showTabs, currentAppAction, onTabClick }) => { 57 | const router = useRouter(); 58 | 59 | const onSplitIconClick = () => { 60 | router.push(PATHS.ROOT); 61 | }; 62 | 63 | return ( 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {showTabs ? ( 73 | 74 | {Object.values(AppAction).map((appAction: AppAction) => { 75 | return ( 76 | 81 | {APP_ACTION_TO_TAB_TITLE[appAction]} 82 | 83 | ); 84 | })} 85 | 86 | ) : null} 87 | 88 | 89 | 90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /interface/components/icons/arrow-right.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface ArrowRightIconProps { 4 | className?: string; 5 | } 6 | 7 | export const ArrowRightIcon: React.FC = ({ className }) => { 8 | return ( 9 | 17 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /interface/components/icons/fortmatic.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface FortmaticIconProps { 4 | className?: string; 5 | } 6 | 7 | export const FortmaticIcon: React.FC = ({ className }) => { 8 | return ( 9 | 17 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /interface/components/icons/logo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SVGProps { 4 | width?: string; 5 | height?: string; 6 | } 7 | 8 | export const LogoFull: React.FC = ({ width = "258", height = "117" }) => ( 9 | 10 | 17 | 18 | 25 | 32 | 39 | 46 | 47 | 48 | ); 49 | 50 | export const LogoSmall: React.FC = ({ width = "23", height = "41" }) => ( 51 | 52 | 53 | 54 | 61 | 62 | 63 | 64 | ); 65 | -------------------------------------------------------------------------------- /interface/components/icons/metamask.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | interface IconProps { 4 | className?: string; 5 | } 6 | 7 | export const MetaMaskIcon: FC = ({ className }) => ( 8 | 9 | 10 | 14 | 18 | 22 | 26 | 30 | 31 | 32 | 33 | 34 | 38 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | -------------------------------------------------------------------------------- /interface/components/icons/social.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SVGProps { 4 | width?: string; 5 | height?: string; 6 | } 7 | 8 | export const Discord: React.FC = ({ width = "36", height = "36" }) => ( 9 | 10 | 17 | 24 | 31 | 32 | ); 33 | 34 | export const Github: React.FC = ({ width = "36", height = "36" }) => ( 35 | 36 | 43 | 44 | ); 45 | 46 | export const Twitter: React.FC = ({ width = "36", height = "36" }) => ( 47 | 48 | 54 | 55 | ); 56 | -------------------------------------------------------------------------------- /interface/components/icons/tokens.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SVGProps { 4 | width?: string; 5 | height?: string; 6 | } 7 | 8 | export const CapitalToken: React.FC = ({ width = "297", height = "297" }) => ( 9 | 10 | 11 | 18 | 25 | 26 | c 27 | 28 | 29 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | 44 | export const YieldToken: React.FC = ({ width = "297", height = "297" }) => ( 45 | 46 | 47 | 54 | 61 | 62 | y 63 | 64 | 65 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | 80 | export const GovernanceToken: React.FC = ({ width = "297", height = "297" }) => ( 81 | 82 | 83 | 90 | 97 | 98 | g 99 | 100 | 101 | 109 | 110 | 111 | 112 | 113 | 114 | ); 115 | -------------------------------------------------------------------------------- /interface/components/icons/triangle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SVGProps { 4 | width?: string; 5 | height?: string; 6 | } 7 | 8 | export const Triangle: React.FC = ({ width = "20", height = "17" }) => ( 9 | 19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /interface/components/icons/wallet-connect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface WalletConnectIconProps { 4 | className?: string; 5 | } 6 | 7 | export const WalletConnectIcon: React.FC = ({ className }) => { 8 | return ( 9 | 17 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /interface/components/input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import Decimal from "decimal.js"; 3 | import styled, { css } from "styled-components"; 4 | 5 | import { colors } from "../theme"; 6 | import { useAssetBalance } from "../contexts/asset-balances"; 7 | import { useToken } from "../contexts/tokens"; 8 | import { convertToUnitAmount } from "../utils/number"; 9 | 10 | const SplitInput = styled.input<{ isError?: boolean }>` 11 | background: transparent; 12 | border: none; 13 | border-bottom: 2px solid white; 14 | outline: none; 15 | box-shadow: none; 16 | color: white; 17 | font-size: 40px; 18 | padding: 10px 0px; 19 | min-width: 250px; 20 | ${props => 21 | props.isError && 22 | css` 23 | color: ${colors.red}; 24 | border-bottom: 2px solid ${colors.red} !important; 25 | `} 26 | &:focus { 27 | font-style: italic; 28 | font-weight: bold; 29 | border-bottom: 3px solid white; 30 | } 31 | `; 32 | 33 | const InputContainer = styled.div<{ isDisabled: boolean }>` 34 | display: flex; 35 | flex-direction: column; 36 | ${props => (props.isDisabled ? "opacity: 0.6;" : "")} 37 | `; 38 | 39 | const Message = styled.label<{ isError?: boolean }>` 40 | margin: 10px 0px; 41 | font-weight: bold; 42 | ${props => 43 | props.isError && 44 | css` 45 | color: ${colors.red} !important; 46 | `} 47 | `; 48 | 49 | export interface InputProps { 50 | value: string; 51 | onChange: (value: string) => void; 52 | message?: string; 53 | errorMessage?: string; 54 | className?: string; 55 | maxLength?: number; 56 | isDisabled?: boolean; 57 | } 58 | 59 | export const Input: React.FC = props => { 60 | const { value, onChange, message, errorMessage, className, isDisabled, maxLength = 10 } = props; 61 | const innerOnChange = useCallback( 62 | (newValue: string, oldValue: string) => { 63 | if (newValue.length > maxLength) { 64 | onChange(oldValue); 65 | return; 66 | } 67 | onChange(newValue); 68 | }, 69 | [onChange, maxLength], 70 | ); 71 | const isError = !!errorMessage; 72 | return ( 73 | 74 | innerOnChange(e.target.value, value)} 82 | /> 83 | {!isError && message && {message}} 84 | {isError && {errorMessage}} 85 | 86 | ); 87 | }; 88 | 89 | export interface TokenInputProps { 90 | tokenAddress: string; 91 | value: string; 92 | onChange: (value: string) => void; 93 | } 94 | 95 | export const TokenInput: React.FC = ({ tokenAddress, value, onChange }) => { 96 | const tokenBalance = useAssetBalance(tokenAddress); 97 | const token = useToken(tokenAddress); 98 | let errorMsg = ""; 99 | const unitTokenBalance = convertToUnitAmount(tokenBalance, token.decimals); 100 | const decimalValue = new Decimal(value || "0"); 101 | if (unitTokenBalance.lessThan(decimalValue)) { 102 | errorMsg = `you don't have enough ${token.symbol}`; 103 | } 104 | const maxString = unitTokenBalance.toString(); 105 | return ( 106 | 107 | ); 108 | }; 109 | -------------------------------------------------------------------------------- /interface/components/manage/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo, useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { useWeb3React } from "@web3-react/core"; 5 | import { H1 } from "../typography"; 6 | import { useAssetBalances } from "../../contexts/asset-balances"; 7 | import { AssetsTable } from "../tables/assets"; 8 | 9 | const TableH1 = styled(H1)` 10 | margin-bottom: 24px; 11 | `; 12 | 13 | const ManagePageWrapper = styled.div` 14 | display: grid; 15 | grid-template-columns: 3fr 5fr; 16 | grid-gap: 72px; 17 | width: 1024px; 18 | `; 19 | 20 | const ManageColumnContainer = styled.div``; 21 | 22 | export interface ManageWidgetProps {} 23 | 24 | export const ManageWidget: React.FC = () => { 25 | const { active, error } = useWeb3React(); 26 | 27 | if (!active || error) { 28 | return
Please connect your wallet.
; 29 | } 30 | 31 | return ( 32 | 33 | 34 | capital 35 | 36 | 37 | 38 | yield 39 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /interface/components/modals/common.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { css } from "styled-components"; 3 | import { animated, useTransition, useSpring } from "react-spring"; 4 | import { DialogOverlay, DialogContent } from "@reach/dialog"; 5 | import { isMobile } from "react-device-detect"; 6 | import "@reach/dialog/styles.css"; 7 | import { transparentize } from "polished"; 8 | import { useGesture } from "react-use-gesture"; 9 | 10 | const AnimatedDialogOverlay = animated(DialogOverlay); 11 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 12 | const StyledDialogOverlay = styled(AnimatedDialogOverlay)` 13 | &[data-reach-dialog-overlay] { 14 | z-index: 2; 15 | overflow: hidden; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | background-color: rgba(0, 0, 0, 0.5); 20 | } 21 | `; 22 | 23 | const AnimatedDialogContent = animated(DialogContent); 24 | // destructure to not pass custom props to Dialog DOM element 25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 26 | const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => ( 27 | 28 | )).attrs({ 29 | "aria-label": "dialog", 30 | })` 31 | overflow-y: ${({ mobile }) => (mobile ? "scroll" : "hidden")}; 32 | 33 | &[data-reach-dialog-content] { 34 | margin: 0 0 2rem 0; 35 | background-color: #ffffff; 36 | box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, "#000000")}; 37 | padding: 0px; 38 | width: 50vw; 39 | overflow-y: ${({ mobile }) => (mobile ? "scroll" : "hidden")}; 40 | overflow-x: hidden; 41 | 42 | align-self: ${({ mobile }) => (mobile ? "flex-end" : "center")}; 43 | 44 | max-width: 420px; 45 | ${({ maxHeight }) => 46 | maxHeight && 47 | css` 48 | max-height: ${maxHeight}vh; 49 | `} 50 | ${({ minHeight }) => 51 | minHeight && 52 | css` 53 | min-height: ${minHeight}vh; 54 | `} 55 | display: flex; 56 | @media (max-width: 960px) { 57 | width: 65vw; 58 | margin: 0; 59 | } 60 | @media (max-width: 720px) { 61 | width: 85vw; 62 | margin: 0; 63 | // TODO patch for mobile 64 | width: 100vw; 65 | border-radius: 20px; 66 | border-bottom-left-radius: 0; 67 | border-bottom-right-radius: 0; 68 | } 69 | } 70 | `; 71 | 72 | interface ModalProps { 73 | isOpen: boolean; 74 | onDismiss: () => void; 75 | minHeight?: number | false; 76 | maxHeight?: number; 77 | initialFocusRef?: React.RefObject; 78 | children?: React.ReactNode; 79 | } 80 | 81 | export function Modal({ isOpen, onDismiss, minHeight = false, maxHeight = 90, initialFocusRef, children }: ModalProps) { 82 | const fadeTransition = useTransition(isOpen, null, { 83 | config: { duration: 200 }, 84 | from: { opacity: 0 }, 85 | enter: { opacity: 1 }, 86 | leave: { opacity: 0 }, 87 | }); 88 | 89 | const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } })); 90 | const bind = useGesture({ 91 | onDrag: state => { 92 | set({ 93 | y: state.down ? state.movement[1] : 0, 94 | }); 95 | if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) { 96 | onDismiss(); 97 | } 98 | }, 99 | }); 100 | 101 | return ( 102 | <> 103 | {fadeTransition.map( 104 | ({ item, key, props }) => 105 | item && ( 106 | 107 | `translateY(${y > 0 ? y : 0}px)`) }, 112 | } 113 | : {})} 114 | aria-label="dialog content" 115 | minHeight={minHeight} 116 | maxHeight={maxHeight} 117 | mobile={isMobile} 118 | > 119 | {/* prevents the automatic focusing of inputs on mobile by the reach dialog */} 120 | {!initialFocusRef && isMobile ?
: null} 121 | {children} 122 | 123 | 124 | ), 125 | )} 126 | 127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /interface/components/modals/index.tsx: -------------------------------------------------------------------------------- 1 | import WalletModal from "./wallet"; 2 | 3 | export const Modals = () => { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /interface/components/modals/wallet/Option.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { PrimaryAnchor } from "../../anchor"; 4 | import { SecondaryDarkButton } from "../../button"; 5 | import { FadedDark } from "../../typography"; 6 | 7 | const InfoCard = styled(SecondaryDarkButton)` 8 | position: relative; 9 | width: 100% !important; 10 | `; 11 | 12 | const OptionCard = styled(InfoCard as any)` 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | justify-content: space-between; 17 | margin-top: 2rem; 18 | padding: 1rem; 19 | `; 20 | 21 | const OptionCardLeft = styled.div` 22 | ${({ theme }) => theme.flexColumnNoWrap}; 23 | justify-content: center; 24 | height: 100%; 25 | `; 26 | 27 | const OptionCardClickable = styled(OptionCard as any)<{ clickable?: boolean }>` 28 | margin-top: 0; 29 | &:hover { 30 | cursor: ${({ clickable }) => (clickable ? "pointer" : "")}; 31 | border: ${({ clickable, theme }) => (clickable ? `1px solid ${theme.primary1}` : ``)}; 32 | } 33 | opacity: ${({ disabled }) => (disabled ? "0.5" : "1")}; 34 | `; 35 | 36 | const GreenCircle = styled.div` 37 | ${({ theme }) => theme.flexRowNoWrap} 38 | justify-content: center; 39 | align-items: center; 40 | 41 | &:first-child { 42 | height: 8px; 43 | width: 8px; 44 | margin-right: 8px; 45 | background-color: ${({ theme }) => theme.green1}; 46 | border-radius: 50%; 47 | } 48 | `; 49 | 50 | const CircleWrapper = styled.div` 51 | color: ${({ theme }) => theme.green1}; 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | `; 56 | 57 | const HeaderText = styled.div` 58 | ${({ theme }) => theme.flexRowNoWrap}; 59 | color: ${props => (props.color === "blue" ? ({ theme }) => theme.primary1 : ({ theme }) => theme.text1)}; 60 | font-size: 1rem; 61 | font-weight: 500; 62 | `; 63 | 64 | const SubHeader = styled.div` 65 | color: ${({ theme }) => theme.text1}; 66 | margin-top: 10px; 67 | font-size: 12px; 68 | `; 69 | 70 | export default function Option({ 71 | link = null, 72 | clickable = true, 73 | size, 74 | onClick = null, 75 | color, 76 | header, 77 | subheader = null, 78 | icon, 79 | active = false, 80 | id, 81 | }: { 82 | link?: string | null; 83 | clickable?: boolean; 84 | size?: number | null; 85 | onClick?: null | (() => void); 86 | color: string; 87 | header: React.ReactNode; 88 | subheader: React.ReactNode | null; 89 | icon: React.ReactNode; 90 | active?: boolean; 91 | id: string; 92 | }) { 93 | const content = ( 94 | 95 | {header} 96 | {active ? current : <>} 97 | 98 | ); 99 | if (link) { 100 | return {content}; 101 | } 102 | 103 | return content; 104 | } 105 | -------------------------------------------------------------------------------- /interface/components/modals/wallet/PendingView.tsx: -------------------------------------------------------------------------------- 1 | import { AbstractConnector } from "@web3-react/abstract-connector"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | import Option from "./Option"; 5 | import { SUPPORTED_WALLETS } from "../../../constants"; 6 | import { injected } from "../../../connectors"; 7 | import { P, H1, PDark } from "../../typography"; 8 | import { PrimaryButton } from "../../button"; 9 | 10 | const StyledP = styled(PDark)<{ error?: boolean }>` 11 | text-transform: uppercase; 12 | font-weight: 900; 13 | font-size: 12px; 14 | letter-spacing: 0.05rem; 15 | color: ${props => (props.error ? "white" : "black")}; 16 | `; 17 | 18 | const LargeText = styled(H1)<{ error?: boolean }>` 19 | font-size: 48px; 20 | font-weight: 900; 21 | letter-spacing: 0.05rem; 22 | color: ${props => (props.error ? "white" : "rgba(0,0,0,0.2)")}; 23 | `; 24 | 25 | const PendingSection = styled.div``; 26 | 27 | const LoadingMessage = styled.div<{ error?: boolean }>` 28 | padding: 1rem; 29 | border: 2px rgba(0, 0, 0, 0.05) solid; 30 | background-color: ${props => (props.error ? "#D36D6D" : "none")}; 31 | `; 32 | 33 | const ErrorGroup = styled.div` 34 | ${({ theme }) => theme.flexRowNoWrap}; 35 | align-items: center; 36 | justify-content: flex-start; 37 | `; 38 | 39 | const ErrorButton = styled.div` 40 | border-radius: 8px; 41 | font-size: 12px; 42 | color: ${({ theme }) => theme.text1}; 43 | background-color: ${({ theme }) => theme.bg4}; 44 | margin-left: 1rem; 45 | padding: 0.5rem; 46 | font-weight: 600; 47 | user-select: none; 48 | `; 49 | 50 | const LoadingWrapper = styled.div` 51 | padding: 0.5rem 0 1rem 0; 52 | `; 53 | 54 | export default function PendingView({ 55 | connector, 56 | error = false, 57 | setPendingError, 58 | tryActivation, 59 | }: { 60 | connector?: AbstractConnector; 61 | error?: boolean; 62 | setPendingError: (error: boolean) => void; 63 | tryActivation: (connector: AbstractConnector) => void; 64 | }) { 65 | const isMetamask = (window?.ethereum as any).isMetaMask; 66 | 67 | return ( 68 | 69 | 70 | {Object.keys(SUPPORTED_WALLETS).map(key => { 71 | const option = SUPPORTED_WALLETS[key]; 72 | if (option.connector === connector) { 73 | if (option.connector === injected) { 74 | if (isMetamask && option.name !== "MetaMask") { 75 | return null; 76 | } 77 | if (!isMetamask && option.name === "MetaMask") { 78 | return null; 79 | } 80 | } 81 | return {option.name}; 82 | } 83 | return null; 84 | })} 85 | 86 | {error ? "(✖╭╮✖)" : "Initializing..."} 87 | 88 | {error && ( 89 | { 91 | setPendingError(false); 92 | connector && tryActivation(connector); 93 | }} 94 | > 95 | Try Again 96 | 97 | )} 98 | 99 | 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /interface/components/split/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { useSplitVault } from "../../hooks/contracts"; 5 | import { useFullTokens } from "../../contexts/tokens"; 6 | import { useAssetAllowance } from "../../contexts/asset-allowances"; 7 | import { useTokenContract } from "../../hooks/contracts"; 8 | import { useFullTokenPrice } from "../../contexts/full-token-prices"; 9 | import { useSplitProtocolAddresses } from "../../contexts/split-addresses"; 10 | import { MAX_INT_256 } from "../../constants"; 11 | 12 | import { 13 | componentTokenAmountToFullTokenAmount, 14 | convertToBaseAmount, 15 | fullTokenAmountToComponentTokenAmount, 16 | } from "../../utils/number"; 17 | 18 | import { TokenInput } from "../input"; 19 | import { H1 } from "../typography"; 20 | import { Dropdown } from "../dropdown"; 21 | import { ConfirmButton, InputContainer } from "../widget"; 22 | import { useTransactionActions } from "../../contexts/transaction"; 23 | import { ApproveTransactionMetadata } from "../../types/app"; 24 | import Decimal from "decimal.js"; 25 | 26 | const SplitContainer = styled.div` 27 | display: flex; 28 | flex-direction: column; 29 | `; 30 | 31 | export interface SplitProps {} 32 | 33 | export const SplitWidget: React.FC = () => { 34 | const { splitVault } = useSplitVault(); 35 | const tokens = useFullTokens(); 36 | const { addTransaction } = useTransactionActions(); 37 | const [selectedTokenIndex, setSelectedTokenIndex] = useState(0); 38 | const [value, setValue] = useState(""); 39 | const selectedToken = tokens[selectedTokenIndex]; 40 | const price = useFullTokenPrice(selectedToken.tokenAddress); 41 | const allowance = useAssetAllowance(selectedToken.tokenAddress); 42 | const tokenContract = useTokenContract(selectedToken.tokenAddress); 43 | const deployment = useSplitProtocolAddresses(); 44 | const baseAmount = convertToBaseAmount(value || "0", selectedToken.decimals); 45 | 46 | const onSplitClick = useCallback(async () => { 47 | if (allowance.lessThan(baseAmount)) { 48 | const tx = await tokenContract.approve(deployment.splitVaultAddress, MAX_INT_256); 49 | addTransaction(tx.hash, { 50 | token: selectedToken, 51 | tokenAmount: new Decimal(MAX_INT_256), 52 | type: "approve", 53 | }); 54 | } 55 | const tx = await splitVault.split(baseAmount.toString(), selectedToken.tokenAddress); 56 | addTransaction(tx.hash, { 57 | fullToken: selectedToken, 58 | fullTokenAmount: baseAmount, 59 | type: "split", 60 | }); 61 | // TODO: clear input on success???? 62 | setValue(""); 63 | }, [value, splitVault, selectedToken, deployment]); 64 | 65 | if (!tokens || !tokens.length || !price) { 66 | return
Please connect your wallet.
; 67 | } 68 | 69 | const dropdownItems = tokens.map(asset => ({ 70 | id: asset.tokenAddress, 71 | displayName: asset.symbol, 72 | })); 73 | 74 | // The price from the price oracle is scaled by 18 decimal places. 75 | const componentTokenValue = fullTokenAmountToComponentTokenAmount( 76 | baseAmount, 77 | price, 78 | selectedToken.userlyingAssetMetaData.decimals, 79 | ) 80 | .toDecimalPlaces(4) 81 | .toString(); 82 | return ( 83 | 84 | 85 |

split

86 | 87 | 88 |

to get

89 |

{componentTokenValue}

90 |

{selectedToken.componentTokens.capitalComponentToken.symbol}

91 |

and

92 |

{componentTokenValue}

93 |

{selectedToken.componentTokens.yieldComponentToken.symbol}

94 |
95 | 96 | Split 97 | 98 |
99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /interface/components/tables/common.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { P } from "../typography"; 3 | 4 | export const TableContainer = styled.div` 5 | width: 100%; 6 | `; 7 | export const HeaderTR = styled.div` 8 | border-bottom: 1px solid white; 9 | `; 10 | export const TR = styled.div` 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | width: 100%; 15 | `; 16 | export const TH = styled.div``; 17 | export const TBody = styled.div``; 18 | export const THead = styled.div``; 19 | export const TCell = styled.div` 20 | padding: 12px 0; 21 | border-bottom: 1px solid white; 22 | flex-grow: 1; 23 | height: 72px; 24 | display: flex; 25 | align-items: center; 26 | `; 27 | 28 | export const TCellHeader = styled(P)` 29 | font-size: 28px; 30 | `; 31 | 32 | export const TCellLabel = styled(P)` 33 | font-size: 14px; 34 | `; 35 | -------------------------------------------------------------------------------- /interface/components/typography.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const H1 = styled.h1` 4 | color: white; 5 | padding: 0; 6 | margin: 0; 7 | font-weight: normal; 8 | font-size: 40px; 9 | `; 10 | 11 | export const H2 = styled.h2` 12 | color: white; 13 | padding: 0; 14 | font-weight: normal; 15 | margin: 0; 16 | `; 17 | 18 | export const H3 = styled.h3` 19 | color: white; 20 | padding: 0; 21 | margin: 0; 22 | font-weight: normal; 23 | font-size: 28px; 24 | `; 25 | 26 | export const H4 = styled.h4` 27 | color: white; 28 | padding: 0; 29 | font-weight: normal; 30 | margin: 0; 31 | `; 32 | 33 | export const P = styled.p` 34 | font-size: 16px; 35 | color: white; 36 | padding: 0; 37 | margin: 0; 38 | `; 39 | 40 | export const PDark = styled(P)` 41 | color: black; 42 | `; 43 | 44 | export const H3Dark = styled(H3)` 45 | color: black; 46 | `; 47 | 48 | export const Faded = styled.span` 49 | color: rgba(255, 255, 255, 0.5); 50 | `; 51 | 52 | export const FadedDark = styled.span` 53 | color: rgba(0, 0, 0, 0.5); 54 | `; 55 | -------------------------------------------------------------------------------- /interface/components/widget.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import { PrimaryButton } from "./button"; 4 | 5 | export const ConfirmButton = styled(PrimaryButton)` 6 | cursor: pointer; 7 | margin-top: 50px; 8 | border-radius: 50%; 9 | width: 200px; 10 | height: 200px; 11 | align-self: center; 12 | font-size: 40px; 13 | `; 14 | 15 | export const InputContainer = styled.div` 16 | display: grid; 17 | grid-template-columns: 1fr 3fr 1fr; 18 | align-items: baseline; 19 | gap: 50px 12px; 20 | width: 800px; 21 | `; 22 | -------------------------------------------------------------------------------- /interface/connectors/Fortmatic.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from "../types/ethereum"; 2 | import { FortmaticConnector as FortmaticConnectorCore } from "@web3-react/fortmatic-connector"; 3 | 4 | export const OVERLAY_READY = "OVERLAY_READY"; 5 | 6 | type FormaticSupportedChains = Extract; 7 | 8 | const CHAIN_ID_NETWORK_ARGUMENT: { readonly [chainId in FormaticSupportedChains]: string | undefined } = { 9 | [ChainId.Mainnet]: undefined, 10 | [ChainId.Ropsten]: "ropsten", 11 | [ChainId.Rinkeby]: "rinkeby", 12 | [ChainId.Kovan]: "kovan", 13 | }; 14 | 15 | export class FortmaticConnector extends FortmaticConnectorCore { 16 | async activate() { 17 | if (!this.fortmatic) { 18 | const { default: Fortmatic } = await import("fortmatic"); 19 | 20 | const { apiKey, chainId } = this as any; 21 | if (chainId in CHAIN_ID_NETWORK_ARGUMENT) { 22 | this.fortmatic = new Fortmatic(apiKey, CHAIN_ID_NETWORK_ARGUMENT[chainId as FormaticSupportedChains]); 23 | } else { 24 | throw new Error(`Unsupported network ID: ${chainId}`); 25 | } 26 | } 27 | 28 | const provider = this.fortmatic.getProvider(); 29 | 30 | const pollForOverlayReady = new Promise(resolve => { 31 | const interval = setInterval(() => { 32 | if (provider.overlayReady) { 33 | clearInterval(interval); 34 | this.emit(OVERLAY_READY); 35 | resolve(); 36 | } 37 | }, 200); 38 | }); 39 | 40 | const [account] = await Promise.all([ 41 | provider.enable().then((accounts: string[]) => accounts[0]), 42 | pollForOverlayReady, 43 | ]); 44 | 45 | return { provider: this.fortmatic.getProvider(), chainId: (this as any).chainId, account }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /interface/connectors/fortmatic.d.ts: -------------------------------------------------------------------------------- 1 | declare module "formatic"; 2 | -------------------------------------------------------------------------------- /interface/connectors/index.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { InjectedConnector } from "@web3-react/injected-connector"; 3 | import { WalletConnectConnector } from "@web3-react/walletconnect-connector"; 4 | import { WalletLinkConnector } from "@web3-react/walletlink-connector"; 5 | import { PortisConnector } from "@web3-react/portis-connector"; 6 | 7 | import { FortmaticConnector } from "./Fortmatic"; 8 | import { NetworkConnector } from "./NetworkConnector"; 9 | 10 | const NETWORK_URL = process.env.NEXT_PUBLIC_NETWORK_URL; 11 | const FORMATIC_KEY = process.env.NEXT_PUBLIC_FORTMATIC_KEY; 12 | const PORTIS_ID = process.env.NEXT_APP_PORTIS_ID; 13 | 14 | export const NETWORK_CHAIN_ID: number = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID ?? "1"); 15 | 16 | if (typeof NETWORK_URL === "undefined") { 17 | throw new Error(`NEXT_PUBLIC_NETWORK_URL must be a defined environment variable`); 18 | } 19 | 20 | export const network = new NetworkConnector({ 21 | urls: { [NETWORK_CHAIN_ID]: NETWORK_URL }, 22 | }); 23 | 24 | type Web3Provider = ethers.providers.Web3Provider; 25 | let networkLibrary: Web3Provider | undefined; 26 | export function getNetworkLibrary(): Web3Provider { 27 | return (networkLibrary = networkLibrary ?? new ethers.providers.Web3Provider(network.provider as any)); 28 | } 29 | 30 | export const injected = new InjectedConnector({ 31 | supportedChainIds: [1, 3, 4, 5, 42], 32 | }); 33 | 34 | // mainnet only 35 | export const walletconnect = new WalletConnectConnector({ 36 | rpc: { 1: NETWORK_URL }, 37 | bridge: "https://bridge.walletconnect.org", 38 | qrcode: true, 39 | pollingInterval: 15000, 40 | }); 41 | 42 | // mainnet only 43 | export const fortmatic = new FortmaticConnector({ 44 | apiKey: FORMATIC_KEY ?? "", 45 | chainId: 1, 46 | }); 47 | 48 | // mainnet only 49 | export const portis = new PortisConnector({ 50 | dAppId: PORTIS_ID ?? "", 51 | networks: [1], 52 | }); 53 | 54 | // mainnet only 55 | export const walletlink = new WalletLinkConnector({ 56 | url: NETWORK_URL, 57 | appName: "Split", 58 | appLogoUrl: 59 | "https://mpng.pngfly.com/20181202/bex/kisspng-emoji-domain-unicorn-pin-badges-sticker-unicorn-tumblr-emoji-unicorn-iphoneemoji-5c046729264a77.5671679315437924251569.jpg", 60 | }); 61 | -------------------------------------------------------------------------------- /interface/constants/index.tsx: -------------------------------------------------------------------------------- 1 | import { AbstractConnector } from "@web3-react/abstract-connector"; 2 | import { MetaMaskIcon } from "../components/icons/metamask"; 3 | import { ArrowRightIcon } from "../components/icons/arrow-right"; 4 | import { WalletConnectIcon } from "../components/icons/wallet-connect"; 5 | import { FortmaticIcon } from "../components/icons/fortmatic"; 6 | 7 | import { fortmatic, injected, walletconnect } from "../connectors"; 8 | import { AppAction } from "../types/app"; 9 | import Decimal from "decimal.js"; 10 | 11 | export const MAX_INT_256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935"; 12 | export const ZERO = new Decimal(0); 13 | 14 | export const PATHS = { 15 | ROOT: "/", 16 | SPLIT: "/split", 17 | }; 18 | 19 | // Only for deployed contracts. 20 | export const CHAIN_ID_NAME = { 21 | 1: "mainnet", 22 | 4: "rinkeby", 23 | }; 24 | 25 | export const APP_PARAM_TO_APP_ACTION = { 26 | split: AppAction.SPLIT, 27 | manage: AppAction.MANAGE, 28 | combine: AppAction.COMBINE, 29 | }; 30 | 31 | export interface WalletInfo { 32 | connector?: AbstractConnector; 33 | name: string; 34 | icon: React.FC; 35 | description: string; 36 | href: string | null; 37 | color: string; 38 | primary?: true; 39 | mobile?: true; 40 | mobileOnly?: true; 41 | } 42 | 43 | export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { 44 | INJECTED: { 45 | connector: injected, 46 | name: "Injected", 47 | icon: () => , 48 | description: "Injected web3 provider.", 49 | href: null, 50 | color: "#010101", 51 | primary: true, 52 | }, 53 | METAMASK: { 54 | connector: injected, 55 | name: "MetaMask", 56 | icon: () => , 57 | description: "Easy-to-use browser extension.", 58 | href: null, 59 | color: "#E8831D", 60 | }, 61 | WALLET_CONNECT: { 62 | connector: walletconnect, 63 | name: "WalletConnect", 64 | icon: () => , 65 | description: "Connect to Trust Wallet, Rainbow Wallet and more...", 66 | href: null, 67 | color: "#4196FC", 68 | mobile: true, 69 | }, 70 | // WALLET_LINK: { 71 | // connector: walletlink, 72 | // name: 'Coinbase Wallet', 73 | // iconName: 'coinbaseWalletIcon.svg', 74 | // description: 'Use Coinbase Wallet app on mobile device', 75 | // href: null, 76 | // color: '#315CF5' 77 | // }, 78 | // COINBASE_LINK: { 79 | // name: 'Open in Coinbase Wallet', 80 | // iconName: 'coinbaseWalletIcon.svg', 81 | // description: 'Open in Coinbase Wallet app.', 82 | // href: 'https://go.cb-w.com/mtUDhEZPy1', 83 | // color: '#315CF5', 84 | // mobile: true, 85 | // mobileOnly: true 86 | // }, 87 | FORTMATIC: { 88 | connector: fortmatic, 89 | name: "Fortmatic", 90 | icon: () => , 91 | description: "Login using Fortmatic hosted wallet", 92 | href: null, 93 | color: "#6748FF", 94 | mobile: true, 95 | }, 96 | }; 97 | 98 | export const NETWORK_URL = process.env.NEXT_PUBLIC_NETWORK_URL || ""; 99 | export const CHAIN_ID = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID ?? "1") || 1; 100 | -------------------------------------------------------------------------------- /interface/contexts/asset-allowances.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { Decimal } from "decimal.js"; 4 | import { useBlockchain } from "./blockchain"; 5 | import { useImmer } from "use-immer"; 6 | import { useTokenContracts } from "../hooks/contracts"; 7 | import { useAllTokens } from "./tokens"; 8 | import { useSplitProtocolAddresses } from "./split-addresses"; 9 | 10 | export interface AssetAllowancesProviderState { 11 | [tokenAddress: string]: Decimal | undefined | null; 12 | } 13 | 14 | export interface AssetAllowancesActionsProviderState { 15 | refreshAllowances: () => void; 16 | } 17 | 18 | const AssetAllowancesContext = React.createContext({}); 19 | 20 | const AssetAllowancesActionContext = React.createContext({ 21 | refreshAllowances: () => new Error("AssetAllowancesAction Provider not set"), 22 | }); 23 | 24 | const AssetAllowancesProvider: React.FC = ({ children }) => { 25 | const { account, chainId, library } = useWeb3React(); 26 | const { blockNum } = useBlockchain(); 27 | const protocolAddresses = useSplitProtocolAddresses(); 28 | const tokens = useAllTokens(); 29 | const tokenAddresses = useMemo(() => tokens.map(t => t.tokenAddress), [tokens]); 30 | const tokenContracts = useTokenContracts(tokenAddresses); 31 | const [assetAllowances, setAssetAllowances] = useImmer({}); 32 | const [refreshCounter, setRefreshCounter] = useState(0); 33 | 34 | useEffect(() => { 35 | if (!account) { 36 | return; 37 | } 38 | for (let tokenContract of tokenContracts) { 39 | tokenContract 40 | .allowance(account, protocolAddresses.splitVaultAddress) 41 | .then(bal => { 42 | setAssetAllowances(draft => { 43 | draft[tokenContract.address] = new Decimal(bal.toString()); 44 | }); 45 | }) 46 | .catch(_ => { 47 | setAssetAllowances(draft => { 48 | draft[tokenContract.address] = null; 49 | }); 50 | }); 51 | } 52 | }, [account, chainId, blockNum, tokenAddresses, refreshCounter]); 53 | 54 | const refreshAllowances = useCallback(() => { 55 | setRefreshCounter(refreshCounter + 1); 56 | }, [setRefreshCounter]); 57 | 58 | return ( 59 | 60 | {children} 61 | 62 | ); 63 | }; 64 | 65 | const useRefreshAllowances = (): (() => void) => { 66 | return React.useContext(AssetAllowancesActionContext).refreshAllowances; 67 | }; 68 | 69 | const useAssetAllowances = (): AssetAllowancesProviderState => { 70 | return React.useContext(AssetAllowancesContext); 71 | }; 72 | 73 | const useAssetAllowance = (tokenAddress: string): Decimal | undefined => { 74 | return React.useContext(AssetAllowancesContext)[tokenAddress]; 75 | }; 76 | 77 | export { AssetAllowancesProvider, useAssetAllowances, useAssetAllowance, useRefreshAllowances }; 78 | -------------------------------------------------------------------------------- /interface/contexts/asset-balances.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { JsonRpcProvider, Web3Provider } from "@ethersproject/providers"; 4 | import { Decimal } from "decimal.js"; 5 | import { useBlockchain } from "./blockchain"; 6 | import { useImmer } from "use-immer"; 7 | import { useTokenContracts } from "../hooks/contracts"; 8 | import { useAllTokens } from "./tokens"; 9 | 10 | export interface AssetBalancesProviderState { 11 | eth: Decimal | undefined | null; 12 | [tokenAddress: string]: Decimal | undefined | null; 13 | } 14 | 15 | const initialState: AssetBalancesProviderState = { 16 | eth: undefined, 17 | }; 18 | 19 | export interface AssetBalancesActionsProviderState { 20 | refreshBalances: () => void; 21 | } 22 | 23 | const AssetBalancesContext = React.createContext(initialState); 24 | 25 | const AssetBalancesActionContext = React.createContext({ 26 | refreshBalances: () => new Error("AssetBalancesActions Provider not set"), 27 | }); 28 | 29 | const AssetBalancesProvider: React.FC = ({ children }) => { 30 | const { account, chainId, library } = useWeb3React(); 31 | const { blockNum } = useBlockchain(); 32 | const tokens = useAllTokens(); 33 | const tokenAddresses = useMemo(() => tokens.map(t => t.tokenAddress), [tokens]); 34 | const tokenContracts = useTokenContracts(tokenAddresses); 35 | const [assetBalances, setAssetBalances] = useImmer(initialState); 36 | const [refreshCounter, setRefreshCounter] = useState(0); 37 | 38 | useEffect(() => { 39 | if (!account) { 40 | return; 41 | } 42 | (library as Web3Provider) 43 | .getBalance(account) 44 | .then(bal => { 45 | setAssetBalances(draft => ({ 46 | ...draft, 47 | eth: new Decimal(bal.toString()), 48 | })); 49 | }) 50 | .catch(_ => { 51 | setAssetBalances(draft => ({ 52 | ...draft, 53 | eth: null, 54 | })); 55 | }); 56 | for (let tokenContract of tokenContracts) { 57 | tokenContract 58 | .balanceOf(account) 59 | .then(bal => { 60 | setAssetBalances(draft => { 61 | draft[tokenContract.address] = new Decimal(bal.toString()); 62 | }); 63 | }) 64 | .catch(_ => { 65 | setAssetBalances(draft => { 66 | draft[tokenContract.address] = null; 67 | }); 68 | }); 69 | } 70 | }, [account, chainId, blockNum, tokenAddresses, refreshCounter]); 71 | 72 | const refreshBalances = useCallback(() => { 73 | setRefreshCounter(refreshCounter + 1); 74 | }, [setRefreshCounter]); 75 | 76 | return ( 77 | 78 | {children} 79 | 80 | ); 81 | }; 82 | 83 | const useAssetBalances = (): AssetBalancesProviderState => { 84 | return React.useContext(AssetBalancesContext); 85 | }; 86 | 87 | const useAssetBalance = (tokenAddress: string): Decimal => { 88 | return React.useContext(AssetBalancesContext)[tokenAddress] || new Decimal(0); 89 | }; 90 | 91 | const useEthBalance = (): Decimal | undefined => { 92 | return React.useContext(AssetBalancesContext).eth; 93 | }; 94 | 95 | export { AssetBalancesProvider, useAssetBalances, useAssetBalance, useEthBalance }; 96 | -------------------------------------------------------------------------------- /interface/contexts/banner.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { BannerMetadata, BannerType, TxBannerMetadata } from "../types/app"; 3 | import { useImmer } from "use-immer"; 4 | 5 | export interface BannerActionsProviderState { 6 | addBanner: (banner: BannerMetadata) => void; 7 | updateBanner: (index: number, changes: Partial) => void; 8 | } 9 | 10 | export type BannerProviderState = BannerMetadata[]; 11 | 12 | const BannerContext = React.createContext([]); 13 | const BannerActionsContext = React.createContext({ 14 | addBanner: () => new Error("BannerProvider not set."), 15 | updateBanner: () => new Error("BannerProvider not set."), 16 | }); 17 | 18 | const BannerProvider: React.FC = ({ children }) => { 19 | const [banners, setBanners] = useImmer([]); 20 | 21 | const addBanner = useCallback( 22 | (banner: BannerMetadata) => { 23 | setBanners(draft => { 24 | draft.push(banner); 25 | }); 26 | }, 27 | [banners, setBanners], 28 | ); 29 | 30 | const updateBanner = useCallback( 31 | (index: number, changes: Partial) => { 32 | if (index < 0 || index >= banners.length) { 33 | return; 34 | } 35 | setBanners(draft => { 36 | draft[index] = { 37 | ...draft[index], 38 | ...changes, 39 | }; 40 | }); 41 | }, 42 | [banners, setBanners], 43 | ); 44 | 45 | return ( 46 | 47 | 53 | {children} 54 | 55 | 56 | ); 57 | }; 58 | 59 | const useBanners = () => { 60 | return React.useContext(BannerContext); 61 | }; 62 | 63 | const useTxsBanners = () => { 64 | const banners = useBanners(); 65 | return banners.filter(b => !!(b as TxBannerMetadata).txHash) as TxBannerMetadata[]; 66 | }; 67 | 68 | const useTxBannerMap = () => { 69 | const txBanners = useTxsBanners(); 70 | return useMemo(() => { 71 | return txBanners.reduce((a: { [txHash: string]: TxBannerMetadata }, c: TxBannerMetadata) => { 72 | return { 73 | ...a, 74 | [c.txHash]: c, 75 | }; 76 | }, {} as { [txHash: string]: TxBannerMetadata }); 77 | }, [txBanners]); 78 | }; 79 | 80 | const useTxBanner = (txHash: string) => { 81 | const txBannerMap = useTxBannerMap(); 82 | return txBannerMap[txHash]; 83 | }; 84 | 85 | const useBannerActions = () => { 86 | return React.useContext(BannerActionsContext); 87 | }; 88 | 89 | const useTxBannerActions = () => { 90 | const bannerActions = useBannerActions(); 91 | const txBanners = useTxsBanners(); 92 | const txHashToIndexMap = useMemo(() => { 93 | return txBanners.reduce((a: { [txHash: string]: number }, c: TxBannerMetadata, i: number) => { 94 | return { 95 | ...a, 96 | [c.txHash]: i + 1, // add one to get around falsy value 97 | }; 98 | }, {} as { [txHash: string]: number }); 99 | }, [txBanners]); 100 | return useMemo( 101 | () => ({ 102 | dismissTxBanner: (txHash: string) => { 103 | if (!txHashToIndexMap[txHash]) { 104 | return; 105 | } 106 | bannerActions.updateBanner(txHashToIndexMap[txHash] - 1, { dismissed: true }); 107 | }, 108 | updateTxBanner: (txHash: string, changes: Partial) => { 109 | if (!txHashToIndexMap[txHash]) { 110 | return; 111 | } 112 | bannerActions.updateBanner(txHashToIndexMap[txHash] - 1, changes); 113 | }, 114 | addPendingTxBanner: (txHash: string, description: string) => { 115 | bannerActions.addBanner({ 116 | dismissed: false, 117 | type: "loading", 118 | description: description, 119 | txHash, 120 | } as TxBannerMetadata); 121 | }, 122 | }), 123 | [txHashToIndexMap, bannerActions], 124 | ); 125 | }; 126 | 127 | export { BannerProvider, useBanners, useTxsBanners, useTxBannerMap, useTxBanner, useBannerActions, useTxBannerActions }; 128 | -------------------------------------------------------------------------------- /interface/contexts/blockchain.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { JsonRpcProvider, Web3Provider } from "@ethersproject/providers"; 4 | import { NETWORK_URL } from "../constants"; 5 | 6 | export interface BlockchainProviderState { 7 | blockNum: number | undefined | null; 8 | } 9 | 10 | const initialState: BlockchainProviderState = { 11 | blockNum: undefined, 12 | }; 13 | 14 | const BlockchainContext = React.createContext(initialState); 15 | 16 | const BlockchainProvider: React.FC = ({ children }) => { 17 | const { chainId } = useWeb3React(); 18 | 19 | const [blockNum, setblockNum] = useState(); 20 | 21 | useEffect(() => { 22 | if (chainId === undefined) { 23 | return; 24 | } 25 | 26 | const provider = new JsonRpcProvider(NETWORK_URL); 27 | 28 | let stale = false; 29 | 30 | // set initial value 31 | provider.getBlockNumber().then((blockNum: number) => { 32 | if (!stale) { 33 | setblockNum(blockNum); 34 | } 35 | }); 36 | 37 | provider.on("block", (blockNum: number) => { 38 | if (stale) { 39 | } 40 | setblockNum(blockNum); 41 | }); 42 | 43 | // remove listener when the component is unmounted 44 | return () => { 45 | provider.removeAllListeners("block"); 46 | setblockNum(undefined); 47 | stale = true; 48 | }; 49 | }, [chainId]); 50 | 51 | const value = useMemo(() => { 52 | return { 53 | blockNum, 54 | }; 55 | }, [blockNum]); 56 | 57 | return {children}; 58 | }; 59 | 60 | const useBlockchain = (): BlockchainProviderState => { 61 | return React.useContext(BlockchainContext); 62 | }; 63 | 64 | export { BlockchainProvider, useBlockchain }; 65 | -------------------------------------------------------------------------------- /interface/contexts/full-token-prices.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { useImmer } from "use-immer"; 3 | import { useWeb3React } from "@web3-react/core"; 4 | import { Decimal } from "decimal.js"; 5 | 6 | import { useCTokenPriceOracle } from "../hooks/contracts"; 7 | 8 | import { useBlockchain } from "./blockchain"; 9 | import { useFullTokens } from "./tokens"; 10 | 11 | export interface FullTokenPricesProviderState { 12 | [tokenAddress: string]: Decimal | undefined | null; 13 | } 14 | 15 | export interface FullTokenPricesActionsProviderState { 16 | refreshPrices: () => void; 17 | } 18 | 19 | const FullTokenPricesContext = React.createContext({}); 20 | 21 | const FullTokenPricesActionContext = React.createContext({ 22 | refreshPrices: () => new Error("FullTokenPricesAction Provider not set"), 23 | }); 24 | 25 | const FullTokenPricesProvider: React.FC = ({ children }) => { 26 | const { account, chainId } = useWeb3React(); 27 | const { blockNum } = useBlockchain(); 28 | const tokens = useFullTokens(); 29 | const tokenAddresses = useMemo(() => tokens.map(t => t.tokenAddress), [tokens]); 30 | const [fullTokenPrices, setFullTokenPrices] = useImmer({}); 31 | const [refreshCounter, setRefreshCounter] = useState(0); 32 | const { priceOracle } = useCTokenPriceOracle(); 33 | 34 | useEffect(() => { 35 | for (let tokenAddress of tokenAddresses) { 36 | priceOracle 37 | .getPrice(tokenAddress) 38 | .then(price => { 39 | setFullTokenPrices(draft => { 40 | draft[tokenAddress] = new Decimal(price.toString()); 41 | }); 42 | }) 43 | .catch(e => { 44 | setFullTokenPrices(draft => { 45 | draft[tokenAddress] = null; 46 | }); 47 | }); 48 | } 49 | }, [account, chainId, blockNum, tokenAddresses, priceOracle, refreshCounter]); 50 | 51 | const refreshPrices = useCallback(() => { 52 | setRefreshCounter(refreshCounter + 1); 53 | }, [setRefreshCounter]); 54 | 55 | return ( 56 | 57 | {children} 58 | 59 | ); 60 | }; 61 | 62 | const useRefreshPrices = (): (() => void) => { 63 | return React.useContext(FullTokenPricesActionContext).refreshPrices; 64 | }; 65 | 66 | const useFullTokenPrices = (): FullTokenPricesProviderState => { 67 | return React.useContext(FullTokenPricesContext); 68 | }; 69 | 70 | const useFullTokenPrice = (tokenAddress: string): Decimal | undefined => { 71 | return React.useContext(FullTokenPricesContext)[tokenAddress]; 72 | }; 73 | 74 | export { FullTokenPricesProvider, useFullTokenPrices, useFullTokenPrice, useRefreshPrices }; 75 | -------------------------------------------------------------------------------- /interface/contexts/modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { AppModal } from "../types/app"; 4 | import { AppComponent } from "next/dist/next-server/lib/router/router"; 5 | 6 | export interface ModalStatesMap { 7 | [appModal: string]: boolean; 8 | } 9 | 10 | export interface AppModalContext { 11 | modalStates: ModalStatesMap; 12 | setModalState: (modalKey: AppModal, state: boolean) => void; 13 | } 14 | 15 | const InitialModalStates = { 16 | [AppModal.WALLET]: false, 17 | }; 18 | 19 | const AppModalContext = React.createContext({ 20 | modalStates: InitialModalStates, 21 | setModalState: () => new Error("Missing AddModalContext"), 22 | }); 23 | 24 | const AppModalProvider: React.FC = ({ children }) => { 25 | const [modalStates, setModalStates] = useState(InitialModalStates); 26 | 27 | const setModalState = (modalKey: AppModal | undefined, state: boolean) => { 28 | if (!modalKey) { 29 | return; 30 | } 31 | if (modalStates[modalKey] === undefined) { 32 | return; 33 | } 34 | const newModalStates = { 35 | ...modalStates, 36 | [modalKey]: state, 37 | }; 38 | setModalStates(newModalStates); 39 | }; 40 | 41 | return ( 42 | 48 | {children} 49 | 50 | ); 51 | }; 52 | 53 | const useAppModal = () => { 54 | return React.useContext(AppModalContext); 55 | }; 56 | 57 | const useModalState = (modalKey: AppModal) => { 58 | const { modalStates } = useAppModal(); 59 | return modalStates[modalKey]; 60 | }; 61 | 62 | const useModalStateActions = (modalKey: AppModal) => { 63 | const { setModalState } = useAppModal(); 64 | return { 65 | openModal: () => setModalState(modalKey, true), 66 | closeModal: () => setModalState(modalKey, false), 67 | }; 68 | }; 69 | 70 | export { AppModalProvider, useAppModal, useModalState, useModalStateActions }; 71 | -------------------------------------------------------------------------------- /interface/contexts/split-addresses.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { JsonRpcProvider, Web3Provider } from "@ethersproject/providers"; 4 | import { deployments, Deployment } from "split-contracts"; 5 | import { CHAIN_ID_NAME } from "../constants"; 6 | 7 | export type SplitProtocolAddressesProviderState = Deployment | undefined; 8 | 9 | const initialState: SplitProtocolAddressesProviderState = undefined; 10 | 11 | const SplitProtocolAddressesContext = React.createContext(initialState); 12 | 13 | // TODO(dave4506) as split tokens become more diverse and dynamically added via governance, this context will need to accomodate for that 14 | const SplitProtocolAddressesProvider: React.FC = ({ children }) => { 15 | const { chainId } = useWeb3React(); 16 | 17 | const value = useMemo(() => { 18 | if (!chainId) { 19 | return deployments.mainnet; 20 | } 21 | return deployments[CHAIN_ID_NAME[chainId]]; 22 | }, [chainId]); 23 | 24 | return {children}; 25 | }; 26 | 27 | const useSplitProtocolAddresses = (): SplitProtocolAddressesProviderState => { 28 | return React.useContext(SplitProtocolAddressesContext); 29 | }; 30 | 31 | export { SplitProtocolAddressesProvider, useSplitProtocolAddresses }; 32 | -------------------------------------------------------------------------------- /interface/contexts/tokens.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { Asset, FullAsset, AssetType } from "../types/split"; 4 | import { AVAILABLE_FULL_TOKENS } from "../data/tokens"; 5 | import { ChainId } from "../types/ethereum"; 6 | 7 | export type TokensProviderState = FullAsset[] | undefined; 8 | 9 | const initialState: TokensProviderState = undefined; 10 | 11 | const FullTokensContext = React.createContext(initialState); 12 | 13 | // TODO(dave4506) as split tokens become more diverse and dynamically added via governance, this context will need to accomodate for that 14 | const TokensProvider: React.FC = ({ children }) => { 15 | const { chainId } = useWeb3React(); 16 | 17 | const tokens = useMemo(() => { 18 | if (!chainId) { 19 | // defaults to providing mainnet? TODO 20 | return AVAILABLE_FULL_TOKENS[ChainId.Mainnet]; 21 | } 22 | return AVAILABLE_FULL_TOKENS[chainId]; 23 | }, [chainId]); 24 | return {children}; 25 | }; 26 | 27 | const useFullTokens = (): TokensProviderState => { 28 | return React.useContext(FullTokensContext); 29 | }; 30 | 31 | const useFullTokensByAddress = (): { [address: string]: FullAsset } | undefined => { 32 | const tokens = useFullTokens(); 33 | const tokensMap = tokens.reduce((a, c) => ({ ...a, [c.tokenAddress]: c }), {} as { [address: string]: FullAsset }); 34 | return tokensMap; 35 | }; 36 | 37 | const useAllTokens = (): Asset[] => { 38 | const fullTokens = useFullTokens(); 39 | const allTokensMemo = useMemo(() => { 40 | const allTokens = []; 41 | for (const fullTokenAddress of Object.keys(fullTokens)) { 42 | const fullToken = fullTokens[fullTokenAddress]; 43 | const { capitalComponentToken, yieldComponentToken } = fullToken.componentTokens; 44 | allTokens.push(fullToken, capitalComponentToken, yieldComponentToken); 45 | } 46 | return allTokens; 47 | }, [fullTokens]); 48 | return allTokensMemo; 49 | }; 50 | 51 | const useTokensByAssetType = (assetType: AssetType): Asset[] | undefined => { 52 | const allTokens = useAllTokens(); 53 | const filteredTokens = useMemo(() => { 54 | return allTokens.filter(a => a.type === assetType); 55 | }, [allTokens]); 56 | return filteredTokens; 57 | }; 58 | 59 | const useAllTokensByAddress = (): { [address: string]: Asset } | undefined => { 60 | const tokens = useAllTokens(); 61 | const tokensMap = useMemo(() => { 62 | return tokens.reduce((a, c) => ({ ...a, [c.tokenAddress]: c }), {} as { [address: string]: Asset }); 63 | }, [tokens]); 64 | return tokensMap; 65 | }; 66 | 67 | const useFullToken = (tokenAddress: string): FullAsset => { 68 | const tokensMap = useFullTokensByAddress(); 69 | return tokensMap[tokenAddress]; 70 | }; 71 | 72 | const useToken = (tokenAddress: string): Asset => { 73 | return useAllTokensByAddress()[tokenAddress]; 74 | }; 75 | 76 | export { 77 | TokensProvider, 78 | useAllTokens, 79 | useFullTokens, 80 | useTokensByAssetType, 81 | useFullTokensByAddress, 82 | useFullToken, 83 | useToken, 84 | }; 85 | -------------------------------------------------------------------------------- /interface/contexts/web3-connection.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | 4 | import { useEagerConnect, useInactiveListener } from "../hooks/wallet"; 5 | 6 | export interface Web3ConnectionContext { 7 | triedEagerConnect: boolean; 8 | } 9 | 10 | const Web3ConnectionContext = React.createContext({ 11 | triedEagerConnect: false, 12 | }); 13 | 14 | const Web3ConnectionProvider: React.FC = ({ children }) => { 15 | const { active, error } = useWeb3React(); 16 | 17 | const [triedEagerConnect, setTriedEagerConnect] = useState(false); 18 | 19 | // try to eagerly connect to an injected provider, if it exists and has granted access already 20 | const triedEager = useEagerConnect(); 21 | 22 | useEffect(() => { 23 | if (triedEager) { 24 | setTriedEagerConnect(triedEager); 25 | } 26 | }, [triedEager]); 27 | 28 | // when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists 29 | useInactiveListener(!triedEager); 30 | 31 | return {children}; 32 | }; 33 | 34 | const useWeb3Connection = () => { 35 | return React.useContext(Web3ConnectionContext); 36 | }; 37 | 38 | export { Web3ConnectionProvider, useWeb3Connection }; 39 | -------------------------------------------------------------------------------- /interface/contexts/yield-balances.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect, useCallback } from "react"; 2 | import { useImmer } from "use-immer"; 3 | import { useWeb3React } from "@web3-react/core"; 4 | import { Decimal } from "decimal.js"; 5 | 6 | import { useYieldTokenContracts } from "../hooks/contracts"; 7 | 8 | import { useBlockchain } from "./blockchain"; 9 | import { useTokensByAssetType } from "./tokens"; 10 | 11 | export interface YieldBalancesProviderState { 12 | [tokenAddress: string]: Decimal | undefined | null; 13 | } 14 | 15 | export interface YieldBalancesActionsProviderState { 16 | refreshYieldBalances: () => void; 17 | } 18 | 19 | const YieldBalancesContext = React.createContext({}); 20 | 21 | const YieldBalancesActionContext = React.createContext({ 22 | refreshYieldBalances: () => new Error("YieldBalancesAction Provider not set"), 23 | }); 24 | 25 | const YieldBalancesProvider: React.FC = ({ children }) => { 26 | const { account, chainId } = useWeb3React(); 27 | const { blockNum } = useBlockchain(); 28 | const tokens = useTokensByAssetType("yield-split"); 29 | const tokenAddresses = useMemo(() => tokens.map(t => t.tokenAddress), [tokens]); 30 | const [yieldBalances, setYieldBalances] = useImmer({}); 31 | const [refreshCounter, setRefreshCounter] = useState(0); 32 | const tokenContracts = useYieldTokenContracts(tokenAddresses); 33 | 34 | useEffect(() => { 35 | if (!account) { 36 | return; 37 | } 38 | for (let tokenContract of tokenContracts) { 39 | tokenContract["calculatePayoutAmount(address)"](account) 40 | .then(balance => { 41 | setYieldBalances(draft => { 42 | draft[tokenContract.address] = new Decimal(balance.toString()); 43 | }); 44 | }) 45 | .catch(e => { 46 | setYieldBalances(draft => { 47 | draft[tokenContract.address] = null; 48 | }); 49 | }); 50 | } 51 | }, [account, chainId, blockNum, tokenAddresses, refreshCounter]); 52 | 53 | const refreshYieldBalances = useCallback(() => { 54 | setRefreshCounter(refreshCounter + 1); 55 | }, [setRefreshCounter]); 56 | 57 | return ( 58 | 59 | {children} 60 | 61 | ); 62 | }; 63 | 64 | const useRefreshOutstandingYield = (): (() => void) => { 65 | return React.useContext(YieldBalancesActionContext).refreshYieldBalances; 66 | }; 67 | 68 | const useYieldBalances = (): YieldBalancesProviderState => { 69 | return React.useContext(YieldBalancesContext); 70 | }; 71 | 72 | const useYieldBalance = (tokenAddress: string): Decimal | undefined => { 73 | return React.useContext(YieldBalancesContext)[tokenAddress]; 74 | }; 75 | 76 | export { 77 | YieldBalancesProvider, 78 | useYieldBalances, 79 | useYieldBalance, 80 | useRefreshOutstandingYield as useRefreshYieldBalances, 81 | }; 82 | -------------------------------------------------------------------------------- /interface/data/tokens.ts: -------------------------------------------------------------------------------- 1 | import { deployments } from "split-contracts"; 2 | 3 | import { ChainId } from "../types/ethereum"; 4 | import { Asset, FullAsset } from "../types/split"; 5 | import { CHAIN_ID_NAME } from "../constants"; 6 | 7 | // NOTE: THIS IS NOT THE WETH TOKEN, it is a metadata object for native eth 8 | export const ETH_TOKEN: Asset = { 9 | tokenAddress: "0x0000000000000000000000000000000000000000", 10 | name: "Ether", 11 | symbol: "ETH", 12 | decimals: 18, 13 | type: "full", 14 | }; 15 | 16 | const assetToFullAsset = (asset: Asset, chainId: ChainId): FullAsset => { 17 | const deployment = deployments[CHAIN_ID_NAME[chainId]]; 18 | if (!deployment) { 19 | throw new Error(`Could not find a deployment for chainId: ${chainId}`); 20 | } 21 | const componentSet = deployment.componentSets[asset.tokenAddress]; 22 | if (!componentSet) { 23 | throw new Error(`Could not find a component set for tokenAddress: ${asset.tokenAddress}`); 24 | } 25 | return { 26 | ...asset, 27 | type: "full", 28 | componentTokens: { 29 | capitalComponentToken: { 30 | tokenAddress: componentSet.capitalComponentTokenAddress, 31 | decimals: 18, 32 | symbol: `c${asset.symbol}`, 33 | name: `Capital ${asset.name}`, 34 | type: "capital-split", 35 | fullTokenAddress: asset.tokenAddress, 36 | userlyingAssetMetaData: asset, 37 | }, 38 | yieldComponentToken: { 39 | tokenAddress: componentSet.yieldComponentTokenAddress, 40 | decimals: 18, 41 | symbol: `y${asset.symbol}`, 42 | name: `Yield ${asset.name}`, 43 | type: "yield-split", 44 | fullTokenAddress: asset.tokenAddress, 45 | userlyingAssetMetaData: asset, 46 | }, 47 | }, 48 | }; 49 | }; 50 | 51 | export const AVAILABLE_FULL_TOKENS: { [chainId: number]: FullAsset[] } = { 52 | 1: [ 53 | assetToFullAsset( 54 | { 55 | tokenAddress: "0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5", 56 | name: "Compound Ether", 57 | symbol: "cETH", 58 | decimals: 8, 59 | type: "full", 60 | userlyingAssetMetaData: { 61 | symbol: "ETH", 62 | name: "Ether", 63 | decimals: 18, 64 | }, 65 | }, 66 | 1, 67 | ), 68 | assetToFullAsset( 69 | { 70 | tokenAddress: "0x6c8c6b02e7b2be14d4fa6022dfd6d75921d90e4e", 71 | name: "Compound Basic Attention Token", 72 | symbol: "cBAT", 73 | decimals: 8, 74 | type: "full", 75 | userlyingAssetMetaData: { 76 | symbol: "BAT", 77 | name: "Basic Attention Token", 78 | decimals: 18, 79 | }, 80 | }, 81 | 1, 82 | ), 83 | assetToFullAsset( 84 | { 85 | tokenAddress: "0x35a18000230da775cac24873d00ff85bccded550", 86 | name: "Compound Uniswap", 87 | symbol: "cUNI", 88 | decimals: 8, 89 | type: "full", 90 | userlyingAssetMetaData: { 91 | symbol: "UNI", 92 | name: "Uniswap", 93 | decimals: 18, 94 | }, 95 | }, 96 | 1, 97 | ), 98 | ], 99 | 4: [ 100 | assetToFullAsset( 101 | { 102 | tokenAddress: "0xebf1a11532b93a529b5bc942b4baa98647913002", 103 | name: "Compound Basic Attention Token", 104 | symbol: "cBAT", 105 | decimals: 8, 106 | type: "full", 107 | userlyingAssetMetaData: { 108 | symbol: "BAT", 109 | name: "Basic Attention Token", 110 | decimals: 18, 111 | }, 112 | }, 113 | 4, 114 | ), 115 | assetToFullAsset( 116 | { 117 | tokenAddress: "0x52201ff1720134bbbbb2f6bc97bf3715490ec19b", 118 | name: "Compound ZRX", 119 | symbol: "cZRX", 120 | decimals: 8, 121 | type: "full", 122 | userlyingAssetMetaData: { 123 | symbol: "ZRX", 124 | name: "0x Protocol Token", 125 | decimals: 18, 126 | }, 127 | }, 128 | 4, 129 | ), 130 | assetToFullAsset( 131 | { 132 | tokenAddress: "0xd6801a1dffcd0a410336ef88def4320d6df1883e", 133 | name: "Compound Ether", 134 | symbol: "cETH", 135 | decimals: 8, 136 | type: "full", 137 | userlyingAssetMetaData: { 138 | symbol: "ETH", 139 | name: "Ether", 140 | decimals: 18, 141 | }, 142 | }, 143 | 4, 144 | ), 145 | assetToFullAsset( 146 | { 147 | tokenAddress: "0x5b281a6dda0b271e91ae35de655ad301c976edb1", 148 | name: "Compound USDC", 149 | symbol: "cUSDC", 150 | decimals: 8, 151 | type: "full", 152 | userlyingAssetMetaData: { 153 | symbol: "USDC", 154 | name: "USD Coin", 155 | decimals: 6, 156 | }, 157 | }, 158 | 4, 159 | ), 160 | ], 161 | }; 162 | -------------------------------------------------------------------------------- /interface/hooks/contracts.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcSigner, Web3Provider } from "@ethersproject/providers"; 2 | import { useWeb3React } from "@web3-react/core"; 3 | import { useMemo } from "react"; 4 | import { 5 | CTokenPriceOracle__factory, 6 | ERC20__factory, 7 | SplitVault__factory, 8 | YieldComponentToken__factory, 9 | } from "split-contracts"; 10 | import { useSplitProtocolAddresses } from "../contexts/split-addresses"; 11 | 12 | // account is not optional 13 | export function getSigner(library: Web3Provider, account: string): JsonRpcSigner { 14 | return library.getSigner(account).connectUnchecked(); 15 | } 16 | 17 | // account is optional 18 | export function getProviderOrSigner(library: Web3Provider, account?: string): Web3Provider | JsonRpcSigner { 19 | return account ? getSigner(library, account) : library; 20 | } 21 | 22 | export const useTokenContract = (tokenAddress: string) => { 23 | const { library, account } = useWeb3React(); 24 | return useMemo(() => { 25 | return ERC20__factory.connect(tokenAddress, getProviderOrSigner(library, account)); 26 | }, [library, account, tokenAddress]); 27 | }; 28 | 29 | export const useTokenContracts = (tokenAddresses: string[]) => { 30 | const { library, account } = useWeb3React(); 31 | return useMemo(() => { 32 | return tokenAddresses.map(ta => ERC20__factory.connect(ta, getProviderOrSigner(library, account))); 33 | }, [library, account, tokenAddresses]); 34 | }; 35 | 36 | export const useSplitVault = () => { 37 | const { library, account, active, error } = useWeb3React(); 38 | const { splitVaultAddress } = useSplitProtocolAddresses(); 39 | const splitVault = useMemo( 40 | () => SplitVault__factory.connect(splitVaultAddress, getProviderOrSigner(library, account)), 41 | [library, account, splitVaultAddress], 42 | ); 43 | return { splitVault, active, error }; 44 | }; 45 | 46 | export const useCTokenPriceOracle = () => { 47 | const { library, account, active, error } = useWeb3React(); 48 | const { priceOracleAddress } = useSplitProtocolAddresses(); 49 | const priceOracle = useMemo( 50 | () => CTokenPriceOracle__factory.connect(priceOracleAddress, getProviderOrSigner(library, account)), 51 | [library, account, priceOracleAddress], 52 | ); 53 | return { priceOracle, active, error }; 54 | }; 55 | 56 | export const useYieldTokenContracts = (tokenAddresses: string[]) => { 57 | const { library, account } = useWeb3React(); 58 | return useMemo(() => { 59 | return tokenAddresses.map(ta => YieldComponentToken__factory.connect(ta, getProviderOrSigner(library, account))); 60 | }, [library, account, tokenAddresses]); 61 | }; 62 | -------------------------------------------------------------------------------- /interface/hooks/useEthToken.ts: -------------------------------------------------------------------------------- 1 | import { ETH_TOKEN } from "../data/tokens"; 2 | 3 | export const useEthToken = () => { 4 | return ETH_TOKEN; 5 | }; 6 | -------------------------------------------------------------------------------- /interface/hooks/useMounted.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | function useMounted() { 4 | const [mounted, setMounted] = useState(false); 5 | useEffect(() => setMounted(true), []); 6 | return mounted; 7 | } 8 | 9 | export { useMounted }; 10 | -------------------------------------------------------------------------------- /interface/hooks/useOnClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from "react"; 2 | 3 | export const useIsOpenUntilOutside = (): [ 4 | boolean, 5 | React.Dispatch>, 6 | React.MutableRefObject, 7 | ] => { 8 | const [isOpen, setIsOpen] = useState(false); 9 | const [node] = useOnClickOutside(isOpen, () => setIsOpen(false)); 10 | return [isOpen, setIsOpen, node]; 11 | }; 12 | 13 | export const useOnClickOutside = (isOpen: boolean, onClickOutside: () => void): [React.MutableRefObject] => { 14 | const node = useRef(); 15 | useEffect(() => { 16 | const handleClickOutside = (e: MouseEvent) => { 17 | if (node && (node as any).current.contains(e.target)) { 18 | // inside click 19 | return; 20 | } 21 | // outside click 22 | onClickOutside(); 23 | }; 24 | if (isOpen) { 25 | document.addEventListener("mousedown", handleClickOutside); 26 | } else { 27 | document.removeEventListener("mousedown", handleClickOutside); 28 | } 29 | 30 | return () => { 31 | document.removeEventListener("mousedown", handleClickOutside); 32 | }; 33 | }, [isOpen, onClickOutside]); 34 | 35 | return [node]; 36 | }; 37 | -------------------------------------------------------------------------------- /interface/hooks/wallet.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { ChainId } from "../types/ethereum"; 3 | import { useWeb3React } from "@web3-react/core"; 4 | import { Web3ReactContextInterface } from "@web3-react/core/dist/types"; 5 | import { useEffect, useState } from "react"; 6 | import { isMobile } from "react-device-detect"; 7 | import { injected } from "../connectors"; 8 | import { useMountedState } from "react-use"; 9 | import { useMounted } from "./useMounted"; 10 | 11 | export const NetworkContextName = "NETWORK"; 12 | 13 | // type Web3Provider = ethers.providers.Web3Provider; 14 | // export function useActiveWeb3React(): Web3ReactContextInterface & { chainId?: ChainId } { 15 | // const context = useWeb3ReactCore(); 16 | // const contextNetwork = useWeb3ReactCore(); 17 | // return context.active ? context : contextNetwork; 18 | // } 19 | 20 | export function useEagerConnect() { 21 | const { activate, active } = useWeb3React(); // specifically using useWeb3ReactCore because of what this hook does 22 | const [tried, setTried] = useState(false); 23 | const isMounted = useMounted(); 24 | 25 | useEffect(() => { 26 | if (!isMounted) { 27 | return; 28 | } 29 | 30 | const attemptActivate = async () => { 31 | const isAuthorized = await injected.isAuthorized(); 32 | if (isAuthorized) { 33 | await activate(injected, undefined, true); 34 | setTried(true); 35 | } else { 36 | if (isMobile && window.ethereum) { 37 | activate(injected, undefined, true).catch(() => { 38 | setTried(true); 39 | }); 40 | } else { 41 | setTried(true); 42 | } 43 | } 44 | }; 45 | attemptActivate(); 46 | }, [activate, isMounted, tried]); // intentionally only running on mount (make sure it's only mounted once :)) 47 | 48 | // if the connection worked, wait until we get confirmation of that to flip the flag 49 | useEffect(() => { 50 | if (!tried && active) { 51 | setTried(true); 52 | } 53 | }, [tried, active]); 54 | 55 | return tried; 56 | } 57 | 58 | /** 59 | * Use for network and injected - logs user in 60 | * and out after checking what network theyre on 61 | */ 62 | export function useInactiveListener(suppress = false) { 63 | const { active, error, activate } = useWeb3React(); // specifically using useWeb3React because of what this hook does 64 | 65 | useEffect(() => { 66 | const ethereum = window.ethereum; 67 | 68 | if (ethereum && ethereum.on && !active && !error && !suppress) { 69 | const handleChainChanged = () => { 70 | // eat errors 71 | activate(injected, undefined, true).catch(error => { 72 | console.error("Failed to activate after chain changed", error); 73 | }); 74 | }; 75 | 76 | const handleAccountsChanged = (accounts: string[]) => { 77 | if (accounts.length > 0) { 78 | // eat errors 79 | activate(injected, undefined, true).catch(error => { 80 | console.error("Failed to activate after accounts changed", error); 81 | }); 82 | } 83 | }; 84 | 85 | ethereum.on("chainChanged", handleChainChanged); 86 | ethereum.on("accountsChanged", handleAccountsChanged); 87 | 88 | return () => { 89 | if (ethereum.removeListener) { 90 | ethereum.removeListener("chainChanged", handleChainChanged); 91 | ethereum.removeListener("accountsChanged", handleAccountsChanged); 92 | } 93 | }; 94 | } 95 | return undefined; 96 | }, [active, error, suppress, activate]); 97 | } 98 | -------------------------------------------------------------------------------- /interface/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /interface/next.config.js: -------------------------------------------------------------------------------- 1 | const withTM = require("next-transpile-modules")(["split-contracts"]); 2 | 3 | module.exports = Object.assign({}, withTM(), { target: "serverless" }); 4 | -------------------------------------------------------------------------------- /interface/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "split-interface", 3 | "version": "1.0.0", 4 | "description": "UI interface to interact with Split Protocol", 5 | "scripts": { 6 | "dev": "next dev --port 4000", 7 | "build": "next build", 8 | "start": "next start", 9 | "test": "echo 'No tests for interface.'" 10 | }, 11 | "author": "me@bydavidsun.com", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@reach/dialog": "^0.11.2", 15 | "@web3-react/core": "^6.1.1", 16 | "@web3-react/fortmatic-connector": "^6.1.6", 17 | "@web3-react/injected-connector": "^6.0.7", 18 | "@web3-react/portis-connector": "^6.1.6", 19 | "@web3-react/walletconnect-connector": "^6.1.6", 20 | "@web3-react/walletlink-connector": "^6.1.6", 21 | "decimal.js": "^10.2.1", 22 | "ethers": "^5.0.17", 23 | "immer": "^7.0.9", 24 | "lodash": "^4.17.20", 25 | "lottie-react": "^2.1.0", 26 | "next": "^10.0.2", 27 | "polished": "^4.0.3", 28 | "react": "^16.14.0", 29 | "react-device-detect": "^1.14.0", 30 | "react-dom": "^16.14.0", 31 | "react-feather": "^2.0.8", 32 | "react-spring": "^8.0.27", 33 | "react-table": "^7.6.0", 34 | "react-use": "^15.3.4", 35 | "react-use-gesture": "^7.0.16", 36 | "styled-components": "^5.2.0", 37 | "use-immer": "^0.4.1" 38 | }, 39 | "devDependencies": { 40 | "@types/lodash": "^4.14.162", 41 | "@types/node": "^14.11.8", 42 | "@types/react": "^16.9.52", 43 | "@types/styled-components": "^5.1.4", 44 | "next-transpile-modules": "^4.1.0", 45 | "typescript": "^4.1.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /interface/pages/[...actionParams].tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from "react"; 2 | import Head from "next/head"; 3 | import styled from "styled-components"; 4 | import findKey from "lodash/findKey"; 5 | import { useRouter } from "next/router"; 6 | import { Footer } from "../components/footer"; 7 | import { SplitWidget } from "../components/split"; 8 | import { Header } from "../components/header"; 9 | import { APP_PARAM_TO_APP_ACTION, PATHS } from "../constants"; 10 | import { HEADER_HEIGHT } from "../components/header/common"; 11 | import { FOOTER_HEIGHT } from "../components/footer/common"; 12 | import { AppAction } from "../types/app"; 13 | import { ManageWidget } from "../components/manage"; 14 | import { CombineWidget } from "../components/combine"; 15 | 16 | const MARGIN_TOP = 120; 17 | 18 | const LayoutContainer = styled.main` 19 | max-width: 1024px; 20 | margin: 0 auto; 21 | height: calc(100vh - ${HEADER_HEIGHT + FOOTER_HEIGHT + MARGIN_TOP}px); 22 | display: flex; 23 | align-items: start; 24 | justify-content: center; 25 | margin-top: ${MARGIN_TOP}px; 26 | `; 27 | 28 | const AppActionsPage: React.FC = () => { 29 | const router = useRouter(); 30 | 31 | const { query } = router; 32 | 33 | const routerProvidedParams = Array.isArray(query.actionParams) ? query.actionParams : [null]; 34 | 35 | const [appActionFromParams] = routerProvidedParams as [string | null | undefined]; 36 | 37 | const [currentAppAction, setAppAction] = useState("split" as AppAction); 38 | 39 | const setAppActionAndShallowPush = useCallback( 40 | (appAction: AppAction) => { 41 | setAppAction(appAction); 42 | const appActionParam = findKey(APP_PARAM_TO_APP_ACTION, a => a === appAction); 43 | router.push(appActionParam, undefined, { shallow: true }); 44 | }, 45 | [router, setAppAction], 46 | ); 47 | 48 | // TODO(dave4506) lift this logic into a proper routing logic with next.js + the constants 49 | useEffect(() => { 50 | if (!appActionFromParams) { 51 | return; 52 | } 53 | 54 | if (!!APP_PARAM_TO_APP_ACTION[appActionFromParams]) { 55 | setAppAction(APP_PARAM_TO_APP_ACTION[appActionFromParams]); 56 | } else { 57 | setAppActionAndShallowPush(AppAction.SPLIT); 58 | } 59 | }, [router, appActionFromParams]); 60 | 61 | const content = useMemo(() => { 62 | if (currentAppAction === AppAction.SPLIT) { 63 | return ; 64 | } 65 | if (currentAppAction === AppAction.MANAGE) { 66 | return ; 67 | } 68 | if (currentAppAction === AppAction.COMBINE) { 69 | return ; 70 | } 71 | return null; 72 | }, [currentAppAction]); 73 | 74 | return ( 75 | <> 76 | 77 | Split – {currentAppAction.toLowerCase()} 78 | 79 |
80 | {content} 81 |