diff --git a/package.json b/package.json index 3479a5cbf..d231055d9 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@across-protocol/across-token": "^0.0.1", "@across-protocol/contracts-v2": "^0.0.34", "@across-protocol/sdk-v2": "^0.1.24", "@datapunt/matomo-tracker-js": "^0.5.1", diff --git a/src/Routes.tsx b/src/Routes.tsx index d4a53ee83..30e92c061 100644 --- a/src/Routes.tsx +++ b/src/Routes.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { Switch, Route, useLocation, useHistory } from "react-router-dom"; + import { Send, Pool, @@ -7,6 +8,7 @@ import { MyTransactions, AllTransactions, Rewards, + Staking, Claim, NotFound, } from "views"; @@ -20,6 +22,8 @@ import { WrongNetworkError, rewardsBannerWarning, generalMaintenanceMessage, + stringValueInArray, + getConfig, } from "utils"; import { ReactComponent as InfoLogo } from "assets/icons/info-24.svg"; import Toast from "components/Toast"; @@ -36,6 +40,7 @@ function useRoutes() { const location = useLocation(); const history = useHistory(); const { error, removeError } = useError(); + const config = getConfig(); // force the user on /pool page if showMigrationPage is active. useEffect(() => { if (enableMigration && location.pathname !== "/pool") { @@ -51,6 +56,7 @@ function useRoutes() { removeError, location, isContractAddress, + config, }; } // Need this component for useLocation hook @@ -61,6 +67,7 @@ const Routes: React.FC = () => { error, removeError, location, + config, isContractAddress, } = useRoutes(); @@ -105,7 +112,23 @@ const Routes: React.FC = () => { - + + { + const poolIdFound = stringValueInArray( + match.params.poolId.toLowerCase(), + config.getPoolSymbols() + ); + + if (poolIdFound) { + return ; + } else { + return ; + } + }} + /> diff --git a/src/assets/acx.svg b/src/assets/acx.svg new file mode 100644 index 000000000..e4e19df02 --- /dev/null +++ b/src/assets/acx.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/airdrop-gift-bg.svg b/src/assets/airdrop-gift-bg.svg new file mode 100644 index 000000000..2f8a224f1 --- /dev/null +++ b/src/assets/airdrop-gift-bg.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/airdrop-waves-bg.svg b/src/assets/airdrop-waves-bg.svg new file mode 100644 index 000000000..20d4913d7 --- /dev/null +++ b/src/assets/airdrop-waves-bg.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/airdrop-x.svg b/src/assets/airdrop-x.svg new file mode 100644 index 000000000..c893ec600 --- /dev/null +++ b/src/assets/airdrop-x.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/check-star-ring-filled.svg b/src/assets/check-star-ring-filled.svg new file mode 100644 index 000000000..c1acce71c --- /dev/null +++ b/src/assets/check-star-ring-filled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/check-star-ring.svg b/src/assets/check-star-ring.svg new file mode 100644 index 000000000..50291292f --- /dev/null +++ b/src/assets/check-star-ring.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/claim-heart-wave.svg b/src/assets/claim-heart-wave.svg new file mode 100644 index 000000000..a59f15815 --- /dev/null +++ b/src/assets/claim-heart-wave.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/claim-pie-chart-wave.svg b/src/assets/claim-pie-chart-wave.svg new file mode 100644 index 000000000..7171ac583 --- /dev/null +++ b/src/assets/claim-pie-chart-wave.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/eth-white.svg b/src/assets/eth-white.svg new file mode 100644 index 000000000..56879e1f6 --- /dev/null +++ b/src/assets/eth-white.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/chevron-left-vector.svg b/src/assets/icons/chevron-left-vector.svg new file mode 100644 index 000000000..0b485ef7c --- /dev/null +++ b/src/assets/icons/chevron-left-vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/heart-crack.svg b/src/assets/icons/heart-crack.svg new file mode 100644 index 000000000..ff49e69a5 --- /dev/null +++ b/src/assets/icons/heart-crack.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/usdc-24.svg b/src/assets/icons/usdc-24.svg new file mode 100644 index 000000000..bac4cac14 --- /dev/null +++ b/src/assets/icons/usdc-24.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/usdc-green-16.svg b/src/assets/icons/usdc-green-16.svg new file mode 100644 index 000000000..4011ca82f --- /dev/null +++ b/src/assets/icons/usdc-green-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/ExternalLink/ExternalLink.tsx b/src/components/ExternalLink/ExternalLink.tsx new file mode 100644 index 000000000..5d8eb1cfc --- /dev/null +++ b/src/components/ExternalLink/ExternalLink.tsx @@ -0,0 +1,47 @@ +import styled from "@emotion/styled"; + +import { ReactComponent as ExternalLink12Icon } from "assets/icons/external-link-12.svg"; + +type Props = { + text: string; + href: string; +}; + +export function ExternalLink(props: Props) { + return ( + + {props.text} + + ); +} + +const Link = styled.a` + display: flex; + align-items: center; + font-size: ${16 / 16}rem; + line-height: ${20 / 16}rem; + font-weight: 500; + text-decoration: none; + color: #e0f3ff; + transition: opacity 0.1s; + cursor: pointer; + + svg { + path { + fill: #e0f3ff; + } + } + + &:hover { + opacity: 0.8; + } + + @media (max-width: 428px) { + font-size: ${14 / 16}rem; + line-height: ${18 / 16}rem; + } +`; + +export const ExternalLinkIcon = styled(ExternalLink12Icon)` + margin: 2px 0 0 4px; +`; diff --git a/src/components/ExternalLink/index.ts b/src/components/ExternalLink/index.ts new file mode 100644 index 000000000..094bf8d13 --- /dev/null +++ b/src/components/ExternalLink/index.ts @@ -0,0 +1 @@ +export { ExternalLink } from "./ExternalLink"; diff --git a/src/components/GlobalStyles/GlobalStyles.tsx b/src/components/GlobalStyles/GlobalStyles.tsx index 1e1dd86ca..4019530f7 100644 --- a/src/components/GlobalStyles/GlobalStyles.tsx +++ b/src/components/GlobalStyles/GlobalStyles.tsx @@ -91,14 +91,14 @@ const globalStyles = css` } html, body { - height: 100%; + min-height: 100vh; } body { background-color: var(--color-gray); color: var(--color-white); } #root { - height: 100%; + min-height: 100vh; isolation: isolate; } // iphone query diff --git a/src/components/Header/Header.styles.tsx b/src/components/Header/Header.styles.tsx index 126f67162..bd4deee6d 100644 --- a/src/components/Header/Header.styles.tsx +++ b/src/components/Header/Header.styles.tsx @@ -71,6 +71,10 @@ export const Item = styled.li` background-color: #e0f3ff; transform: translateX(-50%); } + + svg { + stroke-width: 2px; + } } :hover { @@ -103,3 +107,11 @@ export const MobileNavigation = styled(motion.nav)` margin-left: 8px; } `; + +export const TextWithIcon = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 8px; +`; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 97fe727a7..a28c58b6d 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,5 +1,6 @@ import { useLocation } from "react-router"; import { Link as UnstyledLink } from "react-router-dom"; +import { Gift } from "react-feather"; import Wallet from "../Wallet"; import { Wrapper, @@ -10,6 +11,7 @@ import { Item, WalletWrapper, Spacing, + TextWithIcon, } from "./Header.styles"; import MenuToggle from "./MenuToggle"; import { enableMigration } from "utils"; @@ -23,6 +25,14 @@ const LINKS = !enableMigration { href: "/pool", name: "Pool" }, { href: "/rewards", name: "Rewards" }, { href: "/transactions", name: "Transactions" }, + { + href: "/airdrop", + name: ( + + Airdrop + + ), + }, ] : []; diff --git a/src/components/IconPair/IconPair.tsx b/src/components/IconPair/IconPair.tsx new file mode 100644 index 000000000..959ba217f --- /dev/null +++ b/src/components/IconPair/IconPair.tsx @@ -0,0 +1,40 @@ +import styled from "@emotion/styled"; +import React from "react"; + +type Props = { + MainIcon: React.ReactElement; + SmallIcon?: React.ReactElement; +}; + +export function IconPair(props: Props) { + return ( + + {props.MainIcon} + {props.SmallIcon && ( + {props.SmallIcon} + )} + + ); +} + +const Container = styled.div` + position: relative; +`; + +const MainIconContainer = styled.div` + height: 32px; + width: 32px; +`; + +const SmallIconContainer = styled.div` + position: absolute; + right: -8px; + bottom: -1px; + height: 18px; + width: 18px; + + svg { + border: 2px solid #3e4047; + border-radius: 50%; + } +`; diff --git a/src/components/IconPair/index.ts b/src/components/IconPair/index.ts new file mode 100644 index 000000000..8b5596d24 --- /dev/null +++ b/src/components/IconPair/index.ts @@ -0,0 +1 @@ +export { IconPair } from "./IconPair"; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 4812a41b1..7715f9058 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -127,8 +127,7 @@ const Wrapper = styled.div` display: grid; padding: 0 10px; grid-template-columns: 1fr min(var(--central-content), 100%) 1fr; - min-height: 100%; - height: fit-content; + min-height: 100vh; @media ${QUERIES.tabletAndUp} { padding: 0 30px; } diff --git a/src/components/LayoutV2/LayoutV2.tsx b/src/components/LayoutV2/LayoutV2.tsx new file mode 100644 index 000000000..ae5fa48a6 --- /dev/null +++ b/src/components/LayoutV2/LayoutV2.tsx @@ -0,0 +1,45 @@ +import styled from "@emotion/styled"; +import Footer from "components/Footer"; +import { QUERIESV2 } from "utils"; + +const LayoutV2: React.FC = ({ children }) => { + return ( + + {children} + + + ); +}; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + + /* Subtract to account for header */ + min-height: calc(100vh - 72px); + @media ${QUERIESV2.sm} { + /* Subtract to account for header */ + min-height: calc(100vh - 64px); + } ; +`; + +const InnerWrapper = styled.div` + background-color: transparent; + + max-width: 600px; + width: calc(100% - 24px); + + min-height: fit-content; + + margin: 64px auto 32px; + display: flex; + flex-direction: column; + gap: 16px; + + @media ${QUERIESV2.sm} { + margin: 16px auto; + } +`; + +export default LayoutV2; diff --git a/src/components/LayoutV2/index.ts b/src/components/LayoutV2/index.ts new file mode 100644 index 000000000..eca3852e6 --- /dev/null +++ b/src/components/LayoutV2/index.ts @@ -0,0 +1,2 @@ +import Layout from "./LayoutV2"; +export default Layout; diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx new file mode 100644 index 000000000..ba6f3749c --- /dev/null +++ b/src/components/Loader/Loader.tsx @@ -0,0 +1,23 @@ +import styled from "@emotion/styled"; +import { Loader as LoaderIcon } from "react-feather"; + +type Props = { + size?: number; +}; + +export const Loader = styled(LoaderIcon)` + width: ${({ size = 24 }) => size}px; + height: ${({ size = 24 }) => size}px; + animation: rotation 2s infinite linear; + + @keyframes rotation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(359deg); + } + } +`; + +export default Loader; diff --git a/src/components/Loader/index.tsx b/src/components/Loader/index.tsx new file mode 100644 index 000000000..6594bde9b --- /dev/null +++ b/src/components/Loader/index.tsx @@ -0,0 +1 @@ +export { Loader } from "./Loader"; diff --git a/src/components/index.ts b/src/components/index.ts index b167bbc07..94dd256c6 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,7 +10,9 @@ export { default as SuperHeader } from "./SuperHeader"; export { default as Stepper } from "./Stepper"; export { default as Banner } from "./Banner"; export { default as Sidebar } from "./Sidebar"; +export { default as LayoutV2 } from "./LayoutV2"; export * from "./Box"; export * from "./Buttons"; export * from "./Section"; +export * from "./ExternalLink"; diff --git a/src/data/routes_5_0xA44A832B994f796452e4FaF191a041F791AD8A0A.json b/src/data/routes_5_0xA44A832B994f796452e4FaF191a041F791AD8A0A.json index 6f7451e74..8348bdb05 100644 --- a/src/data/routes_5_0xA44A832B994f796452e4FaF191a041F791AD8A0A.json +++ b/src/data/routes_5_0xA44A832B994f796452e4FaF191a041F791AD8A0A.json @@ -1,6 +1,6 @@ { "hubPoolChain": 5, - "hubPoolAddress": "0xA44A832B994f796452e4FaF191a041F791AD8A0A", + "hubPoolAddress": "0x0e2817C49698cc0874204AeDf7c72Be2Bb7fCD5d", "hubPoolWethAddress": "0x60D4dB9b534EF9260a88b0BED6c486fe13E604Fc", "routes": [ { diff --git a/src/utils/bridge.ts b/src/utils/bridge.ts index 7dfd2b562..686464e5c 100644 --- a/src/utils/bridge.ts +++ b/src/utils/bridge.ts @@ -26,7 +26,7 @@ import { daiLpCushion, } from "./constants"; -import { parseEther, tagAddress } from "./format"; +import { parseEtherLike, tagAddress } from "./format"; import { getConfig } from "utils"; export type Fee = { @@ -105,7 +105,7 @@ export async function getLpFee( ); result.isLiquidityInsufficient = await lpFeeCalculator.isLiquidityInsufficient(l1TokenAddress, amount); - result.total = amount.mul(result.pct).div(parseEther("1")); + result.total = amount.mul(result.pct).div(parseEtherLike("1")); return result; } diff --git a/src/utils/config.ts b/src/utils/config.ts index 2133d0a86..829c6e72b 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -9,6 +9,10 @@ import { } from "@across-protocol/contracts-v2"; import filter from "lodash/filter"; import sortBy from "lodash/sortBy"; +import { + AcceleratingDistributor, + AcceleratingDistributor__factory, +} from "@across-protocol/across-token"; export type Token = constants.TokenInfo & { l1TokenAddress: string; @@ -78,6 +82,9 @@ export class ConfigClient { getHubPoolAddress(): string { return this.config.hubPoolAddress; } + getAcceleratingDistributorAddress(): string { + return "0xbcfbCE9D92A516e3e7b0762AE218B4194adE34b4"; + } getL1TokenAddressBySymbol(symbol: string) { // all routes have an l1Token address, so just find the first symbol that matches const route = this.getRoutes().find((x) => x.fromTokenSymbol === symbol); @@ -89,6 +96,11 @@ export class ConfigClient { const provider = signer ?? constants.getProvider(this.getHubPoolChainId()); return HubPool__factory.connect(address, provider); } + getAcceleratingDistributor(signer?: Signer): AcceleratingDistributor { + const address = this.getAcceleratingDistributorAddress(); + const provider = signer ?? constants.getProvider(this.getHubPoolChainId()); + return AcceleratingDistributor__factory.connect(address, provider); + } filterRoutes(query: Partial): constants.Routes { const cleanQuery: Partial = Object.fromEntries( Object.entries(query).filter((entry) => { @@ -193,6 +205,11 @@ export class ConfigClient { // use token sorting when returning reachable tokens return sortBy(reachableTokens, (token) => this.tokenOrder[token.symbol]); } + getPoolSymbols(): string[] { + const tokenList = this.getTokenList(1); + const poolSymbols = tokenList.map((token) => token.symbol.toLowerCase()); + return poolSymbols; + } } // singleton diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 0ac02c4bd..93808790d 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -14,6 +14,7 @@ import { relayFeeCalculator } from "@across-protocol/sdk-v2"; import KovanRoutes from "data/routes_42_0x8d84F51710dfa9D409027B167371bBd79e0539e5.json"; import MainnetRoutes from "data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json"; import GoerliRoutes from "data/routes_5_0xA44A832B994f796452e4FaF191a041F791AD8A0A.json"; +import { parseEtherLike } from "./format"; /* Chains and Tokens section */ export enum ChainId { @@ -45,6 +46,11 @@ export const QUERIES = { mobileAndDown: `(max-width: ${(BREAKPOINTS.tabletMin - 1) / 16}rem)`, }; +export const QUERIESV2 = { + xs: `(max-width: 400px)`, + sm: `(max-width: 576px)`, +}; + export const COLORS = { gray: { 100: "0deg 0% 89%", @@ -708,6 +714,8 @@ const getQueriesTable = () => { }; }; +export const fixedPointAdjustment = parseEtherLike("1.0"); + export const queriesTable = getQueriesTable(); export const referrerDelimiterHex = "0xd00dfeeddeadbeef"; @@ -716,3 +724,7 @@ export const usdcLpCushion = process.env.REACT_APP_USDC_LP_CUSHION || "0"; export const wethLpCushion = process.env.REACT_APP_WETH_LP_CUSHION || "0"; export const wbtcLpCushion = process.env.REACT_APP_WBTC_LP_CUSHION || "0"; export const daiLpCushion = process.env.REACT_APP_DAI_LP_CUSHION || "0"; + +export function stringValueInArray(value: string, arr: string[]) { + return arr.indexOf(value) !== -1; +} diff --git a/src/utils/format.ts b/src/utils/format.ts index b3e1b3e19..8306680d1 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -1,4 +1,4 @@ -import { ethers } from "ethers"; +import { BigNumber, BigNumberish, ethers } from "ethers"; import assert from "assert"; export function isValidString(s: string | null | undefined | ""): s is string { @@ -84,6 +84,13 @@ export function formatUnits( return smallNumberFormatter(value); } +export function formatUnitsFnBuilder(decimals: number) { + function closure(wei: ethers.BigNumberish) { + return formatUnits(wei, decimals); + } + return closure; +} + export function formatEther(wei: ethers.BigNumberish): string { return formatUnits(wei, 18); } @@ -96,10 +103,48 @@ export function parseUnits(value: string, decimals: number): ethers.BigNumber { return ethers.utils.parseUnits(value, decimals); } -export function parseEther(value: string): ethers.BigNumber { +export function parseUnitsFnBuilder(decimals: number) { + function closure(value: string) { + return parseUnits(value, decimals); + } + return closure; +} + +export function parseEtherLike(value: string): ethers.BigNumber { return parseUnits(value, 18); } +/** + * Checks if a given input is parseable + * @param amount A bignumberish value that will be attempted to be parsed + * @returns A boolean if this value can be parsed + */ +export function isNumberEthersParseable(amount: BigNumberish): boolean { + try { + parseEtherLike(amount.toString()); + return true; + } catch (_e) { + return false; + } +} + +/** + * Returns the formatted number version of a BigNumber value + * @param value A bignumber to be converted via `formatEther` and returned + * @param decimals The number of units to format `value` with. Default: 18 + * @returns `formatEther(value)` as a Number, or NaN if impossible + */ +export function formattedBigNumberToNumber( + value: BigNumber, + decimals: number = 18 +): number { + try { + return Number(formatUnits(value, decimals)); + } catch (_e) { + return Number.NaN; + } +} + export function stringToHex(value: string) { return ethers.utils.hexlify(ethers.utils.toUtf8Bytes(value)); } diff --git a/src/utils/math.ts b/src/utils/math.ts index b078a7f82..e14d1c130 100644 --- a/src/utils/math.ts +++ b/src/utils/math.ts @@ -1,5 +1,6 @@ import type { BridgeFees } from "./bridge"; import { BigNumber, BigNumberish } from "ethers"; +import { isNumberEthersParseable } from "./format"; export function max(a: BigNumberish, b: BigNumberish) { if (BigNumber.from(a).gte(b)) return BigNumber.from(a); @@ -9,3 +10,48 @@ export function max(a: BigNumberish, b: BigNumberish) { export function receiveAmount(amount: BigNumber, fees: BridgeFees) { return max(amount.sub(fees.relayerFee.total).sub(fees.lpFee.total), 0); } + +export function safeDivide(numerator: BigNumber, divisor: BigNumber) { + if (divisor.isZero()) { + throw new Error( + `Cannot divide by zero. Attempting to divide ${numerator.toString()} by 0` + ); + } + return numerator.div(divisor); +} + +/** + * Tests whether a BigNumberish is within range of a maximum and minimum + * @param value The value to test the range + * @param inclusive Whether this range is inclusive or exclusive of the range + * @param minimum The minimum of the range + * @param maximum The maximum of the range + * @param parser An optional parser to parse the `value` into a BigNumber. Defaults to `BigNumber.from` + * @returns `true` if `value` is valid and within range of the `maximum` and `minimum` + */ +export function isNumericWithinRange( + value: BigNumberish, + inclusive: boolean, + minimum?: BigNumberish, + maximum?: BigNumberish, + parser: (arg0: string) => BigNumber = BigNumber.from +): boolean { + if (!isNumberEthersParseable(value)) return false; + const valueBN = parser(value.toString()); + const min = (v: BigNumberish) => (inclusive ? valueBN.gte(v) : valueBN.gt(v)); + const max = (v: BigNumberish) => (inclusive ? valueBN.lte(v) : valueBN.lt(v)); + + if (maximum && minimum) { + // Both min and max are defined + return min(minimum) && max(maximum); + } else if (!maximum && minimum) { + // minimum is only defined + return min(minimum); + } else if (maximum && !minimum) { + // maximum is only defined + return max(maximum); + } else { + // no range is defined. value is always within range of -inf, inf + return true; + } +} diff --git a/src/utils/ternary.ts b/src/utils/ternary.ts new file mode 100644 index 000000000..0830bddbb --- /dev/null +++ b/src/utils/ternary.ts @@ -0,0 +1,24 @@ +/** + * Creates a reusable ternary operation function. + * @param expression The logical expression to test the ternary. For example, the variable A in -> A ? B : C + * @param fallbackValue The Else value in a ternary. For example, the variable C in -> A ? B : C + * @returns A function that emulates a ternary operation. This closure takes the truthy return and evaluates the ternary. + */ +export function repeatableTernaryBuilder( + expression: boolean, + fallbackValue: Type +) { + /** + * Represents a ternary operation. + * @param value The returned in a ternary if the expression is true. For example, the variable B in -> A ? B : C + * @returns `value` if `expression` is true and `value` is defined, else `fallbackValue` + */ + function closure(value?: Type): Type { + if (expression && value) { + return value; + } else { + return fallbackValue; + } + } + return closure; +} diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 000000000..ec584c619 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,14 @@ +import { Theme } from "@emotion/react"; +import { StyledComponent } from "@emotion/styled"; + +export type StylizedSVG = StyledComponent< + React.SVGProps & { + title?: string | undefined; + } & { + children?: React.ReactNode; + } & { + theme?: Theme | undefined; + }, + {}, + {} +>; diff --git a/src/views/Claim/Claim.styles.tsx b/src/views/Claim/Claim.styles.tsx index 047abf3fe..e7f7cb962 100644 --- a/src/views/Claim/Claim.styles.tsx +++ b/src/views/Claim/Claim.styles.tsx @@ -1,15 +1,114 @@ import styled from "@emotion/styled"; -export const Wrapper = styled.div` - background-color: transparent; +import { ButtonV2 } from "components"; +import { QUERIESV2 } from "utils/constants"; + +export const PageContainer = styled.div` + background-color: #2d2e33; + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: calc(100vh - 72px); + @media (max-width: 428px) { + min-height: calc(100vh - 64px); + } +`; + +export const BodyContainer = styled.div` + color: #e0f3ff; + max-width: 600px; - margin: 64px auto 30px; + width: 100%; + margin: 64px auto; + + @media (max-width: 630px) { + padding-left: 16px; + padding-right: 16px; + } + + @media ${QUERIESV2.sm} { + margin: 48px auto; + } + + h1, + h2, + h3, + h4, + h5, + h6, + p { + font-style: normal; + font-weight: 400; + } + + h1 { + font-size: ${26 / 16}rem; + line-height: ${31 / 16}rem; + + @media ${QUERIESV2.sm} { + font-size: ${22 / 16}rem; + line-height: ${26 / 16}rem; + } + } + + h2 { + font-size: ${22 / 16}rem; + line-height: ${26 / 16}rem; + + @media ${QUERIESV2.sm} { + font-size: ${18 / 16}rem; + line-height: ${26 / 16}rem; + } + } + + h6 { + font-size: ${18 / 16}rem; + line-height: ${26 / 16}rem; + + @media ${QUERIESV2.sm} { + font-size: ${16 / 16}rem; + line-height: ${20 / 16}rem; + } + } + + p { + @media ${QUERIESV2.sm} { + font-size: ${14 / 16}rem; + line-height: ${18 / 16}rem; + } + } + + a { + :hover { + cursor: pointer; + } + + :visited { + color: inherit; + } + } `; export const Title = styled.h2` - font-family: "Barlow"; - font-style: normal; - font-weight: 400; - font-size: ${22 / 16} rem; - line-height: ${26 / 16} rem; + padding-left: 16px; + padding-bottom: 24px; + + @media ${QUERIESV2.sm} { + padding-bottom: 12px; + } +`; + +export const Button = styled(ButtonV2)` + @media ${QUERIESV2.sm} { + font-size: ${16 / 16}rem; + line-height: ${20 / 16}rem; + padding: 10px 20px; + } +`; + +export const FullWidthButton = styled(Button)` + display: flex; + justify-content: center; + gap: 8px; + width: 100%; `; diff --git a/src/views/Claim/Claim.tsx b/src/views/Claim/Claim.tsx index d35bbb2b8..36539f945 100644 --- a/src/views/Claim/Claim.tsx +++ b/src/views/Claim/Claim.tsx @@ -1,10 +1,56 @@ -import { Wrapper, Title } from "./Claim.styles"; -const Claim = () => { +import Footer from "components/Footer"; + +import { DisconnectedWallet } from "./components/DisconnectedWallet"; +import { NotEligibleWallet } from "./components/NotEligibleWallet"; +import { EligibleWallet } from "./components/EligibleWallet"; +import { useClaimView } from "./hooks/useClaimView"; + +import { PageContainer, BodyContainer, Title } from "./Claim.styles"; + +export function Claim() { + const { + isConnected, + airdropRecipientQuery, + handleClaim, + handleConnectWallet, + handleAddTokenToWallet, + hasClaimedState, + claimState, + } = useClaimView(); + + const isEligible = + !airdropRecipientQuery.isLoading && airdropRecipientQuery.data; + return ( - - Airdrop - + + + Airdrop + {!isConnected || airdropRecipientQuery.isLoading ? ( + + ) : !isEligible ? ( + + ) : ( + + )} + + + ); -}; +} export default Claim; diff --git a/src/views/Claim/components/Card.tsx b/src/views/Claim/components/Card.tsx new file mode 100644 index 000000000..f53a322c7 --- /dev/null +++ b/src/views/Claim/components/Card.tsx @@ -0,0 +1,25 @@ +import styled from "@emotion/styled"; + +import { QUERIESV2 } from "utils/constants"; + +export const Card = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + padding: 24px; + + background: #34353b; + + border: 1px solid #3e4047; + border-radius: 10px; + + @media ${QUERIESV2.sm} { + padding: 16px; + } +`; + +export const LightCard = styled(Card)` + background-color: #3e4047; + border-color: #4c4e57; +`; diff --git a/src/views/Claim/components/ClaimAirdrop.tsx b/src/views/Claim/components/ClaimAirdrop.tsx new file mode 100644 index 000000000..e58c2847d --- /dev/null +++ b/src/views/Claim/components/ClaimAirdrop.tsx @@ -0,0 +1,214 @@ +import styled from "@emotion/styled"; + +import { ReactComponent as ClaimHeartWave } from "assets/claim-heart-wave.svg"; +import { ReactComponent as AcrossIcon } from "assets/acx.svg"; +import { Loader } from "components/Loader"; +import { formatUnits } from "utils/format"; + +import { LightCard } from "./Card"; +import { Button, FullWidthButton } from "../Claim.styles"; +import { QUERIESV2 } from "utils"; + +const DECIMALS = 18; + +export type Props = { + isClaiming?: boolean; + isLoading?: boolean; + hasClaimed?: boolean; + amount?: string; + amountBreakdown?: { + liquidity: string; + bridging: string; + community: string; + }; + onClickClaim: () => void; + onClickAddToken: () => void; +}; + +export function ClaimAirdrop({ + isClaiming, + isLoading, + hasClaimed, + amount, + amountBreakdown, + onClickAddToken, + onClickClaim, +}: Props) { + return ( + + + + You're awesome! + + We want to thank you for being part of the community powering the + cheapest, fastest and most secure cross-bridge protocol. + + + + Airdrop breakdown + + + Liquidity providing + + {formatUnits(amountBreakdown?.liquidity || 0, DECIMALS)} ACX + + + + Bridging activity + {formatUnits(amountBreakdown?.bridging || 0, DECIMALS)} ACX + + + Community reward + + {formatUnits(amountBreakdown?.community || 0, DECIMALS)} ACX + + + + Total reward + {formatUnits(amount || 0, DECIMALS)} ACX + + + + {isLoading ? ( + + + + ) : hasClaimed ? ( + + + + Claimed tokens + {formatUnits(amount || 0, DECIMALS)} ACX + + + Add token to wallet + + + ) : ( + + {isClaiming ? ( + <> + Claiming airdrop... + > + ) : ( + "Claim airdrop" + )} + + )} + + ); +} + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 48px; + + @media ${QUERIESV2.sm} { + gap: 32px; + } +`; + +const ClaimHeartImage = styled(ClaimHeartWave)` + height: 200px; + width: 218px; + margin-top: 48px; +`; + +const TextContainer = styled.div` + text-align: center; + h6 { + margin-top: 16px; + color: #c5d5e0; + } +`; + +const BreakdownCardContainer = styled(LightCard)` + display: flex; + align-self: stretch; + flex-direction: column; + padding: 0; + + @media ${QUERIESV2.sm} { + padding: 0; + } +`; + +const BreakdownTitle = styled.h6` + align-self: stretch; + padding: 16px 24px; + + @media ${QUERIESV2.sm} { + padding: 12px; + } +`; + +const BreakdownStats = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + align-self: stretch; + padding: 16px 24px; + border-top: 1px solid #4c4e57; + + @media ${QUERIESV2.sm} { + padding: 12px; + } +`; + +const BreakdownRow = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + color: #9daab2; +`; + +const BreakdownTotalRow = styled(BreakdownRow)` + border-top: 1px solid #4c4e57; + margin-top: 8px; + padding-top: 16px; + color: inherit; +`; + +const AddTokenToWalletContainer = styled.div` + display: flex; + align-self: stretch; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 16px; + svg { + height: 48px; + width: 48px; + } + + @media ${QUERIESV2.sm} { + flex-direction: column; + text-align: center; + } +`; + +const InverseButton = styled(Button)` + border: 1px solid #6cf9d8; + color: #6cf9d8; + background: transparent; +`; + +const ClaimedTokensContainer = styled.div` + flex: 1; + h6 { + color: #9daab2; + } + h2 { + color: inherit; + } +`; + +const CenteredLoaderContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + height: 66px; +`; diff --git a/src/views/Claim/components/DisconnectedWallet.tsx b/src/views/Claim/components/DisconnectedWallet.tsx new file mode 100644 index 000000000..54d118e86 --- /dev/null +++ b/src/views/Claim/components/DisconnectedWallet.tsx @@ -0,0 +1,53 @@ +import styled from "@emotion/styled"; + +import AirdropBackground from "assets/airdrop-gift-bg.svg"; +import { ExternalLink } from "components/ExternalLink"; + +import { Card } from "./Card"; +import { Button } from "../Claim.styles"; +import { QUERIESV2 } from "utils"; + +type Props = { + onClickConnect?: () => void; + isCheckingEligibility?: boolean; +}; + +export function DisconnectedWallet(props: Props) { + return ( + + + {props.isCheckingEligibility + ? "Checking your eligibility..." + : "Connect to check eligibility"} + + + + + + ); +} + +const ContainerCard = styled(Card)` + background-position: center; + background-image: url(${AirdropBackground}); + background-size: cover; + + height: 348px; + display: flex; + flex-direction: column; + justify-content: flex-end; + + @media ${QUERIESV2.sm} { + height: 280px; + } +`; + +const LinkContainer = styled.div` + margin-top: 24px; + margin-bottom: 24px; + z-index: 10; +`; diff --git a/src/views/Claim/components/EarnOptionCard.tsx b/src/views/Claim/components/EarnOptionCard.tsx new file mode 100644 index 000000000..89024ec0b --- /dev/null +++ b/src/views/Claim/components/EarnOptionCard.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import styled from "@emotion/styled"; + +import { IconPair } from "components/IconPair"; +import { PopperTooltip } from "components/Tooltip"; +import { ReactComponent as InfoIcon } from "assets/info.svg"; +import { QUERIESV2 } from "utils/constants"; + +import { LightCard } from "./Card"; +import { Button } from "../Claim.styles"; + +export function EarnOptionCard(props: { + title: string; + subTitle: string; + buttonLabel: string; + href: string; + apyRange: number[]; + MainIcon: React.ReactElement; + SmallIcon?: React.ReactElement; +}) { + return ( + + + + + + + {props.title} + {props.subTitle} + + + {props.buttonLabel} + + APY: {props.apyRange[0]} — {props.apyRange[1]}%{" "} + + + + + + ); +} + +const Container = styled(LightCard)` + width: 100%; + display: flex; + flex-direction: column; + gap: 32px; + + @media ${QUERIESV2.sm} { + gap: 24px; + } +`; + +const EarnOptionTopContainer = styled.div` + display: flex; + width: 100%; + flex-direction: row; + align-items: center; + justify-content: flex-start; +`; + +const IconPairContainer = styled.div` + margin-right: 18px; +`; + +const FullWidthButton = styled(Button)` + width: 100%; +`; + +const ApyContainer = styled.div` + border-top: 1px solid #4c4e57; + width: 100%; + padding-top: 16px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 8px; + line-height: ${20 / 16}rem; +`; diff --git a/src/views/Claim/components/EligibleWallet.tsx b/src/views/Claim/components/EligibleWallet.tsx new file mode 100644 index 000000000..690f8a788 --- /dev/null +++ b/src/views/Claim/components/EligibleWallet.tsx @@ -0,0 +1,78 @@ +import styled from "@emotion/styled"; +import { useState } from "react"; + +import { ReactComponent as PieChartWave } from "assets/claim-pie-chart-wave.svg"; + +import { StepCard } from "./StepCard"; +import { ClaimAirdrop, Props as ClaimAirdropProps } from "./ClaimAirdrop"; +import { WaysToEarn } from "./WaysToEarn"; + +type Props = ClaimAirdropProps; + +const StepIndex = { + CLAIM: 0, + EARN: 1, +}; +const totalNumSteps = Object.keys(StepIndex).length; + +export function EligibleWallet(props: Props) { + const [expandedStepIndex, setExpandedStepIndex] = useState(StepIndex.CLAIM); + + const toggleExpandedStep = (stepIndex: number) => { + setExpandedStepIndex((expandedStepIndex) => + expandedStepIndex === stepIndex + ? (stepIndex + 1) % totalNumSteps + : stepIndex + ); + }; + + const activeStepIndex = props.hasClaimed ? StepIndex.EARN : StepIndex.CLAIM; + + return ( + + + + + + <> + + + + + > + + + ); +} + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const Step2ImageContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +const PieChartWaveImage = styled(PieChartWave)` + height: 200px; + width: 180px; + margin: 48px; + align-self: center; + display: flex; +`; diff --git a/src/views/Claim/components/NotEligibleWallet.tsx b/src/views/Claim/components/NotEligibleWallet.tsx new file mode 100644 index 000000000..586bca41a --- /dev/null +++ b/src/views/Claim/components/NotEligibleWallet.tsx @@ -0,0 +1,55 @@ +import styled from "@emotion/styled"; + +import { ReactComponent as AirdropXIcon } from "assets/airdrop-x.svg"; +import AirdropWavesBackground from "assets/airdrop-waves-bg.svg"; + +import { Card } from "./Card"; +import { WaysToEarn } from "./WaysToEarn"; + +export function NotEligibleWallet() { + return ( + + + + + This wallet hasn't executed any of the required actions to earn an + airdrop and thus have nothing to claim. Learn how the airdrop was + distributed by visiting our FAQ. + + + + + + + ); +} + +const Text = styled.h6` + z-index: 1; + text-align: center; +`; + +const InfoCard = styled(Card)` + background-position: center; + background-image: url(${AirdropWavesBackground}); + background-size: cover; + + height: 302px; + display: flex; + flex-direction: column; + justify-content: space-around; +`; + +const Icon = styled(AirdropXIcon)` + z-index: 10; +`; + +const Container = styled.div` + flex-direction: column; + display: flex; + gap: 24px; +`; + +const WaysToEarnCard = styled(Card)` + padding-top: 46px; +`; diff --git a/src/views/Claim/components/StepCard.tsx b/src/views/Claim/components/StepCard.tsx new file mode 100644 index 000000000..08443de21 --- /dev/null +++ b/src/views/Claim/components/StepCard.tsx @@ -0,0 +1,118 @@ +import styled from "@emotion/styled"; +import React from "react"; +import { ChevronUp, ChevronDown } from "react-feather"; +import { motion, AnimatePresence } from "framer-motion"; + +import { ReactComponent as CheckIcon } from "assets/check-star-ring.svg"; +import { ReactComponent as CheckFilledIcon } from "assets/check-star-ring-filled.svg"; + +import { Card } from "./Card"; +import { QUERIESV2 } from "utils"; + +type Props = { + children: React.ReactElement; + onClickTopRow: (stepIndex: number) => void; + stepIndex: number; + activeStepIndex: number; + expandedStepIndex: number; + title: string; +}; + +export function StepCard(props: Props) { + const isExpanded = props.expandedStepIndex === props.stepIndex; + const isStepCompleted = props.activeStepIndex > props.stepIndex; + + const Chevron = isExpanded ? ChevronUp : ChevronDown; + + const handleClickTopRow = () => { + props.onClickTopRow(props.stepIndex); + }; + + return ( + + + + {isStepCompleted ? : } + + + Step {props.stepIndex + 1} of 2 + {props.title} + + + + + {isExpanded && ( + + {props.children} + + )} + + + ); +} + +const Container = styled(Card)` + display: flex; + flex-direction: column; +`; + +const TopRow = styled.div` + display: flex; + flex-direction: row; + align-items: center; + align-self: stretch; + gap: 12px; + cursor: pointer; + + h6 { + margin-bottom: 4px; + } + + @media ${QUERIESV2.sm} { + h6 { + margin-bottom: 0; + } + + > svg { + width: 16px; + height: 16px; + } + } +`; + +const CheckIconContainer = styled.div` + height: 48px; + width: 48px; + + @media ${QUERIESV2.sm} { + height: 40px; + width: 40px; + } +`; + +const TopRowTextContainer = styled.div` + flex: 1; + + h6 { + color: #9daab2; + } +`; + +const BodyContainer = styled.div` + border-top: 1px solid #3e4047; + margin-top: 24px; + + @media ${QUERIESV2.sm} { + margin-top: 16px; + } +`; diff --git a/src/views/Claim/components/WaysToEarn.tsx b/src/views/Claim/components/WaysToEarn.tsx new file mode 100644 index 000000000..44f15bd76 --- /dev/null +++ b/src/views/Claim/components/WaysToEarn.tsx @@ -0,0 +1,60 @@ +import styled from "@emotion/styled"; + +import { ReactComponent as EthIcon } from "assets/eth-white.svg"; +import { ReactComponent as AcxIcon } from "assets/acx.svg"; + +import { EarnOptionCard } from "./EarnOptionCard"; + +const OPTIONS = [ + { + MainIcon: , + SmallIcon: , + title: "ACX/ETH Liquidity", + subTitle: "Provide liquidity in the ACX/ETH pool and stake your LP tokens", + buttonLabel: "Pool ACX/ETH", + href: "/", // TODO: replace with Uniswap link ACT/ETH + apyRange: [5, 6], + }, + { + MainIcon: , + title: "Stake Across LP Tokens", + subTitle: "Provide liquidity on Across and stake your LP tokens", + buttonLabel: "Stake", + href: "/rewards/staking", + apyRange: [5, 6], + }, +]; + +export function WaysToEarn() { + return ( + <> + More ways to earn ACX + + Did you know that you can provide liquidity to earn ACX? Across offers + reward locking which increases your APY the longer you provide + liquidity. + + + {OPTIONS.map((option) => ( + + ))} + + > + ); +} + +const Title = styled.h1` + text-align: center; + margin-bottom: ${16 / 16}rem; +`; + +const SubTitle = styled.h6` + text-align: center; + margin-bottom: 48px; +`; + +const OptionsContainer = styled.div` + gap: 16px; + display: flex; + flex-direction: column; +`; diff --git a/src/views/Claim/hooks/useAirdropRecipient.tsx b/src/views/Claim/hooks/useAirdropRecipient.tsx new file mode 100644 index 000000000..2812664af --- /dev/null +++ b/src/views/Claim/hooks/useAirdropRecipient.tsx @@ -0,0 +1,40 @@ +import axios from "axios"; +import { useQuery } from "react-query"; + +import { useConnection } from "state/hooks"; + +type RecipientsWithProof = { + [address: string]: { + accountIndex: number; + amount: string; + proof: string[]; + metadata: { + amountBreakdown: { + liquidity: string; + bridging: string; + community: string; + }; + }; + }; +}; + +export function useAirdropRecipient() { + const { isConnected, account } = useConnection(); + + return useQuery(["airdrop", account], () => getAirdropRecipient(account), { + enabled: isConnected && !!account, + }); +} + +async function getAirdropRecipient(account?: string) { + if (!account) { + return undefined; + } + + const { data } = await axios.get( + // TODO: replace with IPFS gateway url + "https://gist.githubusercontent.com/dohaki/53b241c26793260b6423fa1706e4cb96/raw/75bf9bb6e5adae3b059630de1b08b5db6d003959/recipients.json" + ); + + return data[account]; +} diff --git a/src/views/Claim/hooks/useClaimView.tsx b/src/views/Claim/hooks/useClaimView.tsx new file mode 100644 index 000000000..8dfb466a8 --- /dev/null +++ b/src/views/Claim/hooks/useClaimView.tsx @@ -0,0 +1,37 @@ +import { useConnection } from "state/hooks"; +import { onboard } from "utils"; + +import { useMerkleDistributor } from "./useMerkleDistributor"; +import { useAirdropRecipient } from "./useAirdropRecipient"; + +export function useClaimView() { + const { init } = onboard; + const { isConnected, provider } = useConnection(); + + const airdropRecipientQuery = useAirdropRecipient(); + const { handleClaim, claimState, hasClaimedState } = useMerkleDistributor(); + + const handleAddTokenToWallet = async () => { + if (provider) { + await (provider as any).send("wallet_watchAsset", { + type: "ERC20", + options: { + address: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", // TODO + symbol: "ACX", + decimals: 18, + image: "https://foo.io/token-image.svg", // TODO + }, + }); + } + }; + + return { + handleConnectWallet: init, + handleAddTokenToWallet, + isConnected, + airdropRecipientQuery, + handleClaim, + claimState, + hasClaimedState, + }; +} diff --git a/src/views/Claim/hooks/useMerkleDistributor.tsx b/src/views/Claim/hooks/useMerkleDistributor.tsx new file mode 100644 index 000000000..521d242e6 --- /dev/null +++ b/src/views/Claim/hooks/useMerkleDistributor.tsx @@ -0,0 +1,126 @@ +import { useState, useEffect } from "react"; +// import { Contract, providers } from "ethers"; + +import { useConnection } from "state/hooks"; + +type ClaimState = + | { + status: "idle"; + } + | { + status: "pending"; + } + | { + status: "pendingTx"; + txHash: string; + } + | { + status: "success"; + txHash: string; + } + | { + status: "error"; + error: Error; + }; + +type HasClaimedState = + | { + status: "idle"; + } + | { + status: "pending"; + } + | { + status: "success"; + hasClaimed: boolean; + } + | { + status: "error"; + error: Error; + }; + +// const merkleDistributorContract = new Contract( +// process.env.MERKLE_DISTRIBUTOR_ADDRESS || "", +// [ +// "function hasClaimed(address) public view returns (bool)", +// "function claim(address to, uint256 amount, bytes32[] calldata proof) external", +// ] +// ); + +export function useMerkleDistributor() { + const { provider, account } = useConnection(); + + const [claimState, setClaimState] = useState({ + status: "idle", + }); + const [hasClaimedState, setHasClaimedState] = useState({ + status: "idle", + }); + + useEffect(() => { + if (account && provider) { + setHasClaimedState({ status: "pending" }); + // merkleDistributorContract + // .connect(provider) + // .hasClaimed(account) + // .then((hasClaimed) => + // setHasClaimedState({ status: "success", hasClaimed }) + // ) + // .catch((error) => setHasClaimedState({ status: "error", error })); + mockedHasClaimed() + .then((hasClaimed) => + setHasClaimedState({ status: "success", hasClaimed }) + ) + .catch((error) => setHasClaimedState({ status: "error", error })); + } + }, [provider, account]); + + const handleClaim = async () => { + try { + if (!provider || !account) { + throw new Error("No wallet connected"); + } + + setClaimState({ status: "pending" }); + // const [amount, proof] = getClaimAmountAndProof(account); + // const txResponse: providers.TransactionResponse = + // await merkleDistributorContract + // .connect(provider) + // .claim(account, amount, proof); + const txResponse = await mockedClaim(); + + setClaimState({ status: "pendingTx", txHash: txResponse.hash }); + const txReceipt = await txResponse.wait(); + + setClaimState({ status: "success", txHash: txReceipt.transactionHash }); + setHasClaimedState({ status: "success", hasClaimed: true }); + } catch (error) { + setClaimState({ status: "error", error: error as Error }); + } + }; + + return { handleClaim, claimState, hasClaimedState }; +} + +// TODO: implement +// function getClaimAmountAndProof(account: string) { +// return ["1", "0x"]; +// } + +async function mockedClaim() { + await new Promise((resolve) => setTimeout(() => resolve(true), 5_000)); + return { + hash: "0xTX_HASH", + wait: async () => { + await new Promise((resolve) => setTimeout(() => resolve(true), 15_000)); + return { + transactionHash: "0xTX_HASH", + }; + }, + }; +} + +async function mockedHasClaimed() { + await new Promise((resolve) => setTimeout(() => resolve(true), 5_000)); + return false; +} diff --git a/src/views/NotFound/NotFound.styles.tsx b/src/views/NotFound/NotFound.styles.tsx index 8a1750eae..fe900f3c5 100644 --- a/src/views/NotFound/NotFound.styles.tsx +++ b/src/views/NotFound/NotFound.styles.tsx @@ -7,10 +7,10 @@ export const Wrapper = styled.div` display: flex; flex-direction: column; justify-content: space-between; - min-height: calc(100% - 72px); + min-height: calc(100vh - 72px); @media (max-width: 428px) { - min-height: calc(100% - 64px); + min-height: calc(100vh - 64px); } `; diff --git a/src/views/Rewards/comp/RewardReferral/RewardReferral.styles.tsx b/src/views/Rewards/comp/RewardReferral/RewardReferral.styles.tsx index f2e301c94..c05647b4f 100644 --- a/src/views/Rewards/comp/RewardReferral/RewardReferral.styles.tsx +++ b/src/views/Rewards/comp/RewardReferral/RewardReferral.styles.tsx @@ -1,7 +1,6 @@ import styled from "@emotion/styled"; import { ButtonV2 } from "components/Buttons"; import { ReactComponent as ReferralSVG } from "assets/across-referrals.svg"; -import { ReactComponent as ExternalLink12Icon } from "assets/icons/external-link-12.svg"; import { ReactComponent as CopyIcon16 } from "assets/icons/copy-16.svg"; import { ReactComponent as CopyIcon24 } from "assets/icons/copy-24.svg"; import { ReactComponent as II } from "assets/icons/info-16.svg"; @@ -241,10 +240,6 @@ export const ConnectButton = styled(ButtonV2)` } `; -export const ExternalLinkIcon = styled(ExternalLink12Icon)` - margin: 2px 0 0 4px; -`; - export const CopyIconDesktop = styled(CopyIcon24)` display: block; margin-left: 8px; diff --git a/src/views/Rewards/comp/RewardReferral/RewardReferral.tsx b/src/views/Rewards/comp/RewardReferral/RewardReferral.tsx index 2a087e889..ecafb20fb 100644 --- a/src/views/Rewards/comp/RewardReferral/RewardReferral.tsx +++ b/src/views/Rewards/comp/RewardReferral/RewardReferral.tsx @@ -16,19 +16,18 @@ import { LightGrayItemText, WarningInfoItem, ConnectButton, - ExternalLink, CopyIconDesktop, CopyIconMobile, InfoIcon, CopyCheckmark, - ExternalLinkIcon, ArrowSeparator, RewardsInfo, } from "./RewardReferral.styles"; import { shortenAddress } from "utils"; import { ReferralsSummary } from "hooks/useReferralSummary"; -import { PopperTooltip } from "../../../../components/Tooltip"; +import { PopperTooltip } from "components/Tooltip"; +import { ExternalLink } from "components/ExternalLink"; import StepperWithTooltips from "../StepperWithTooltips"; import { useConnection } from "state/hooks"; @@ -126,11 +125,8 @@ const ReferralLinkComponent: React.FC<{ - Learn more - + text="Learn more" + /> )} diff --git a/src/views/Staking/Staking.styles.tsx b/src/views/Staking/Staking.styles.tsx new file mode 100644 index 000000000..06942ae97 --- /dev/null +++ b/src/views/Staking/Staking.styles.tsx @@ -0,0 +1,51 @@ +import styled from "@emotion/styled"; +import { QUERIESV2 } from "utils"; + +export const Wrapper = styled.div` + background-color: transparent; + + max-width: 600px; + width: calc(100% - 24px); + + margin: 64px auto 20px; + display: flex; + flex-direction: column; + gap: 16px; + + @media ${QUERIESV2.sm} { + margin: 16px auto; + } +`; + +export const Card = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: flex-start; + + background: #34353b; + + border: 1px solid #3e4047; + border-radius: 10px; + + flex-wrap: nowrap; + + padding: 24px; + gap: 24px; + @media ${QUERIESV2.sm} { + padding: 12px 16px; + gap: 16px; + } +`; + +export const Divider = styled.div` + width: 100%; + height: 1px; + + background: #3e4047; + + flex: none; + order: 0; + align-self: stretch; + flex-grow: 0; +`; diff --git a/src/views/Staking/Staking.tsx b/src/views/Staking/Staking.tsx new file mode 100644 index 000000000..42f849268 --- /dev/null +++ b/src/views/Staking/Staking.tsx @@ -0,0 +1,108 @@ +import { Wrapper } from "./Staking.styles"; +import { StakingReward, StakingForm, StakingExitAction } from "./components"; +import { useStakingView } from "./hooks/useStakingView"; +import Footer from "components/Footer"; +import { repeatableTernaryBuilder } from "utils/ternary"; +import { BigNumber, BigNumberish } from "ethers"; +import { + StakingActionFunctionType, + stakingActionNOOPFn, +} from "./hooks/useStakingActionsResolver"; +import { SuperHeader } from "components"; +import { getChainInfo, hubPoolChainId } from "utils"; + +const Staking = () => { + const { + poolName, + exitLinkURI, + poolLogoURI, + isConnected, + connectWalletHandler, + stakingData, + isStakingDataLoading, + isWrongNetwork, + isWrongNetworkHandler, + } = useStakingView(); + + const numericTernary = repeatableTernaryBuilder( + !isStakingDataLoading, + "0" + ); + const numberTernary = repeatableTernaryBuilder( + !isStakingDataLoading, + 0 + ); + const stringTernary = repeatableTernaryBuilder( + !isStakingDataLoading, + "" + ); + + const stakingFnTernary = repeatableTernaryBuilder( + !isStakingDataLoading, + stakingActionNOOPFn + ); + + return ( + <> + {isWrongNetwork && ( + + + You are on an incorrect network. Please{" "} + + switch to {getChainInfo(hubPoolChainId).name} + + + + )} + + + "0")} + lpTokenParser={ + stakingData?.lpTokenParser ?? (() => BigNumber.from("0")) + } + estimatedPoolApy={numericTernary(stakingData?.estimatedApy)} + lpTokenName={stringTernary(stakingData?.lpTokenSymbolName)} + stakeActionFn={stakingFnTernary(stakingData?.stakeActionFn)} + unstakeActionFn={stakingFnTernary(stakingData?.unstakeActionFn)} + usersTotalLPTokens={numericTernary(stakingData?.usersTotalLPTokens)} + userCumulativeStake={numericTernary( + stakingData?.userAmountOfLPStaked + )} + currentMultiplier={numericTernary( + stakingData?.currentUserRewardMultiplier + )} + usersMultiplierPercentage={numberTernary( + stakingData?.usersMultiplierPercentage + )} + globalCumulativeStake={numericTernary( + stakingData?.globalAmountOfLPStaked + )} + ageOfCapital={numberTernary(stakingData?.elapsedTimeSinceAvgDeposit)} + availableLPTokenBalance={numericTernary( + stakingData?.availableLPTokenBalance + )} + shareOfPool={numericTernary(stakingData?.shareOfPool)} + /> + + + + > + ); +}; + +export default Staking; diff --git a/src/views/Staking/components/StakingExitAction/StakingExitAction.styles.tsx b/src/views/Staking/components/StakingExitAction/StakingExitAction.styles.tsx new file mode 100644 index 000000000..17573bed1 --- /dev/null +++ b/src/views/Staking/components/StakingExitAction/StakingExitAction.styles.tsx @@ -0,0 +1,56 @@ +import styled from "@emotion/styled"; +import { Link } from "react-router-dom"; +import { QUERIESV2 } from "utils"; +import { ReactComponent as ChevronLeft } from "assets/icons/chevron-left-vector.svg"; + +export const Wrapper = styled(Link)` + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + + padding: 0px 16px; + width: fit-content; + + text-decoration: none; + + @media ${QUERIESV2.sm} { + padding: 0px 12px; + } +`; + +export const ExitIcon = styled(ChevronLeft)` + height: 24px; + width: 24px; +`; + +export const Logo = styled.img` + height: 32px; + width: 32px; + + @media ${QUERIESV2.sm} { + height: 24px; + width: 24px; + } +`; + +export const Text = styled.span` + color: #e0f3ff; + line-height: 26px; + + font-size: 22px; + @media ${QUERIESV2.sm} { + font-size: 18px; + } +`; + +export const TitleLogo = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + + @media ${QUERIESV2.sm} { + gap: 8px; + } +`; diff --git a/src/views/Staking/components/StakingExitAction/StakingExitAction.tsx b/src/views/Staking/components/StakingExitAction/StakingExitAction.tsx new file mode 100644 index 000000000..4afeb8724 --- /dev/null +++ b/src/views/Staking/components/StakingExitAction/StakingExitAction.tsx @@ -0,0 +1,31 @@ +import { + Wrapper, + Text, + Logo, + ExitIcon, + TitleLogo, +} from "./StakingExitAction.styles"; + +type StakingExitActionAttributes = { + poolName: string; + exitLinkURI: string; + poolLogoURI: string; +}; + +export const StakingExitAction = ({ + poolName, + exitLinkURI, + poolLogoURI, +}: StakingExitActionAttributes) => { + return ( + + + + + {poolName} Pool + + + ); +}; + +export default StakingExitAction; diff --git a/src/views/Staking/components/StakingExitAction/index.tsx b/src/views/Staking/components/StakingExitAction/index.tsx new file mode 100644 index 000000000..822e65b14 --- /dev/null +++ b/src/views/Staking/components/StakingExitAction/index.tsx @@ -0,0 +1 @@ +export { default } from "./StakingExitAction"; diff --git a/src/views/Staking/components/StakingForm/StakingForm.styles.tsx b/src/views/Staking/components/StakingForm/StakingForm.styles.tsx new file mode 100644 index 000000000..da4b8bf4e --- /dev/null +++ b/src/views/Staking/components/StakingForm/StakingForm.styles.tsx @@ -0,0 +1,193 @@ +import styled from "@emotion/styled"; +import ProgressBar from "components/ProgressBar"; +import { ReactComponent as UnstyedUsdcLogo } from "assets/icons/usdc-24.svg"; +import { ReactComponent as UnstyledArrowIcon } from "assets/icons/arrow-16.svg"; +import { ReactComponent as II } from "assets/icons/info-16.svg"; +import { + Card as ExternalCard, + Divider as ExternalDivider, +} from "../../Staking.styles"; + +import { QUERIESV2 } from "utils"; + +export const Card = styled(ExternalCard)``; + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; + background-color: #34353b; + border: 1px solid #3e4047; + border-radius: 10px; + box-sizing: border-box; +`; + +export const Tabs = styled.div` + display: flex; + justify-content: center; + width: 100%; + margin: 0 auto 0px; + justify-items: center; +`; + +export const Divider = ExternalDivider; + +interface ITab { + active: boolean; +} +export const Tab = styled.div` + flex-grow: 1; + font-family: "Barlow"; + font-style: normal; + font-weight: 500; + font-size: ${16 / 16}rem; + line-height: ${20 / 16}rem; + color: ${({ active }) => (active ? "#e0f3ff" : "#9DAAB2")}; + text-align: center; + padding: 0 0 20px; + border-bottom: ${(props) => + props.active ? "2px solid #e0f3ff" : "1px solid #3E4047"}; + cursor: pointer; +`; + +export const UsdcLogo = styled(UnstyedUsdcLogo)``; + +export const InputWrapper = styled.div` + flex-grow: 8; + position: relative; +`; + +export const StakeInfo = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +`; + +interface IInnerPoolStakeInfo { + visible: boolean; +} +export const InnerPoolStakeInfo = styled(StakeInfo)` + display: ${({ visible }) => (visible ? `flex` : `none`)}; +`; + +export const StakeInfoItem = styled.div` + display: flex; + align-items: center; + font-size: ${16 / 16}rem; + line-height: ${20 / 16}rem; + font-weight: 400; + + width: 100%; + color: #9daab2; + + justify-content: start; + &:nth-of-type(2n) { + justify-content: flex-end; + } + + @media ${QUERIESV2.sm} { + &:nth-of-type(2n) { + justify-content: start; + } + font-size: ${14 / 16}rem; + flex-shrink: 0; + width: fit-content; + } +`; + +export const StakeInfoRow = styled.div` + display: flex; + justify-content: space-between; + flex-shrink: 0; + + @media ${QUERIESV2.sm} { + flex-direction: column; + gap: 6px; + } +`; + +export const StakeAPYInfoRow = styled(StakeInfoRow)` + cursor: pointer; + @media ${QUERIESV2.sm} { + flex-direction: row; + width: fit-content; + gap: 12px; + & > { + width: fit-content; + } + } +`; + +export const LightGrayItemText = styled.span<{ margin?: number }>` + color: #e0f3ff; + margin-right: ${({ margin }) => (margin ? `${margin}px` : 0)}; +`; + +export const StakeInfoItemSmall = styled(StakeInfoItem)` + font-size: ${14 / 16}rem; + + @media ${QUERIESV2.sm} { + font-size: ${12 / 16}rem; + } +`; + +interface IStyledProgressBar { + className?: string; +} +export const StyledProgressBar = styled(ProgressBar)` + width: 80px; + height: 14px; + margin-top: 5px; + padding-right: 4px; + > div { + height: 8px; + } +`; + +export const MutliplierValue = styled.div` + font-weight: 400; + gap: 12px; + color: #e0f3ff; + display: inline-flex; + justify-content: flex-end; + @media ${QUERIESV2.sm} { + justify-content: flex-start; + } +`; + +export const APYInfo = styled(StakeInfo)` + padding-bottom: 24px; + @media ${QUERIESV2.sm} { + flex-direction: row; + justify-content: flex-start; + } + margin-bottom: 24px; +`; + +export const APYInfoItem = styled(StakeInfoItem)` + color: #c5d5e0; +`; + +export const ArrowIconDown = styled(UnstyledArrowIcon)` + margin-right: 11px; + path { + stroke: #9daab2; + } +`; + +export const ArrowIconUp = styled(ArrowIconDown)` + rotate: 180deg; + margin-bottom: -4px; +`; + +export const InfoIcon = styled(II)` + margin-left: 8px; + cursor: pointer; +`; + +export const InputBlockWrapper = styled.div` + width: 100%; + margin: 0 auto; +`; diff --git a/src/views/Staking/components/StakingForm/StakingForm.tsx b/src/views/Staking/components/StakingForm/StakingForm.tsx new file mode 100644 index 000000000..57da08ca1 --- /dev/null +++ b/src/views/Staking/components/StakingForm/StakingForm.tsx @@ -0,0 +1,263 @@ +import { useEffect, useRef, useState } from "react"; +import { + Card, + Tabs, + Tab, + StakeInfo, + StakeInfoItem, + StakeInfoItemSmall, + LightGrayItemText, + MutliplierValue, + StyledProgressBar, + APYInfoItem, + UsdcLogo, + InfoIcon, + Divider, + InputBlockWrapper, + StakeInfoRow, + StakeAPYInfoRow, + InnerPoolStakeInfo, + ArrowIconUp, + ArrowIconDown, +} from "./StakingForm.styles"; + +import { PopperTooltip } from "components/Tooltip"; +import StakingInputBlock from "../StakingInputBlock"; +import { StakingFormPropType } from "../../types"; +import { repeatableTernaryBuilder } from "utils/ternary"; +import { + formatEther, + formatNumberMaxFracDigits, + isNumericWithinRange, +} from "utils"; + +type StakeTab = "stake" | "unstake"; + +export const StakingForm = ({ + isConnected, + walletConnectionHandler, + userCumulativeStake, + lpTokenName, + currentMultiplier, + usersMultiplierPercentage, + usersTotalLPTokens, + ageOfCapital, + availableLPTokenBalance, + globalCumulativeStake, + shareOfPool, + lpTokenFormatter: formatLPToken, + lpTokenParser: parseLPToken, + stakeActionFn, + unstakeActionFn, + isWrongNetwork, + estimatedPoolApy, + isDataLoading, +}: StakingFormPropType) => { + const [activeTab, setActiveTab] = useState("stake"); + const [isPoolInfoVisible, setIsPoolInfoVisible] = useState(false); + const [stakeAmount, setStakeAmount] = useState(""); + const [isTransitioning, setTransitioning] = useState(false); + const isRendered = useRef(false); + + const buttonHandler = isWrongNetwork + ? () => {} + : isConnected + ? () => { + (activeTab === "stake" ? stakeActionFn : unstakeActionFn)( + parseLPToken(stakeAmount), + setTransitioning, + isRendered + ); + } + : walletConnectionHandler; + + const buttonTextPrefix = isConnected ? "" : "Connect wallet to "; + const buttonMaxValue = + activeTab === "stake" ? availableLPTokenBalance : userCumulativeStake; + const buttonMaxValueText = formatLPToken(buttonMaxValue).replaceAll(",", ""); + + const ArrowIcon = isPoolInfoVisible ? ArrowIconUp : ArrowIconDown; + + const validateStakeAmount = (amount: string) => + isNumericWithinRange(amount, true, "0", buttonMaxValue, parseLPToken); + + const valueOrEmpty = repeatableTernaryBuilder( + isConnected && !isWrongNetwork && !isDataLoading, + <>-> + ); + + useEffect(() => { + if (!isTransitioning) { + setStakeAmount(""); + } + }, [activeTab, isTransitioning]); + + useEffect(() => { + setIsPoolInfoVisible(false); + }, [isConnected]); + + useEffect(() => { + isRendered.current = true; + return () => { + isRendered.current = false; + }; + }, []); + + return ( + + + setActiveTab("stake")} + active={activeTab === "stake"} + > + Stake + + setActiveTab("unstake")} + active={activeTab === "unstake"} + > + Unstake + + + + + + + + + Staked LP Tokens + + {valueOrEmpty( + + + {formatLPToken(userCumulativeStake)} / + + {formatLPToken(usersTotalLPTokens)} {lpTokenName} + + )} + + + + + Age of capital + + + + + + {valueOrEmpty( + <> + {ageOfCapital <= 0 + ? "-" + : `${formatNumberMaxFracDigits(ageOfCapital)} Days`} + > + )} + + + + + Multiplier + + + + + + {valueOrEmpty( + + + {formatEther(currentMultiplier)} x + + )} + + + + + Note: Multipliers of previously staked tokens are not impacted + + + + { + setIsPoolInfoVisible((value) => !value); + }} + > + + + + Your total APY + + + + {valueOrEmpty(2.81%)} + + + + + + Your Rewards APY + + {valueOrEmpty( + + 2.43% + Base 2.11% + + )} + + + + Pool liquidity + + {valueOrEmpty( + + + ${formatLPToken(globalCumulativeStake)} + + + )} + + + + Pool APY + + {valueOrEmpty( + + {formatEther(estimatedPoolApy)}% + + )} + + + + Share of pool + + {valueOrEmpty( + + {formatEther(shareOfPool)}% + + )} + + + + + + ); +}; + +export default StakingForm; diff --git a/src/views/Staking/components/StakingForm/index.ts b/src/views/Staking/components/StakingForm/index.ts new file mode 100644 index 000000000..7fbe5b536 --- /dev/null +++ b/src/views/Staking/components/StakingForm/index.ts @@ -0,0 +1 @@ +export { default } from "./StakingForm"; diff --git a/src/views/Staking/components/StakingInputBlock/StakingInputBlock.styles.tsx b/src/views/Staking/components/StakingInputBlock/StakingInputBlock.styles.tsx new file mode 100644 index 000000000..fbaec502d --- /dev/null +++ b/src/views/Staking/components/StakingInputBlock/StakingInputBlock.styles.tsx @@ -0,0 +1,133 @@ +import styled from "@emotion/styled"; +import { QUERIESV2 } from "utils"; +import { SecondaryButtonWithoutShadow as UnstyledButton } from "components/Buttons"; + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +export const InputRow = styled.div` + display: flex; + gap: 16px; + flex-direction: row; + align-items: center; + + & svg { + height: 24px; + width: 24px; + flex-shrink: 0; + } + + @media ${QUERIESV2.sm} { + flex-direction: column; + gap: 12px; + } +`; + +export const InputWrapper = styled.div` + display: flex; + gap: 12px; + justify-content: space-between; + align-items: center; + width: 100%; + + background: #2d2e33; + border-radius: 32px; + + height: 64px; + + padding: 0px 24px; + + border: 1px solid ${({ valid }) => (valid ? "#4c4e57" : "#f96c6c")}; + color: ${({ valid }) => (valid ? "#e0f3ff" : "#f96c6c")}; + + @media ${QUERIESV2.sm} { + padding: 0px 12px; + height: 48px; + } +`; + +interface IStakeInput { + valid: boolean; +} +export const Input = styled.input` + background: transparent; + color: #9daab2; + font-size: 16px; + border: none; + width: 100%; + padding: 0; + + &:focus { + outline: 0; + } + @media ${QUERIESV2.sm} { + font-size: 16px; + } +`; + +export const MaxButton = styled(UnstyledButton)` + font-size: 12px; + line-height: 14px; + + letter-spacing: 0.04em; + text-transform: uppercase; + color: #c5d5e0; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + border: 1px solid #4c4e57; + border-radius: 24px; + &:hover { + color: #e0f3ff; + border-color: #e0f3ff; + } + + padding: 8px 16px; + @media ${QUERIESV2.sm} { + height: 24px; + padding: 0px 10px; + } +`; + +export const ButtonWrapper = styled.div` + flex-grow: 1; + @media ${QUERIESV2.sm} { + width: 100%; + } +`; + +interface IStakeButton { + valid: boolean; + fullWidth?: boolean; +} +export const StakeButton = styled(UnstyledButton)` + width: ${({ fullWidth }) => (fullWidth ? "100%" : "inherit")}; + text-transform: capitalize; + background: #6cf9d8; + padding: 0px 40px; + height: 64px; + color: #2d2e33; + opacity: ${({ valid }) => (valid ? 1 : 0.25)}; + @media ${QUERIESV2.sm} { + text-align: center; + width: 100%; + height: 40px; + padding: 0px 20px; + border-radius: 20px; + } +`; + +export const StakeButtonContentWrapper = styled.div` + display: flex; + gap: 6px; + justify-content: center; + flex-direction: row; +`; + +export const LoaderWrapper = styled.div` + height: fit-content; +`; diff --git a/src/views/Staking/components/StakingInputBlock/StakingInputBlock.tsx b/src/views/Staking/components/StakingInputBlock/StakingInputBlock.tsx new file mode 100644 index 000000000..7ee8c12bf --- /dev/null +++ b/src/views/Staking/components/StakingInputBlock/StakingInputBlock.tsx @@ -0,0 +1,86 @@ +import { + InputRow, + InputWrapper, + Input, + ButtonWrapper, + StakeButton, + MaxButton, + Wrapper, + LoaderWrapper, + StakeButtonContentWrapper, +} from "./StakingInputBlock.styles"; +import { capitalizeFirstLetter } from "utils/format"; +import { AlertInfo } from "../StakingReward/AlertInfo"; +import BouncingDotsLoader from "components/BouncingDotsLoader"; +import { StylizedSVG } from "utils/types"; + +interface Props { + value: string; + setValue: React.Dispatch>; + valid: boolean; + buttonText: string; + Logo: StylizedSVG; + maxValue: string; + omitInput?: boolean; + onClickHandler: () => void | Promise; + displayLoader?: boolean; + errorMessage?: string; +} + +const StakingInputBlock: React.FC = ({ + value, + setValue, + valid, + buttonText, + Logo, + maxValue, + displayLoader, + omitInput, + onClickHandler, + errorMessage, +}) => ( + + + {!omitInput && ( + + + setValue(e.target.value)} + disabled={displayLoader} + /> + setValue(maxValue ?? "")} + > + Max + + + )} + + + + {capitalizeFirstLetter(buttonText)} + {displayLoader && ( + + + + )} + + + + + {!!value && !valid && !!errorMessage && ( + {errorMessage} + )} + +); + +export default StakingInputBlock; diff --git a/src/views/Staking/components/StakingInputBlock/index.ts b/src/views/Staking/components/StakingInputBlock/index.ts new file mode 100644 index 000000000..3c6e2fc83 --- /dev/null +++ b/src/views/Staking/components/StakingInputBlock/index.ts @@ -0,0 +1 @@ +export { default } from "./StakingInputBlock"; diff --git a/src/views/Staking/components/StakingReward/AlertInfo.tsx b/src/views/Staking/components/StakingReward/AlertInfo.tsx new file mode 100644 index 000000000..d349bc5a2 --- /dev/null +++ b/src/views/Staking/components/StakingReward/AlertInfo.tsx @@ -0,0 +1,23 @@ +import { + AlertInfoWrapper, + InfoIcon, + InfoText, + InfoTextWrapper, +} from "./StakingReward.styles"; + +type AlertInfoPropType = { + danger?: boolean; +}; + +export const AlertInfo: React.FC = ({ + danger, + children, +}) => { + const Wrapper = !!danger ? AlertInfoWrapper : InfoTextWrapper; + return ( + + + {children} + + ); +}; diff --git a/src/views/Staking/components/StakingReward/StakingReward.styles.tsx b/src/views/Staking/components/StakingReward/StakingReward.styles.tsx new file mode 100644 index 000000000..773e1d28c --- /dev/null +++ b/src/views/Staking/components/StakingReward/StakingReward.styles.tsx @@ -0,0 +1,140 @@ +import styled from "@emotion/styled"; +import { AlertCircle, Gift } from "react-feather"; +import { ReactComponent as AcrossLogo } from "assets/Across-logo-bullet.svg"; +import { QUERIESV2 } from "utils"; +import { + Card as ExternalCard, + Divider as ExternalDivider, +} from "../../Staking.styles"; + +export const Card = ExternalCard; + +export const Wrapper = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: flex-start; + + background: #34353b; + + border: 1px solid #3e4047; + border-radius: 10px; + + flex-wrap: nowrap; + + padding: 24px; + gap: 24px; + @media ${QUERIESV2.sm} { + padding: 12px 16px; + gap: 16px; + } +`; + +export const InnerWrapper = styled.div` + width: 100%; +`; + +export const StakingInputBlockWrapper = styled(InnerWrapper)` + display: flex; + flex-direction: column; + gap: 12px; +`; + +export const Title = styled.p` + font-family: "Barlow"; + font-style: normal; + font-weight: 400; + line-height: 26px; + color: #c5d5e0; + + font-size: 18px; + @media ${QUERIESV2.sm} { + font-size: 16px; + } +`; + +export const InfoTextWrapper = styled.div` + display: flex; + flex-direction: row; + /* align-items: center; */ + align-items: stretch; + padding: 0px; + gap: 6px; + + width: 100%; + + color: #9daab2; +`; + +export const AlertInfoWrapper = styled(InfoTextWrapper)` + color: #f96c6c; +`; + +export const InfoText = styled.span` + font-family: "Barlow"; + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 18px; +`; + +export const InfoIcon = styled(AlertCircle)` + flex-shrink: 0; + margin-top: 3px; + width: 13.5px; + height: 13.5px; +`; + +export const Divider = ExternalDivider; + +export const StakingClaimAmountWrapper = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0px; + gap: 6px; + width: 100%; + height: 20px; +`; + +export const StakingClaimAmountText = styled.div` + font-family: "Barlow"; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 20px; + + color: #e0f3ff; + + @media ${QUERIESV2.sm} { + font-size: 14px; + line-height: 18px; + } +`; + +export const StakingClaimAmountTitle = styled(StakingClaimAmountText)` + color: #9daab2; +`; + +export const PresentIcon = styled(Gift)` + color: #6cf9d8; + width: 16px; + height: 16px; +`; + +export const StakingClaimAmountInnerWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: 0px; + gap: 6px; + height: 20px; + border-radius: 4px; + flex: none; + order: 1; + flex-grow: 0; +`; + +export const StyledAcrossLogo = styled(AcrossLogo)``; diff --git a/src/views/Staking/components/StakingReward/StakingReward.tsx b/src/views/Staking/components/StakingReward/StakingReward.tsx new file mode 100644 index 000000000..e88d15dcc --- /dev/null +++ b/src/views/Staking/components/StakingReward/StakingReward.tsx @@ -0,0 +1,108 @@ +import { useState } from "react"; +import { formatEther, isNumericWithinRange, parseEtherLike } from "utils"; +import { repeatableTernaryBuilder } from "utils/ternary"; +import { StakingRewardPropType } from "../../types"; +import StakingInputBlock from "../StakingInputBlock"; +import { AlertInfo } from "./AlertInfo"; +import { + Title, + Card, + InnerWrapper, + Divider, + StakingClaimAmountWrapper, + StakingClaimAmountText, + StakingClaimAmountTitle, + StakingClaimAmountInnerWrapper, + PresentIcon, + StyledAcrossLogo, + StakingInputBlockWrapper, +} from "./StakingReward.styles"; + +export const StakingReward = ({ + maximumClaimableAmount, + isConnected, + walletConnectionHandler, +}: StakingRewardPropType) => { + const [amountToClaim, setAmountToClaim] = useState(""); + const [isTransitioning] = useState(false); + + const buttonHandler = isConnected ? () => {} : walletConnectionHandler; + const buttonTextPrefix = isConnected ? "" : "Connect wallet to "; + const buttonMaxValue = formatEther(maximumClaimableAmount); + + const valueOrEmpty = repeatableTernaryBuilder(isConnected, <>->); + + // Stub Function + const stakingAmountValidationHandler = (value: string): boolean => + isNumericWithinRange( + value, + true, + "0", + maximumClaimableAmount, + parseEtherLike + ); + + // Stub Function + const isAmountExceeded = (value: string): boolean => + isNumericWithinRange( + value, + false, + maximumClaimableAmount, + undefined, + parseEtherLike + ); + + // Stub + const errorMessage = (): string => { + if (isAmountExceeded(amountToClaim)) { + return "The amount entered exceeds your claimable amount"; + } else { + return ""; + } + }; + + return ( + + + Rewards + + + {isConnected && ( + + Claiming tokens will reset your multiplier and decrease your ACX APY + + )} + + + + + + + + Claimable Rewards + {valueOrEmpty( + + + + {formatEther(maximumClaimableAmount)} + + + )} + + + + ); +}; + +export default StakingReward; diff --git a/src/views/Staking/components/StakingReward/index.ts b/src/views/Staking/components/StakingReward/index.ts new file mode 100644 index 000000000..b7944388a --- /dev/null +++ b/src/views/Staking/components/StakingReward/index.ts @@ -0,0 +1 @@ +export { default } from "./StakingReward"; diff --git a/src/views/Staking/components/index.ts b/src/views/Staking/components/index.ts new file mode 100644 index 000000000..9a964ffe0 --- /dev/null +++ b/src/views/Staking/components/index.ts @@ -0,0 +1,3 @@ +export { default as StakingExitAction } from "./StakingExitAction"; +export { default as StakingForm } from "./StakingForm"; +export { default as StakingReward } from "./StakingReward"; diff --git a/src/views/Staking/hooks/useStakingActionsResolver.ts b/src/views/Staking/hooks/useStakingActionsResolver.ts new file mode 100644 index 000000000..54d145012 --- /dev/null +++ b/src/views/Staking/hooks/useStakingActionsResolver.ts @@ -0,0 +1,369 @@ +import { useConnection } from "state/hooks"; +import { useEffect, useState } from "react"; +import { + addEtherscan, + fixedPointAdjustment, + formattedBigNumberToNumber, + formatUnitsFnBuilder, + getConfig, + hubPoolChainId, + MAX_APPROVAL_AMOUNT, + parseEtherLike, + safeDivide, + switchChain, + toWeiSafe, +} from "utils"; +import { useStakingPoolResolver } from "./useStakingPoolResolver"; +import { BigNumber, BigNumberish, providers, Signer } from "ethers"; +import { ERC20__factory } from "@across-protocol/contracts-v2"; +import { API } from "bnc-notify"; +import axios from "axios"; + +export type StakingActionFunctionType = ( + amount: BigNumber, + setTransition: React.Dispatch>, + isRelevantComponentRende: React.MutableRefObject +) => Promise; +export type FormatterFnType = (wei: BigNumberish) => string; +export type ParserFnType = (wei: string) => BigNumber; + +type ResolvedDataType = + | { + lpTokenAddress: string; + lpTokenSymbolName: string; + acrossTokenAddress: string; + poolEnabled: boolean; + globalAmountOfLPStaked: BigNumberish; + userAmountOfLPStaked: BigNumberish; + maxMultiplier: BigNumberish; + outstandingRewards: BigNumberish; + currentUserRewardMultiplier: BigNumberish; + availableLPTokenBalance: BigNumberish; + elapsedTimeSinceAvgDeposit: number; + usersMultiplierPercentage: number; + usersTotalLPTokens: BigNumberish; + shareOfPool: BigNumberish; + estimatedApy: BigNumberish; + lpTokenFormatter: FormatterFnType; + lpTokenParser: ParserFnType; + stakeActionFn: StakingActionFunctionType; + unstakeActionFn: StakingActionFunctionType; + } + | undefined; + +export const stakingActionNOOPFn: StakingActionFunctionType = async () => {}; + +export const useStakingActionsResolver = () => { + const { account, provider, signer, notify, chainId } = useConnection(); + const { mainnetAddress } = useStakingPoolResolver(); + + const [isLoading, setIsLoading] = useState(false); + const [stakingData, setStakingData] = useState(undefined); + const [reloadData, setReloadData] = useState(false); + const [isWrongNetwork, setIsWrongNetwork] = useState(false); + + useEffect(() => { + setIsLoading(true); + if (!mainnetAddress || !provider || !account || !signer || !chainId) { + setIsLoading(false); + } else if (String(chainId) !== String(hubPoolChainId)) { + setIsWrongNetwork(true); + setIsLoading(false); + } else { + setIsWrongNetwork(false); + resolveRequestedData( + mainnetAddress, + provider, + signer, + account, + notify, + setReloadData + ).then((resolvedData) => { + setStakingData(resolvedData); + setIsLoading(false); + }); + } + }, [mainnetAddress, account, provider, signer, notify, reloadData, chainId]); + + const isWrongNetworkHandler = () => + provider && switchChain(provider, hubPoolChainId); + + return { + isStakingDataLoading: isLoading, + stakingData, + isWrongNetwork, + isWrongNetworkHandler, + }; +}; + +/** + * Calls on-chain data & the ACX API to resolve information about the AcceleratingDistributor Contract + * @param tokenAddress The address of the ERC-20 token on the current chain + * @param account A user address to query against the on-chain data + * @returns A ResolvedDataType promise with the extracted information + */ +const resolveRequestedData = async ( + tokenAddress: string, + provider: providers.Provider, + signer: Signer, + account: string, + notify: API, + setReloadData: React.Dispatch> +): Promise => { + const config = getConfig(); + const hubPool = config.getHubPool(); + const acceleratingDistributor = config.getAcceleratingDistributor(); + const acceleratingDistributorAddress = acceleratingDistributor.address; + + // Get the corresponding LP token from the hub pool directly + // Resolve the ACX reward token address from the AcceleratingDistributor + const [{ lpToken: lpTokenAddress }, acrossTokenAddress] = await Promise.all([ + hubPool.pooledTokens(tokenAddress), + acceleratingDistributor.rewardToken() as Promise, + ]); + + const lpTokenERC20 = ERC20__factory.connect(lpTokenAddress, provider); + + // Check information about this LP token on the AcceleratingDistributor contract + // Resolve the provided account's outstanding rewards (if an account is connected) as well + // as the global pool information + const [ + { enabled: poolEnabled, maxMultiplier }, + currentUserRewardMultiplier, + { + rewardsOutstanding: outstandingRewards, + cumulativeBalance: userAmountOfLPStaked, + averageDepositTime, + }, + availableLPTokenBalance, + lpTokenDecimalCount, + lpTokenAllowance, + lpTokenSymbolName, + poolQuery, + ] = await Promise.all([ + acceleratingDistributor.stakingTokens(lpTokenAddress) as Promise<{ + enabled: boolean; + baseEmissionRate: BigNumber; + maxMultiplier: BigNumber; + cumulativeStaked: BigNumber; + }>, + acceleratingDistributor.getUserRewardMultiplier( + lpTokenAddress, + account + ) as Promise, + acceleratingDistributor.getUserStake(lpTokenAddress, account) as Promise<{ + cumulativeBalance: BigNumber; + averageDepositTime: BigNumber; + rewardsOutstanding: BigNumber; + }>, + lpTokenERC20.balanceOf(account), + lpTokenERC20.decimals(), + lpTokenERC20.allowance(account, acceleratingDistributorAddress), + Promise.resolve((await lpTokenERC20.symbol()).slice(4)), + axios.get(`/api/pools`, { + params: { token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, + }), + ]); + + // Resolve the data retrieved from the serverless /pools API call + const { estimatedApy: estimatedApyFromQuery, totalPoolSize } = + poolQuery.data as { + estimatedApy: string; + totalPoolSize: BigNumberish; + }; + + // The Average Deposit Time retrieves the # seconds since the last + // deposit, weighted by all the deposits in a user's account. To calculate the + // days elapsed, we can divide by 1 day in seconds (86,400 seconds) + const daysElapsed = formattedBigNumberToNumber( + averageDepositTime.mul(fixedPointAdjustment).div(86400) + ); + + // Resolve the users reward multiplier as a percentage. + const usersMultiplierPercentage = formattedBigNumberToNumber( + currentUserRewardMultiplier + .mul(fixedPointAdjustment) + .div(maxMultiplier) + .mul(100) + ); + + // We need the amount of tokens that the user has in both their balance + // and in the staked contract. We can add the staked + balance to get this + // figure. + const usersTotalLPTokens = availableLPTokenBalance.add(userAmountOfLPStaked); + + // We can divide the amount of LP staked in the contract with the total pool + // size. + const shareOfPool = safeDivide( + userAmountOfLPStaked.mul(fixedPointAdjustment), + BigNumber.from(totalPoolSize) + ).mul(100); + + const estimatedApy = parseEtherLike(estimatedApyFromQuery).mul(100); + + // We can resolve custom formatter & parsers for the current LP + // token that we are working with. + const lpTokenFormatter = formatUnitsFnBuilder(lpTokenDecimalCount); + const lpTokenParser = (wei: BigNumberish) => + toWeiSafe(wei.toString(), lpTokenDecimalCount); + + // Determine if the contract has an allowance of at least the current + // user's entire balance. + const requiresApproval = lpTokenAllowance.lte(availableLPTokenBalance); + + // Call the closure function twice to create customized functions for + // staking and unstaking against the AcceleratingDistributor contract + + const stakeActionFn = performStakingActionBuilderFn( + lpTokenAddress, + signer, + "stake", + requiresApproval, + notify, + setReloadData + ); + const unstakeActionFn = performStakingActionBuilderFn( + lpTokenAddress, + signer, + "unstake", + requiresApproval, + notify, + setReloadData + ); + + return { + lpTokenAddress, + acrossTokenAddress, + poolEnabled, + globalAmountOfLPStaked: totalPoolSize, + userAmountOfLPStaked, + maxMultiplier, + outstandingRewards, + currentUserRewardMultiplier, + availableLPTokenBalance, + elapsedTimeSinceAvgDeposit: daysElapsed, + lpTokenSymbolName, + usersMultiplierPercentage, + usersTotalLPTokens, + shareOfPool, + estimatedApy, + lpTokenFormatter, + lpTokenParser, + stakeActionFn, + unstakeActionFn, + }; +}; + +/** + * A function builder which returns a closure of a function that can be used to stake/unstake with the AcceleratingDistributor contract + * @param lpTokenAddress The ERC20 address of the LP Token + * @param signer A valid ethers signer + * @param action The action that will build this function. Either 'stake' or 'unstake' + * @param requiresApproval Whether or not this function will first attempt have the user set an allowance with the AcceleratingDistributor contract + * @param notify A BNC notification API that will be used to visually notify the user of a successful/rejected transaction + * @param setReloadData A React dispatch function which signals to React to regenerate the staking information extracted from on-chain / api sources + * @returns A closure function that is designed to stake or unstake a given LP token with the AcceleratingDistributor contract + */ +const performStakingActionBuilderFn = ( + lpTokenAddress: string, + signer: Signer, + action: "stake" | "unstake", + requiresApproval: boolean, + notify: API, + setReloadData: React.Dispatch> +) => { + // The purpose of this variable is to keep track of whether or not + // the closure needs to request approval from the ERC20 token to be + // used within the AcceleratingDistributor contract. This allows us + // to stake/unstake multiple times without needing to directly refresh + // to determine if approval is required anymore + let innerApprovalRequired = requiresApproval; + + /** + * Enables the user to stake/unstake with the AcceleratingDistributor contract + * @param amount The amount of LP tokens to either stake or unstake + * @param setTransition A React Dispatch function that sets whether a `boolean` transition state is active or not + * @param isRelevantComponentRendered A reference to whether the component that is calling this function is still rendered + */ + const closure = async ( + amount: BigNumber, + setTransition: React.Dispatch>, + isRelevantComponentRendered: React.MutableRefObject + ): Promise => { + try { + // Ensure that this component is still rendered + // and set the `inTransition` stake from the calling component + // to `true` + if (isRelevantComponentRendered.current) { + setTransition(true); + } + // Resolve the AcceleratingDistributor and connect it with + // the signer to execute transactions on behalf of the wallet + // holder + const acceleratingDistributor = getConfig() + .getAcceleratingDistributor() + .connect(signer); + // Check if this wallet has permissions to interract with the + // AcceleratingDistibutor function regarding staking/unstaking + // with the provided LP Token + if (innerApprovalRequired) { + const lpER20 = ERC20__factory.connect(lpTokenAddress, signer); + const approvalResult = await lpER20.approve( + acceleratingDistributor.address, + MAX_APPROVAL_AMOUNT + ); + // Wait for the transaction to return successful + await notificationEmitter(approvalResult.hash, notify); + innerApprovalRequired = false; + } + const callingFn = acceleratingDistributor[action]; + const amountAsBigNumber = BigNumber.from(amount); + // Ensure that the user is within the rendered function + // before executing this stake command + if (isRelevantComponentRendered.current) { + // Call the generate the transaction to stake/unstake and + // wait until the tx has been resolved + const result = await callingFn(lpTokenAddress, amountAsBigNumber); + await notificationEmitter(result.hash, notify); + } + } catch (e) { + console.log(e); + } finally { + // No matter what, attempt to set the transition to false + // for the calling component and also allow the user to + // reload the data so that the user's interraction with the + // contract is visually displayed + if (isRelevantComponentRendered.current) { + setTransition(false); + setReloadData((data) => !data); + } + } + }; + return closure; +}; + +/** + * Calls and waits on the Notify API to resolve the status of a TX + * @param txHash The transaction hash to wait for + * @param notify The BNC Notify API that is used to handle the UI visualization + * @returns Nothing. + */ +const notificationEmitter = async ( + txHash: string, + notify: API +): Promise => { + return new Promise((resolve, reject) => { + const { emitter } = notify.hash(txHash, String(hubPoolChainId)); + emitter.on("all", addEtherscan); + emitter.on("txConfirmed", () => { + notify.unsubscribe(txHash); + setTimeout(() => { + resolve(); + }, 5000); + }); + emitter.on("txFailed", () => { + notify.unsubscribe(txHash); + reject(); + }); + }); +}; diff --git a/src/views/Staking/hooks/useStakingPoolResolver.ts b/src/views/Staking/hooks/useStakingPoolResolver.ts new file mode 100644 index 000000000..dcdebf9dd --- /dev/null +++ b/src/views/Staking/hooks/useStakingPoolResolver.ts @@ -0,0 +1,25 @@ +import { useParams } from "react-router-dom"; +import { TokenInfo, tokenList } from "utils"; + +type StakingPathParams = { + poolId: string; +}; + +export const useStakingPoolResolver = () => { + const { poolId } = useParams(); + + const resolvedToken: TokenInfo = { + ...tokenList[0], + decimals: 18, + symbol: "TEST", + mainnetAddress: "0xd35cceead182dcee0f148ebac9447da2c4d449c4", + }; + + return { + poolId, + exitLinkURI: "/rewards", + poolLogoURI: resolvedToken.logoURI, + poolName: resolvedToken.symbol.toUpperCase(), + mainnetAddress: resolvedToken.mainnetAddress, + }; +}; diff --git a/src/views/Staking/hooks/useStakingView.ts b/src/views/Staking/hooks/useStakingView.ts new file mode 100644 index 000000000..f48001ab3 --- /dev/null +++ b/src/views/Staking/hooks/useStakingView.ts @@ -0,0 +1,32 @@ +import { useConnection } from "state/hooks"; +import { onboard } from "utils"; +import { useStakingActionsResolver } from "./useStakingActionsResolver"; +import { useStakingPoolResolver } from "./useStakingPoolResolver"; + +export const useStakingView = () => { + const { isConnected, provider } = useConnection(); + const { poolId, exitLinkURI, poolLogoURI, poolName, mainnetAddress } = + useStakingPoolResolver(); + + const { + isStakingDataLoading, + stakingData, + isWrongNetwork, + isWrongNetworkHandler, + } = useStakingActionsResolver(); + + return { + poolId, + exitLinkURI, + poolLogoURI, + poolName, + mainnetAddress, + isStakingDataLoading: isStakingDataLoading, + isWrongNetwork, + isWrongNetworkHandler, + stakingData, + isConnected, + provider, + connectWalletHandler: onboard.init, + }; +}; diff --git a/src/views/Staking/index.ts b/src/views/Staking/index.ts new file mode 100644 index 000000000..76830648e --- /dev/null +++ b/src/views/Staking/index.ts @@ -0,0 +1 @@ +export { default } from "./Staking"; diff --git a/src/views/Staking/types.ts b/src/views/Staking/types.ts new file mode 100644 index 000000000..8ec289a40 --- /dev/null +++ b/src/views/Staking/types.ts @@ -0,0 +1,34 @@ +import { BigNumberish } from "ethers"; +import { + FormatterFnType, + ParserFnType, + StakingActionFunctionType, +} from "./hooks/useStakingActionsResolver"; + +type GenericStakingComponentProps = { + isConnected: boolean; + walletConnectionHandler: () => void; +}; + +export type StakingRewardPropType = GenericStakingComponentProps & { + maximumClaimableAmount: BigNumberish; +}; + +export type StakingFormPropType = GenericStakingComponentProps & { + lpTokenName: string; + userCumulativeStake: BigNumberish; + globalCumulativeStake: BigNumberish; + ageOfCapital: number; + usersMultiplierPercentage: number; + currentMultiplier: BigNumberish; + usersTotalLPTokens: BigNumberish; + availableLPTokenBalance: BigNumberish; + shareOfPool: BigNumberish; + isWrongNetwork: boolean; + estimatedPoolApy: BigNumberish; + lpTokenFormatter: FormatterFnType; + lpTokenParser: ParserFnType; + stakeActionFn: StakingActionFunctionType; + unstakeActionFn: StakingActionFunctionType; + isDataLoading: boolean; +}; diff --git a/src/views/Transactions/MyTransactions.tsx b/src/views/Transactions/MyTransactions.tsx index 50f21a815..32e8742e9 100644 --- a/src/views/Transactions/MyTransactions.tsx +++ b/src/views/Transactions/MyTransactions.tsx @@ -10,7 +10,7 @@ import { useMyTransactionsView } from "./hooks/useMyTransactionsView"; import { ConnectButton, Account, ButtonWrapper } from "./Transactions.styles"; import { TransactionsLayout } from "./components/TransactionsLayout"; -export function MyTransactions() { +export default function MyTransactions() { const { connectWallet, account, diff --git a/src/views/index.ts b/src/views/index.ts index 926bd5685..01b53c608 100644 --- a/src/views/index.ts +++ b/src/views/index.ts @@ -6,3 +6,4 @@ export { MyTransactions, AllTransactions } from "./Transactions"; export { default as Rewards } from "./Rewards"; export { default as Claim } from "./Claim"; export { default as NotFound } from "./NotFound"; +export { default as Staking } from "./Staking"; diff --git a/yarn.lock b/yarn.lock index 1c36cf712..624cf7500 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,15 @@ # yarn lockfile v1 +"@across-protocol/across-token@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@across-protocol/across-token/-/across-token-0.0.1.tgz#9fe58cd603759584bd5f340899e208303df998ac" + integrity sha512-J6PQvMSYkVC34GzYpK9KmTAGFnocmcm7ZRnK51TtSSE/cMJERXMJi9eLL1R7O9/meizHtrZxCi71gbqDq9PVlg== + dependencies: + "@openzeppelin/contracts" "^4.5.0" + "@uma/common" "^2.17.0" + hardhat "^2.9.3" + "@across-protocol/contracts-v2@^0.0.34": version "0.0.34" resolved "https://registry.yarnpkg.com/@across-protocol/contracts-v2/-/contracts-v2-0.0.34.tgz#e36b5a641e5fe75bd5c08706082750abd489968c" @@ -5454,6 +5463,16 @@ jsbi "^3.1.5" sha.js "^2.4.11" +"@noble/hashes@1.1.2", "@noble/hashes@~1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" + integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA== + +"@noble/secp256k1@1.6.3", "@noble/secp256k1@~1.6.0": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.6.3.tgz#7eed12d9f4404b416999d0c87686836c4c5c9b94" + integrity sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -5480,6 +5499,204 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@nomicfoundation/ethereumjs-block@4.0.0-rc.3", "@nomicfoundation/ethereumjs-block@^4.0.0-rc.3": + version "4.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-4.0.0-rc.3.tgz#759d361968b23f06fd0f3f24023005bd3f05aa76" + integrity sha512-T+KzsCOEB4iP2Wy0OmjsxARbX8czN8LjF2pfdz9ucx37jAHfVAhWmEZaB+wfh7NZqumsBfgRtYbRJ572+nlTBQ== + dependencies: + "@nomicfoundation/ethereumjs-common" "3.0.0-rc.3" + "@nomicfoundation/ethereumjs-rlp" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-trie" "5.0.0-rc.3" + "@nomicfoundation/ethereumjs-tx" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-util" "8.0.0-rc.3" + ethereum-cryptography "0.1.3" + +"@nomicfoundation/ethereumjs-blockchain@6.0.0-rc.3", "@nomicfoundation/ethereumjs-blockchain@^6.0.0-rc.3": + version "6.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-6.0.0-rc.3.tgz#d6f4111447caad4f2f9c30fbe71117a7fdf081c2" + integrity sha512-GxaMYLXcyY/aFFXOiIwYYDVwHFffnddymldOsBGtGHbs0HM/kYLLF+dp3C31Q0+EaFNa6mF1L0NqAbC82CJRNA== + dependencies: + "@nomicfoundation/ethereumjs-block" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-common" "3.0.0-rc.3" + "@nomicfoundation/ethereumjs-ethash" "2.0.0-rc.3" + "@nomicfoundation/ethereumjs-rlp" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-trie" "5.0.0-rc.3" + "@nomicfoundation/ethereumjs-util" "8.0.0-rc.3" + abstract-level "^1.0.3" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + level "^8.0.0" + lru-cache "^5.1.1" + memory-level "^1.0.0" + +"@nomicfoundation/ethereumjs-common@3.0.0-rc.3", "@nomicfoundation/ethereumjs-common@^3.0.0-rc.3": + version "3.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-3.0.0-rc.3.tgz#f0a43d0a9db0a0ebb587e8b5bac022c152c1da31" + integrity sha512-r7qLtNabVEHNihLZevHV0weNshDpXo/o7i0JD9O10OExdicpgHPsU4qGnAvzO9bby9ANO2ydrOIlrYSm4lBkTg== + dependencies: + "@nomicfoundation/ethereumjs-util" "8.0.0-rc.3" + crc-32 "^1.2.0" + +"@nomicfoundation/ethereumjs-ethash@2.0.0-rc.3": + version "2.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-2.0.0-rc.3.tgz#78476d68fd15f3ab3ade8b1ad68407d8ac7b96eb" + integrity sha512-l75FH3KYUXuXjEdVZ3P7iVBbFhsghIMUuOBVfau4vx90SEGUQZnrU6cg9jBTyYvn0w9IIKJ76ZmDV8RDohZktA== + dependencies: + "@nomicfoundation/ethereumjs-block" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-rlp" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-util" "8.0.0-rc.3" + abstract-level "^1.0.3" + bigint-crypto-utils "^3.0.23" + ethereum-cryptography "0.1.3" + +"@nomicfoundation/ethereumjs-evm@1.0.0-rc.3", "@nomicfoundation/ethereumjs-evm@^1.0.0-rc.3": + version "1.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-1.0.0-rc.3.tgz#b09b470e33984211df9e1ca7fdff3261b6caef84" + integrity sha512-FY/SxIazYeJQ2uvx5uXV+MRgThrPjzr0nKMEyrFZPgbZb4KvcZarJuQVaJhQ4a5foqq8aHHRbWLdJQyWn9c2jw== + dependencies: + "@nomicfoundation/ethereumjs-common" "3.0.0-rc.3" + "@nomicfoundation/ethereumjs-util" "8.0.0-rc.3" + "@types/async-eventemitter" "^0.2.1" + async-eventemitter "^0.2.4" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + mcl-wasm "^0.7.1" + rustbn.js "~0.2.0" + +"@nomicfoundation/ethereumjs-rlp@4.0.0-rc.3", "@nomicfoundation/ethereumjs-rlp@^4.0.0-beta.2", "@nomicfoundation/ethereumjs-rlp@^4.0.0-rc.3": + version "4.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-4.0.0-rc.3.tgz#f654b6aaf74b0859ba68bac9522df82d847070cd" + integrity sha512-4F3fYTdqJhBNDoZ4o7uGzorvcbXuSeRXz46X/Z1TGMri5FjpWFl48qEOse2RpXCFudlAv7n/MpgJSuFzN1vreQ== + +"@nomicfoundation/ethereumjs-statemanager@1.0.0-rc.3", "@nomicfoundation/ethereumjs-statemanager@^1.0.0-rc.3": + version "1.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-1.0.0-rc.3.tgz#1057e81406f058166f68c6af9aac48693cc9ad1a" + integrity sha512-c69I4eZN9LFXUp1OI8hGwTvQMmcICus+MLgK5HELKLexV1SKs+K0iA4jgTK6VMM4wrzkmljyVxU5pM0Cb82XAQ== + dependencies: + "@nomicfoundation/ethereumjs-common" "3.0.0-rc.3" + "@nomicfoundation/ethereumjs-rlp" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-trie" "5.0.0-rc.3" + "@nomicfoundation/ethereumjs-util" "8.0.0-rc.3" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + functional-red-black-tree "^1.0.1" + +"@nomicfoundation/ethereumjs-trie@5.0.0-rc.3", "@nomicfoundation/ethereumjs-trie@^5.0.0-rc.3": + version "5.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-5.0.0-rc.3.tgz#df642ca1883eea0e2c2555028ae912b585d69da6" + integrity sha512-hz84rSGiYOs3vANLGxQm12gKtERMQzkgt1fZBu/OJulMCU+kR1CZxptVpmeg7W8n4NCyIcMPpGeshTMhg8zC5A== + dependencies: + "@nomicfoundation/ethereumjs-rlp" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-util" "8.0.0-rc.3" + ethereum-cryptography "0.1.3" + readable-stream "^3.6.0" + +"@nomicfoundation/ethereumjs-tx@4.0.0-rc.3", "@nomicfoundation/ethereumjs-tx@^4.0.0-rc.3": + version "4.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-4.0.0-rc.3.tgz#0b56bbaee0908491b21419808a44bc0356438060" + integrity sha512-Z3/EYglP+uKyzQj5pc2oMv/vuJ3ZZ2v3qVqRG9k5EsGXNB1lzN1zIh6NCW/vw/AdGoH69MDNGzG5hqGZ9cJJiw== + dependencies: + "@nomicfoundation/ethereumjs-common" "3.0.0-rc.3" + "@nomicfoundation/ethereumjs-rlp" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-util" "8.0.0-rc.3" + ethereum-cryptography "0.1.3" + +"@nomicfoundation/ethereumjs-util@8.0.0-rc.3", "@nomicfoundation/ethereumjs-util@^8.0.0-rc.3": + version "8.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-8.0.0-rc.3.tgz#d47dca076b5ea41b4498cf8292666d21f31b4e88" + integrity sha512-Ldd1NVbk+FtP/JKCQTOVrBJzHMXpMnUdqE9oetAqKVnaLszXMEUa/B0fBdJaPIXKU/c9tAba29/pGxRpcQbgKQ== + dependencies: + "@nomicfoundation/ethereumjs-rlp" "^4.0.0-beta.2" + ethereum-cryptography "0.1.3" + +"@nomicfoundation/ethereumjs-vm@^6.0.0-rc.3": + version "6.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-6.0.0-rc.3.tgz#e1e3f29b45a206fdaafdff8316081c98a558407b" + integrity sha512-MF6WeU0sx+6zM8ustttlZZFZtI6/c/qIWVnxrT6K5VRaiC1Us1ih3S8HBr6xNkl6JgBHj0e0oC1CA9xiowwlUQ== + dependencies: + "@nomicfoundation/ethereumjs-block" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-blockchain" "6.0.0-rc.3" + "@nomicfoundation/ethereumjs-common" "3.0.0-rc.3" + "@nomicfoundation/ethereumjs-evm" "1.0.0-rc.3" + "@nomicfoundation/ethereumjs-rlp" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-statemanager" "1.0.0-rc.3" + "@nomicfoundation/ethereumjs-trie" "5.0.0-rc.3" + "@nomicfoundation/ethereumjs-tx" "4.0.0-rc.3" + "@nomicfoundation/ethereumjs-util" "8.0.0-rc.3" + "@types/async-eventemitter" "^0.2.1" + async-eventemitter "^0.2.4" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + functional-red-black-tree "^1.0.1" + mcl-wasm "^0.7.1" + rustbn.js "~0.2.0" + +"@nomicfoundation/solidity-analyzer-darwin-arm64@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.0.3.tgz#1d49e4ac028831a3011a9f3dca60bd1963185342" + integrity sha512-W+bIiNiZmiy+MTYFZn3nwjyPUO6wfWJ0lnXx2zZrM8xExKObMrhCh50yy8pQING24mHfpPFCn89wEB/iG7vZDw== + +"@nomicfoundation/solidity-analyzer-darwin-x64@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.0.3.tgz#c0fccecc5506ff5466225e41e65691abafef3dbe" + integrity sha512-HuJd1K+2MgmFIYEpx46uzwEFjvzKAI765mmoMxy4K+Aqq1p+q7hHRlsFU2kx3NB8InwotkkIq3A5FLU1sI1WDw== + +"@nomicfoundation/solidity-analyzer-freebsd-x64@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-freebsd-x64/-/solidity-analyzer-freebsd-x64-0.0.3.tgz#8261d033f7172b347490cd005931ef8168ab4d73" + integrity sha512-2cR8JNy23jZaO/vZrsAnWCsO73asU7ylrHIe0fEsXbZYqBP9sMr+/+xP3CELDHJxUbzBY8zqGvQt1ULpyrG+Kw== + +"@nomicfoundation/solidity-analyzer-linux-arm64-gnu@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.0.3.tgz#1ba64b1d76425f8953dedc6367bd7dd46f31dfc5" + integrity sha512-Eyv50EfYbFthoOb0I1568p+eqHGLwEUhYGOxcRNywtlTE9nj+c+MT1LA53HnxD9GsboH4YtOOmJOulrjG7KtbA== + +"@nomicfoundation/solidity-analyzer-linux-arm64-musl@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.0.3.tgz#8d864c49b55e683f7e3b5cce9d10b628797280ac" + integrity sha512-V8grDqI+ivNrgwEt2HFdlwqV2/EQbYAdj3hbOvjrA8Qv+nq4h9jhQUxFpegYMDtpU8URJmNNlXgtfucSrAQwtQ== + +"@nomicfoundation/solidity-analyzer-linux-x64-gnu@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.0.3.tgz#16e769500cf1a8bb42ab9498cee3b93c30f78295" + integrity sha512-uRfVDlxtwT1vIy7MAExWAkRD4r9M79zMG7S09mCrWUn58DbLs7UFl+dZXBX0/8FTGYWHhOT/1Etw1ZpAf5DTrg== + +"@nomicfoundation/solidity-analyzer-linux-x64-musl@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.0.3.tgz#75f4e1a25526d54c506e4eba63b3d698b6255b8f" + integrity sha512-8HPwYdLbhcPpSwsE0yiU/aZkXV43vlXT2ycH+XlOjWOnLfH8C41z0njK8DHRtEFnp4OVN6E7E5lHBBKDZXCliA== + +"@nomicfoundation/solidity-analyzer-win32-arm64-msvc@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-win32-arm64-msvc/-/solidity-analyzer-win32-arm64-msvc-0.0.3.tgz#ef6e20cfad5eedfdb145cc34a44501644cd7d015" + integrity sha512-5WWcT6ZNvfCuxjlpZOY7tdvOqT1kIQYlDF9Q42wMpZ5aTm4PvjdCmFDDmmTvyXEBJ4WTVmY5dWNWaxy8h/E28g== + +"@nomicfoundation/solidity-analyzer-win32-ia32-msvc@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-win32-ia32-msvc/-/solidity-analyzer-win32-ia32-msvc-0.0.3.tgz#98c4e3af9cee68896220fa7e270aefdf7fc89c7b" + integrity sha512-P/LWGZwWkyjSwkzq6skvS2wRc3gabzAbk6Akqs1/Iiuggql2CqdLBkcYWL5Xfv3haynhL+2jlNkak+v2BTZI4A== + +"@nomicfoundation/solidity-analyzer-win32-x64-msvc@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.0.3.tgz#12da288e7ef17ec14848f19c1e8561fed20d231d" + integrity sha512-4AcTtLZG1s/S5mYAIr/sdzywdNwJpOcdStGF3QMBzEt+cGn3MchMaS9b1gyhb2KKM2c39SmPF5fUuWq1oBSQZQ== + +"@nomicfoundation/solidity-analyzer@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.0.3.tgz#d1029f872e66cb1082503b02cc8b0be12f8dd95e" + integrity sha512-VFMiOQvsw7nx5bFmrmVp2Q9rhIjw2AFST4DYvWVVO9PMHPE23BY2+kyfrQ4J3xCMFC8fcBbGLt7l4q7m1SlTqg== + optionalDependencies: + "@nomicfoundation/solidity-analyzer-darwin-arm64" "0.0.3" + "@nomicfoundation/solidity-analyzer-darwin-x64" "0.0.3" + "@nomicfoundation/solidity-analyzer-freebsd-x64" "0.0.3" + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu" "0.0.3" + "@nomicfoundation/solidity-analyzer-linux-arm64-musl" "0.0.3" + "@nomicfoundation/solidity-analyzer-linux-x64-gnu" "0.0.3" + "@nomicfoundation/solidity-analyzer-linux-x64-musl" "0.0.3" + "@nomicfoundation/solidity-analyzer-win32-arm64-msvc" "0.0.3" + "@nomicfoundation/solidity-analyzer-win32-ia32-msvc" "0.0.3" + "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.0.3" + "@nomiclabs/ethereumjs-vm@^4.2.2": version "4.2.2" resolved "https://registry.yarnpkg.com/@nomiclabs/ethereumjs-vm/-/ethereumjs-vm-4.2.2.tgz#2f8817113ca0fb6c44c1b870d0a809f0e026a6cc" @@ -5598,6 +5815,11 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.4.2.tgz#4e889c9c66e736f7de189a53f8ba5b8d789425c2" integrity sha512-NyJV7sJgoGYqbtNUWgzzOGW4T6rR19FmX1IJgXGdapGPWsuMelGJn9h03nos0iqfforCbCB0iYIR0MtIuIFLLw== +"@openzeppelin/contracts@^4.5.0": + version "4.7.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.7.3.tgz#939534757a81f8d69cc854c7692805684ff3111e" + integrity sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw== + "@openzeppelin/upgrades-core@^1.7.6": version "1.14.1" resolved "https://registry.yarnpkg.com/@openzeppelin/upgrades-core/-/upgrades-core-1.14.1.tgz#a0e1c83f9811186ac49d286e6b43ae129097422b" @@ -5779,6 +6001,28 @@ estree-walker "^1.0.1" picomatch "^2.2.2" +"@scure/base@~1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" + integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== + +"@scure/bip32@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.0.tgz#dea45875e7fbc720c2b4560325f1cf5d2246d95b" + integrity sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q== + dependencies: + "@noble/hashes" "~1.1.1" + "@noble/secp256k1" "~1.6.0" + "@scure/base" "~1.1.0" + +"@scure/bip39@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.0.tgz#92f11d095bae025f166bef3defcc5bf4945d419a" + integrity sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w== + dependencies: + "@noble/hashes" "~1.1.1" + "@scure/base" "~1.1.0" + "@sentry/core@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" @@ -7423,6 +7667,11 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== +"@types/async-eventemitter@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@types/async-eventemitter/-/async-eventemitter-0.2.1.tgz#f8e6280e87e8c60b2b938624b0a3530fb3e24712" + integrity sha512-M2P4Ng26QbAeITiH7w1d7OxtldgfAe0wobpyJzVK/XOb0cUGKU2R4pfAhqcJBXAe2ife5ZOhSv4wk7p+ffURtg== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.17" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.17.tgz#f50ac9d20d64153b510578d84f9643f9a3afbe64" @@ -8953,6 +9202,19 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +abstract-level@^1.0.0, abstract-level@^1.0.2, abstract-level@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/abstract-level/-/abstract-level-1.0.3.tgz#78a67d3d84da55ee15201486ab44c09560070741" + integrity sha512-t6jv+xHy+VYwc4xqZMn2Pa9DjcdzvzZmQGRjTFc8spIbRGHgBrEKbPq+rYXc7CCo0lxgYvSgKVg9qZAhpVQSjA== + dependencies: + buffer "^6.0.3" + catering "^2.1.0" + is-buffer "^2.0.5" + level-supports "^4.0.0" + level-transcoder "^1.0.1" + module-error "^1.0.1" + queue-microtask "^1.2.3" + abstract-leveldown@^5.0.0, abstract-leveldown@~5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-5.0.0.tgz#f7128e1f86ccabf7d2893077ce5d06d798e386c6" @@ -10893,6 +11155,18 @@ bigint-buffer@^1.1.5: dependencies: bindings "^1.3.0" +bigint-crypto-utils@^3.0.23: + version "3.1.4" + resolved "https://registry.yarnpkg.com/bigint-crypto-utils/-/bigint-crypto-utils-3.1.4.tgz#b00aa00eb792b14f2f71ead916105c17aac98a4c" + integrity sha512-niSkvARUEe8MiAiH+zKXPkgXzlvGDbOqXL3JDevWaA1TrPhUGSCgV+iedm8qMEBQwvSlMMn8GpSuoUjvsm2QfQ== + dependencies: + bigint-mod-arith "^3.1.0" + +bigint-mod-arith@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bigint-mod-arith/-/bigint-mod-arith-3.1.0.tgz#ee7186ff512248e245f8c6ed0aa5c0ccf0c116b4" + integrity sha512-vpiKCiv9B1nK8HhFOU7PMC4k9nrufQxeivgCj5yOH2ZMLD+UPwc/RfNgBCX+v8C6t0sF4q7mEZgZij6k53zpWA== + bignumber.js@*, bignumber.js@^9.0.0, bignumber.js@^9.0.1: version "9.0.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673" @@ -11307,6 +11581,16 @@ brorand@^1.0.1, brorand@^1.1.0: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= +browser-level@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browser-level/-/browser-level-1.0.1.tgz#36e8c3183d0fe1c405239792faaab5f315871011" + integrity sha512-XECYKJ+Dbzw0lbydyQuJzwNXtOpbMSq737qxJN11sIRTErOMShvDpbzTlgju7orJKvx4epULolZAuJGLzCmWRQ== + dependencies: + abstract-level "^1.0.2" + catering "^2.1.1" + module-error "^1.0.2" + run-parallel-limit "^1.1.0" + browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -11844,6 +12128,11 @@ caseless@^0.12.0, caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +catering@^2.1.0, catering@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510" + integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w== + catharsis@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" @@ -12212,6 +12501,17 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +classic-level@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/classic-level/-/classic-level-1.2.0.tgz#2d52bdec8e7a27f534e67fdeb890abef3e643c27" + integrity sha512-qw5B31ANxSluWz9xBzklRWTUAJ1SXIdaVKTVS7HcTGKOAmExx65Wo5BUICW+YGORe2FOUaDghoI9ZDxj82QcFg== + dependencies: + abstract-level "^1.0.2" + catering "^2.1.0" + module-error "^1.0.1" + napi-macros "~2.0.0" + node-gyp-build "^4.3.0" + classnames@^2.2.6: version "2.3.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" @@ -13426,6 +13726,13 @@ debug@4, debug@4.3.3, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: dependencies: ms "2.1.2" +debug@4.3.4, debug@^4.3.2, debug@^4.3.3: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debug@=3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -13440,13 +13747,6 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.2, debug@^4.3.3: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - decamelize-keys@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -15329,7 +15629,7 @@ ethereum-common@^0.0.18: resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f" integrity sha1-L9w1dvIykDNYl26znaeDIT/5Uj8= -ethereum-cryptography@^0.1.2, ethereum-cryptography@^0.1.3: +ethereum-cryptography@0.1.3, ethereum-cryptography@^0.1.2, ethereum-cryptography@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz#8d6143cfc3d74bf79bbd8edecdf29e4ae20dd191" integrity sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ== @@ -15350,6 +15650,16 @@ ethereum-cryptography@^0.1.2, ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" +ethereum-cryptography@^1.0.3: + version "1.1.2" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz#74f2ac0f0f5fe79f012c889b3b8446a9a6264e6d" + integrity sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ== + dependencies: + "@noble/hashes" "1.1.2" + "@noble/secp256k1" "1.6.3" + "@scure/bip32" "1.1.0" + "@scure/bip39" "1.1.0" + ethereum-ens@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/ethereum-ens/-/ethereum-ens-0.8.0.tgz#6d0f79acaa61fdbc87d2821779c4e550243d4c57" @@ -17765,6 +18075,62 @@ hardhat@^2.5.0, hardhat@^2.6.1, hardhat@^2.6.4: uuid "^8.3.2" ws "^7.4.6" +hardhat@^2.9.3: + version "2.11.1" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.11.1.tgz#9d7967dd360b9a217ac6b7d9ca7f5087db4db01d" + integrity sha512-7FoyfKjBs97GHNpQejHecJBBcRPOEhAE3VkjSWXB3GeeiXefWbw+zhRVOjI4eCsUUt7PyNFAdWje/lhnBT9fig== + dependencies: + "@ethersproject/abi" "^5.1.2" + "@metamask/eth-sig-util" "^4.0.0" + "@nomicfoundation/ethereumjs-block" "^4.0.0-rc.3" + "@nomicfoundation/ethereumjs-blockchain" "^6.0.0-rc.3" + "@nomicfoundation/ethereumjs-common" "^3.0.0-rc.3" + "@nomicfoundation/ethereumjs-evm" "^1.0.0-rc.3" + "@nomicfoundation/ethereumjs-rlp" "^4.0.0-rc.3" + "@nomicfoundation/ethereumjs-statemanager" "^1.0.0-rc.3" + "@nomicfoundation/ethereumjs-trie" "^5.0.0-rc.3" + "@nomicfoundation/ethereumjs-tx" "^4.0.0-rc.3" + "@nomicfoundation/ethereumjs-util" "^8.0.0-rc.3" + "@nomicfoundation/ethereumjs-vm" "^6.0.0-rc.3" + "@nomicfoundation/solidity-analyzer" "^0.0.3" + "@sentry/node" "^5.18.1" + "@types/bn.js" "^5.1.0" + "@types/lru-cache" "^5.1.0" + abort-controller "^3.0.0" + adm-zip "^0.4.16" + aggregate-error "^3.0.0" + ansi-escapes "^4.3.0" + chalk "^2.4.2" + chokidar "^3.4.0" + ci-info "^2.0.0" + debug "^4.1.1" + enquirer "^2.3.0" + env-paths "^2.2.0" + ethereum-cryptography "^1.0.3" + ethereumjs-abi "^0.6.8" + find-up "^2.1.0" + fp-ts "1.19.3" + fs-extra "^7.0.1" + glob "7.2.0" + immutable "^4.0.0-rc.12" + io-ts "1.10.4" + keccak "^3.0.2" + lodash "^4.17.11" + mnemonist "^0.38.0" + mocha "^10.0.0" + p-map "^4.0.0" + qs "^6.7.0" + raw-body "^2.4.1" + resolve "1.17.0" + semver "^6.3.0" + solc "0.7.3" + source-map-support "^0.5.13" + stacktrace-parser "^0.1.10" + tsort "0.0.1" + undici "^5.4.0" + uuid "^8.3.2" + ws "^7.4.6" + harmony-reflect@^1.4.6: version "1.6.2" resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710" @@ -18739,7 +19105,7 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@^2.0.0, is-buffer@^2.0.2, is-buffer@~2.0.3: +is-buffer@^2.0.2, is-buffer@^2.0.5, is-buffer@~2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== @@ -20579,6 +20945,11 @@ level-packager@~4.0.0: encoding-down "~5.0.0" levelup "^3.0.0" +level-supports@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-4.0.1.tgz#431546f9d81f10ff0fea0e74533a0e875c08c66a" + integrity sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA== + level-supports@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d" @@ -20586,6 +20957,14 @@ level-supports@~1.0.0: dependencies: xtend "^4.0.2" +level-transcoder@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/level-transcoder/-/level-transcoder-1.0.1.tgz#f8cef5990c4f1283d4c86d949e73631b0bc8ba9c" + integrity sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w== + dependencies: + buffer "^6.0.3" + module-error "^1.0.1" + level-ws@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/level-ws/-/level-ws-0.0.0.tgz#372e512177924a00424b0b43aef2bb42496d228b" @@ -20612,6 +20991,14 @@ level-ws@^2.0.0: readable-stream "^3.1.0" xtend "^4.0.1" +level@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/level/-/level-8.0.0.tgz#41b4c515dabe28212a3e881b61c161ffead14394" + integrity sha512-ypf0jjAk2BWI33yzEaaotpq7fkOPALKAgDBxggO6Q9HGX2MRXn0wbP1Jn/tJv1gtL867+YOjOB49WaUF3UoJNQ== + dependencies: + browser-level "^1.0.1" + classic-level "^1.2.0" + levelup@^1.2.1: version "1.3.9" resolved "https://registry.yarnpkg.com/levelup/-/levelup-1.3.9.tgz#2dbcae845b2bb2b6bea84df334c475533bbd82ab" @@ -21368,6 +21755,15 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" +memory-level@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/memory-level/-/memory-level-1.0.0.tgz#7323c3fd368f9af2f71c3cd76ba403a17ac41692" + integrity sha512-UXzwewuWeHBz5krr7EvehKcmLFNoXxGcvuYhC41tRnkrTbJohtS7kVn9akmgirtRygg+f7Yjsfi8Uu5SGSQ4Og== + dependencies: + abstract-level "^1.0.0" + functional-red-black-tree "^1.0.1" + module-error "^1.0.1" + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" @@ -21675,6 +22071,13 @@ minimatch@4.2.1: dependencies: brace-expansion "^1.1.7" +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + minimatch@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" @@ -21808,6 +22211,34 @@ mnemonist@^0.38.0: dependencies: obliterator "^2.0.0" +mocha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.0.0.tgz#205447d8993ec755335c4b13deba3d3a13c4def9" + integrity sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + mocha@^7.1.1: version "7.2.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.2.0.tgz#01cc227b00d875ab1eed03a75106689cfed5a604" @@ -21873,6 +22304,11 @@ mock-fs@^4.1.0: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" integrity sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw== +module-error@^1.0.1, module-error@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/module-error/-/module-error-1.0.2.tgz#8d1a48897ca883f47a45816d4fb3e3c6ba404d86" + integrity sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA== + moment@^2.24.0: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" @@ -22009,6 +22445,11 @@ nanoid@3.3.1, nanoid@^3.1.30: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + nanoid@^3.3.1: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" @@ -22041,6 +22482,11 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +napi-macros@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" + integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg== + native-url@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/native-url/-/native-url-0.2.6.tgz#ca1258f5ace169c716ff44eccbddb674e10399ae" @@ -24625,7 +25071,7 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== -queue-microtask@^1.2.2: +queue-microtask@^1.2.2, queue-microtask@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== @@ -26033,6 +26479,13 @@ rtcpeerconnection-shim@^1.2.15: dependencies: sdp "^2.6.0" +run-parallel-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz#be80e936f5768623a38a963262d6bef8ff11e7ba" + integrity sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw== + dependencies: + queue-microtask "^1.2.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -28560,6 +29013,11 @@ undici@^4.14.1: resolved "https://registry.yarnpkg.com/undici/-/undici-4.16.0.tgz#469bb87b3b918818d3d7843d91a1d08da357d5ff" integrity sha512-tkZSECUYi+/T1i4u+4+lwZmQgLXd4BLGlrc7KZPcLIW7Jpq99+Xpc30ONv7nS6F5UNOxp/HBZSSL9MafUrvJbw== +undici@^5.4.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.10.0.tgz#dd9391087a90ccfbd007568db458674232ebf014" + integrity sha512-c8HsD3IbwmjjbLvoZuRI26TZic+TSEe8FPMLLOkN1AfYRhdjnKBU6yL+IwcSCbdZiX4e5t0lfMDLDCqj4Sq70g== + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" @@ -30774,6 +31232,11 @@ workerpool@6.2.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
{props.subTitle}