= ({ well }) => {
return (
@@ -30,7 +46,7 @@ export const LearnPump: FC = () => {
What is a Pump?
-
+
);
diff --git a/projects/dex-ui/src/components/Well/LearnWellFunction.tsx b/projects/dex-ui/src/components/Well/LearnWellFunction.tsx
index 3854df4929..24732ef86f 100644
--- a/projects/dex-ui/src/components/Well/LearnWellFunction.tsx
+++ b/projects/dex-ui/src/components/Well/LearnWellFunction.tsx
@@ -1,63 +1,85 @@
-import React from "react";
+import React, { useEffect } from "react";
import styled from "styled-components";
import { ExpandBox } from "src/components/ExpandBox";
import { TextNudge } from "../Typography";
import { FC } from "src/types";
-import { WellFunction } from "../Icons";
+import { WellFunction as WellFunctionIcon } from "../Icons";
+import { Well } from "@beanstalk/sdk-wells";
+import { CONSTANT_PRODUCT_2_ADDRESS } from "src/utils/addresses";
+import { formatWellTokenSymbols } from "src/wells/utils";
type Props = {
- name: string;
+ well: Well | undefined;
};
-function WellFunctionDetails(functionName: any) {
- if (functionName.functionName === "Constant Product") {
+function WellFunctionDetails({ well }: Props) {
+ const functionName = well?.wellFunction?.name;
+
+ useEffect(() => {
+ if (!functionName) {
+ well?.getWellFunction();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [functionName]);
+
+ if (functionName === "Constant Product") {
return (
- A Well Function is a pricing function for determining how many tokens users receive for swaps, how
- many LP tokens a user receives for adding liquidity, etc.
+ A Well Function is a pricing function for determining how many tokens users receive for
+ swaps, how many LP tokens a user receives for adding liquidity, etc.
- Constant Product is a reusable pricing function which prices tokens using:
+ Constant Product is a reusable pricing function
+ which prices tokens using:
- x * y = k, where x is the amount of one token, y is the amount of the other and{" "}
- k is a fixed constant.
+ x * y = k, where x is the amount of one token, y is
+ the amount of the other and k is a fixed constant.
);
- } else if (functionName.functionName === "Constant Product 2") {
+ } else if (well?.wellFunction?.address.toLowerCase() === CONSTANT_PRODUCT_2_ADDRESS) {
return (
- A Well Function is a pricing function for determining how many tokens users receive for swaps, how
- many LP tokens a user receives for adding liquidity, etc.
+ A Well Function is a pricing function for determining how many tokens users receive for
+ swaps, how many LP tokens a user receives for adding liquidity, etc.
- The BEAN:WETH Well uses the Constant Product 2 Well Function, which is a gas-efficient pricing function
- for Wells with 2 tokens.
+ The {formatWellTokenSymbols(well)} uses the Constant Product 2 Well Function, which is a
+ gas-efficient pricing function for Wells with 2 tokens.
);
} else {
return (
- {"Each Well utilizes a unique pricing function to price the tokens in the Well."}
- {"Brief descriptions of a Well's pricing function will appear in this box."}
+
+ A Well Function is a pricing function for determining how many tokens users receive for
+ swaps, how many LP tokens a user receives for adding liquidity, etc.
+
+ Each Well utilizes a unique pricing function to price the tokens in the Well.
);
}
}
-export const LearnWellFunction: FC = ({ name }) => {
+export const LearnWellFunction: FC = ({ well }) => {
+ const name = well?.wellFunction?.name;
+
+ const drawerHeaderText = well?.wellFunction?.name
+ ? `What is ${name}?`
+ : "What is a Well Function?";
+
return (
-
+
-
+
What is {name}?
-
+
);
diff --git a/projects/dex-ui/src/components/Well/LearnYield.tsx b/projects/dex-ui/src/components/Well/LearnYield.tsx
index 246deb3865..847319c830 100644
--- a/projects/dex-ui/src/components/Well/LearnYield.tsx
+++ b/projects/dex-ui/src/components/Well/LearnYield.tsx
@@ -5,14 +5,14 @@ import { TextNudge } from "../Typography";
import { FC } from "src/types";
import { YieldSparkle } from "../Icons";
-type Props = {};
+type Props = { isWhitelisted?: boolean };
function YieldDetails() {
return (
- Liquidity providers can earn yield by depositing BEANETH LP in the Beanstalk Silo. You can add liquidity and deposit the LP token in
- the Silo in a single transaction on the{" "}
+ Liquidity providers can earn yield by depositing BEANETH LP in the Beanstalk Silo. You can
+ add liquidity and deposit the LP token in the Silo in a single transaction on the{" "}
= () => {
+export const LearnYield: FC = ({ isWhitelisted }) => {
+ if (!isWhitelisted) return null;
+
return (
diff --git a/projects/dex-ui/src/components/Well/LiquidityBox.tsx b/projects/dex-ui/src/components/Well/LiquidityBox.tsx
index d259b8b05b..1d3893fbf6 100644
--- a/projects/dex-ui/src/components/Well/LiquidityBox.tsx
+++ b/projects/dex-ui/src/components/Well/LiquidityBox.tsx
@@ -43,10 +43,11 @@ export const LiquidityBox: FC = ({ well: _well, loading }) => {
const position = getPositionWithWell(well);
const isWhitelisted = getIsWhitelisted(well);
- const { data: lpTokenPriceMap } = useWellLPTokenPrice(well);
+ const { data: lpTokenPriceMap = {} } = useWellLPTokenPrice(well);
const lpAddress = well?.lpToken?.address;
- const lpTokenPrice = lpAddress && lpAddress in lpTokenPriceMap ? lpTokenPriceMap[lpAddress] : TokenValue.ZERO;
+ const lpTokenPrice =
+ lpAddress && lpAddress in lpTokenPriceMap ? lpTokenPriceMap[lpAddress] : TokenValue.ZERO;
const siloUSD = position?.silo.mul(lpTokenPrice) || TokenValue.ZERO;
const externalUSD = position?.external.mul(lpTokenPrice) || TokenValue.ZERO;
@@ -88,7 +89,12 @@ export const LiquidityBox: FC = ({ well: _well, loading }) => {
content={
BEANETH LP token holders can Deposit their LP tokens in the{" "}
-
+
Beanstalk Silo
for yield.
@@ -114,11 +120,17 @@ export const LiquidityBox: FC
= ({ well: _well, loading }) => {
-
+
Farm Balances
- allow Beanstalk users to hold assets in the protocol on their behalf. Using Farm Balances can reduce gas costs
- and facilitate efficient movement of assets within Beanstalk.
+ allow Beanstalk users to hold assets in the protocol on their behalf.
+ Using Farm Balances can reduce gas costs and facilitate efficient movement
+ of assets within Beanstalk.
}
offsetX={-40}
@@ -160,10 +172,10 @@ export const LiquidityBox: FC = ({ well: _well, loading }) => {
}
>
- <>USD TOTAL: {formatUSD(USDTotal)}>
+ <>USD TOTAL: {USDTotal.gt(0) ? formatUSD(USDTotal) : "$--"}>
) : (
- <>USD TOTAL: {formatUSD(USDTotal)}>
+ <>USD TOTAL: {USDTotal.gt(0) ? formatUSD(USDTotal) : "$--"}>
)}
diff --git a/projects/dex-ui/src/components/Well/OtherSection.tsx b/projects/dex-ui/src/components/Well/OtherSection.tsx
index 0918d985ce..f506d5c45a 100644
--- a/projects/dex-ui/src/components/Well/OtherSection.tsx
+++ b/projects/dex-ui/src/components/Well/OtherSection.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
import { FC } from "src/types";
import { Row, TBody, THead, Table, Td, Th } from "./Table";
import { Well } from "@beanstalk/sdk/Wells";
@@ -7,33 +7,92 @@ import { size } from "src/breakpoints";
import { displayTokenSymbol } from "src/utils/format";
import { Token } from "@beanstalk/sdk";
import { Skeleton } from "../Skeleton";
+import { useWhitelistedWellComponents } from "../Create/useWhitelistedWellComponents";
+import { useWellImplementations } from "src/wells/useWellImplementations";
type Props = { well: Well };
-const tableItems = [
- { name: "Multi Flow Pump", address: "0xBA510f10E3095B83a0F33aa9ad2544E22570a87C" },
- { name: "Constant Product 2", address: "0xBA510C20FD2c52E4cb0d23CFC3cCD092F9165a6E" },
- { name: "Well Implementation", address: "0xBA510e11eEb387fad877812108a3406CA3f43a4B" },
- { name: "Aquifer", address: "0xBA51AAAA95aeEFc1292515b36D86C51dC7877773" }
-];
-
const OtherSectionContent: FC = ({ well }) => {
+ const { data: implementations } = useWellImplementations();
+ const {
+ lookup: { pumps: pumpLookup }
+ } = useWhitelistedWellComponents();
+
+ const [items, setItems] = useState<{ name: string; address: string }[]>([]);
+ const [wellFunctionName, setWellFunctionName] = useState("");
+
+ const implementationAddress = implementations?.[well.address.toLowerCase()];
+
+ const wellTokenDetail = well.tokens
+ ?.map((token) => token.symbol)
+ .filter(Boolean)
+ .join(":");
+
+ useEffect(() => {
+ const run = async () => {
+ if (!well.wellFunction) return;
+ const name = await well.wellFunction.getName();
+ setWellFunctionName(name);
+ };
+ run();
+ }, [well.wellFunction]);
+
+ useEffect(() => {
+ const data: typeof items = [];
+ well.pumps?.forEach((pump) => {
+ const pumpAddress = pump.address.toLowerCase();
+ if (pumpAddress in pumpLookup) {
+ const pumpInfo = pumpLookup[pumpAddress].component;
+ data.push({
+ name: pumpInfo?.fullName || pumpInfo.name,
+ address: pump.address
+ });
+ } else {
+ data.push({
+ name: "Pump",
+ address: pump.address || "--"
+ });
+ }
+ });
+ data.push({
+ name: wellFunctionName ?? "Well Function",
+ address: well.wellFunction?.address || "--"
+ });
+ data.push({
+ name: "Well Implementation",
+ address: implementationAddress || "--"
+ });
+ data.push({
+ name: "Aquifer",
+ address: well.aquifer?.address || "--"
+ });
+
+ setItems(data);
+ }, [
+ implementationAddress,
+ pumpLookup,
+ well.aquifer?.address,
+ well.pumps,
+ well.wellFunction?.address,
+ wellFunctionName
+ ]);
+
return (
Name |
- Address
- Address
+ Address
+ Address
- BEAN:WETH Well
+ {wellTokenDetail} Well
|
-
+
{well.address}
@@ -46,7 +105,7 @@ const OtherSectionContent: FC = ({ well }) => {
Well LP Token - {displayTokenSymbol(well.lpToken as Token)}
|
-
+
{well.address}
@@ -61,9 +120,13 @@ const OtherSectionContent: FC = ({ well }) => {
{`Token ${index + 1} - ${token.symbol}`}
|
-
+
@@ -72,28 +135,38 @@ const OtherSectionContent: FC = ({ well }) => {
- {token.address.substr(0, 5) + "..." + token.address.substr(token.address.length - 5) || `-`}
+ {token.address.substr(0, 5) +
+ "..." +
+ token.address.substr(token.address.length - 5) || `-`}
);
})}
- {tableItems.map(function (tableItem, index) {
+ {items.map(function (tableItem, index) {
return (
{tableItem.name}
|
-
- {tableItem.address}
+
+
+ {tableItem.address}
+
- {tableItem.address.substr(0, 5) + "..." + tableItem.address.substr(tableItem.address.length - 5)}
+ {tableItem.address.substr(0, 5) +
+ "..." +
+ tableItem.address.substr(tableItem.address.length - 5)}
@@ -110,7 +183,10 @@ const loadingItemProps = {
lg: { height: 24, width: 200 }
};
-export const OtherSection: FC<{ well: Well | undefined; loading?: boolean }> = ({ well, loading }) => {
+export const OtherSection: FC<{ well: Well | undefined; loading?: boolean }> = ({
+ well,
+ loading
+}) => {
if (!well || loading) {
return (
@@ -155,6 +231,7 @@ const Link = styled.a`
font-weight: 600;
text-decoration: underline;
text-decoration-thickness: 0.5px;
+ color: black;
:link {
color: black;
diff --git a/projects/dex-ui/src/components/Well/Reserves.tsx b/projects/dex-ui/src/components/Well/Reserves.tsx
index fce88a82de..3303471c2d 100644
--- a/projects/dex-ui/src/components/Well/Reserves.tsx
+++ b/projects/dex-ui/src/components/Well/Reserves.tsx
@@ -31,24 +31,32 @@ export const Reserves: FC
= ({ reserves, well, twaReserves }) =>
if (!well) return null;
+ const noPriceData = reserves.some((rsv) => rsv.dollarAmount === null);
+
const rows = (reserves ?? []).map((r, i) => (
-
{r.token?.symbol}
{getIsMultiPumpWell(well) && (
-
+
)}
- {formatNum(r.amount, { minDecimals: 2 })}
-
-
- {formatPercent(r.percentage)}
+ {formatNum(r.amount, { minDecimals: 2, minValue: 0.001 })}
+ {!noPriceData ? (
+
+ {formatPercent(r.percentage)}
+
+ ) : null}
));
diff --git a/projects/dex-ui/src/components/Well/Table/MyWellPositionRow.tsx b/projects/dex-ui/src/components/Well/Table/MyWellPositionRow.tsx
index 778d005e57..e0a441b1cd 100644
--- a/projects/dex-ui/src/components/Well/Table/MyWellPositionRow.tsx
+++ b/projects/dex-ui/src/components/Well/Table/MyWellPositionRow.tsx
@@ -98,7 +98,7 @@ export const MyWellPositionRow: FC<{
symbols.push(token.symbol);
});
- const lpPrice = lpAddress && lpAddress in prices ? prices[lpAddress] : undefined;
+ const lpPrice = lpAddress && prices && lpAddress in prices ? prices[lpAddress] : undefined;
const whitelisted = getIsWhitelisted(well);
const positionsUSD = {
@@ -128,7 +128,7 @@ export const MyWellPositionRow: FC<{
-
+
@@ -148,7 +148,7 @@ export const MyWellPositionRow: FC<{
-
+
diff --git a/projects/dex-ui/src/components/Well/Table/WellDetailRow.tsx b/projects/dex-ui/src/components/Well/Table/WellDetailRow.tsx
index e6abe1250a..6b1cc5c364 100644
--- a/projects/dex-ui/src/components/Well/Table/WellDetailRow.tsx
+++ b/projects/dex-ui/src/components/Well/Table/WellDetailRow.tsx
@@ -14,6 +14,7 @@ import { Item } from "src/components/Layout";
/// format value with 2 decimals, if value is less than 1M, otherwise use short format
const formatMayDecimals = (tv: TokenValue | undefined) => {
if (!tv) return "-.--";
+ if (tv.gt(0) && tv.lt(0.001)) return "<0.001";
if (tv.lt(1_000_000)) {
return formatNum(tv, { minDecimals: 2, maxDecimals: 2 });
}
@@ -24,7 +25,9 @@ export const WellDetailRow: FC<{
well: Well | undefined;
liquidity: TokenValue | undefined;
functionName: string | undefined;
-}> = ({ well, liquidity, functionName }) => {
+ price: TokenValue | undefined;
+ volume: TokenValue | undefined;
+}> = ({ well, liquidity, functionName, price, volume }) => {
const navigate = useNavigate();
if (!well) return null;
@@ -63,6 +66,12 @@ export const WellDetailRow: FC<{
${liquidity ? liquidity.toHuman("short") : "-.--"}
+
+ ${price && price.gt(0) ? price.toHuman("short") : "-.--"}
+
+
+ ${volume ? volume.toHuman("short") : "-.--"}
+
{{smallLogos[0]}}
@@ -103,6 +112,12 @@ export const WellDetailLoadingRow: FC<{}> = () => {
+
+
+
+
+
+
@@ -139,6 +154,23 @@ const TableRow = styled(Row)`
`;
const DesktopContainer = styled(Td)`
+ :nth-child(5) {
+ @media (max-width: ${size.desktop}) {
+ display: none;
+ }
+ }
+ :nth-child(6) {
+ @media (max-width: ${size.desktop}) {
+ display: none;
+ }
+ }
+
+ :nth-child(3) {
+ @media (max-width: ${size.tablet}) {
+ display: none;
+ }
+ }
+
@media (max-width: ${size.mobile}) {
display: none;
}
@@ -191,7 +223,7 @@ const Amount = styled.div`
const Reserves = styled.div`
display: flex;
flex-direction: row;
- justify-content flex-end;
+ justify-content: flex-end;
align-items: center;
gap: 4px;
flex: 1;
diff --git a/projects/dex-ui/src/components/Well/WellYieldWithTooltip.tsx b/projects/dex-ui/src/components/Well/WellYieldWithTooltip.tsx
index 762ef78f49..d57e24c9b3 100644
--- a/projects/dex-ui/src/components/Well/WellYieldWithTooltip.tsx
+++ b/projects/dex-ui/src/components/Well/WellYieldWithTooltip.tsx
@@ -17,9 +17,14 @@ type Props = {
apy?: TokenValue;
loading?: boolean;
tooltipProps?: Partial>;
+ returnNullOnNoAPY?: boolean;
};
-export const WellYieldWithTooltip: React.FC = ({ tooltipProps, well }) => {
+export const WellYieldWithTooltip: React.FC = ({
+ tooltipProps,
+ well,
+ returnNullOnNoAPY = false,
+}) => {
const sdk = useSdk();
const bean = sdk.tokens.BEAN;
@@ -37,6 +42,7 @@ export const WellYieldWithTooltip: React.FC = ({ tooltipProps, well }) =>
const tooltipWidth = isMobile ? 250 : 360;
if (!apy) {
+ if (returnNullOnNoAPY) return null;
return <>{"-"}>;
}
diff --git a/projects/dex-ui/src/pages/Build.tsx b/projects/dex-ui/src/pages/Build.tsx
index b41ad50e42..d6e566d451 100644
--- a/projects/dex-ui/src/pages/Build.tsx
+++ b/projects/dex-ui/src/pages/Build.tsx
@@ -1,24 +1,78 @@
import React from "react";
+import { useNavigate } from "react-router-dom";
+import { ButtonPrimary } from "src/components/Button";
+
+import { Flex } from "src/components/Layout";
import { Page } from "src/components/Page";
import { Title } from "src/components/PageComponents/Title";
+import { Text } from "src/components/Typography";
+
+import { theme } from "src/utils/ui/theme";
import styled from "styled-components";
+import { ComponentLibraryTable } from "src/components/Create/ComponentLibraryTable";
+
export const Build = () => {
+ const navigate = useNavigate();
+
+ const handleNavigate = () => {
+ navigate("/create");
+ };
+
return (
-
-
-
+
+
+
+ Basin has three unique components which can be composed together to create a custom
+ liquidity pool, or Well.
+
+
+
+
+ Use the Well Deployer to deploy your own Wells.
+
+ Well Deployer →
+
+
+ COMPONENT LIBRARY
+
+ Use existing components which are already available for developers to extend, copy or
+ compose together when building Wells. Select a component to view its implementation.
+
+
+
);
};
-const Box = styled.div`
- outline: 0.5px solid black;
- outline-offset: -0.5px;
- background-color: #ffffffff;
- height: 96px;
- width: 384px;
- margin-left: 96px;
- box-sizing: borderbox;
+const ActionBanner = styled(Flex).attrs({
+ $py: 2,
+ $px: 3,
+ $justifyContent: "space-between",
+ $alignItems: "center",
+ $direction: "row",
+ $gap: 2
+})`
+ background: ${theme.colors.white};
+ border: 0.25px solid ${theme.colors.gray};
+
+ @media (max-width: 500px) {
+ flex-direction: column;
+ gap: ${theme.spacing(2)};
+
+ .banner-text {
+ align-self: flex-start;
+ }
+ }
+`;
+
+const NavigateButton = styled(ButtonPrimary)`
+ white-space: nowrap;
+
+ /* ${theme.media.query.sm.only} { */
+ @media (max-width: 500px) {
+ align-self: flex-end;
+ width: 100%;
+ }
`;
diff --git a/projects/dex-ui/src/pages/Create.tsx b/projects/dex-ui/src/pages/Create.tsx
new file mode 100644
index 0000000000..1f2f8d217d
--- /dev/null
+++ b/projects/dex-ui/src/pages/Create.tsx
@@ -0,0 +1,54 @@
+import React from "react";
+
+import { Page } from "src/components/Page";
+import { Flex } from "src/components/Layout";
+import {
+ CreateWellStep1,
+ CreateWellStep2,
+ CreateWellStep3,
+ CreateWellStep4,
+ CreateWellProvider,
+ useCreateWell
+} from "src/components/Create";
+
+export const Create = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+const CreateSteps = () => {
+ const { step } = useCreateWell();
+
+ return (
+ <>
+ {step === 0 && (
+
+
+
+ )}
+ {step === 1 && (
+
+
+
+ )}
+ {step === 2 && (
+
+
+
+ )}
+ {step === 3 && (
+
+
+
+ )}
+ >
+ );
+};
+
+const CONTENT_MAX_WIDTH = "1234px";
+const PREVIEW_MAX_WIDTH = "710px";
diff --git a/projects/dex-ui/src/pages/Home.tsx b/projects/dex-ui/src/pages/Home.tsx
index c3eaaee3ca..b6ecf9a1bf 100644
--- a/projects/dex-ui/src/pages/Home.tsx
+++ b/projects/dex-ui/src/pages/Home.tsx
@@ -8,8 +8,10 @@ import { BodyL } from "src/components/Typography";
import { ContractInfoMarquee } from "src/components/Frame/ContractInfoMarquee";
const copy = {
- build: "Use DEX components written, audited and deployed by other developers for your custom liquidity pool.",
- deploy: "Deploy liquidity in pools with unique pricing functions for more granular market making.",
+ build:
+ "Use DEX components written, audited and deployed by other developers for your custom liquidity pool.",
+ deploy:
+ "Deploy liquidity in pools with unique pricing functions for more granular market making.",
fees: "Exchange assets in liquidity pools that don't impose trading fees."
};
@@ -18,7 +20,8 @@ const links = {
whitepaper: "/basin.pdf",
docs: "https://docs.basin.exchange/implementations/overview",
wells: "/#/wells",
- swap: "/#/swap"
+ swap: "/#/swap",
+ build: "/#/build"
};
export const Home = () => {
@@ -31,11 +34,18 @@ export const Home = () => {
Multi Flow Pump is here!
- Explore the inter-block MEV manipulation resistant oracle implementation used by
- the BEAN:WETH Well.
+ Explore the{" "}
+
+ inter-block MEV manipulation resistant oracle implementation
+ {" "}
+ used by the BEAN:WETH Well.
-
+
Read the whitepaper →
@@ -51,7 +61,7 @@ export const Home = () => {
-
+
🔮
@@ -209,10 +219,10 @@ const InfoContainer = styled.div`
gap: 8px;
box-sizing: border-box;
height: 100%;
-
+
${mediaQuery.sm.up} {
padding-top: min(25%, 185px);
- justify-content: flex-start
+ justify-content: flex-start;
align-items: center;
width: 100%;
gap: 72px;
@@ -368,7 +378,9 @@ const AccordionItem = styled.a`
const AccordionContent = styled.div`
overflow: hidden;
opacity: 0; // Initially hidden
- transition: opacity 0.3s ease-out, max-height 0.3s ease-out;
+ transition:
+ opacity 0.3s ease-out,
+ max-height 0.3s ease-out;
max-height: 0;
width: 100%; // Ensure it takes full width
diff --git a/projects/dex-ui/src/pages/Liquidity.tsx b/projects/dex-ui/src/pages/Liquidity.tsx
index 50f29b6a45..32c2cbbd1f 100644
--- a/projects/dex-ui/src/pages/Liquidity.tsx
+++ b/projects/dex-ui/src/pages/Liquidity.tsx
@@ -23,8 +23,7 @@ import { useWellWithParams } from "src/wells/useWellWithParams";
export const Liquidity = () => {
const { well, loading, error } = useWellWithParams();
const navigate = useNavigate();
-
- const [wellFunctionName, setWellFunctionName] = useState("This Well's Function");
+
const [tab, setTab] = useState(0);
const scrollRef = useRef(null);
@@ -47,20 +46,12 @@ export const Liquidity = () => {
setOpen(!open);
}, [open]);
- useEffect(() => {
- const run = async () => {
- if (well && well.wellFunction) {
- const _wellName = await well.wellFunction.contract.name();
- setWellFunctionName(_wellName);
- }
- };
- run();
- }, [well]);
-
if (error) {
return ;
}
+ const nonEmptyReserves = well && well?.reserves?.some((reserve) => reserve.gt(0));
+
return (
@@ -97,10 +88,10 @@ export const Liquidity = () => {
}>
-
+
}>
-
+
@@ -116,7 +107,7 @@ export const Liquidity = () => {
-
- setTab(1)} active={tab === 1} stretch bold justify hover>
+ setTab(1)} active={tab === 1} stretch bold justify hover disabled={!nonEmptyReserves}>
{""}>}>
Remove Liquidity
@@ -132,7 +123,7 @@ export const Liquidity = () => {
handleSlippageValueChange={handleSlippageValueChange}
/>
)}
- {tab === 1 && (
+ {tab === 1 && nonEmptyReserves && (
{
const { well, loading: dataLoading, error } = useWellWithParams();
const { isLoading: apysLoading } = useBeanstalkSiloAPYs();
const { isLoading: twaLoading, getTWAReservesWithWell } = useMultiFlowPumpTWAReserves();
+ const { getIsWhitelisted } = useBeanstalkSiloWhitelist();
+
+ const isWhitelistedWell = getIsWhitelisted(well);
const loading = useLagLoading(dataLoading || apysLoading || twaLoading);
@@ -66,7 +70,7 @@ export const Well = () => {
}
if (well.wellFunction) {
- const _wellName = await well.wellFunction.contract.name();
+ const _wellName = await well.wellFunction.getName();
setWellFunctionName(_wellName);
}
};
@@ -75,7 +79,9 @@ export const Well = () => {
}, [sdk, well]);
const title = (well?.tokens ?? []).map((t) => t.symbol).join("/");
- const logos: ReactNode[] = (well?.tokens || []).map((token) => );
+ const logos: ReactNode[] = (well?.tokens || []).map((token) => (
+
+ ));
const reserves = (well?.reserves ?? []).map((amount, i) => {
const token = well!.tokens![i];
@@ -88,10 +94,16 @@ export const Well = () => {
percentage: TokenValue.ZERO
};
});
- const totalUSD = reserves.reduce((total, r) => total.add(r.dollarAmount ?? TokenValue.ZERO), TokenValue.ZERO);
+ const totalUSD = reserves.reduce(
+ (total, r) => total.add(r.dollarAmount ?? TokenValue.ZERO),
+ TokenValue.ZERO
+ );
reserves.forEach((reserve) => {
- reserve.percentage = reserve.dollarAmount && totalUSD.gt(TokenValue.ZERO) ? reserve.dollarAmount.div(totalUSD) : TokenValue.ZERO;
+ reserve.percentage =
+ reserve.dollarAmount && totalUSD.gt(TokenValue.ZERO)
+ ? reserve.dollarAmount.div(totalUSD)
+ : TokenValue.ZERO;
});
const twaReserves = useMemo(() => getTWAReservesWithWell(well), [well, getTWAReservesWithWell]);
@@ -99,7 +111,9 @@ export const Well = () => {
const goLiquidity = () => navigate(`./liquidity`);
const goSwap = () =>
- well && well.tokens ? navigate(`../swap?fromToken=${well.tokens[0].symbol}&toToken=${well.tokens[1].symbol}`) : null;
+ well && well.tokens
+ ? navigate(`../swap?fromToken=${well.tokens[0].symbol}&toToken=${well.tokens[1].symbol}`)
+ : null;
// Code below detects if the component with the Add/Remove Liq + Swap buttons is sticky
const [isSticky, setIsSticky] = useState(false);
@@ -139,7 +153,12 @@ export const Well = () => {
return (
-
+
{/*
*Header
@@ -160,6 +179,7 @@ export const Well = () => {
offsetY: 0,
side: "top"
}}
+ returnNullOnNoAPY={true}
/>
@@ -193,12 +213,26 @@ export const Well = () => {
}>
-
- showTab(e, 0)} active={tab === 0} stretch justify bold hover>
+ showTab(e, 0)}
+ active={tab === 0}
+ stretch
+ justify
+ bold
+ hover
+ >
Activity
-
- showTab(e, 1)} active={tab === 1} stretch justify bold hover>
+ showTab(e, 1)}
+ active={tab === 1}
+ stretch
+ justify
+ bold
+ hover
+ >
Contract Addresses
@@ -209,7 +243,14 @@ export const Well = () => {
* Well History & Contract Info Tables
*/}
- {tab === 0 && }
+ {tab === 0 && (
+
+ )}
{tab === 1 && }
@@ -273,13 +314,13 @@ export const Well = () => {
}>
-
+
}>
-
+
}>
-
+
@@ -358,7 +399,7 @@ const HeaderContainer = styled(Row)`
}
${mediaQuery.md.up} {
- align-item: space-between;
+ align-items: space-between;
}
`;
@@ -472,6 +513,7 @@ const BottomContainer = styled.div`
const FunctionName = styled.div`
${BodyL}
+ text-align: right;
${mediaQuery.lg.down} {
${BodyS}
diff --git a/projects/dex-ui/src/pages/Wells.tsx b/projects/dex-ui/src/pages/Wells.tsx
index f497176036..5b8bbbffee 100644
--- a/projects/dex-ui/src/pages/Wells.tsx
+++ b/projects/dex-ui/src/pages/Wells.tsx
@@ -1,4 +1,4 @@
-import { TokenValue } from "@beanstalk/sdk";
+import { BeanstalkSDK, TokenValue } from "@beanstalk/sdk";
import React, { useMemo, useState } from "react";
import { Item } from "src/components/Layout";
import { Page } from "src/components/Page";
@@ -6,8 +6,6 @@ import { Title } from "src/components/PageComponents/Title";
import { TabButton } from "src/components/TabButton";
import { Row, TBody, THead, Table, Th } from "src/components/Table";
import { Row as TabRow } from "src/components/Layout";
-import { getPrice } from "src/utils/price/usePrice";
-import useSdk from "src/utils/sdk/useSdk";
import { useWells } from "src/wells/useWells";
import styled from "styled-components";
import { mediaQuery, size } from "src/breakpoints";
@@ -16,51 +14,46 @@ import { useWellLPTokenPrice } from "src/wells/useWellLPTokenPrice";
import { useLPPositionSummary } from "src/tokens/useLPPositionSummary";
import { WellDetailLoadingRow, WellDetailRow } from "src/components/Well/Table/WellDetailRow";
-import { MyWellPositionLoadingRow, MyWellPositionRow } from "src/components/Well/Table/MyWellPositionRow";
+import {
+ MyWellPositionLoadingRow,
+ MyWellPositionRow
+} from "src/components/Well/Table/MyWellPositionRow";
import { useBeanstalkSiloAPYs } from "src/wells/useBeanstalkSiloAPYs";
import { useLagLoading } from "src/utils/ui/useLagLoading";
+import useBasinStats from "src/wells/useBasinStats";
+import { useTokenPrices } from "src/utils/price/useTokenPrices";
+import { useWellFunctionNames } from "src/wells/wellFunction/useWellFunctionNames";
+import { BasinAPIResponse } from "src/types";
+import { Well } from "@beanstalk/sdk-wells";
+import useSdk from "src/utils/sdk/useSdk";
+import { theme } from "src/utils/ui/theme";
export const Wells = () => {
const { data: wells, isLoading, error } = useWells();
+ const { data: wellStats = [] } = useBasinStats();
const sdk = useSdk();
- const [wellLiquidity, setWellLiquidity] = useState<(TokenValue | undefined)[]>([]);
- const [wellFunctionNames, setWellFunctionNames] = useState([]);
const [tab, showTab] = useState(0);
- const { data: lpTokenPrices } = useWellLPTokenPrice(wells);
+ const { data: lpTokenPrices, isLoading: lpTokenPricesLoading } = useWellLPTokenPrice(wells);
const { hasPositions, getPositionWithWell, isLoading: positionsLoading } = useLPPositionSummary();
const { isLoading: apysLoading } = useBeanstalkSiloAPYs();
+ const { data: tokenPrices, isLoading: tokenPricesLoading } = useTokenPrices(wells);
+ const { data: wellFnNames, isLoading: wellNamesLoading } = useWellFunctionNames(wells);
- const loading = useLagLoading(isLoading || apysLoading || positionsLoading);
-
- useMemo(() => {
- const run = async () => {
- if (!wells || !wells.length) return;
- let _wellsLiquidityUSD = [];
- for (let i = 0; i < wells.length; i++) {
- if (!wells[i].tokens) return;
- const _tokenPrices = await Promise.all(wells[i].tokens!.map((token) => getPrice(token, sdk)));
- const _reserveValues = wells[i].reserves?.map((tokenReserve, index) =>
- tokenReserve.mul((_tokenPrices[index] as TokenValue) || TokenValue.ZERO)
- );
- let initialValue = TokenValue.ZERO;
- const _totalWellLiquidity = _reserveValues?.reduce((accumulator, currentValue) => currentValue.add(accumulator), initialValue);
- _wellsLiquidityUSD[i] = _totalWellLiquidity;
- }
- setWellLiquidity(_wellsLiquidityUSD);
-
- let _wellsFunctionNames = [];
- for (let i = 0; i < wells.length; i++) {
- if (!wells[i].wellFunction) return;
- const _wellName = await wells[i].wellFunction!.contract.name();
- _wellsFunctionNames[i] = _wellName;
- }
- setWellFunctionNames(_wellsFunctionNames);
- };
+ const tableData = useMemo(
+ () => makeTableData(sdk, wells, wellStats, tokenPrices),
+ [sdk, tokenPrices, wellStats, wells]
+ );
- run();
- }, [sdk, wells]);
+ const loading = useLagLoading(
+ isLoading ||
+ apysLoading ||
+ positionsLoading ||
+ lpTokenPricesLoading ||
+ tokenPricesLoading ||
+ wellNamesLoading
+ );
if (error) {
return ;
@@ -89,6 +82,8 @@ export const Wells = () => {
Well Function
Yield
Total Liquidity
+ Price
+ 24H Volume
Reserves
All Wells
@@ -105,7 +100,7 @@ export const Wells = () => {
)}
- {loading ? (
+ {loading || !tableData.length ? (
<>
{Array(5)
.fill(null)
@@ -129,15 +124,24 @@ export const Wells = () => {
>
) : (
- wells?.map((well, index) => {
- return tab === 0 ? (
-
- ) : (
+ tableData?.map(({ well, baseTokenPrice, liquidityUSD, targetVolume }, index) => {
+ if (tab === 0) {
+ const priceFnName =
+ well.wellFunction?.name || wellFnNames?.[well.wellFunction?.address || ""];
+
+ return (
+
+ );
+ }
+
+ return (
{
)}
+
);
};
+const makeTableData = (
+ sdk: BeanstalkSDK,
+ wells?: Well[],
+ stats?: BasinAPIResponse[],
+ tokenPrices?: Record
+) => {
+ if (!wells || !wells.length || !tokenPrices) return [];
+
+ const statsByPoolId = (stats ?? []).reduce>(
+ (prev, curr) => ({ ...prev, [curr.pool_id.toLowerCase()]: curr }),
+ {}
+ );
+
+ const data = (wells || []).map((well) => {
+ let baseTokenPrice: TokenValue | undefined = undefined;
+ let liquidityUSD: TokenValue | undefined = undefined;
+ let targetVolume: TokenValue | undefined = undefined;
+
+ let liquidityUSDInferred: TokenValue | undefined = undefined;
+
+ const token1 = well.tokens?.[0];
+ const token2 = well.tokens?.[1];
+
+ if (token1 && token2) {
+ const basePrice = tokenPrices[token1.symbol] || TokenValue.ZERO;
+ const targetPrice = tokenPrices[token2.symbol] || TokenValue.ZERO;
+
+ const reserve1 = well.reserves?.[0];
+ const reserve2 = well.reserves?.[1];
+ const reserve1USD = reserve1?.mul(basePrice);
+ const reserve2USD = reserve2?.mul(targetPrice);
+
+ if (reserve2USD && reserve1 && reserve1.gt(0)) {
+ baseTokenPrice = reserve2USD.div(reserve1);
+ }
+ if (reserve1USD && reserve2USD && reserve2USD.gt(0)) {
+ liquidityUSD = reserve1USD.add(reserve2USD);
+ }
+
+ const baseVolume = token2.fromHuman(
+ statsByPoolId[well.address.toLowerCase()]?.target_volume || 0
+ );
+ targetVolume = baseVolume.mul(targetPrice);
+
+ const bothPricesAvailable = !!(reserve1USD && reserve2USD);
+ const atLeastOnePriceAvailable = !!(reserve1USD || reserve1USD);
+
+ if (atLeastOnePriceAvailable && !bothPricesAvailable) {
+ // Since we don't have the other price, we assume reserves are balanced 50% - 50%
+ if (reserve1USD) liquidityUSDInferred = reserve1USD.mul(2);
+ if (reserve2USD) liquidityUSDInferred = reserve2USD.mul(2);
+ } else if (bothPricesAvailable) {
+ liquidityUSDInferred = liquidityUSD;
+ }
+ }
+
+ const hasReserves = well.reserves?.[0]?.gt(0) && well.reserves?.[1]?.gt(0);
+
+ return {
+ well,
+ baseTokenPrice,
+ liquidityUSD,
+ targetVolume,
+ liquidityUSDInferred,
+ hasReserves
+ };
+ });
+
+ const whitelistedSort = data.sort(getSortByWhitelisted(sdk));
+
+ const sortedByLiquidity = whitelistedSort.sort((a, b) => {
+ if (!a.liquidityUSDInferred) return 1;
+ if (!b.liquidityUSDInferred) return -1;
+
+ const diff = a.liquidityUSDInferred.sub(b.liquidityUSDInferred);
+ if (diff.eq(0)) return 0;
+ return diff.gt(0) ? -1 : 1;
+ });
+
+ const sortedByHasReserves = sortedByLiquidity.sort((a, b) => {
+ if (a.hasReserves === b.hasReserves) return 0;
+ return a.hasReserves && !b.hasReserves ? -1 : 1;
+ });
+
+ return sortedByHasReserves;
+};
+
+const getSortByWhitelisted =
+ (sdk: BeanstalkSDK) =>
+ (a: T, b: T) => {
+ const aWhitelisted = a.well.lpToken && sdk.tokens.isWhitelisted(a.well.lpToken);
+ const bWhitelisted = b.well.lpToken && sdk.tokens.isWhitelisted(b.well.lpToken);
+
+ if (aWhitelisted) return -1;
+ if (bWhitelisted) return 1;
+ return 0;
+ };
+
const StyledTable = styled(Table)`
overflow: auto;
`;
@@ -183,6 +286,32 @@ const MobileHeader = styled(Th)`
`;
const DesktopHeader = styled(Th)`
+ :nth-child(1) {
+ width: 10em;
+ }
+ :nth-child(2) {
+ width: 12em;
+ }
+ :nth-child(3) {
+ width: 12em;
+ }
+
+ :nth-child(5) {
+ @media (max-width: ${size.desktop}) {
+ display: none;
+ }
+ }
+ :nth-child(6) {
+ @media (max-width: ${size.desktop}) {
+ display: none;
+ }
+ }
+
+ :nth-child(3) {
+ @media (max-width: ${size.tablet}) {
+ display: none;
+ }
+ }
@media (max-width: ${size.mobile}) {
display: none;
}
@@ -217,3 +346,12 @@ const NoLPMessage = styled.div`
font-size: 14px;
}
`;
+
+const MobileBottomNudge = styled.div`
+ height: ${theme.spacing(8)};
+ width: 100%;
+
+ ${theme.media.query.sm.up} {
+ display: none;
+ }
+`;
\ No newline at end of file
diff --git a/projects/dex-ui/src/queries/GetWellChartData.graphql b/projects/dex-ui/src/queries/GetWellChartData.graphql
index 0ac4d4f2f8..a51405c22a 100644
--- a/projects/dex-ui/src/queries/GetWellChartData.graphql
+++ b/projects/dex-ui/src/queries/GetWellChartData.graphql
@@ -3,7 +3,7 @@ query GetWellChartData($id: ID!, $lastUpdateTimestamp_gte: BigInt!, $resultsToSk
hourlySnapshots(first: 1000, skip: $resultsToSkip, orderBy: lastUpdateTimestamp, orderDirection: asc, where: { lastUpdateTimestamp_gte: $lastUpdateTimestamp_gte }) {
lastUpdateTimestamp
totalLiquidityUSD
- deltaVolumeUSD
+ deltaTradeVolumeUSD
}
}
}
diff --git a/projects/dex-ui/src/settings/index.ts b/projects/dex-ui/src/settings/index.ts
index 4adc6711b6..1eff6a01ee 100644
--- a/projects/dex-ui/src/settings/index.ts
+++ b/projects/dex-ui/src/settings/index.ts
@@ -19,7 +19,7 @@ export type DexSettings = {
NETLIFY_BUILD_ID?: string;
};
-const temp = netlifyContext === "production" ? ProdSettings : DevSettings;
+const temp = netlifyContext === "production" || netlifyContext === "deploy-preview" ? ProdSettings : DevSettings;
export const Settings = {
...temp,
@@ -28,5 +28,7 @@ export const Settings = {
NETLIFY_BUILD_ID: netlifyBuildId
};
+export const isNetlifyContext = netlifyContext === 'deploy-preview';
+
// @ts-ignore
globalThis.settings = () => Log.module("settings").log(Settings);
diff --git a/projects/dex-ui/src/token-metadata.json b/projects/dex-ui/src/token-metadata.json
new file mode 100644
index 0000000000..8fb98ef21e
--- /dev/null
+++ b/projects/dex-ui/src/token-metadata.json
@@ -0,0 +1,151 @@
+{
+ "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": {
+ "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
+ "logoURI": "https://static.alchemyapi.io/images/assets/3717.png",
+ "name": "Wrapped BTC",
+ "symbol": "WBTC",
+ "displayName": "Wrapped Bitcoin",
+ "decimals": 8,
+ "displayDecimals": 3
+ },
+ "0x6982508145454ce325ddbe47a25d4ec3d2311933": {
+ "address": "0x6982508145454ce325ddbe47a25d4ec3d2311933",
+ "logoURI": "https://static.alchemyapi.io/images/assets/24478.png",
+ "name": "Pepe",
+ "symbol": "PEPE",
+ "decimals": 18
+ },
+ "0x576e2bed8f7b46d34016198911cdf9886f78bea7": {
+ "address": "0x576e2bed8f7b46d34016198911cdf9886f78bea7",
+ "logoURI": "https://static.alchemyapi.io/images/assets/27872.png",
+ "name": "MAGA",
+ "symbol": "TRUMP",
+ "decimals": 9
+ },
+ "0x02f92800f57bcd74066f5709f1daa1a4302df875": {
+ "address": "0x02f92800f57bcd74066f5709f1daa1a4302df875",
+ "logoURI": "https://static.alchemyapi.io/images/assets/29186.png",
+ "name": "Peapods",
+ "symbol": "PEAS",
+ "decimals": 18
+ },
+ "0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5": {
+ "address": "0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5",
+ "logoURI": "https://static.alchemyapi.io/images/assets/9067.png",
+ "name": "Olympus",
+ "symbol": "OHM",
+ "decimals": 9
+ },
+ "0x514910771af9ca656af840dff83e8264ecf986ca": {
+ "address": "0x514910771af9ca656af840dff83e8264ecf986ca",
+ "logoURI": "https://static.alchemyapi.io/images/assets/1975.png",
+ "name": "ChainLink Token",
+ "symbol": "LINK",
+ "decimals": 18
+ },
+ "0xd29da236dd4aac627346e1bba06a619e8c22d7c5": {
+ "address": "0xd29da236dd4aac627346e1bba06a619e8c22d7c5",
+ "logoURI": "https://static.alchemyapi.io/images/assets/31305.png",
+ "name": "MAGA",
+ "symbol": "MAGA",
+ "decimals": 9
+ },
+ "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984": {
+ "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
+ "logoURI": "https://static.alchemyapi.io/images/assets/7083.png",
+ "name": "Uniswap",
+ "symbol": "UNI",
+ "decimals": 18
+ },
+ "0xb8c77482e45f1f44de1745f52c74426c631bdd52": {
+ "address": "0xb8c77482e45f1f44de1745f52c74426c631bdd52",
+ "logoURI": "https://static.alchemyapi.io/images/assets/1839.png",
+ "name": "BNB",
+ "symbol": "BNB",
+ "decimals": 18
+ },
+ "0x4da27a545c0c5b758a6ba100e3a049001de870f5": {
+ "address": "0x4da27a545c0c5b758a6ba100e3a049001de870f5",
+ "logoURI": "https://etherscan.io/token/images/stakedaave_32.png",
+ "name": "Staked Aave",
+ "symbol": "stkAAVE",
+ "decimals": 18
+ },
+ "0x5a98fcbea516cf06857215779fd812ca3bef1b32": {
+ "address": "0x5a98fcbea516cf06857215779fd812ca3bef1b32",
+ "logoURI": "https://static.alchemyapi.io/images/assets/8000.png",
+ "name": "Lido DAO Token",
+ "symbol": "LDO",
+ "decimals": 18
+ },
+ "0xd533a949740bb3306d119cc777fa900ba034cd52": {
+ "address": "0xd533a949740bb3306d119cc777fa900ba034cd52",
+ "logoURI": "https://static.alchemyapi.io/images/assets/6538.png",
+ "name": "Curve DAO Token",
+ "symbol": "CRV",
+ "decimals": 18
+ },
+ "0xb50721bcf8d664c30412cfbc6cf7a15145234ad1": {
+ "address": "0xb50721bcf8d664c30412cfbc6cf7a15145234ad1",
+ "logoURI": "https://static.alchemyapi.io/images/assets/11841.png",
+ "name": "Arbitrum",
+ "symbol": "ARB",
+ "decimals": 18
+ },
+ "0x6985884c4392d348587b19cb9eaaf157f13271cd": {
+ "address": "0x6985884c4392d348587b19cb9eaaf157f13271cd",
+ "logoURI": "https://static.alchemyapi.io/images/assets/26997.png",
+ "name": "LayerZero",
+ "symbol": "ZRO",
+ "decimals": 18
+ },
+ "0x85f17cf997934a597031b2e18a9ab6ebd4b9f6a4": {
+ "address": "0x85f17cf997934a597031b2e18a9ab6ebd4b9f6a4",
+ "logoURI": "https://assets.coingecko.com/coins/images/10365/standard/near.jpg?1696510367",
+ "name": "NEAR",
+ "symbol": "NEAR",
+ "decimals": 24
+ },
+ "0x163f8c2467924be0ae7b5347228cabf260318753": {
+ "address": "0x163f8c2467924be0ae7b5347228cabf260318753",
+ "logoURI": "https://static.alchemyapi.io/images/assets/13502.png",
+ "name": "Worldcoin",
+ "symbol": "WLD",
+ "decimals": 18
+ },
+ "0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3": {
+ "address": "0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3",
+ "logoURI": "https://static.alchemyapi.io/images/assets/21159.png",
+ "name": "Ondo",
+ "symbol": "ONDO",
+ "decimals": 18
+ },
+ "0xe28b3b32b6c345a34ff64674606124dd5aceca30": {
+ "address": "0xe28b3b32b6c345a34ff64674606124dd5aceca30",
+ "logoURI": "https://static.alchemyapi.io/images/assets/7226.png",
+ "name": "Injective Token",
+ "symbol": "INJ",
+ "decimals": 18
+ },
+ "0xcd5fe23c85820f7b72d0926fc9b05b43e359b7ee": {
+ "address": "0xcd5fe23c85820f7b72d0926fc9b05b43e359b7ee",
+ "logoURI": "https://static.alchemyapi.io/images/assets/28695.png",
+ "name": "Wrapped eETH",
+ "symbol": "weETH",
+ "decimals": 18
+ },
+ "0xb9f599ce614feb2e1bbe58f180f370d05b39344e": {
+ "address": "0xb9f599ce614feb2e1bbe58f180f370d05b39344e",
+ "logoURI": "https://static.alchemyapi.io/images/assets/29220.png",
+ "name": "PepeFork",
+ "symbol": "PORK",
+ "decimals": 18
+ },
+ "0x853d955acef822db058eb8505911ed77f175b99e": {
+ "address": "0x853d955acef822db058eb8505911ed77f175b99e",
+ "logoURI": "https://static.alchemyapi.io/images/assets/6952.png",
+ "name": "Frax",
+ "symbol": "FRAX",
+ "decimals": 18
+ }
+}
diff --git a/projects/dex-ui/src/tokens/TokenProvider.tsx b/projects/dex-ui/src/tokens/TokenProvider.tsx
index ae71d6661b..6c814fea61 100644
--- a/projects/dex-ui/src/tokens/TokenProvider.tsx
+++ b/projects/dex-ui/src/tokens/TokenProvider.tsx
@@ -19,7 +19,10 @@ export const TokenProvider = ({ children }: { children: React.ReactNode }) => {
for (const token of tokens || []) {
let logo = images[token.symbol] ?? images.DEFAULT;
- token.setMetadata({ logo });
+
+ if (!logo && token.isLP) logo = images.LP;
+ if (!token.logo) token.setMetadata({ logo });
+
add(token);
}
diff --git a/projects/dex-ui/src/tokens/useAllTokenBalance.tsx b/projects/dex-ui/src/tokens/useAllTokenBalance.tsx
index cbdc43e21c..91624a98e0 100644
--- a/projects/dex-ui/src/tokens/useAllTokenBalance.tsx
+++ b/projects/dex-ui/src/tokens/useAllTokenBalance.tsx
@@ -7,6 +7,8 @@ import { useAccount } from "wagmi";
import { useTokens } from "./TokenProvider";
import { Log } from "src/utils/logger";
import { config } from "src/utils/wagmi/config";
+import { ContractFunctionParameters } from "viem";
+import { queryKeys } from "src/utils/query/queryKeys";
const TokenBalanceABI = [
{
@@ -20,63 +22,78 @@ const TokenBalanceABI = [
}
] as const;
+const MAX_PER_CALL = 20;
+
export const useAllTokensBalance = () => {
const tokens = useTokens();
const { address } = useAccount();
const queryClient = useQueryClient();
- // Remove ETH from this list, manually get the balance below
const tokensToLoad = Object.values(tokens).filter((t) => t.symbol !== "ETH");
- if (tokensToLoad.length > 20) throw new Error("Too many tokens to load balances. Fix me");
const calls = useMemo(() => {
- const contractCalls: any[] = [];
- Log.module("app").debug(`Fetching token balances for ${tokensToLoad.length} tokens, for address ${address}`);
- for (const t of tokensToLoad) {
- contractCalls.push({
- address: t.address as `0x{string}`,
+ const contractCalls: ContractFunctionParameters[][] = [];
+ Log.module("app").debug(
+ `Fetching token balances for ${tokensToLoad.length} tokens, for address ${address}`
+ );
+
+ let callBucket: ContractFunctionParameters[] = [];
+
+ tokensToLoad.forEach((token, i) => {
+ callBucket.push({
+ address: token.address as `0x{string}`,
abi: TokenBalanceABI,
functionName: "balanceOf",
args: [address]
});
- }
+
+ if (i % MAX_PER_CALL === MAX_PER_CALL - 1) {
+ contractCalls.push([...callBucket]);
+ callBucket = [];
+ }
+ });
return contractCalls;
// eslint-disable-next-line react-hooks/exhaustive-deps -- doing just tokensToLoad doesn't work and causes multiple calls
}, [address, tokensToLoad.map((t) => t.symbol).join()]);
const { data, isLoading, error, refetch, isFetching } = useQuery({
- queryKey: ["token", "balance"],
-
+ queryKey: queryKeys.tokenBalancesAll,
queryFn: async () => {
if (!address) return {};
- const res = (await multicall(config, {
- contracts: calls,
- allowFailure: false
- })) as unknown as BigNumber[];
+
+ const ETH = tokens.ETH;
+
+ const [ethBalance, ...results] = await Promise.all([
+ ETH.getBalance(address),
+ ...(calls.map((calls) =>
+ multicall(config, { contracts: calls, allowFailure: false })
+ ) as unknown as BigNumber[])
+ ]);
+
+ const res = results.flat();
const balances: Record = {};
+ if (ethBalance) {
+ Log.module("app").debug(`ETH balance: `, ethBalance.toHuman());
+ queryClient.setQueryData(queryKeys.tokenBalance(ETH.symbol), { ETH: ethBalance });
+ balances.ETH = ethBalance;
+ }
+
for (let i = 0; i < res.length; i++) {
const value = res[i];
const token = tokensToLoad[i];
balances[token.symbol] = token.fromBlockchain(value);
// set the balance in the query cache too
- queryClient.setQueryData(["token", "balance", token.symbol], { [token.symbol]: balances[token.symbol] });
-
- }
-
- const ETH = tokens.ETH;
- if (ETH) {
- const ethBalance = await ETH.getBalance(address);
- Log.module("app").debug(`ETH balance: `, ethBalance.toHuman());
- queryClient.setQueryData(["token", "balance", "ETH"], { ETH: ethBalance });
- balances.ETH = ethBalance;
+ queryClient.setQueryData(queryKeys.tokenBalance(token.symbol), {
+ [token.symbol]: balances[token.symbol]
+ });
}
return balances;
},
-
+ enabled: !!address && !!tokensToLoad.length,
staleTime: 1000 * 30,
refetchInterval: 1000 * 30
});
diff --git a/projects/dex-ui/src/tokens/useERC20Token.ts b/projects/dex-ui/src/tokens/useERC20Token.ts
new file mode 100644
index 0000000000..2f4f00f492
--- /dev/null
+++ b/projects/dex-ui/src/tokens/useERC20Token.ts
@@ -0,0 +1,159 @@
+import { BEANETH_ADDRESS, getIsValidEthereumAddress } from "../utils/addresses";
+import useSdk from "../utils/sdk/useSdk";
+import { ERC20Token } from "@beanstalk/sdk";
+import { useQuery } from "@tanstack/react-query";
+
+import { queryKeys } from "src/utils/query/queryKeys";
+import { erc20Abi as abi } from "viem";
+import { multicall } from "@wagmi/core";
+import { useWells } from "src/wells/useWells";
+import { images } from "src/assets/images/tokens";
+import { alchemy } from "src/utils/alchemy";
+import { config } from "src/utils/wagmi/config";
+
+export const USE_ERC20_TOKEN_ERRORS = {
+ notERC20Ish: "Invalid ERC20 Token Address"
+} as const;
+
+const getERC20Data = async (_address: string) => {
+ const address = _address as `0x{string}`;
+ const args: any[] = [];
+
+ const calls: any[] = [
+ { address, abi, functionName: "decimals", args },
+ { address, abi, functionName: "totalSupply", args }
+ ];
+
+ return multicall(config, { contracts: calls });
+};
+
+export const useERC20TokenWithAddress = (_address: string | undefined = "") => {
+ const address = _address.toLowerCase();
+
+ const { data: wells = [] } = useWells();
+ const sdk = useSdk();
+
+ const lpTokens = wells.map((w) => w.lpToken).filter(Boolean) as ERC20Token[];
+ const isValidAddress = getIsValidEthereumAddress(address);
+ const sdkToken = sdk.tokens.findByAddress(address);
+
+ const {
+ data: tokenMetadata,
+ refetch: refetchTokenMetadata,
+ ...tokenMetadataQuery
+ } = useQuery({
+ queryKey: queryKeys.tokenMetadata(isValidAddress ? address : "invalid"),
+ queryFn: async () => {
+ console.debug("[useERC20Token] fetching: ", address);
+ const multiCallResponse = await getERC20Data(address);
+
+ // Validate as much as we can that this is an ERC20 token
+ if (multiCallResponse[0]?.error || multiCallResponse[1]?.error) {
+ throw new Error(USE_ERC20_TOKEN_ERRORS.notERC20Ish);
+ }
+ console.debug("[useERC20Token] erc20 multicall response: ", multiCallResponse);
+
+ const metadata = await alchemy.core.getTokenMetadata(address);
+
+ console.debug("[useERC20Token] token metadata: ", metadata);
+
+ return {
+ name: metadata?.name ?? "",
+ symbol: metadata?.symbol ?? "",
+ decimals: metadata?.decimals ?? undefined,
+ logo: metadata?.logo ?? images.DEFAULT
+ };
+ },
+ enabled: isValidAddress && !sdkToken,
+ // We never need to refetch this data
+ staleTime: Infinity,
+ refetchOnMount: false,
+ retry: false
+ });
+
+ const getTokenLogo = async (token: ERC20Token) => {
+ let logo: string | undefined = token.logo ?? images[token.symbol];
+ if (logo) return logo;
+
+ if (tokenMetadata) {
+ logo = tokenMetadata.logo;
+ } else {
+ const tokenMetadata = await refetchTokenMetadata();
+ logo = tokenMetadata?.data?.logo ?? undefined;
+ }
+
+ return logo ?? images.DEFAULT;
+ };
+
+ const handleIsLPToken = async () => {
+ const lpToken = lpTokens.find((lp) => lp.address.toLowerCase() === address.toLowerCase());
+ if (!lpToken) return undefined;
+
+ if (!lpToken.logo) {
+ const logo = await getTokenLogo(lpToken);
+ lpToken.setMetadata({ logo: logo });
+ }
+ return lpToken;
+ };
+
+ const handleIsSdkERC20Token = async () => {
+ let token = sdk.tokens.findByAddress(address);
+ if (!token) return undefined;
+
+ if (!(token instanceof ERC20Token)) {
+ return Promise.reject(new Error(USE_ERC20_TOKEN_ERRORS.notERC20Ish));
+ }
+
+ if (!token.logo) {
+ const logo = await getTokenLogo(token);
+ token.setMetadata({ logo: logo });
+ }
+ return token;
+ };
+
+ const shouldRunWhenNonSdkToken = Boolean(!sdkToken && isValidAddress && tokenMetadata);
+ const shouldRunWhenSdkToken = Boolean(sdkToken && isValidAddress && lpTokens.length);
+
+ const erc20Query = useQuery({
+ queryKey: queryKeys.erc20TokenWithAddress(isValidAddress ? address : "invalid"),
+ queryFn: async () => {
+ let token: ERC20Token | undefined = undefined;
+ token = await handleIsLPToken();
+ if (token) return token;
+
+ token = await handleIsSdkERC20Token();
+ if (token) return token;
+
+ // The query have run if this we get to this point
+ const { decimals = 0, name = "", symbol = "", logo } = tokenMetadata ?? {};
+
+ if (!decimals || !name || !symbol) {
+ return undefined;
+ }
+
+ const erc20 = new ERC20Token(
+ sdk.chainId,
+ address.toLowerCase(),
+ decimals,
+ symbol.toString() ?? "",
+ {
+ name: name,
+ logo: logo,
+ displayName: name
+ },
+ sdk.providerOrSigner
+ );
+
+ return erc20;
+ },
+ enabled: shouldRunWhenNonSdkToken || shouldRunWhenSdkToken,
+ staleTime: Infinity
+ });
+
+ return {
+ ...erc20Query,
+ error: erc20Query.error || tokenMetadataQuery.error,
+ isError: erc20Query.isError || tokenMetadataQuery.isError,
+ isLoading: erc20Query.isLoading || tokenMetadataQuery.isLoading
+ };
+};
diff --git a/projects/dex-ui/src/tokens/useTokenAllowance.ts b/projects/dex-ui/src/tokens/useTokenAllowance.ts
new file mode 100644
index 0000000000..da3277c1d4
--- /dev/null
+++ b/projects/dex-ui/src/tokens/useTokenAllowance.ts
@@ -0,0 +1,19 @@
+import { ERC20Token } from "@beanstalk/sdk";
+import { useQuery } from "@tanstack/react-query";
+import { queryKeys } from "src/utils/query/queryKeys";
+import { useAccount } from "wagmi";
+
+export const useTokenAllowance = (token: ERC20Token | undefined, spender: string) => {
+ const { address: walletAddress } = useAccount();
+
+ return useQuery({
+ queryKey: queryKeys.tokenAllowance(token?.address, spender),
+ queryFn: async () => {
+ if (!token) return;
+ return token.getAllowance(walletAddress as string, spender);
+ },
+ enabled: !!token,
+ refetchOnWindowFocus: "always",
+ staleTime: 1000 * 30 // 30 seconds,
+ });
+};
diff --git a/projects/dex-ui/src/tokens/useTokenBalance.tsx b/projects/dex-ui/src/tokens/useTokenBalance.tsx
index e0a94cb5af..f0221083a3 100644
--- a/projects/dex-ui/src/tokens/useTokenBalance.tsx
+++ b/projects/dex-ui/src/tokens/useTokenBalance.tsx
@@ -1,17 +1,20 @@
import { Token, TokenValue } from "@beanstalk/sdk";
import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { queryKeys } from "src/utils/query/queryKeys";
import { useAccount } from "wagmi";
-export const useTokenBalance = (token: Token) => {
+type TokenBalanceCache = undefined | void | Record;
+
+export const useTokenBalance = (token: Token | undefined) => {
const { address } = useAccount();
const queryClient = useQueryClient();
- const key = ["token", "balance", token.symbol];
-
const { data, isLoading, error, refetch, isFetching } = useQuery({
- queryKey: key,
+ queryKey: queryKeys.tokenBalance(token?.symbol),
queryFn: async () => {
+ if (!token) return;
+
let balance: TokenValue;
if (!address) {
balance = TokenValue.ZERO;
@@ -24,7 +27,7 @@ export const useTokenBalance = (token: Token) => {
};
// Also update the cache of "ALL" token query
- queryClient.setQueryData(["token", "balance"], (oldData: undefined | void | Record) => {
+ queryClient.setQueryData(queryKeys.tokenBalancesAll, (oldData: TokenBalanceCache) => {
if (!oldData) return result;
return { ...oldData, ...result };
@@ -32,7 +35,7 @@ export const useTokenBalance = (token: Token) => {
return result;
},
-
+ enabled: !!token,
staleTime: 1000 * 15,
refetchInterval: 1000 * 15,
refetchIntervalInBackground: false,
diff --git a/projects/dex-ui/src/tokens/useTokenMetadata.ts b/projects/dex-ui/src/tokens/useTokenMetadata.ts
new file mode 100644
index 0000000000..e3f3084f9c
--- /dev/null
+++ b/projects/dex-ui/src/tokens/useTokenMetadata.ts
@@ -0,0 +1,100 @@
+import { useQuery } from "@tanstack/react-query";
+import { alchemy } from "../utils/alchemy";
+import { TokenMetadataResponse } from "alchemy-sdk";
+
+import { useTokens } from "src/tokens/TokenProvider";
+import { useWells } from "src/wells/useWells";
+import { getIsValidEthereumAddress } from "src/utils/addresses";
+import { queryKeys } from "src/utils/query/queryKeys";
+import { ERC20Token, Token } from "@beanstalk/sdk";
+import { images } from "src/assets/images/tokens";
+import { useMemo } from "react";
+
+const emptyMetas: TokenMetadataResponse = {
+ decimals: null,
+ logo: null,
+ name: null,
+ symbol: null
+};
+
+const defaultMetas: TokenMetadataResponse = {
+ name: "UNKNOWN",
+ symbol: "UNKNOWN",
+ logo: images.DEFAULT,
+ decimals: null
+};
+
+type TokenIsh = Token | ERC20Token | undefined;
+
+export const useTokenMetadata = (params: string | TokenIsh): TokenMetadataResponse | undefined => {
+ const address = (params instanceof Token ? params.address : params || "").toLowerCase();
+
+ const isValidAddress = getIsValidEthereumAddress(address);
+ const { data: wells } = useWells();
+ const tokens = useTokens();
+
+ const wellPairToken = Object.values(tokens).find((t) => t.address.toLowerCase() === address);
+ const lpToken = wells?.find((well) => well.address.toLowerCase() === address)?.lpToken;
+ const existingToken = wellPairToken || lpToken;
+
+ const existingMetas = useMemo(() => {
+ const metas = { ...emptyMetas };
+ if (isValidAddress && existingToken) {
+ if (existingToken.name) metas.name = existingToken.name;
+ if (existingToken.decimals) metas.decimals = existingToken.decimals;
+ if (existingToken.symbol) metas.symbol = existingToken.symbol;
+ if (existingToken.logo && !existingToken.logo?.includes("DEFAULT.svg")) {
+ metas.logo = existingToken.logo;
+ };
+ }
+
+ return metas;
+ }, [isValidAddress, existingToken]);
+
+ const metaValues = Object.values(existingMetas);
+ const hasAllMetas = metaValues.length && metaValues.every(Boolean);
+
+ const query = useQuery({
+ queryKey: queryKeys.tokenMetadata(address || "invalid"),
+ queryFn: async () => {
+ if (!wells?.length) return;
+
+ let metas = { ...existingMetas };
+ const tokenMeta = await alchemy.core.getTokenMetadata(address ?? "");
+ if (!tokenMeta) return { ...defaultMetas };
+
+ metas = mergeMetas(tokenMeta, metas);
+
+ return metas;
+ },
+ enabled: isValidAddress && !!wells?.length && !hasAllMetas,
+ retry: false,
+ // We never need to refetch this data
+ staleTime: Infinity
+ });
+
+ const metadatas = useMemo(() => {
+ const meta: TokenMetadataResponse = {
+ name: existingMetas?.name ?? query.data?.name ?? null,
+ symbol: existingMetas?.symbol ?? query.data?.symbol ?? null,
+ logo: existingMetas?.logo ?? query.data?.logo ?? null,
+ decimals: existingMetas?.decimals ?? query.data?.decimals ?? null
+ };
+
+ return meta;
+ }, [existingMetas, query.data]);
+
+ return metadatas;
+};
+
+const mergeMetas = (
+ data: Token | TokenMetadataResponse | undefined,
+ meta: TokenMetadataResponse
+) => {
+ if (!data) return meta;
+ if (!meta.decimals && data?.decimals) meta.decimals = data.decimals;
+ if (!meta.symbol && data?.symbol) meta.symbol = data.symbol;
+ if (!meta.name && data?.name) meta.name = data.name;
+ if (!meta.logo && data?.logo) meta.logo = data.logo;
+ return meta;
+};
diff --git a/projects/dex-ui/src/types.tsx b/projects/dex-ui/src/types.tsx
index 78cb440709..eb277adeff 100644
--- a/projects/dex-ui/src/types.tsx
+++ b/projects/dex-ui/src/types.tsx
@@ -3,3 +3,34 @@ import React from "react";
export type FC = React.FC>;
export type Address = `0x${string}`;
+
+export type BasinAPIResponse = {
+ ticker_id: `${Address}_${Address}`;
+ base_currency: Address;
+ target_currency: Address;
+ pool_id: Address;
+ last_price: number;
+ base_volume: number;
+ target_volume: number;
+ liquidity_in_usd: number;
+ high: number;
+ low: number;
+};
+
+// Primitives
+export type MayArray = T | T[];
+
+// Objects
+export type AddressMap = Record;
+
+/// JSON objects
+export type TokenMetadataMap = AddressMap<{
+ address: string;
+ name: string;
+ symbol: string;
+ decimals: number;
+ logoURI: string;
+ displayName?: string;
+ displayDecimals?: number;
+}>;
+
diff --git a/projects/dex-ui/src/utils/addresses.ts b/projects/dex-ui/src/utils/addresses.ts
index 15752e4abe..50d11793e7 100644
--- a/projects/dex-ui/src/utils/addresses.ts
+++ b/projects/dex-ui/src/utils/addresses.ts
@@ -1,7 +1,45 @@
/// All addresses are in lowercase for consistency
+import { ethers } from "ethers";
+import { AddressMap } from "src/types";
+
/// Well LP Tokens
export const BEANETH_ADDRESS = "0xbea0e11282e2bb5893bece110cf199501e872bad";
/// Pump Addresses
-export const BEANETH_MULTIPUMP_ADDRESS = "0xba510f10e3095b83a0f33aa9ad2544e22570a87c";
+export const MULTI_FLOW_PUMP_ADDRESS = "0xBA51AaaAa95bA1d5efB3cB1A3f50a09165315A17";
+
+/// Well Function Addresses
+export const CONSTANT_PRODUCT_2_ADDRESS = "0xba510c20fd2c52e4cb0d23cfc3ccd092f9165a6e";
+
+// Well Implementation
+export const WELL_DOT_SOL_ADDRESS = "0xba510e11eeb387fad877812108a3406ca3f43a4b";
+
+// ---------- METHODS ----------
+
+export const getIsValidEthereumAddress = (
+ address: string | undefined,
+ enforce0Suffix = true
+): boolean => {
+ if (!address) return false;
+ if (enforce0Suffix && !address.startsWith("0x")) return false;
+ return ethers.utils.isAddress(address ?? "");
+};
+
+/**
+ * Converts an object or array of objects with an address property to a map of address to object.
+ */
+export const toAddressMap = (
+ hasAddress: T | T[],
+ options?: {
+ keyLowercase?: boolean;
+ }
+) => {
+ const arr = Array.isArray(hasAddress) ? hasAddress : [hasAddress];
+
+ return arr.reduce>((prev, curr) => {
+ const key = options?.keyLowercase ? curr.address.toLowerCase() : curr.address;
+ prev[key] = curr;
+ return prev;
+ }, {});
+};
\ No newline at end of file
diff --git a/projects/dex-ui/src/utils/alchemy.ts b/projects/dex-ui/src/utils/alchemy.ts
new file mode 100644
index 0000000000..e0c9cbd460
--- /dev/null
+++ b/projects/dex-ui/src/utils/alchemy.ts
@@ -0,0 +1,6 @@
+import { Alchemy, Network } from "alchemy-sdk";
+
+export const alchemy = new Alchemy({
+ apiKey: import.meta.env.VITE_ALCHEMY_API_KEY,
+ network: Network.ETH_MAINNET
+});
diff --git a/projects/dex-ui/src/utils/bytes.ts b/projects/dex-ui/src/utils/bytes.ts
new file mode 100644
index 0000000000..599e7e162b
--- /dev/null
+++ b/projects/dex-ui/src/utils/bytes.ts
@@ -0,0 +1,9 @@
+import { ethers } from "ethers";
+
+export function getBytesHexString(value: string | number, padding?: number) {
+ const bigNumber = ethers.BigNumber.from(value.toString());
+ const hexStr = bigNumber.toHexString();
+ if (!padding) return hexStr;
+
+ return ethers.utils.hexZeroPad(bigNumber.toHexString(), padding);
+}
diff --git a/projects/dex-ui/src/utils/check.ts b/projects/dex-ui/src/utils/check.ts
new file mode 100644
index 0000000000..4c9360cd0f
--- /dev/null
+++ b/projects/dex-ui/src/utils/check.ts
@@ -0,0 +1,7 @@
+export function exists(value: T | undefined | null): value is NonNullable {
+ return value !== undefined && value !== null;
+}
+
+export function existsNot(value: any): value is undefined | null {
+ return !exists(value);
+}
diff --git a/projects/dex-ui/src/utils/format.ts b/projects/dex-ui/src/utils/format.ts
index ebda31e637..e80263aeac 100644
--- a/projects/dex-ui/src/utils/format.ts
+++ b/projects/dex-ui/src/utils/format.ts
@@ -13,12 +13,19 @@ export const formatNum = (
defaultValue?: string;
minDecimals?: number;
maxDecimals?: number;
+ minValue?: number;
}
) => {
if (val === undefined || val === null) return options?.defaultValue || "-.--";
const normalised = val instanceof TokenValue ? val.toHuman() : val.toString();
+ const num = Number(normalised);
+
+ if (options?.minValue && num > 0 && num < options.minValue) {
+ return `<${options.minValue}`
+ }
+
return Number(normalised).toLocaleString("en-US", {
minimumFractionDigits: options?.minDecimals || 0,
maximumFractionDigits: options?.maxDecimals || 2
diff --git a/projects/dex-ui/src/utils/price/priceLookups.ts b/projects/dex-ui/src/utils/price/priceLookups.ts
index 2a958655bd..9dce0f1fc5 100644
--- a/projects/dex-ui/src/utils/price/priceLookups.ts
+++ b/projects/dex-ui/src/utils/price/priceLookups.ts
@@ -12,12 +12,37 @@ import { Log } from "../logger";
*
*/
-const ETH_USD = "0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419";
-const USDC_USD = "0x8fffffd4afb6115b954bd326cbe7b4ba576818f6";
-const DAI_USD = "0xaed0c38402a5d19df6e4c03f4e2dced6e29c1ee9";
+const FEEDS = {
+ /// BTC Feeds
+ WBTC_BTC: "0xfdFD9C85aD200c506Cf9e21F1FD8dd01932FBB23",
-const chainlinkLookup = (address: string) => async (sdk: BeanstalkSDK) => {
- Log.module("price").debug(`Fetching ${sdk.tokens.findByAddress(address)?.symbol || address} price`);
+ /// ETH Data Feeds
+ LDO_ETH: "0x4e844125952D32AcdF339BE976c98E22F6F318dB",
+ WeETH_ETH: "0x5c9C449BbC9a6075A2c061dF312a35fd1E05fF22",
+
+ /// USD Data Feeds
+ ETH_USD: "0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419",
+ USDC_USD: "0x8fffffd4afb6115b954bd326cbe7b4ba576818f6",
+ DAI_USD: "0xaed0c38402a5d19df6e4c03f4e2dced6e29c1ee9",
+ USDT_USD: "0x3E7d1eAB13ad0104d2750B8863b489D65364e32D",
+ BTC_USD: "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c",
+ BNB_USD: "0x14e613AC84a31f709eadbdF89C6CC390fDc9540A",
+ ARB_USD: "0x31697852a68433DbCc2Ff612c516d69E3D9bd08F",
+ AMPL_USD: "0xe20CA8D7546932360e37E9D72c1a47334af57706",
+ AAVE_USD: "0x547a514d5e3769680Ce22B2361c10Ea13619e8a9",
+ CRV_USD: "0xCd627aA160A6fA45Eb793D19Ef54f5062F20f33f",
+ FRAX_USD: "0xB9E1E3A9feFf48998E45Fa90847ed4D467E8BcfD",
+ LINK_USD: "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c",
+ LUSD_USD: "0x3D7aE7E594f2f2091Ad8798313450130d0Aba3a0",
+ STETH_USD: "0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8",
+ UNI_USD: "0x553303d460EE0afB37EdFf9bE42922D8FF63220e"
+};
+
+const chainlinkLookup = (feed: keyof typeof FEEDS) => async (sdk: BeanstalkSDK) => {
+ const address = FEEDS[feed];
+ Log.module("price").debug(
+ `Fetching ${sdk.tokens.findByAddress(address)?.symbol || address} price`
+ );
const contract = PriceContract__factory.connect(address, sdk.providerOrSigner);
const { answer } = await contract.latestRoundData();
const decimals = await contract.decimals();
@@ -25,6 +50,16 @@ const chainlinkLookup = (address: string) => async (sdk: BeanstalkSDK) => {
return TokenValue.fromBlockchain(answer, decimals);
};
+const multiChainlinkLookup =
+ (from: keyof typeof FEEDS, to: keyof typeof FEEDS) => async (sdk: BeanstalkSDK) => {
+ const [fromPrice, toPrice] = await Promise.all([
+ chainlinkLookup(from)(sdk),
+ chainlinkLookup(to)(sdk)
+ ]);
+
+ return toPrice.mul(fromPrice);
+ };
+
const BEAN = async (sdk: BeanstalkSDK) => {
Log.module("price").debug("Fetching BEAN price");
return sdk.bean.getPrice();
@@ -34,8 +69,23 @@ const PRICE_EXPIRY_TIMEOUT = 60 * 5; // 5 minute cache
export const PriceLookups: Record Promise> = {
BEAN: memoize(BEAN, PRICE_EXPIRY_TIMEOUT),
- ETH: memoize(chainlinkLookup(ETH_USD), PRICE_EXPIRY_TIMEOUT),
- WETH: memoize(chainlinkLookup(ETH_USD), PRICE_EXPIRY_TIMEOUT),
- USDC: memoize(chainlinkLookup(USDC_USD), PRICE_EXPIRY_TIMEOUT),
- DAI: memoize(chainlinkLookup(DAI_USD), PRICE_EXPIRY_TIMEOUT)
+ ETH: memoize(chainlinkLookup("ETH_USD"), PRICE_EXPIRY_TIMEOUT),
+ WETH: memoize(chainlinkLookup("ETH_USD"), PRICE_EXPIRY_TIMEOUT),
+ USDC: memoize(chainlinkLookup("USDC_USD"), PRICE_EXPIRY_TIMEOUT),
+ DAI: memoize(chainlinkLookup("DAI_USD"), PRICE_EXPIRY_TIMEOUT),
+ USDT: memoize(chainlinkLookup("USDT_USD"), PRICE_EXPIRY_TIMEOUT),
+ BNB: memoize(chainlinkLookup("BNB_USD"), PRICE_EXPIRY_TIMEOUT),
+ ARB: memoize(chainlinkLookup("ARB_USD"), PRICE_EXPIRY_TIMEOUT),
+ AMPL: memoize(chainlinkLookup("AMPL_USD"), PRICE_EXPIRY_TIMEOUT),
+ AAVE: memoize(chainlinkLookup("AAVE_USD"), PRICE_EXPIRY_TIMEOUT),
+ CRV: memoize(chainlinkLookup("CRV_USD"), PRICE_EXPIRY_TIMEOUT),
+ FRAX: memoize(chainlinkLookup("FRAX_USD"), PRICE_EXPIRY_TIMEOUT),
+ LINK: memoize(chainlinkLookup("LINK_USD"), PRICE_EXPIRY_TIMEOUT),
+ LUSD: memoize(chainlinkLookup("LUSD_USD"), PRICE_EXPIRY_TIMEOUT),
+ STETH: memoize(chainlinkLookup("STETH_USD"), PRICE_EXPIRY_TIMEOUT),
+ UNI: memoize(chainlinkLookup("UNI_USD"), PRICE_EXPIRY_TIMEOUT),
+ BTC: memoize(chainlinkLookup("BTC_USD"), PRICE_EXPIRY_TIMEOUT),
+ WBTC: memoize(multiChainlinkLookup("WBTC_BTC", "BTC_USD"), PRICE_EXPIRY_TIMEOUT),
+ LDO: memoize(multiChainlinkLookup("LDO_ETH", "ETH_USD"), PRICE_EXPIRY_TIMEOUT),
+ weETH: memoize(multiChainlinkLookup("WeETH_ETH", "ETH_USD"), PRICE_EXPIRY_TIMEOUT)
};
diff --git a/projects/dex-ui/src/utils/price/useTokenPrices.ts b/projects/dex-ui/src/utils/price/useTokenPrices.ts
new file mode 100644
index 0000000000..5ae5946518
--- /dev/null
+++ b/projects/dex-ui/src/utils/price/useTokenPrices.ts
@@ -0,0 +1,81 @@
+import { ERC20Token, TokenValue } from "@beanstalk/sdk";
+import { Well } from "@beanstalk/sdk-wells";
+import { queryKeys } from "../query/queryKeys";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import useSdk from "../sdk/useSdk";
+import { getPrice } from "./usePrice";
+import { PriceLookups } from "./priceLookups";
+import { Log } from "../../utils/logger";
+import { AddressMap } from "src/types";
+import { UseReactQueryOptions } from "../query/types";
+
+type WellOrToken = Well | ERC20Token;
+
+type TokenPricesAllCache = undefined | void | Record;
+
+const getTokens = (wellOrToken: WellOrToken | WellOrToken[] | undefined) => {
+ let tokens: ERC20Token[] = [];
+
+ if (Array.isArray(wellOrToken)) {
+ tokens = wellOrToken.flatMap((w) => (w instanceof Well ? w.tokens || [] : w));
+ } else if (wellOrToken instanceof Well) {
+ tokens = wellOrToken.tokens || [];
+ } else if (wellOrToken instanceof ERC20Token) {
+ tokens = [wellOrToken];
+ }
+
+ return tokens;
+};
+
+/**
+ * returns
+ */
+export const useTokenPrices = >(
+ params: WellOrToken | WellOrToken[] | undefined,
+ options?: UseReactQueryOptions, K>
+) => {
+ const queryClient = useQueryClient();
+ const sdk = useSdk();
+
+ const tokens = getTokens(params);
+
+ const tokenSymbol = tokens.map((token) => token.symbol);
+
+ const query = useQuery({
+ queryKey: queryKeys.tokenPrices(tokenSymbol),
+ queryFn: async () => {
+ const pricesResult = await Promise.all(
+ tokens.map((token) => {
+ if (PriceLookups[token.symbol]) return getPrice(token, sdk);
+
+ Log.module("useTokenPrices").debug("No price lookup function for ", token.symbol, "... resolving with 0");
+ return Promise.resolve(token.fromHuman("0"));
+ })
+ );
+
+ const addressToPriceMap = tokens.reduce>((prev, curr, i) => {
+ const result = pricesResult[i];
+ if (result && result.gt(0)) {
+ prev[curr.symbol] = result;
+ }
+ return prev;
+ }, {});
+
+ /// set the cache for all token prices
+ queryClient.setQueryData(queryKeys.tokenPricesAll, (oldData: TokenPricesAllCache) => {
+ if (!oldData) return { ...addressToPriceMap };
+ return { ...oldData, ...addressToPriceMap };
+ });
+
+ return addressToPriceMap;
+ },
+ enabled: !!params && !!tokenSymbol.length,
+ refetchInterval: 60 * 1000,
+ staleTime: 60 * 1000,
+ refetchOnWindowFocus: false,
+ refetchIntervalInBackground: false,
+ ...options
+ });
+
+ return query;
+};
diff --git a/projects/dex-ui/src/utils/query/queryKeys.ts b/projects/dex-ui/src/utils/query/queryKeys.ts
new file mode 100644
index 0000000000..ec79a4935b
--- /dev/null
+++ b/projects/dex-ui/src/utils/query/queryKeys.ts
@@ -0,0 +1,26 @@
+export const queryKeys = {
+ erc20TokenWithAddress: (address: string) => ["token", "erc20", address],
+ tokenMetadata: (address: string) => ["token", "metadata", address],
+ tokenAllowance: (tokenAddress: string | undefined, spender: string) => [
+ "token",
+ "allowance",
+ tokenAddress || "invalid",
+ spender
+ ],
+
+ // wells
+ wellImplementations: (addresses: string[]) => ["wells", "implementations", addresses],
+
+ // well Function
+ wellFunctionValid: (address: string, data: string) => ["wellFunction", "isValid", address, data],
+ wellFunctionNames: (addresses: string[] | undefined) => ["wellFunctions", "names", addresses],
+
+ // prices
+ tokenPricesAll: ["prices", "token"],
+ tokenPrices: (symbols: string[]) => ["prices", "token", ...symbols],
+ lpTokenPrices: (addresses: string[]) => ["prices", "lp-token", ...addresses],
+
+ // token balance
+ tokenBalancesAll: ["token", "balance"],
+ tokenBalance: (symbol: string | undefined) => ["token", "balance", symbol || "invalid"]
+} as const;
diff --git a/projects/dex-ui/src/utils/query/types.ts b/projects/dex-ui/src/utils/query/types.ts
new file mode 100644
index 0000000000..d03606f0b8
--- /dev/null
+++ b/projects/dex-ui/src/utils/query/types.ts
@@ -0,0 +1,18 @@
+import { UseQueryOptions } from "@tanstack/react-query";
+
+type Options =
+ | "enabled"
+ | "staleTime"
+ | "refetchInterval"
+ | "refetchIntervalInBackground"
+ | "refetchOnWindowFocus"
+ | "refetchOnReconnect"
+ | "refetchOnMount"
+ | "retryOnMount"
+ | "notifyOnChangeProps"
+ | "throwOnError"
+ | "placeholderData";
+
+export type UseReactQueryOptions = Pick, Options> & {
+ select: (data: T) => K;
+};
diff --git a/projects/dex-ui/src/utils/sdk/SdkProvider.tsx b/projects/dex-ui/src/utils/sdk/SdkProvider.tsx
index d67b90c0f0..676aa27917 100644
--- a/projects/dex-ui/src/utils/sdk/SdkProvider.tsx
+++ b/projects/dex-ui/src/utils/sdk/SdkProvider.tsx
@@ -13,15 +13,20 @@ const getSDK = (provider?: JsonRpcProvider, signer?: Signer) => {
provider: provider,
DEBUG: IS_DEVELOPMENT_ENV
});
+
Log.module("sdk").debug("sdk initialized", sdk);
return sdk;
};
const ALCHEMY_API_KEY = import.meta.env.VITE_ALCHEMY_API_KEY;
// TODO: use the correct RPC_URL for the current network
-const RPC_URL = IS_DEVELOPMENT_ENV ? "http://localhost:8545" : `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`;
+const RPC_URL = IS_DEVELOPMENT_ENV
+ ? "http://localhost:8545"
+ : `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`;
-export const BeanstalkSDKContext = createContext(new BeanstalkSDK({ rpcUrl: RPC_URL, DEBUG: import.meta.env.DEV }));
+export const BeanstalkSDKContext = createContext(
+ new BeanstalkSDK({ rpcUrl: RPC_URL, DEBUG: import.meta.env.DEV })
+);
function BeanstalkSDKProvider({ children }: { children: React.ReactNode }) {
const signer = useEthersSigner();
diff --git a/projects/dex-ui/src/utils/ui/styled/box-model.ts b/projects/dex-ui/src/utils/ui/styled/box-model.ts
new file mode 100644
index 0000000000..eb5588dc6c
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/styled/box-model.ts
@@ -0,0 +1,60 @@
+import { css } from "styled-components";
+import { theme } from "../theme";
+import { exists } from "src/utils/check";
+
+export type BoxModelProps = PaddingProps & MarginProps;
+
+export type PaddingProps = {
+ $p?: number;
+ $px?: number;
+ $py?: number;
+ $pt?: number;
+ $pr?: number;
+ $pb?: number;
+ $pl?: number;
+};
+
+export type MarginProps = {
+ $m?: number;
+ $mx?: number;
+ $my?: number;
+ $mt?: number;
+ $mr?: number;
+ $mb?: number;
+ $ml?: number;
+};
+
+type BoxModelSuffix = "y" | "x" | "t" | "r" | "b" | "l" | "";
+type BoxModelAlias = "padding" | "margin";
+
+const emptyStyle = css``;
+
+const makeBoxModelStyles = (_type: BoxModelAlias, props?: BoxModelProps) => {
+ if (!props) return emptyStyle;
+
+ const type = _type === "padding" ? "p" : "m";
+ const getValue = (suffix: BoxModelSuffix) => (props || {})[`$${type}${suffix}`];
+
+ const base = getValue("") ?? 0;
+ const x = getValue("x") ?? 0;
+ const y = getValue("y") ?? 0;
+ const top = getValue("t");
+ const right = getValue("r");
+ const bottom = getValue("b");
+ const left = getValue("l");
+
+ let baseArr: number[] = base + y + x !== 0 ? [base + y, base + x] : [];
+
+ return css`
+ ${baseArr.length ? `${_type}: ${theme.spacing(...baseArr)};` : ""}
+ ${exists(top) ? `${_type}-top: ${theme.spacing(top)};` : ""}
+ ${exists(right) ? `${_type}-right: ${theme.spacing(right)};` : ""}
+ ${exists(bottom) ? `${_type}-bottom: ${theme.spacing(bottom)};` : ""}
+ ${exists(left) ? `${_type}-left: ${theme.spacing(left)};` : ""}
+ `;
+};
+
+export const BoxModelBase = css`
+ ${(props) => makeBoxModelStyles("padding", props)}
+ ${(props) => makeBoxModelStyles("margin", props)}
+`;
diff --git a/projects/dex-ui/src/utils/ui/styled/common.ts b/projects/dex-ui/src/utils/ui/styled/common.ts
new file mode 100644
index 0000000000..15ae266129
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/styled/common.ts
@@ -0,0 +1,95 @@
+import type { CSSProperties } from "react";
+import { ThemedStyledProps, css } from "styled-components";
+import { BoxModelBase, BoxModelProps } from "./box-model";
+
+const CSS_PROP_MAP = {
+ // display
+ $display: "display",
+
+ // dimensions
+ $height: "height",
+ $minHeight: "min-height",
+ $maxHeight: "max-height",
+ $width: "width",
+ $minWidth: "min-width",
+ $maxWidth: "max-width",
+
+ // box
+ $boxSizing: "box-sizing",
+
+ // flex
+ $flex: "flex",
+ $flexFlow: "flex-flow",
+ $flexShrink: "flex-shrink",
+ $flexGrow: "flex-grow",
+ $flexBasis: "flex-basis",
+ $direction: "flex-direction",
+ $alignItems: "align-items",
+ $alignSelf: "align-self",
+ $alignContent: "align-content",
+ $justifyContent: "justify-content",
+ $justifySelf: "justify-self",
+ $justifyItems: "justify-items",
+ $order: "order",
+ $gap: "gap",
+
+ //
+ $whiteSpace: "white-space"
+
+ // we can't handle margin / padding here b/c we calculate them differently
+};
+
+export const makeCssStyle = (
+ props: ThemedStyledProps,
+ propKey: keyof typeof CSS_PROP_MAP
+): string => {
+ const prop = props[propKey as keyof typeof props];
+ const cssKey = CSS_PROP_MAP[propKey];
+ return prop && cssKey ? `${cssKey}: ${prop};` : "";
+};
+
+export type DisplayStyleProps = {
+ $display?: CSSProperties["display"];
+};
+
+export const BlockDisplayStyle = css`
+ ${(p) => makeCssStyle(p, "$display")}
+`;
+
+export type DimensionStyleProps = {
+ $height?: CSSProperties["height"];
+ $minHeight?: CSSProperties["minHeight"];
+ $maxHeight?: CSSProperties["maxHeight"];
+ $width?: CSSProperties["width"];
+ $minWidth?: CSSProperties["minWidth"];
+ $maxWidth?: CSSProperties["maxWidth"];
+};
+
+export const DimensionStyles = css`
+ ${(p) => makeCssStyle(p, "$height")}
+ ${(p) => makeCssStyle(p, "$minHeight")}
+ ${(p) => makeCssStyle(p, "$maxHeight")}
+ ${(p) => makeCssStyle(p, "$width")}
+ ${(p) => makeCssStyle(p, "$minWidth")}
+ ${(p) => makeCssStyle(p, "$maxWidth")}
+`;
+
+export type BoxSizingProps = {
+ $boxSizing?: CSSProperties["boxSizing"];
+};
+
+export const BoxSizingStyles = css`
+ ${(p) => makeCssStyle(p, "$boxSizing")}
+`;
+
+export type CommonCssProps = DimensionStyleProps &
+ BoxSizingProps &
+ DisplayStyleProps &
+ BoxModelProps;
+
+export const CommonCssStyles = css`
+ ${DimensionStyles}
+ ${BoxSizingStyles}
+ ${BlockDisplayStyle}
+ ${BoxModelBase}
+`;
diff --git a/projects/dex-ui/src/utils/ui/styled/css-model.ts b/projects/dex-ui/src/utils/ui/styled/css-model.ts
new file mode 100644
index 0000000000..d2ec1f6abf
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/styled/css-model.ts
@@ -0,0 +1,6 @@
+import { css } from "styled-components";
+import { CssProps } from "../theme";
+
+export const AdditionalCssBase = css`
+ ${(props) => (props.$css ? props.$css : "")}
+`;
diff --git a/projects/dex-ui/src/utils/ui/styled/flex-model.ts b/projects/dex-ui/src/utils/ui/styled/flex-model.ts
new file mode 100644
index 0000000000..66550f866e
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/styled/flex-model.ts
@@ -0,0 +1,58 @@
+import { exists } from "src/utils/check";
+import type { CSSProperties } from "react";
+import { css } from "styled-components";
+import { theme } from "../theme";
+import { CommonCssProps, CommonCssStyles, makeCssStyle } from "./common";
+
+type FlexModelDirection = "row" | "column" | "row-reverse" | "column-reverse";
+
+export type FlexPropertiesProps = {
+ $flex?: CSSProperties["flex"];
+ $flexFlow?: CSSProperties["flexFlow"];
+ $direction?: FlexModelDirection;
+ $flexShrink?: CSSProperties["flexShrink"];
+ $flexGrow?: CSSProperties["flexGrow"];
+ $flexBasis?: CSSProperties["flexBasis"];
+ $alignItems?: CSSProperties["alignItems"];
+ $alignSelf?: CSSProperties["alignSelf"];
+ $alignContent?: CSSProperties["alignContent"];
+ $justifySelf?: CSSProperties["justifySelf"];
+ $justifyContent?: CSSProperties["justifyContent"];
+ $justifyItems?: CSSProperties["justifyItems"];
+ $order?: CSSProperties["order"];
+ $gap?: number;
+ $rowGap?: number;
+ $columnGap?: number;
+};
+
+export const FlexPropertiesStyle = css`
+ ${(p) => makeCssStyle(p, "$flex")}
+ ${(p) => makeCssStyle(p, "$flexShrink")}
+ ${(p) => makeCssStyle(p, "$flexFlow")}
+ ${(p) => makeCssStyle(p, "$flexGrow")}
+ ${(p) => makeCssStyle(p, "$flexBasis")}
+ ${(p) => makeCssStyle(p, "$alignItems")}
+ ${(p) => makeCssStyle(p, "$alignSelf")}
+ ${(p) => makeCssStyle(p, "$alignContent")}
+ ${(p) => makeCssStyle(p, "$justifySelf")}
+ ${(p) => makeCssStyle(p, "$justifyContent")}
+ ${(p) => makeCssStyle(p, "$justifyItems")}
+ ${(p) => makeCssStyle(p, "$order")}
+ ${(p) => makeCssStyle(p, "$direction")}
+ ${(p) => (exists(p.$gap) ? `gap: ${theme.spacing(p.$gap)};` : "")}
+ ${(p) => (exists(p.$rowGap) ? `row-gap: ${theme.spacing(p.$rowGap)};` : "")}
+ ${(p) => (exists(p.$columnGap) ? `column-gap: ${theme.spacing(p.$columnGap)};` : "")}
+`;
+
+export type FlexModelProps = FlexPropertiesProps &
+ CommonCssProps & {
+ $fullWidth?: boolean;
+ };
+
+export const FlexBase = css`
+ ${FlexPropertiesStyle}
+ display: ${(p) => p.$display || "flex"};
+ flex-direction: ${(p) => p.$direction || "column"};
+ ${CommonCssStyles}
+ ${(p) => (p.$fullWidth ? "width: 100%;" : "")}
+`;
diff --git a/projects/dex-ui/src/utils/ui/styled/index.ts b/projects/dex-ui/src/utils/ui/styled/index.ts
new file mode 100644
index 0000000000..f69a6ec7c6
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/styled/index.ts
@@ -0,0 +1,4 @@
+export * from "./box-model";
+export * from "./flex-model";
+export * from "./css-model";
+export * from "./common";
\ No newline at end of file
diff --git a/projects/dex-ui/src/utils/ui/theme/colors.ts b/projects/dex-ui/src/utils/ui/theme/colors.ts
new file mode 100644
index 0000000000..73e555f7e1
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/theme/colors.ts
@@ -0,0 +1,56 @@
+import { css } from "styled-components";
+
+export type ThemeColor =
+ | "primary"
+ | "primaryLight"
+ | "black"
+ | "white"
+ | "lightGray"
+ | "gray"
+ | "darkGray"
+ | "disabled"
+ | "errorRed"
+ | "stone"
+ | "stoneLight";
+
+export const THEME_COLORS: Record = {
+ primary: "#46b955",
+ primaryLight: "#F0FDF4",
+ black: "#000",
+ white: "#fff",
+ gray: "#4B5563",
+ darkGray: "#4b5563",
+ lightGray: "#9ca3af",
+ disabled: "#D1D5DB",
+ errorRed: "#DA2C38",
+ stone: "#78716c",
+ stoneLight: "#F9F8F6"
+} as const;
+
+export type FontColor =
+ | "error"
+ | "primary"
+ | "text.primary"
+ | "text.secondary"
+ | "text.light"
+ | "disabled";
+
+export const FONT_COLORS: Record = {
+ ["text.primary"]: THEME_COLORS.black,
+ ["text.secondary"]: THEME_COLORS.gray,
+ ["text.light"]: THEME_COLORS.lightGray,
+ primary: THEME_COLORS.primary,
+ disabled: THEME_COLORS.disabled,
+ error: THEME_COLORS.errorRed
+};
+
+export const getFontColor = (color: FontColor) => FONT_COLORS[color];
+
+export const FontColorStyle = css<{ $color?: FontColor }>`
+ ${(props) => {
+ const color = props.$color || "text.primary";
+ return `
+ color: ${getFontColor(color)};
+ `;
+ }}
+`;
diff --git a/projects/dex-ui/src/utils/ui/theme/font.ts b/projects/dex-ui/src/utils/ui/theme/font.ts
new file mode 100644
index 0000000000..bb7b7a4dbf
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/theme/font.ts
@@ -0,0 +1,107 @@
+import { H1, H2, H3, BodyL, BodyS, BodyXS, LinksButtonText } from "src/components/Typography";
+import { exists } from "src/utils/check";
+import { css } from "styled-components";
+
+export type FontWeight = "normal" | "semi-bold" | "bold";
+
+export type TextAlign = "left" | "center" | "right" | "inherit";
+
+export type FontSize = "h1" | "h2" | "h3" | "l" | "s" | "xs";
+
+export type FontVariant = FontSize | "button-link";
+
+const FONT_SIZE_MAP = {
+ h1: 48, // H1
+ h2: 32, // H2
+ h3: 24, // H3
+ l: 20, // BodyL
+ s: 16, // BodyS
+ xs: 14 // BodyXS
+};
+
+/// --------------- Font Size ---------------
+export const getFontSize = (_size: number | FontSize) => {
+ if (typeof _size === "number") return `${_size}px`;
+ return `${FONT_SIZE_MAP[_size in FONT_SIZE_MAP ? _size : "s"]}px`;
+};
+
+export const FontSizeStyle = css<{ $size?: number | FontSize }>`
+ ${({ $size }) => {
+ if (!exists($size)) return "";
+ return `
+ font-size: ${getFontSize($size)};
+ `;
+ }}
+`;
+
+export const LineHeightStyle = css<{ $lineHeight?: number | FontSize }>`
+ ${(props) => {
+ if (!exists(props.$lineHeight)) return "";
+ return `
+ line-height: ${getFontSize(props.$lineHeight)};
+ `;
+ }}
+`;
+
+// --------------- Font Weight ---------------
+export const getFontWeight = (weight: FontWeight) => {
+ switch (weight) {
+ case "semi-bold":
+ return 600;
+ case "bold":
+ return 700;
+ default:
+ return 400;
+ }
+};
+
+export const FontWeightStyle = css<{ $weight?: FontWeight }>`
+ ${(props) => {
+ if (!exists(props.$weight)) return "";
+ return `
+ font-weight: ${getFontWeight(props.$weight)};
+ `;
+ }}
+`;
+
+// --------------- Text Align ---------------
+export const TextAlignStyle = css<{ $align?: TextAlign }>`
+ ${(props) => {
+ if (!exists(props.$align)) return "";
+ return `
+ text-align: ${props.$align};
+ `;
+ }}
+`;
+
+export const getTextAlignStyles = (align: TextAlign) => {
+ return css`
+ text-align: ${align};
+ `;
+};
+
+export const FontUtil = {
+ size: getFontSize,
+ weight: getFontWeight
+};
+
+export const getFontVariantStyles = (variant: FontVariant) => {
+ switch (variant) {
+ case "h1":
+ return H1;
+ case "h2":
+ return H2;
+ case "h3":
+ return H3;
+ case "l":
+ return BodyL;
+ case "s":
+ return BodyS;
+ case "xs":
+ return BodyXS;
+ case "button-link":
+ return LinksButtonText;
+ default:
+ return BodyS;
+ }
+};
diff --git a/projects/dex-ui/src/utils/ui/theme/index.ts b/projects/dex-ui/src/utils/ui/theme/index.ts
new file mode 100644
index 0000000000..2e20a5b678
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/theme/index.ts
@@ -0,0 +1,4 @@
+export * from "./theme";
+export * from "./font";
+export * from "./colors";
+export * from "./types";
diff --git a/projects/dex-ui/src/utils/ui/theme/spacing.ts b/projects/dex-ui/src/utils/ui/theme/spacing.ts
new file mode 100644
index 0000000000..e541862721
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/theme/spacing.ts
@@ -0,0 +1,7 @@
+export const GRID_UNIT = 8;
+
+export const themeSpacing = (...args: number[]): string =>
+ args
+ .slice(0, 4)
+ .reduce((str, pts) => `${str} ${pts * GRID_UNIT}px`, "")
+ .trim();
diff --git a/projects/dex-ui/src/utils/ui/theme/theme.ts b/projects/dex-ui/src/utils/ui/theme/theme.ts
new file mode 100644
index 0000000000..1d8bfa3d23
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/theme/theme.ts
@@ -0,0 +1,21 @@
+import { THEME_COLORS, getFontColor } from "./colors";
+import { themeSpacing } from "./spacing";
+import { getFontSize, getFontVariantStyles, getTextAlignStyles } from "./font";
+import { mediaQuery, size } from "src/breakpoints";
+
+export const theme = {
+ colors: THEME_COLORS,
+ spacing: themeSpacing,
+ font: {
+ styles: {
+ variant: getFontVariantStyles,
+ textAlign: getTextAlignStyles
+ },
+ color: getFontColor,
+ size: getFontSize
+ },
+ media: {
+ size: size,
+ query: mediaQuery
+ }
+} as const;
diff --git a/projects/dex-ui/src/utils/ui/theme/types.ts b/projects/dex-ui/src/utils/ui/theme/types.ts
new file mode 100644
index 0000000000..a533b4ad46
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/theme/types.ts
@@ -0,0 +1,5 @@
+import { FlattenSimpleInterpolation } from "styled-components";
+
+export type CssProps = {
+ $css?: FlattenSimpleInterpolation;
+};
diff --git a/projects/dex-ui/src/utils/ui/useBoolean.ts b/projects/dex-ui/src/utils/ui/useBoolean.ts
new file mode 100644
index 0000000000..c43b335ce2
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/useBoolean.ts
@@ -0,0 +1,25 @@
+import { useState, useMemo } from "react";
+
+export const useBoolean = (
+ initialValue?: boolean
+): readonly [
+ boolean,
+ {
+ set: (val: boolean) => void;
+ toggle: () => void;
+ }
+] => {
+ const [value, setValue] = useState(initialValue ?? false);
+
+ const utils = useMemo(() => {
+ const set = (val: boolean) => setValue(val);
+ const toggle = () => setValue((prev) => !prev);
+
+ return {
+ toggle,
+ set
+ };
+ }, []);
+
+ return [value, utils] as const;
+};
diff --git a/projects/dex-ui/src/utils/ui/useDimensions.ts b/projects/dex-ui/src/utils/ui/useDimensions.ts
new file mode 100644
index 0000000000..671a909fb8
--- /dev/null
+++ b/projects/dex-ui/src/utils/ui/useDimensions.ts
@@ -0,0 +1,40 @@
+import { useState, useEffect, useRef } from "react";
+
+// Define a type for the dimensions
+export type ElementDimensions = {
+ width: number;
+ height: number;
+};
+
+// Hook to get element dimensions using a ref
+function useElementDimensions(): [React.RefObject, ElementDimensions] {
+ const ref = useRef(null);
+ const [dimensions, setDimensions] = useState({
+ width: 0, // Default width
+ height: 0 // Default height
+ });
+
+ useEffect(() => {
+ // Function to update dimensions
+ const updateDimensions = () => {
+ if (!ref.current) return;
+
+ setDimensions({
+ width: ref.current.offsetWidth,
+ height: ref.current.offsetHeight
+ });
+ };
+
+ // Update dimensions initially and whenever the window resizes
+ window.addEventListener("resize", updateDimensions);
+ updateDimensions(); // Initial dimensions update
+
+ return () => {
+ window.removeEventListener("resize", updateDimensions);
+ };
+ }, []); // Effect runs only once on mount
+
+ return [ref, dimensions];
+}
+
+export default useElementDimensions;
diff --git a/projects/dex-ui/src/utils/workflow/steps.ts b/projects/dex-ui/src/utils/workflow/steps.ts
new file mode 100644
index 0000000000..80403adfcf
--- /dev/null
+++ b/projects/dex-ui/src/utils/workflow/steps.ts
@@ -0,0 +1,19 @@
+import { TokenValue } from "@beanstalk/sdk";
+import { ethers } from "ethers";
+
+export const makeLocalOnlyStep = (name: string, frontRunAmount?: TokenValue) => {
+ const step = async (amountInStep: ethers.BigNumber) => {
+ return {
+ name: name,
+ amountOut: frontRunAmount?.toBigNumber() || amountInStep,
+ prepare: () => ({
+ target: "",
+ callData: ""
+ }),
+ decode: () => undefined,
+ decodeResult: () => undefined
+ };
+ };
+
+ return step;
+};
diff --git a/projects/dex-ui/src/wells/aquifer/aquifer.ts b/projects/dex-ui/src/wells/aquifer/aquifer.ts
new file mode 100644
index 0000000000..ba251b3cea
--- /dev/null
+++ b/projects/dex-ui/src/wells/aquifer/aquifer.ts
@@ -0,0 +1,13 @@
+import { useMemo } from "react";
+import { Aquifer } from "@beanstalk/sdk-wells";
+import { Settings } from "src/settings";
+
+import useSdk from "src/utils/sdk/useSdk";
+
+export const useAquifer = () => {
+ const sdk = useSdk();
+
+ return useMemo(() => {
+ return new Aquifer(sdk.wells, Settings.AQUIFER_ADDRESS);
+ }, [sdk.wells]);
+};
diff --git a/projects/dex-ui/src/wells/boreWell.ts b/projects/dex-ui/src/wells/boreWell.ts
new file mode 100644
index 0000000000..1b68575f6e
--- /dev/null
+++ b/projects/dex-ui/src/wells/boreWell.ts
@@ -0,0 +1,280 @@
+import { BeanstalkSDK, ERC20Token, FarmFromMode, FarmToMode, TokenValue } from "@beanstalk/sdk";
+import { Aquifer, WellFunction, Pump, Well } from "@beanstalk/sdk-wells";
+import { BigNumber, ethers } from "ethers";
+import { Settings } from "src/settings";
+
+import { getBytesHexString } from "src/utils/bytes";
+import { makeLocalOnlyStep } from "src/utils/workflow/steps";
+import { Log } from "src/utils/logger";
+import { TransactionToast } from "src/components/TxnToast/TransactionToast";
+
+/**
+ * Prepare the parameters for a Aquifer.boreWell call
+ */
+const prepareBoreWellParameters = async (
+ aquifer: Aquifer,
+ implementation: string,
+ tokens: ERC20Token[], // we assume that these tokens are sorted already
+ wellFunction: WellFunction,
+ pumps: Pump | Pump[],
+ name: string,
+ symbol: string,
+ salt?: number
+) => {
+ const immutableData = Aquifer.getEncodedWellImmutableData(
+ aquifer.address,
+ tokens,
+ wellFunction,
+ Array.isArray(pumps) ? pumps : [pumps]
+ );
+
+ const initFunctionCall = await Aquifer.getEncodedWellInitFunctionData(name, symbol);
+
+ const saltBytes32 = getBytesHexString(salt || 0, 32);
+
+ return [implementation, immutableData, initFunctionCall, saltBytes32] as [
+ string,
+ Uint8Array,
+ Uint8Array,
+ string
+ ];
+};
+
+/**
+ * Decode the result of a boreWell wrapped in a advancedPipe callto get the well address
+ */
+const decodeBoreWellPipeCall = (sdk: BeanstalkSDK, aquifer: Aquifer, pipeResult: string[]) => {
+ if (!pipeResult.length) return;
+ const pipeDecoded = sdk.contracts.pipeline.interface.decodeFunctionResult(
+ "advancedPipe",
+ pipeResult[0]
+ );
+
+ if (!pipeDecoded?.length || !pipeDecoded[0]?.length) return;
+ const boreWellDecoded = aquifer.contract.interface.decodeFunctionResult(
+ "boreWell",
+ pipeDecoded[0][0]
+ );
+ if (!boreWellDecoded?.length) return;
+ return boreWellDecoded[0] as string;
+};
+
+/**
+ * Sorts the tokens in the following manner:
+ * - if tokens includes BEAN, BEAN is first
+ * - if tokens includes WETH, WETH is last
+ * - otherwise, the token order is preserved
+ *
+ * this is so that pairs with BEAN are BEAN:X
+ * and pairs with WETH are X:WETH
+ *
+ * TODO: do this with wStETH
+ */
+const prepareTokenOrderForBoreWell = (sdk: BeanstalkSDK, tokens: ERC20Token[]) => {
+ if (tokens.length < 2) {
+ throw new Error("2 Tokens are required");
+ }
+
+ const wethAddress = sdk.tokens.WETH.address.toLowerCase();
+ const beanAddress = sdk.tokens.BEAN.address.toLowerCase();
+
+ return tokens.sort((a, b) => {
+ const addressA = a.address.toLowerCase();
+ const addressB = b.address.toLowerCase();
+ if (addressA === beanAddress || addressB === wethAddress) return -1;
+ if (addressB === beanAddress || addressA === wethAddress) return 1;
+ return 0;
+ });
+};
+
+/**
+ * function to bore well
+ */
+const boreWell = async (
+ sdk: BeanstalkSDK,
+ account: string,
+ implementation: string,
+ wellFunction: WellFunction,
+ pumps: Pump[],
+ token1: ERC20Token,
+ token2: ERC20Token,
+ name: string,
+ symbol: string,
+ saltValue: number,
+ liquidityAmounts: { token1Amount: TokenValue; token2Amount: TokenValue } | undefined,
+ toast?: TransactionToast
+) => {
+ if (liquidityAmounts) {
+ if (liquidityAmounts.token1Amount?.lte(0) && liquidityAmounts.token2Amount.lte(0)) {
+ throw new Error("At least one token amount must be greater than 0 to seed liquidity");
+ }
+ if (saltValue < 1) {
+ throw new Error("Salt value must be greater than 0 if seeding liquidity");
+ }
+ }
+
+ const aquifer = new Aquifer(sdk.wells, Settings.AQUIFER_ADDRESS);
+ const boreWellParams = await prepareBoreWellParameters(
+ aquifer,
+ implementation,
+ [token1, token2],
+ wellFunction,
+ pumps,
+ name,
+ symbol,
+ saltValue
+ );
+ Log.module("boreWell").debug("boreWellParams: ", boreWellParams);
+
+ const callData = aquifer.contract.interface.encodeFunctionData("boreWell", boreWellParams);
+ Log.module("boreWell").debug("callData: ", callData);
+
+ let wellAddress: string = "";
+
+ const staticFarm = sdk.farm.createAdvancedFarm("static-farm");
+ const advancedFarm = sdk.farm.createAdvancedFarm("adv-farm");
+ const advancedPipe = sdk.farm.createAdvancedPipe("adv-pipe");
+
+ advancedPipe.add(makeBoreWellStep(aquifer, callData));
+
+ /// If we are adding liquidity, add steps to advancedFarm & advancedPipe
+ if (liquidityAmounts) {
+ staticFarm.add(advancedPipe);
+
+ wellAddress = await staticFarm
+ .callStatic(BigNumber.from(0), { slippage: 0.05 })
+ .then((result) => decodeBoreWellPipeCall(sdk, aquifer, result) || "");
+
+ if (!wellAddress) {
+ throw new Error("Unable to determine well address");
+ }
+
+ const well = new Well(sdk.wells, wellAddress);
+ Log.module("boreWell").debug("Expected Well Address: ", wellAddress);
+
+ // add transfer token1 to the undeployed well address
+ advancedFarm.add(makeLocalOnlyStep("token1-amount", liquidityAmounts.token1Amount), {
+ onlyLocal: true
+ });
+ advancedFarm.add(
+ new sdk.farm.actions.TransferToken(
+ token1.address,
+ well.address,
+ FarmFromMode.EXTERNAL,
+ FarmToMode.EXTERNAL
+ )
+ );
+
+ // add transfer token2 to the undeployed well address
+ advancedFarm.add(makeLocalOnlyStep("token2-amount", liquidityAmounts.token2Amount), {
+ onlyLocal: true
+ });
+ advancedFarm.add(
+ new sdk.farm.actions.TransferToken(
+ token2.address,
+ well.address,
+ FarmFromMode.EXTERNAL,
+ FarmToMode.EXTERNAL
+ )
+ );
+
+ advancedPipe.add(
+ makeSyncWellStep(
+ well,
+ wellFunction,
+ liquidityAmounts.token1Amount,
+ liquidityAmounts.token2Amount,
+ account
+ )
+ );
+ }
+
+ advancedFarm.add(advancedPipe);
+
+ // build the workflow
+ await advancedFarm.estimate(BigNumber.from(0));
+ const txn = await advancedFarm.execute(BigNumber.from(0), {
+ slippage: 0.1 // TODO: Add slippage to form.
+ });
+
+ toast?.confirming(txn);
+ Log.module("wellDeployer").debug(`Well deploying... Transaction: ${txn.hash}`);
+
+ const receipt = await txn.wait();
+ Log.module("wellDeployer").debug("Well deployed... txn events: ", receipt.events);
+
+ if (!receipt.events?.length) {
+ throw new Error("No Bore Well events found");
+ }
+
+ toast?.success(receipt);
+
+ if (!wellAddress && !liquidityAmounts) {
+ wellAddress = receipt.events[0].address as string;
+ }
+
+ return {
+ wellAddress,
+ receipt
+ };
+};
+
+const makeBoreWellStep = (aquifer: Aquifer, callData: string) => {
+ const boreWellStep = async (_amountInStep: ethers.BigNumber, _context: any) => ({
+ name: "boreWell",
+ amountOut: _amountInStep,
+ prepare: () => ({
+ target: aquifer.address,
+ callData
+ }),
+ decode: (data: string) => aquifer.contract.interface.decodeFunctionData("boreWell", data),
+ decodeResult: (data: string) =>
+ aquifer.contract.interface.decodeFunctionResult("boreWell", data)
+ });
+
+ return boreWellStep;
+};
+
+const makeSyncWellStep = (
+ well: Well,
+ wellFunction: WellFunction,
+ token1Amount: TokenValue,
+ token2Amount: TokenValue,
+ recipient: string
+) => {
+ const syncStep = async (_amt: BigNumber, context: { data: { slippage?: number } }) => {
+ // this is safe b/c regardless of the wellFunction, all WellFunctions extend IWellFunction, which
+ // requires the definition of a 'calcLpTokenSupply' function.
+ const calculatedLPSupply = await wellFunction.contract.calcLpTokenSupply(
+ [token1Amount.toBigNumber(), token2Amount.toBigNumber()],
+ wellFunction.data
+ );
+
+ // calculate the minimum LP supply with slippage
+ const lpSupplyTV = TokenValue.fromBlockchain(calculatedLPSupply, 0);
+ const lpSubSlippage = lpSupplyTV.subSlippage(context.data.slippage ?? 0.1);
+ const minLPTrimmed = lpSubSlippage.toHuman().split(".")[0];
+ const minLP = BigNumber.from(minLPTrimmed);
+
+ return {
+ name: "sync",
+ amountOut: minLP,
+ prepare: () => ({
+ target: well.address,
+ // this is safe b/c all wells extend the IWell interface & are required to define a 'sync' function.
+ callData: well.contract.interface.encodeFunctionData("sync", [recipient, minLP])
+ }),
+ decode: (data: string) => well.contract.interface.decodeFunctionData("sync", data),
+ decodeResult: (data: string) => well.contract.interface.decodeFunctionResult("sync", data)
+ };
+ };
+
+ return syncStep;
+};
+
+const BoreWellUtils = {
+ prepareTokenOrderForBoreWell,
+ boreWell
+};
+
+export default BoreWellUtils;
diff --git a/projects/dex-ui/src/wells/pump/usePumps.ts b/projects/dex-ui/src/wells/pump/usePumps.ts
new file mode 100644
index 0000000000..4f7bf19268
--- /dev/null
+++ b/projects/dex-ui/src/wells/pump/usePumps.ts
@@ -0,0 +1,24 @@
+import { useMemo } from "react";
+import { Pump } from "@beanstalk/sdk-wells";
+
+import { useWells } from "src/wells/useWells";
+
+export const usePumps = () => {
+ const { data: wells } = useWells();
+
+ return useMemo(() => {
+ if (!wells || !wells.length) return [];
+
+ const pumpMap: Record = {};
+
+ for (const well of wells) {
+ for (const pump of well.pumps || []) {
+ const pumpAddress = pump.address.toLowerCase();
+ if (pumpAddress in pumpMap) continue;
+ pumpMap[pumpAddress] = pump;
+ }
+ }
+
+ return Object.values(pumpMap);
+ }, [wells]);
+};
diff --git a/projects/dex-ui/src/wells/useBasinStats.ts b/projects/dex-ui/src/wells/useBasinStats.ts
new file mode 100644
index 0000000000..458bab5b78
--- /dev/null
+++ b/projects/dex-ui/src/wells/useBasinStats.ts
@@ -0,0 +1,34 @@
+import { useQuery } from "@tanstack/react-query";
+import { BasinAPIResponse } from "src/types";
+import { Log } from "src/utils/logger";
+
+const useBasinStats = () => {
+
+ return useQuery({
+ queryKey: ["wells", "basinStats"],
+
+ queryFn: async () => {
+ let output: BasinAPIResponse[] = [];
+ try {
+ const apiQuery = await fetch("https://api.bean.money/basin/tickers", {
+ headers: { accept: "application/json" }
+ });
+
+ const result = await apiQuery.json();
+ if (Array.isArray(result)) {
+ output = result as BasinAPIResponse[];
+ } else {
+ if ("message" in result) {
+ throw new Error(result);
+ }
+ }
+ } catch (e) {
+ Log.module("useBasinStats").error("Failed to fetch data from Basin API :", e)
+ };
+ return output;
+ },
+ staleTime: 1000 * 120
+ });
+};
+
+export default useBasinStats;
\ No newline at end of file
diff --git a/projects/dex-ui/src/wells/useBeanstalkSiloWhitelist.ts b/projects/dex-ui/src/wells/useBeanstalkSiloWhitelist.ts
index 63a859e4ef..fdffadb925 100644
--- a/projects/dex-ui/src/wells/useBeanstalkSiloWhitelist.ts
+++ b/projects/dex-ui/src/wells/useBeanstalkSiloWhitelist.ts
@@ -1,8 +1,13 @@
import { useCallback } from "react";
import { Well } from "@beanstalk/sdk/Wells";
-import { BEANETH_MULTIPUMP_ADDRESS } from "src/utils/addresses";
+import { MULTI_FLOW_PUMP_ADDRESS } from "src/utils/addresses";
import useSdk from "src/utils/sdk/useSdk";
+export const getIsMultiPumpWell = (well: Well | undefined) => {
+ if (!well?.pumps) return false;
+ return !!well.pumps.find((pump) => pump.address.toLowerCase() === MULTI_FLOW_PUMP_ADDRESS);
+};
+
export const useBeanstalkSiloWhitelist = () => {
const sdk = useSdk();
@@ -23,11 +28,6 @@ export const useBeanstalkSiloWhitelist = () => {
[sdk.tokens]
);
- const getIsMultiPumpWell = useCallback((well: Well | undefined) => {
- if (!well?.pumps) return false;
- return !!well.pumps.find((pump) => pump.address.toLowerCase() === BEANETH_MULTIPUMP_ADDRESS);
- }, []);
-
return {
getIsWhitelisted,
getSeedsWithWell,
diff --git a/projects/dex-ui/src/wells/useMultiFlowPumpTWAReserves.tsx b/projects/dex-ui/src/wells/useMultiFlowPumpTWAReserves.tsx
index d4a0420ce3..88c1dfa7ae 100644
--- a/projects/dex-ui/src/wells/useMultiFlowPumpTWAReserves.tsx
+++ b/projects/dex-ui/src/wells/useMultiFlowPumpTWAReserves.tsx
@@ -12,14 +12,14 @@ import { config } from "src/utils/wagmi/config";
export const useMultiFlowPumpTWAReserves = () => {
const { data: wells } = useWells();
- const { getIsMultiPumpWell } = useBeanstalkSiloWhitelist();
+ const { getIsMultiPumpWell, getIsWhitelisted } = useBeanstalkSiloWhitelist();
const sdk = useSdk();
const query = useQuery({
queryKey: ["wells", "multiFlowPumpTWAReserves"],
queryFn: async () => {
- const whitelistedWells = (wells || []).filter((well) => getIsMultiPumpWell(well));
+ const whitelistedWells = (wells || []).filter((well) => getIsMultiPumpWell(well) && getIsWhitelisted(well) );
const [{ timestamp: seasonTimestamp }, ...wellOracleSnapshots] = await Promise.all([
sdk.contracts.beanstalk.time(),
@@ -42,24 +42,23 @@ export const useMultiFlowPumpTWAReserves = () => {
const twaReservesResult: any[] = await multicall(config, { contracts: calls });
const mapping: Record = {};
- let index = 0;
whitelistedWells.forEach((well) => {
const twa = [TokenValue.ZERO, TokenValue.ZERO];
const numPumps = well.pumps?.length || 1;
- well.pumps?.forEach((_pump) => {
- const twaResult = twaReservesResult[index];
+ well.pumps?.forEach((_pump, index) => {
+ const indexedResult = twaReservesResult[index];
+ if (indexedResult.error) return;
+
+ const reserves = indexedResult?.result?.[0]
const token1 = well.tokens?.[0];
const token2 = well.tokens?.[1];
- const reserves = twaResult["twaReserves"];
-
- if (token1 && token2 && reserves.length === 2) {
+ if (token1 && token2 && reserves.length === 2 && reserves.length === 2) {
twa[0] = twa[0].add(TokenValue.fromBlockchain(reserves[0], token1.decimals));
twa[1] = twa[1].add(TokenValue.fromBlockchain(reserves[1], token2.decimals));
}
- index += 1;
});
/// In case there is more than one pump, divide the reserves by the number of pumps
diff --git a/projects/dex-ui/src/wells/useWellImplementations.ts b/projects/dex-ui/src/wells/useWellImplementations.ts
new file mode 100644
index 0000000000..7147407421
--- /dev/null
+++ b/projects/dex-ui/src/wells/useWellImplementations.ts
@@ -0,0 +1,57 @@
+import { multicall } from "@wagmi/core";
+import { useQuery } from "@tanstack/react-query";
+import { config } from "src/utils/wagmi/config";
+import { useWells } from "./useWells";
+import { queryKeys } from "src/utils/query/queryKeys";
+import { useAquifer } from "./aquifer/aquifer";
+
+const aquiferAbiSnippet = [
+ {
+ inputs: [{ internalType: "address", name: "", type: "address" }],
+ name: "wellImplementation",
+ outputs: [{ internalType: "address", name: "", type: "address" }],
+ stateMutability: "view",
+ type: "function"
+ }
+] as const;
+
+const getCallObjects = (aquiferAddress: string, addresses: string[]) => {
+ return addresses.map((address) => ({
+ address: aquiferAddress as "0x{string}",
+ abi: aquiferAbiSnippet,
+ functionName: "wellImplementation",
+ args: [address]
+ }));
+};
+
+export const useWellImplementations = () => {
+ const { data: wells } = useWells();
+ const aquifer = useAquifer();
+
+ const addresses = (wells || []).map((well) => well.address);
+
+ const query = useQuery({
+ queryKey: queryKeys.wellImplementations(addresses),
+ queryFn: async () => {
+ if (!wells || !wells.length) return [];
+
+ return multicall(config, {
+ contracts: getCallObjects(aquifer.address, addresses)
+ });
+ },
+ select: (data) => {
+ return addresses.reduce>((prev, curr, i) => {
+ const result = data[i];
+ if (result.error) return prev;
+ if (result.result) {
+ prev[curr.toLowerCase()] = result.result.toLowerCase() as string;
+ }
+ return prev;
+ }, {});
+ },
+ enabled: !!addresses.length,
+ staleTime: Infinity
+ });
+
+ return query;
+};
diff --git a/projects/dex-ui/src/wells/useWellLPTokenPrice.tsx b/projects/dex-ui/src/wells/useWellLPTokenPrice.tsx
index 64bc3e73fd..6b026b2ce9 100644
--- a/projects/dex-ui/src/wells/useWellLPTokenPrice.tsx
+++ b/projects/dex-ui/src/wells/useWellLPTokenPrice.tsx
@@ -1,11 +1,9 @@
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useMemo } from "react";
import { ERC20Token, TokenValue } from "@beanstalk/sdk";
import { Well } from "@beanstalk/sdk/Wells";
-import useSdk from "src/utils/sdk/useSdk";
-import { getPrice } from "src/utils/price/usePrice";
import { useTokenSupplyMany } from "src/tokens/useTokenSupply";
-
-type TokenMap = Record;
+import { AddressMap } from "src/types";
+import { useTokenPrices } from "src/utils/price/useTokenPrices";
/**
* LP Token Price is calculated as: TVL / total supply
@@ -13,10 +11,7 @@ type TokenMap = Record;
* - TVL = (reserve1 amount * token1 price ) + (reserve2 amount + token2 price)
*/
-export const useWellLPTokenPrice = (params: Well | (Well | undefined)[] | undefined) => {
- const [lpTokenPriceMap, setLPTokenPriceMap] = useState>({});
- const sdk = useSdk();
-
+export const useWellLPTokenPrice = (params: Well | Well[] | undefined) => {
const wells = useMemo(() => {
// Make into array for easier processing
if (!params) return [];
@@ -30,52 +25,45 @@ export const useWellLPTokenPrice = (params: Well | (Well | undefined)[] | undefi
return _tokens;
}, [wells]);
- const { totalSupply: tokenSupplies } = useTokenSupplyMany(lpTokens);
-
- const fetchData = useCallback(async () => {
- if (!wells || !tokenSupplies?.length) return;
-
- const fetchTokenPrices = async () => {
- const _tokenMap = wells.reduce>((memo, well) => {
- if (!well || !well?.tokens) return memo;
- well.tokens.forEach((token) => (memo[token.address] = token));
- return memo;
- }, {});
-
- const tokenLyst = Object.entries(_tokenMap);
+ const { totalSupply: tokenSupplies, loading } = useTokenSupplyMany(lpTokens);
+ const { data: prices, isLoading: tokenPricesLoading } = useTokenPrices(wells);
- const prices = await Promise.all(tokenLyst.map(([, token]) => getPrice(token, sdk)));
- const data = tokenLyst.reduce>((memo, [tokenAddress], index) => {
- memo[tokenAddress] = prices[index] || TokenValue.ZERO;
- return memo;
- }, {});
- return data;
- };
-
- const tokenPriceMap = await fetchTokenPrices();
-
- const lpTokenPrices: TokenMap = {};
+ const lpTokenPrices = useMemo(() => {
+ if (!wells || !tokenSupplies?.length || !prices) return undefined;
+ const lpTokenPrices: AddressMap = {};
for (const wellIdx in wells) {
const well = wells[wellIdx];
const tokens = well?.tokens;
- const reserves = well?.reserves && well.reserves.length === 2 ? well.reserves : [TokenValue.ZERO, TokenValue.ZERO];
+ const reserves =
+ well?.reserves && well.reserves.length === 2
+ ? well.reserves
+ : [TokenValue.ZERO, TokenValue.ZERO];
const lpToken = well?.lpToken;
const lpTokenSupply = tokenSupplies[wellIdx] || TokenValue.ONE;
if (well && tokens && lpToken) {
- const wellReserveValues = reserves.map((reserve, rIdx) => reserve.mul(tokenPriceMap[tokens[rIdx].address] || TokenValue.ZERO));
+ const hasAllPrices = tokens.every((tk) => tk.symbol in prices);
+
+ const wellReserveValues = reserves.map((reserve, rIdx) => {
+ if (hasAllPrices) {
+ return reserve.mul(prices[tokens[rIdx].symbol] || TokenValue.ZERO);
+ }
+ return TokenValue.ZERO;
+ });
+
const wellTVL = wellReserveValues?.reduce((acc, val) => acc.add(val));
- lpTokenPrices[lpToken.address] = wellTVL && lpTokenSupply.gt(0) ? wellTVL.div(lpTokenSupply) : TokenValue.ZERO;
+ lpTokenPrices[lpToken.address] =
+ wellTVL && lpTokenSupply.gt(0) ? wellTVL.div(lpTokenSupply) : TokenValue.ZERO;
}
}
- setLPTokenPriceMap(lpTokenPrices);
- }, [sdk, tokenSupplies, wells]);
- useEffect(() => {
- fetchData();
- }, [fetchData]);
+ return lpTokenPrices;
+ }, [prices, tokenSupplies, wells]);
- return { data: lpTokenPriceMap, fetch: fetchData } as const;
+ return {
+ data: lpTokenPrices,
+ isLoading: loading || tokenPricesLoading
+ };
};
diff --git a/projects/dex-ui/src/wells/useWells.tsx b/projects/dex-ui/src/wells/useWells.tsx
index 4c9cb2ce0a..2e373cecbc 100644
--- a/projects/dex-ui/src/wells/useWells.tsx
+++ b/projects/dex-ui/src/wells/useWells.tsx
@@ -3,6 +3,11 @@ import { useQuery } from "@tanstack/react-query";
import { Well } from "@beanstalk/sdk/Wells";
import { findWells } from "./wellLoader";
import { Log } from "src/utils/logger";
+import tokenMetadataJson from 'src/token-metadata.json';
+import { TokenMetadataMap } from "src/types";
+import { images } from "src/assets/images/tokens";
+
+export const clearWellsCache = () => findWells.cache.clear?.();
export const useWells = () => {
const sdk = useSdk();
@@ -23,6 +28,7 @@ export const useWells = () => {
name: true,
tokens: true,
wellFunction: true,
+ pumps: true,
reserves: true,
lpToken: true
})
@@ -33,7 +39,12 @@ export const useWells = () => {
);
// filter out errored calls
- return res.map((promise) => (promise.status === "fulfilled" ? promise.value : null)).filter((p): p is Well => !!p);
+ const wellsResult = res
+ .map((promise) => (promise.status === "fulfilled" ? promise.value : null))
+ .filter((p): p is Well => !!p);
+ // set token metadatas
+ setTokenMetadatas(wellsResult);
+ return wellsResult;
} catch (err: unknown) {
Log.module("useWells").debug(`Error during findWells(): ${(err as Error).message}`);
return [];
@@ -44,3 +55,23 @@ export const useWells = () => {
staleTime: Infinity
});
};
+
+const tokenMetadata = tokenMetadataJson as TokenMetadataMap;
+
+const setTokenMetadatas = (wells: Well[]) => {
+ for (const well of wells) {
+ if (!well.tokens) continue;
+ well.tokens.forEach((token) => {
+ const address = token.address.toLowerCase();
+
+ let logo = images[token.symbol];
+ if (address in tokenMetadata) {
+ const metadata = tokenMetadata[address];
+ if (metadata?.logoURI) logo = metadata.logoURI;
+ if (metadata.displayDecimals) token.displayDecimals = metadata.displayDecimals;
+ if (metadata.displayName) token.displayName = metadata.displayName;
+ }
+ if (logo) token.setMetadata({ logo });
+ });
+ }
+};
\ No newline at end of file
diff --git a/projects/dex-ui/src/wells/utils.ts b/projects/dex-ui/src/wells/utils.ts
new file mode 100644
index 0000000000..895b53dae5
--- /dev/null
+++ b/projects/dex-ui/src/wells/utils.ts
@@ -0,0 +1,6 @@
+import { Well } from "@beanstalk/sdk-wells";
+
+export const formatWellTokenSymbols = (well: Well | undefined, separator?: string) => {
+ const tokenNames = well?.tokens?.map((token) => token.symbol);
+ return tokenNames?.join(separator || ":");
+};
diff --git a/projects/dex-ui/src/wells/wellFunction/useValidateWellFunction.ts b/projects/dex-ui/src/wells/wellFunction/useValidateWellFunction.ts
new file mode 100644
index 0000000000..23d5195af3
--- /dev/null
+++ b/projects/dex-ui/src/wells/wellFunction/useValidateWellFunction.ts
@@ -0,0 +1,115 @@
+import { multicall } from "@wagmi/core";
+import { useCallback } from "react";
+import { useWellFunctions } from "./useWellFunctions";
+import { WellFunction } from "@beanstalk/sdk-wells";
+import useSdk from "src/utils/sdk/useSdk";
+import { config } from "src/utils/wagmi/config";
+import { BigNumber } from "ethers";
+import { BeanstalkSDK } from "@beanstalk/sdk";
+import { useQueryClient } from "@tanstack/react-query";
+import { queryKeys } from "src/utils/query/queryKeys";
+
+const getWellFunctionCalls = (wellFunction: WellFunction) => {
+ const address = wellFunction.address as `0x${string}`;
+ const bn = BigNumber.from(100); // random big number
+ const abi = WellFunction.abi;
+
+ return [
+ {
+ address,
+ abi,
+ functionName: "calcLPTokenUnderlying",
+ args: [bn, [bn, bn], bn, wellFunction.data]
+ },
+ { address, abi, functionName: "calcLpTokenSupply", args: [[bn, bn], wellFunction.data] },
+ // { // might be flaky
+ // address,
+ // abi,
+ // functionName: "calcReserve",
+ // args: [
+ // [bn, bn],
+ // one,
+ // bn,
+ // wellFunction.data
+ // ]
+ // },
+ { address, abi, functionName: "name", args: [] },
+ { address, abi, functionName: "symbol", args: [] }
+ ];
+};
+
+const validateWellFunction = async (
+ sdk: BeanstalkSDK,
+ knownWellFunctions: WellFunction[],
+ params: {
+ address?: string;
+ data?: string;
+ wellFunction?: WellFunction;
+ }
+) => {
+ const { address, data, wellFunction: wellFn } = params;
+
+ if (!wellFn && !address && !data) return undefined;
+
+ const foundWellFunction =
+ address && knownWellFunctions.find((wf) => wf.address.toLowerCase() === address.toLowerCase());
+ if (foundWellFunction) return foundWellFunction;
+
+ const wellFunction = wellFn || (data && address && new WellFunction(sdk.wells, address, data));
+ if (!wellFunction) return undefined;
+
+ const calls = await multicall(config, { contracts: getWellFunctionCalls(wellFunction) });
+ const allValid = calls.filter((call) => !call.error);
+
+ return allValid.length === calls.length ? wellFunction : undefined;
+};
+
+type ValidateWellFunctionParams = {
+ address?: string;
+ data?: string;
+ wellFunction?: WellFunction;
+};
+
+type CachedWellFunctionData = WellFunction | string | undefined;
+
+// why set the invalidWellFunctionData to a string?
+// react-query doesn't cache undefined values, so we need to set it to a string
+const invalidWellFunctionData = "invalid-well-function";
+
+export const useValidateWellFunction = () => {
+ const wellFunctions = useWellFunctions();
+ const sdk = useSdk();
+
+ const queryClient = useQueryClient();
+
+ const validate = useCallback(
+ async ({ address, data, wellFunction }: ValidateWellFunctionParams) => {
+ const queryKey = queryKeys.wellFunctionValid(address || "no-address", data || "no-data");
+ try {
+ // check the queryClientCache first
+ const cachedWellFunction = queryClient.getQueryData(queryKey) as CachedWellFunctionData;
+ if (cachedWellFunction) {
+ if (typeof cachedWellFunction === "string") return undefined;
+ return cachedWellFunction;
+ }
+
+ const result = await validateWellFunction(sdk, wellFunctions, {
+ address,
+ data,
+ wellFunction
+ });
+
+ // set the queryClientCache for future use.
+ queryClient.setQueryData(queryKey, result || invalidWellFunctionData);
+ return result;
+ } catch (e) {
+ // set the queryClientCache for future use.
+ queryClient.setQueryData(queryKey, invalidWellFunctionData);
+ return undefined;
+ }
+ },
+ [wellFunctions, sdk, queryClient]
+ );
+
+ return [validate] as const;
+};
diff --git a/projects/dex-ui/src/wells/wellFunction/useWellFunctionNames.ts b/projects/dex-ui/src/wells/wellFunction/useWellFunctionNames.ts
new file mode 100644
index 0000000000..5d7e5e51c6
--- /dev/null
+++ b/projects/dex-ui/src/wells/wellFunction/useWellFunctionNames.ts
@@ -0,0 +1,34 @@
+import { Well, WellFunction } from "@beanstalk/sdk-wells";
+import { useQuery } from "@tanstack/react-query";
+import { AddressMap } from "src/types";
+import { queryKeys } from "src/utils/query/queryKeys";
+
+interface WellWithWellFn extends Well {
+ wellFunction: WellFunction;
+}
+
+/**
+ * Returns a Record of well function addresses to their names.
+ */
+export const useWellFunctionNames = (_wells: Well[] | undefined) => {
+ const wells = _wells || [];
+
+ const wellsWithWellFunctions = wells.filter((w) => !!w.wellFunction) as WellWithWellFn[];
+ const addresses = wellsWithWellFunctions.map((well) => well.wellFunction.address);
+
+ return useQuery({
+ queryKey: queryKeys.wellFunctionNames(addresses.length ? addresses : ["invalid"]),
+ queryFn: async () => {
+ // TODO: make me into a multi call at some point.
+ const names = await Promise.all(
+ wellsWithWellFunctions.map((well) => well.wellFunction.getName())
+ );
+
+ return wellsWithWellFunctions.reduce>((prev, curr, i) => {
+ prev[curr.wellFunction.address] = names[i];
+ return prev;
+ }, {});
+ },
+ enabled: !!wells.length
+ });
+};
diff --git a/projects/dex-ui/src/wells/wellFunction/useWellFunctions.ts b/projects/dex-ui/src/wells/wellFunction/useWellFunctions.ts
new file mode 100644
index 0000000000..922b1b00dc
--- /dev/null
+++ b/projects/dex-ui/src/wells/wellFunction/useWellFunctions.ts
@@ -0,0 +1,25 @@
+import { useMemo } from "react";
+import { WellFunction } from "@beanstalk/sdk-wells";
+
+import { useWells } from "src/wells/useWells";
+
+export const useWellFunctions = () => {
+ const { data: wells } = useWells();
+
+ return useMemo(() => {
+ if (!wells || !wells.length) return [];
+
+ const wellFunctionMap: Record = {};
+
+ for (const well of wells) {
+ if (!well.wellFunction) continue;
+ const address = well.wellFunction.address.toLowerCase();
+
+ if (!(address in wellFunctionMap)) {
+ wellFunctionMap[address] = well.wellFunction;
+ }
+ }
+
+ return Object.values(wellFunctionMap);
+ }, [wells]);
+};
diff --git a/projects/dex-ui/src/wells/wellLoader.ts b/projects/dex-ui/src/wells/wellLoader.ts
index ca21c28677..473c3725dd 100644
--- a/projects/dex-ui/src/wells/wellLoader.ts
+++ b/projects/dex-ui/src/wells/wellLoader.ts
@@ -8,7 +8,10 @@ import { GetWellAddressesDocument } from "src/generated/graph/graphql";
type WellAddresses = string[];
-const WELL_BLACKLIST = ["0x875b1da8dcba757398db2bc35043a72b4b62195d"];
+const WELL_BLACKLIST = [
+ "0x875b1da8dcba757398db2bc35043a72b4b62195d",
+ "0xBea0061680A2DEeBFA59076d77e0b6c769660595"
+];
const loadFromChain = async (sdk: BeanstalkSDK): Promise => {
const aquifer = new Aquifer(sdk.wells, Settings.AQUIFER_ADDRESS);
diff --git a/projects/examples/src/silo/convert.ts b/projects/examples/src/silo/convert.ts
index bf4c510163..02e5371e8f 100644
--- a/projects/examples/src/silo/convert.ts
+++ b/projects/examples/src/silo/convert.ts
@@ -8,7 +8,7 @@ main().catch((e) => {
console.log(e);
});
-let sdk:BeanstalkSDK;
+let sdk: BeanstalkSDK;
async function main() {
const account = process.argv[3] || _account;
@@ -16,14 +16,20 @@ async function main() {
let { sdk: _sdk, stop } = await impersonate(account);
sdk = _sdk;
sdk.DEBUG = false;
+ await sdk.refresh();
+ // const fromToken = sdk.tokens.UNRIPE_BEAN_WETH;
+ // const toToken = sdk.tokens.BEAN_ETH_WELL_LP;
+ const fromToken = sdk.tokens.UNRIPE_BEAN;
+ const toToken = sdk.tokens.BEAN;
-
- const fromToken = sdk.tokens.BEAN
- const toToken = sdk.tokens.UNRIPE_BEAN
- const amount = fromToken.amount(2500)
+ const maxConvert = await sdk.contracts.beanstalk.getMaxAmountIn(fromToken.address, toToken.address);
- let tx = await sdk.silo.convert(fromToken, toToken, amount)
+ const amount = fromToken.amount(1000);
+ const quote = await sdk.contracts.beanstalk.getAmountOut(fromToken.address, toToken.address, amount.toBlockchain());
+ console.log(quote.toString());
+
+ let tx = await sdk.silo.convert(fromToken, toToken, amount);
await tx.wait();
-
+
await stop();
}
diff --git a/projects/sdk-wells/src/lib/Aquifer.ts b/projects/sdk-wells/src/lib/Aquifer.ts
index a3adc0d873..e2743fd7be 100644
--- a/projects/sdk-wells/src/lib/Aquifer.ts
+++ b/projects/sdk-wells/src/lib/Aquifer.ts
@@ -1,10 +1,17 @@
import { Aquifer as AquiferContract, Aquifer__factory } from "src/constants/generated";
-import { encodeWellImmutableData, encodeWellInitFunctionCall, setReadOnly } from "./utils";
+import {
+ encodeWellImmutableData,
+ encodeWellInitFunctionCall,
+ getBytesHexString,
+ makeCallObject,
+ setReadOnly,
+ validateAddress,
+ validateHasMinTokensForWell
+} from "./utils";
import { WellsSDK } from "./WellsSDK";
import { WellFunction } from "./WellFunction";
import { ERC20Token } from "@beanstalk/sdk-core";
import { Pump } from "./Pump";
-import { Call } from "src/types";
import { constants } from "ethers";
import { Well } from "./Well";
@@ -33,31 +40,17 @@ export class Aquifer {
* @param pumps
* @returns
*/
- async boreWell(wellAddress: string, tokens: ERC20Token[], wellFunction: WellFunction, pumps: Pump[]): Promise {
- if (tokens.length < 2) {
- throw new Error("Well must have at least 2 tokens");
- }
-
- const tokensAddresses = tokens.map((t) => t.address);
-
- const wellFunctionCall = {
- target: wellFunction.address,
- data: new Uint8Array()
- } as Call;
-
- const pumpCalls = pumps.map(
- (p) =>
- ({
- target: p.address,
- data: new Uint8Array()
- } as Call)
- );
+ async boreWell(wellAddress: string, tokens: ERC20Token[], wellFunction: WellFunction, pumps: Pump[], _symbol?: string, _name?: string, salt?: number): Promise {
+ validateHasMinTokensForWell(tokens);
+ validateSalt(salt);
// Prepare Data
- const immutableData = encodeWellImmutableData(this.address, tokensAddresses, wellFunctionCall, pumpCalls);
- const { name, symbol } = await getNameAndSymbol(wellFunction, tokens);
- const initFunctionCall = await encodeWellInitFunctionCall(name, symbol);
- const saltBytes32 = constants.HashZero;
+ const immutableData = Aquifer.getEncodedWellImmutableData(this.address, tokens, wellFunction, pumps);
+ const { name, symbol } = await getNameAndSymbol(wellFunction, tokens, _name, _symbol);
+ const initFunctionCall = await Aquifer.getEncodedWellInitFunctionData(name, symbol);
+
+ // Default salt to 0. salt gt 0 is required for deterministic address
+ const saltBytes32 = salt ? getBytesHexString(salt, 32) : constants.HashZero;
// Bore It
const deployedWell = await this.contract.boreWell(wellAddress, immutableData, initFunctionCall, saltBytes32);
@@ -73,6 +66,65 @@ export class Aquifer {
return new Well(this.sdk, boredWellAddress);
}
+ async predictWellAddress(implementation: string, tokens: ERC20Token[], wellFunction: WellFunction, pumps: Pump[], salt?: number) {
+ validateHasMinTokensForWell(tokens);
+ validateSalt(salt);
+
+ const immutableData = Aquifer.getEncodedWellImmutableData(this.address, tokens, wellFunction, pumps);
+ const saltBytes32 = salt ? getBytesHexString(salt, 32) : constants.HashZero;
+
+ return this.contract.predictWellAddress(implementation, immutableData, saltBytes32);
+ }
+
+ // Static Methods
+
+ /**
+ * returns pack encoded data (immutableData) to deploy a well via aquifer.boreWell & predict a deterministic well address via aquifer.predictWellAddress
+ * @param aquifer
+ * @param wellImplementation
+ * @param tokens
+ * @param wellFunction
+ * @param pumps
+ * @returns
+ */
+ static getEncodedWellImmutableData(aquifer: string, tokens: ERC20Token[], wellFunction: WellFunction, pumps: Pump[]) {
+ validateAddress(wellFunction.address, wellFunction.address);
+
+ if (tokens.length < 2) {
+ throw new Error("Well must have at least 2 tokens");
+ }
+
+ const pumpCalls = pumps.map((p) => {
+ validateAddress(p.address, p.address);
+ return makeCallObject(p);
+ });
+ const wellFunctionCall = makeCallObject(wellFunction);
+ const tokensAddresses = tokens.map((t) => t.address);
+
+ return encodeWellImmutableData(aquifer, tokensAddresses, wellFunctionCall, pumpCalls);
+ }
+
+ /**
+ * Returns pack encoded data (initFunctionCall) to deploy a well via aquifer.boreWell
+ * @param name
+ * @param symbol
+ * @returns
+ */
+ static async getEncodedWellInitFunctionData(name: string, symbol: string) {
+ if (!name) {
+ throw new Error("Name must be provided");
+ }
+ if (!symbol) {
+ throw new Error("Symbol must be provided");
+ }
+ return encodeWellInitFunctionCall(name, symbol);
+ }
+
+ /**
+ * Deploy a new instance of Aquifer
+ * @param sdk
+ * @returns
+ */
static async BuildAquifer(sdk: WellsSDK): Promise {
const aquiferContract = new Aquifer__factory(sdk.signer);
const deployedAquifer = await aquiferContract.deploy();
@@ -80,14 +132,33 @@ export class Aquifer {
}
}
-async function getNameAndSymbol(wellFunction: WellFunction, tokens: ERC20Token[]) {
- // TODO: make this a multicall
- const fnName = await wellFunction.getName();
- const fnSymbol = await wellFunction.getSymbol();
+function validateSalt(salt?: number) {
+ if (!salt) return;
+ if (!Number.isInteger(salt)) {
+ throw new Error("Salt must be an integer");
+ }
+ if (salt < 0) {
+ throw new Error("Salt must be greater than 0");
+ }
+}
+
+async function getNameAndSymbol(wellFunction: WellFunction, tokens: ERC20Token[], _name?: string, _symbol?: string) {
+ let name = _name ?? "";
+ let symbol = _symbol ?? "";
const symbols = tokens.map((t) => t.symbol);
- const name = symbols.join(":") + " " + fnName + " Well";
- const symbol = symbols.join("") + fnSymbol + "w";
+
+ // TODO: make this a multicall
+
+ if (!name) {
+ const fnName = await wellFunction.getName();
+ name = symbols.join(":") + " " + fnName + " Well";
+ }
+
+ if (!symbol) {
+ const fnSymbol = await wellFunction.getSymbol();
+ symbol = symbols.join("") + fnSymbol + "w";
+ }
return { name, symbol };
}
diff --git a/projects/sdk-wells/src/lib/Pump.ts b/projects/sdk-wells/src/lib/Pump.ts
index 6a674d0eda..1a54df3286 100644
--- a/projects/sdk-wells/src/lib/Pump.ts
+++ b/projects/sdk-wells/src/lib/Pump.ts
@@ -1,13 +1,14 @@
import { MultiFlowPump__factory, MockPump__factory } from "src/constants/generated";
import { WellsSDK } from "./WellsSDK";
import { setReadOnly } from "./utils";
+import { BigNumber } from "ethers";
export class Pump {
readonly sdk: WellsSDK;
readonly contract: MultiFlowPump__factory;
readonly address: string;
- constructor(sdk: WellsSDK, address: string) {
+ constructor(sdk: WellsSDK, address: string, public readonly data: string) {
this.address = address;
setReadOnly(this, "sdk", sdk, false);
const contract = MultiFlowPump__factory.connect(address, sdk.providerOrSigner);
@@ -18,19 +19,20 @@ export class Pump {
const mockPumpContract = new MockPump__factory(sdk.signer);
const deployedMockPump = await mockPumpContract.deploy();
- return new Pump(sdk, deployedMockPump.address);
+ return new Pump(sdk, deployedMockPump.address, "0x");
}
static async BuildMultiFlowPump(sdk: WellsSDK): Promise {
const contract = new MultiFlowPump__factory(sdk.signer);
- // TODO: these are dummy values, this method isn't used yet.
// these will need to be passed in as params and converted to bytelike or whatever
- const inc = "0x";
- const dec = "0x";
- const cap = "0x";
- const alpha = "0x";
- const deployedMockPump = await contract.deploy(inc, dec, cap, alpha);
- return new Pump(sdk, deployedMockPump.address);
+ const maxPctIncrease = "0x3ff50624dd2f1a9fbe76c8b439581062"; // 0.001
+ const maxPctDecrease = "0x3ff505e1d27a3ee9bffd7f3dd1a32671"; // 1 - 1 / (1 + .001)
+ const capInterval = BigNumber.from(12).toHexString(); // 12 seconds
+ const alpha = "0x3ffeef368eb04325c526c2246eec3e55"; // 0.967213114754098360 = 1 - 2 / (1 + blocks) where blocks = 60
+
+ const deployedMockPump = await contract.deploy(maxPctIncrease, maxPctDecrease, capInterval, alpha);
+
+ return new Pump(sdk, deployedMockPump.address, "0x");
}
}
diff --git a/projects/sdk-wells/src/lib/Well.ts b/projects/sdk-wells/src/lib/Well.ts
index 077922c92a..fc4ec1d3b7 100644
--- a/projects/sdk-wells/src/lib/Well.ts
+++ b/projects/sdk-wells/src/lib/Well.ts
@@ -1,5 +1,5 @@
import { ERC20Token, Token, TokenValue } from "@beanstalk/sdk-core";
-import { BigNumber, CallOverrides, constants, ContractFactory, ContractTransaction, Overrides } from "ethers";
+import { BigNumber, CallOverrides, ContractTransaction, Overrides } from "ethers";
import { Well__factory } from "src/constants/generated";
import { Well as WellContract } from "src/constants/generated";
@@ -7,8 +7,6 @@ import { Aquifer } from "./Aquifer";
import { Pump } from "./Pump";
import {
deadlineSecondsToBlockchain,
- encodeWellImmutableData,
- encodeWellInitFunctionCall,
loadToken,
setReadOnly,
validateAddress,
@@ -19,7 +17,6 @@ import {
} from "./utils";
import { WellFunction } from "./WellFunction";
import { WellsSDK } from "./WellsSDK";
-import { Call } from "src/types";
export type WellDetails = {
tokens: ERC20Token[];
@@ -222,7 +219,7 @@ export class Well {
}
private setPumps(pumpData: CallStruct[]) {
- let pumps = (pumpData ?? []).map((p) => new Pump(this.sdk, p.target));
+ let pumps = (pumpData ?? []).map((p, i) => new Pump(this.sdk, p.target, pumpData[i].data));
Object.freeze(pumps);
setReadOnly(this, "pumps", pumps, true);
}
@@ -859,7 +856,7 @@ export class Well {
validateToken(toToken, "toToken");
validateAmount(minAmountOut, "minAmountOut");
validateAddress(recipient, "recipient");
-
+
return this.contract.shift(toToken.address, minAmountOut.toBigNumber(), recipient, overrides ?? {});
}
diff --git a/projects/sdk-wells/src/lib/WellFunction.ts b/projects/sdk-wells/src/lib/WellFunction.ts
index 0f2c6a2e2c..ba74908693 100644
--- a/projects/sdk-wells/src/lib/WellFunction.ts
+++ b/projects/sdk-wells/src/lib/WellFunction.ts
@@ -1,32 +1,54 @@
-import { ConstantProduct__factory, ConstantProduct2__factory, IWellFunction, IWellFunction__factory } from "src/constants/generated";
+import {
+ ConstantProduct__factory,
+ ConstantProduct2__factory,
+ IWellFunction,
+ IWellFunction__factory
+} from "src/constants/generated";
import { WellsSDK } from "./WellsSDK";
+import { setReadOnly } from "./utils";
export class WellFunction {
contract: IWellFunction;
+ name: string | undefined;
+ symbol: string | undefined;
- constructor(public readonly sdk: WellsSDK, public readonly address: string, public readonly data: string) {
+ constructor(
+ public readonly sdk: WellsSDK,
+ public readonly address: string,
+ public readonly data: string
+ ) {
this.sdk = sdk;
this.contract = IWellFunction__factory.connect(address, sdk.providerOrSigner);
}
// TODO: provide these as multicalls
async getName(): Promise {
- return this.contract.name();
+ if (!this.name) {
+ this.name = await this.contract.name();
+ setReadOnly(this, "name", this.name, true);
+ }
+ return this.name;
}
async getSymbol(): Promise {
- return this.contract.symbol();
+ if (!this.symbol) {
+ this.symbol = await this.contract.symbol();
+ setReadOnly(this, "symbol", this.symbol, true);
+ }
+ return this.symbol;
}
static async BuildConstantProduct(sdk: WellsSDK): Promise {
- const constantProductConstract = new ConstantProduct__factory(sdk.signer);
- const deployedWellFunction = await constantProductConstract.deploy();
+ const constantProductContract = new ConstantProduct__factory(sdk.signer);
+ const deployedWellFunction = await constantProductContract.deploy();
return new WellFunction(sdk, deployedWellFunction.address, "0x");
}
static async BuildConstantProduct2(sdk: WellsSDK): Promise {
- const constantProduct2Constract = new ConstantProduct2__factory(sdk.signer);
- const deployedWellFunction = await constantProduct2Constract.deploy();
+ const constantProduct2Contract = new ConstantProduct2__factory(sdk.signer);
+ const deployedWellFunction = await constantProduct2Contract.deploy();
return new WellFunction(sdk, deployedWellFunction.address, "0x");
}
+
+ static abi = IWellFunction__factory.abi;
}
diff --git a/projects/sdk-wells/src/lib/utils.ts b/projects/sdk-wells/src/lib/utils.ts
index f1b590c7b7..aea7bfe64b 100644
--- a/projects/sdk-wells/src/lib/utils.ts
+++ b/projects/sdk-wells/src/lib/utils.ts
@@ -2,6 +2,7 @@ import { ERC20Token, Token, TokenValue } from "@beanstalk/sdk-core";
import { ethers } from "ethers";
import { WellsSDK } from "./WellsSDK";
import { Call } from "src/types";
+import { WellFunction } from "./WellFunction";
export const loadToken = async (sdk: WellsSDK, address: string): Promise => {
// First see this is a built in token provided by the SDK
@@ -24,6 +25,12 @@ export const validateToken = (token: Token, name: string) => {
validateAddress(token.address, name);
};
+export const validateHasMinTokensForWell = (tokens: ERC20Token[]) => {
+ if (tokens.length < 2) {
+ throw new Error("Well must have at least 2 tokens");
+ }
+}
+
export const validateAmount = (value: TokenValue, name: string) => {
if (!(value instanceof TokenValue)) {
throw new Error(`${name} is not an instance of TokenValue`);
@@ -111,3 +118,18 @@ export async function encodeWellInitFunctionCall(name: string, symbol: string):
const initFunctionCall = wellInitInterface.encodeFunctionData("init", [name, symbol]);
return ethers.utils.arrayify(initFunctionCall);
}
+
+export function getBytesHexString(value: string | number, padding?: number) {
+ const bigNumber = ethers.BigNumber.from(value.toString());
+ const hexStr = bigNumber.toHexString();
+ if (!padding) return hexStr;
+
+ return ethers.utils.hexZeroPad(bigNumber.toHexString(), padding);
+}
+
+export function makeCallObject(params: T) {
+ return {
+ target: params.address,
+ data: ethers.utils.arrayify(params.data)
+ } satisfies Call;
+}
diff --git a/projects/sdk/src/classes/Token/Token.ts b/projects/sdk/src/classes/Token/Token.ts
index a33d1cd990..ebba0e4dc5 100644
--- a/projects/sdk/src/classes/Token/Token.ts
+++ b/projects/sdk/src/classes/Token/Token.ts
@@ -9,7 +9,7 @@ declare module "@beanstalk/sdk-core" {
abstract class Token {
static _source: string;
isUnripe: boolean;
- rewards?: { stalk: TokenValue; seeds: TokenValue };
+ rewards?: { stalk: TokenValue; seeds: TokenValue | null };
getStalk(bdv?: TokenValue): TokenValue;
getSeeds(bdv?: TokenValue): TokenValue;
approveBeanstalk(amount: TokenValue | BigNumber): Promise;
@@ -34,7 +34,9 @@ CoreToken.prototype.getStalk = function (bdv?: TokenValue): TokenValue {
* Get the amount of Seeds rewarded per deposited BDV of this Token.
* */
CoreToken.prototype.getSeeds = function (bdv?: TokenValue): TokenValue {
- if (!this.rewards?.seeds) return TokenValue.fromHuman(0, SEED_DECIMALS);
+ if (this.rewards?.seeds === undefined || this.rewards.seeds === null) {
+ throw new Error(`Token ${this.symbol} has no seeds defined!`);
+ }
if (!bdv) return this.rewards.seeds;
return this.rewards.seeds.mul(bdv);
diff --git a/projects/sdk/src/defaultSettings.json b/projects/sdk/src/defaultSettings.json
index 8b9174003c..15dd8ff3aa 100644
--- a/projects/sdk/src/defaultSettings.json
+++ b/projects/sdk/src/defaultSettings.json
@@ -1,3 +1,3 @@
{
- "subgraphUrl": "https://graph.node.bean.money/subgraphs/name/beanstalk-2-1-0"
+ "subgraphUrl": "https://graph.node.bean.money/subgraphs/name/beanstalk-dev"
}
diff --git a/projects/sdk/src/lib/BeanstalkSDK.ts b/projects/sdk/src/lib/BeanstalkSDK.ts
index e320d67e68..ccab9c1074 100644
--- a/projects/sdk/src/lib/BeanstalkSDK.ts
+++ b/projects/sdk/src/lib/BeanstalkSDK.ts
@@ -46,6 +46,7 @@ export class BeanstalkSDK {
public providerOrSigner: Signer | Provider;
public source: DataSource;
public subgraphUrl: string;
+ public lastRefreshTimestamp: number;
public readonly chainId: ChainId;
public readonly addresses: typeof addresses;
@@ -99,6 +100,19 @@ export class BeanstalkSDK {
this.wells = new WellsSDK(config);
}
+ /**
+ * Refreshes the SDK's state with updated data from contracts. This should be called immediately after sdk initialization and after every season
+ */
+ async refresh() {
+ // Reload dynamic stalk per wl token
+ const whitelist = this.tokens.siloWhitelist;
+ for await (const token of whitelist) {
+ const { stalkEarnedPerSeason } = await this.contracts.beanstalk.tokenSettings(token.address);
+ token.rewards!.seeds = this.tokens.SEEDS.fromBlockchain(stalkEarnedPerSeason);
+ }
+ this.lastRefreshTimestamp = Date.now();
+ }
+
debug(...args: any[]) {
if (!this.DEBUG) return;
console.debug(...args);
@@ -167,6 +181,7 @@ export class BeanstalkSDK {
toJSON() {
return {
chainId: this.chainId,
+ lastRefreshTimestamp: this.lastRefreshTimestamp,
provider: {
url: this.provider?.connection?.url,
network: this.provider?._network
diff --git a/projects/sdk/src/lib/events/processor.test.ts b/projects/sdk/src/lib/events/processor.test.ts
index e4e530c13e..7390b20bb0 100644
--- a/projects/sdk/src/lib/events/processor.test.ts
+++ b/projects/sdk/src/lib/events/processor.test.ts
@@ -32,11 +32,14 @@ const account = "0xFARMER";
* and not the indices; this is more for consistency.
*/
const propArray = (o: { [key: string]: any }) =>
- Object.keys(o).reduce((prev, key) => {
- prev[prev.length] = o[key];
- prev[key] = o[key];
- return prev;
- }, [] as (keyof typeof o)[] & typeof o);
+ Object.keys(o).reduce(
+ (prev, key) => {
+ prev[prev.length] = o[key];
+ prev[key] = o[key];
+ return prev;
+ },
+ [] as (keyof typeof o)[] & typeof o
+ );
const mockProcessor = () => new EventProcessor(sdk, account);
@@ -205,8 +208,8 @@ describe("the Silo", () => {
bdv: bdv1
})
} as AddDepositEvent);
-
- expect(p.deposits.get(Bean)?.["6074"]).toStrictEqual({
+ const t = p.deposits.get(Bean);
+ expect(p.deposits.get(Bean)?.["6074000000"]).toStrictEqual({
amount: amount1,
bdv: bdv1
});
@@ -225,7 +228,7 @@ describe("the Silo", () => {
})
} as AddDepositEvent);
- expect(p.deposits.get(Bean)?.["6074"]).toStrictEqual({
+ expect(p.deposits.get(Bean)?.["6074000000"]).toStrictEqual({
amount: amount1.add(amount2),
bdv: bdv1.add(bdv2)
});
@@ -244,7 +247,7 @@ describe("the Silo", () => {
})
} as AddDepositEvent);
- expect(p.deposits.get(BeanCrv3)?.["6100"]).toStrictEqual({
+ expect(p.deposits.get(BeanCrv3)?.["6100000000"]).toStrictEqual({
amount: amount3,
bdv: bdv3
});
@@ -281,7 +284,7 @@ describe("the Silo", () => {
})
} as RemoveDepositEvent);
- expect(p.deposits.get(Bean)?.["6074"]).toStrictEqual({
+ expect(p.deposits.get(Bean)?.["6074000000"]).toStrictEqual({
amount: amount1.sub(amount2),
bdv: bdv1.sub(bdv2)
});
@@ -300,7 +303,7 @@ describe("the Silo", () => {
})
} as RemoveDepositEvent);
- expect(p.deposits.get(Bean)?.["6074"]).toBeUndefined();
+ expect(p.deposits.get(Bean)?.["6074000000"]).toBeUndefined();
});
});
diff --git a/projects/sdk/src/lib/events/processor.ts b/projects/sdk/src/lib/events/processor.ts
index 8a6f82364c..e4f66261fe 100644
--- a/projects/sdk/src/lib/events/processor.ts
+++ b/projects/sdk/src/lib/events/processor.ts
@@ -11,6 +11,7 @@ import {
import { BeanstalkSDK } from "../BeanstalkSDK";
import { EventManager } from "src/lib/events/EventManager";
import { ZERO_BN } from "src/constants";
+import { TokenValue } from "@beanstalk/sdk-core";
// ----------------------------------------
@@ -394,7 +395,7 @@ export class EventProcessor {
AddDeposit(event: EventManager.Simplify) {
const token = this.getToken(event);
- const stem = event.args.stem.toString();
+ const stem = this.migrateStem(event.args.stem);
if (!this.whitelist.has(token)) throw new Error(`Attempted to process an event with an unknown token: ${token}`);
@@ -407,14 +408,22 @@ export class EventProcessor {
RemoveDeposit(event: EventManager.Simplify) {
const token = this.getToken(event);
- const stem = event.args.stem.toString();
+ const stem = this.migrateStem(event.args.stem);
this._removeDeposit(stem, token, event.args.amount);
}
RemoveDeposits(event: EventManager.Simplify) {
const token = this.getToken(event);
event.args.stems.forEach((stem, index) => {
- this._removeDeposit(stem.toString(), token, event.args.amounts[index]);
+ this._removeDeposit(this.migrateStem(stem), token, event.args.amounts[index]);
});
}
+
+ migrateStem(stem: ethers.BigNumber): string {
+ let stemTV = TokenValue.fromBlockchain(stem, 0);
+ if (stemTV.abs().lt(10 ** 6)) stemTV = stemTV.mul(10 ** 6);
+ const migratedStem = stemTV.toHuman();
+
+ return migratedStem;
+ }
}
diff --git a/projects/sdk/src/lib/farm/actions/Convert.ts b/projects/sdk/src/lib/farm/actions/Convert.ts
new file mode 100644
index 0000000000..3fb47effea
--- /dev/null
+++ b/projects/sdk/src/lib/farm/actions/Convert.ts
@@ -0,0 +1,58 @@
+import { BasicPreparedResult, RunContext, Step, StepClass } from "src/classes/Workflow";
+import { ethers } from "ethers";
+import { Token } from "src/classes/Token";
+import { Deposit } from "src/lib/silo/types";
+import { TokenValue } from "@beanstalk/sdk-core";
+
+export class Convert extends StepClass {
+ public name: string = "convert";
+
+ constructor(
+ private _tokenIn: Token,
+ private _tokenOut: Token,
+ private _amountIn: TokenValue,
+ private _minAmountOut: TokenValue,
+ private _deposits: Deposit[],
+
+ ) {
+ super();
+ }
+
+ async run(_amountInStep: ethers.BigNumber, context: RunContext) {
+
+ const siloConvert = Convert.sdk.silo.siloConvert;
+
+ const amountIn = this._amountIn;
+ const minAmountOut = this._minAmountOut;
+ const deposits = this._deposits;
+
+ return {
+ name: this.name,
+ amountOut: _amountInStep,
+ prepare: () => {
+ Convert.sdk.debug(`[${this.name}.encode()]`, {
+ tokenIn: this._tokenIn,
+ tokenOut: this._tokenOut,
+ amountIn: amountIn,
+ minAmountOut: minAmountOut,
+ deposits: this._deposits,
+ });
+ return {
+ target: Convert.sdk.contracts.beanstalk.address,
+ callData: Convert.sdk.contracts.beanstalk.interface.encodeFunctionData("convert", [
+ siloConvert.calculateEncoding(
+ this._tokenIn,
+ this._tokenOut,
+ amountIn,
+ minAmountOut
+ ),
+ deposits.map((c) => c.stem.toString()),
+ deposits.map((c) => c.amount.abs().toBlockchain())
+ ])
+ };
+ },
+ decode: (data: string) => Convert.sdk.contracts.beanstalk.interface.decodeFunctionData("convert", data),
+ decodeResult: (result: string) => Convert.sdk.contracts.beanstalk.interface.decodeFunctionResult("convert", result)
+ };
+ }
+}
diff --git a/projects/sdk/src/lib/farm/actions/Mow.ts b/projects/sdk/src/lib/farm/actions/Mow.ts
new file mode 100644
index 0000000000..3c007cdb20
--- /dev/null
+++ b/projects/sdk/src/lib/farm/actions/Mow.ts
@@ -0,0 +1,62 @@
+import { BasicPreparedResult, RunContext, StepClass } from "src/classes/Workflow";
+import { ethers } from "ethers";
+import { Token } from "src/classes/Token";
+import { TokenValue } from "@beanstalk/sdk-core";
+
+export class Mow extends StepClass {
+ public name: string = "mow";
+
+ constructor(
+ private _account: string,
+ private _tokensToMow: Map
+ ) {
+ super();
+ }
+
+ async run(_amountInStep: ethers.BigNumber, context: RunContext) {
+
+ const tokensToMow: string[] = [];
+
+ this._tokensToMow.forEach((grown, token) => {
+ if (grown.gt(0)) {
+ tokensToMow.push(token.address);
+ }
+ });
+
+ if (tokensToMow.length === 1) {
+ return {
+ name: 'mow',
+ amountOut: _amountInStep,
+ prepare: () => {
+ Mow.sdk.debug(`[${this.name}.encode()]`);
+ return {
+ target: Mow.sdk.contracts.beanstalk.address,
+ callData: Mow.sdk.contracts.beanstalk.interface.encodeFunctionData("mow", [
+ this._account,
+ tokensToMow[0]
+ ])
+ };
+ },
+ decode: (data: string) => Mow.sdk.contracts.beanstalk.interface.decodeFunctionData("mow", data),
+ decodeResult: (result: string) => Mow.sdk.contracts.beanstalk.interface.decodeFunctionResult("mow", result)
+ };
+ } else {
+ return {
+ name: 'mowMultiple',
+ amountOut: _amountInStep,
+ prepare: () => {
+ Mow.sdk.debug(`[${this.name}.encode()]`);
+ return {
+ target: Mow.sdk.contracts.beanstalk.address,
+ callData: Mow.sdk.contracts.beanstalk.interface.encodeFunctionData("mowMultiple", [
+ this._account,
+ tokensToMow
+ ])
+ };
+ },
+ decode: (data: string) => Mow.sdk.contracts.beanstalk.interface.decodeFunctionData("mowMultiple", data),
+ decodeResult: (result: string) => Mow.sdk.contracts.beanstalk.interface.decodeFunctionResult("mowMultiple", result)
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/sdk/src/lib/farm/actions/Plant.ts b/projects/sdk/src/lib/farm/actions/Plant.ts
new file mode 100644
index 0000000000..518d0a0b5a
--- /dev/null
+++ b/projects/sdk/src/lib/farm/actions/Plant.ts
@@ -0,0 +1,27 @@
+import { BasicPreparedResult, RunContext, Step, StepClass } from "src/classes/Workflow";
+import { ethers } from "ethers";
+
+export class Plant extends StepClass {
+ public name: string = "plant";
+
+ constructor(
+ ) {
+ super();
+ }
+
+ async run(_amountInStep: ethers.BigNumber, context: RunContext) {
+ return {
+ name: this.name,
+ amountOut: _amountInStep,
+ prepare: () => {
+ Plant.sdk.debug(`[${this.name}.encode()]`);
+ return {
+ target: Plant.sdk.contracts.beanstalk.address,
+ callData: Plant.sdk.contracts.beanstalk.interface.encodeFunctionData("plant", undefined)
+ };
+ },
+ decode: (data: string) => Plant.sdk.contracts.beanstalk.interface.decodeFunctionData("plant", data),
+ decodeResult: (result: string) => Plant.sdk.contracts.beanstalk.interface.decodeFunctionResult("plant", result)
+ };
+ }
+}
diff --git a/projects/sdk/src/lib/farm/actions/index.ts b/projects/sdk/src/lib/farm/actions/index.ts
index 2e4879a90d..58a6f32ebe 100644
--- a/projects/sdk/src/lib/farm/actions/index.ts
+++ b/projects/sdk/src/lib/farm/actions/index.ts
@@ -4,6 +4,9 @@ import { WrapEth } from "./WrapEth";
import { UnwrapEth } from "./UnwrapEth";
import { TransferToken } from "./TransferToken";
import { Deposit } from "./Deposit";
+import { Convert } from "./Convert";
+import { Plant } from "./Plant";
+import { Mow } from "./Mow";
import { WithdrawDeposits } from "./WithdrawDeposits";
import { WithdrawDeposit } from "./WithdrawDeposit";
import { ClaimWithdrawals } from "./ClaimWithdrawals";
@@ -34,6 +37,9 @@ export {
// Beanstalk: Silo
Deposit,
+ Convert,
+ Plant,
+ Mow,
WithdrawDeposits,
WithdrawDeposit,
ClaimWithdrawals,
diff --git a/projects/sdk/src/lib/root.test.ts b/projects/sdk/src/lib/root.test.ts
deleted file mode 100644
index d13780acd8..0000000000
--- a/projects/sdk/src/lib/root.test.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { getTestUtils } from "src/utils/TestUtils/provider";
-
-const { sdk, account, utils } = getTestUtils();
-
-describe("mint function", () => {
- it("uses the right function", async () => {
- expect(true).toBe(true);
- // const typedData = await sdk.root.permit(
- // [sdk.tokens.BEAN],
- // [new BigNumber(1000)],
- // );
- // const permit = await sdk.permit.sign(
- // account,
- // typedData,
- // )
- });
-});
-
-describe("estimateRoots", () => {
- it("test", async () => {
- const estimate = await sdk.root.estimateRoots(sdk.tokens.BEAN, [utils.mockDepositCrate(sdk.tokens.BEAN, 6000, "1000")], true);
-
- expect(estimate.amount.gt(0)).toBe(true);
- });
-});
diff --git a/projects/sdk/src/lib/silo.ts b/projects/sdk/src/lib/silo.ts
index 9b51ffc323..7a01d6fa49 100644
--- a/projects/sdk/src/lib/silo.ts
+++ b/projects/sdk/src/lib/silo.ts
@@ -187,10 +187,11 @@ export class Silo {
options?: { source: DataSource.LEDGER } | { source: DataSource.SUBGRAPH }
): Promise {
const source = Silo.sdk.deriveConfig("source", options);
- const [account, currentSeason, stemTip] = await Promise.all([
+ const [account, currentSeason, stemTip, germinatingStem] = await Promise.all([
Silo.sdk.getAccount(_account),
Silo.sdk.sun.getSeason(),
- this.getStemTip(_token)
+ this.getStemTip(_token),
+ Silo.sdk.contracts.beanstalk.getGerminatingStem(_token.address)
]);
if (!Silo.sdk.tokens.siloWhitelist.has(_token)) throw new Error(`${_token.address} is not whitelisted in the Silo`);
@@ -212,7 +213,8 @@ export class Silo {
utils.applyDeposit(balance, _token, stemTip, {
stem,
amount: deposits[stem].amount,
- bdv: deposits[stem].bdv
+ bdv: deposits[stem].bdv,
+ germinatingStem
});
}
@@ -237,7 +239,8 @@ export class Silo {
utils.applyDeposit(balance, _token, stemTip, {
stem: deposit.season, // FIXME
amount: deposit.amount,
- bdv: deposit.bdv
+ bdv: deposit.bdv,
+ germinatingStem
})
);
@@ -262,17 +265,25 @@ export class Silo {
_account?: string,
options?: { source: DataSource.LEDGER } | { source: DataSource.SUBGRAPH }
): Promise