Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

If user attempts to withdraw more than liquid reserves direct them to our discord #1243

Merged
merged 4 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 66 additions & 14 deletions src/components/_forms/WithdrawForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Stack,
Avatar,
Text,
Link
} from "@chakra-ui/react"
import { useForm } from "react-hook-form"
import { BaseButton } from "components/_buttons/BaseButton"
Expand Down Expand Up @@ -40,6 +41,9 @@ import {
import { useUserStrategyData } from "data/hooks/useUserStrategyData"
import { useStrategyData } from "data/hooks/useStrategyData"
import { useDepositModalStore } from "data/hooks/useDepositModalStore"
import { fetchCellarRedeemableReserves } from "queries/get-cellar-redeemable-asssets"
import { fetchCellarPreviewRedeem } from "queries/get-cellar-preview-redeem"

interface FormValues {
withdrawAmount: number
}
Expand Down Expand Up @@ -172,31 +176,79 @@ export const WithdrawForm: VFC<WithdrawFormProps> = ({ onClose }) => {
})
} catch (e) {
const error = e as Error
if (error.message === "GAS_LIMIT_ERROR") {

// Get Redeemable Assets
const redeemableAssets: number = parseInt(await fetchCellarRedeemableReserves(id))

// previewRedeem on the shares the user is attempting to withdraw
// Only get previewRedeem on 1 share to optimize caching and do relevant math below
const previewRedeem: number = parseInt(await fetchCellarPreviewRedeem(id, BigInt(1e18)))
const redeemAmt: number = Math.floor(previewRedeem * watchWithdrawAmount)
const redeemingMoreThanAvailible = redeemAmt > redeemableAssets

/*
console.log("---")
console.log("Reedemable assets: ", redeemableAssets)
console.log("Withdraw amount: ", watchWithdrawAmount)
console.log("Preview redeem: ", previewRedeem)
console.log("Redeeming amt: ", redeemAmt)
console.log("Redeeming more than availible: ", redeemingMoreThanAvailible)
console.log("---")
*/

// Check if attempting to withdraw more than availible
if (redeemingMoreThanAvailible) {
addToast({
heading: "Transaction not submitted",
heading: "Transaction not submitted.",
body: (
<Text>
The gas fees are particularly high right now. To avoid a
failed transaction leading to wasted gas, please try
again later.
You are attempting to withdraw beyond the the liquid
reserve. The strategist will need to initiate a
rebalance to service your full withdrawal. Please send a
message in our{" "}
{
<Link
href="https://discord.com/channels/814266181267619840/814279703622844426"
isExternal
textDecoration="underline"
>
Discord Support channel
</Link>
}{" "}
tagging @StrategistSupport
</Text>
),
status: "info",
closeHandler: closeAll,
duration: null, // Persist this toast until user closes it.
})
} else {
addToast({
heading: "Withdraw",
body: <Text>Withdraw Cancelled</Text>,
status: "error",
closeHandler: closeAll,
})
if (error.message === "GAS_LIMIT_ERROR") {
addToast({
heading: "Transaction not submitted",
body: (
<Text>
The gas fees are particularly high right now. To avoid
a failed transaction leading to wasted gas, please try
again later.
</Text>
),
status: "info",
closeHandler: closeAll,
})
} else {
addToast({
heading: "Withdraw",
body: <Text>Withdraw Cancelled</Text>,
status: "error",
closeHandler: closeAll,
})
}

refetch()
setValue("withdrawAmount", 0)
}
}

refetch()
setValue("withdrawAmount", 0)
}

function fixed(num: number, fixed: number) {
Expand Down
45 changes: 45 additions & 0 deletions src/context/rpc_context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Contract, providers } from 'ethers';

const ALCHEMY_API_KEY = process.env.NEXT_PUBLIC_ALCHEMY_KEY
const INFURA_API_KEY = process.env.NEXT_PUBLIC_INFURA_API_KEY

// TODO: Generalzie for multichain at some point
const ALCHEMY_PROVIDER = new providers.JsonRpcProvider(`https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_API_KEY}`);
const INFURA_PROVIDER = new providers.JsonRpcProvider(`https://mainnet.infura.io/v3/${INFURA_API_KEY}`);

export async function getActiveProvider() {
// Try connecting to Alchemy first
try {
await ALCHEMY_PROVIDER.getBlockNumber();
console.log("Connected to Alchemy");
return ALCHEMY_PROVIDER;
} catch (error) {
console.warn("Failed to connect to Alchemy. Trying Infura...");
}

// If Alchemy fails, try connecting to Infura
try {
await INFURA_PROVIDER.getBlockNumber();
console.log("Connected to Infura");
return INFURA_PROVIDER;
} catch (error) {
console.error("Failed to connect to both Alchemy and Infura!");
return null;
}
}

export async function queryContract(
contractAddress: string,
abi: readonly {}[]
) {
const activeProvider = await getActiveProvider()

if (!activeProvider) {
console.error("No provider is available!")
return null
}

const contract = new Contract(contractAddress, abi, activeProvider)
return contract // Now you can run any queries on this contract instance
}

1 change: 1 addition & 0 deletions src/data/uiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export const isTokenPriceEnabledApp = (config: ConfigProps) => {
config.cellarNameKey === CellarNameKey.REAL_YIELD_SNX ||
config.cellarNameKey === CellarNameKey.REAL_YIELD_ENS ||
config.cellarNameKey === CellarNameKey.REAL_YIELD_1INCH ||
config.cellarNameKey === CellarNameKey.REAL_YIELD_LINK ||
config.cellarNameKey === CellarNameKey.AAVE
)
}
Expand Down
53 changes: 53 additions & 0 deletions src/pages/api/cellar-preview-redeem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NextApiRequest, NextApiResponse } from "next"
import { cellarDataMap } from "data/cellarDataMap"
import { queryContract } from "context/rpc_context"

const baseUrl =
process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"

const cellarPreviewRedeem = async (
req: NextApiRequest,
res: NextApiResponse
) => {
try {
let { cellarId, shares } = req.query
cellarId = cellarId as string
const sharesNum: BigInt = shares as unknown as BigInt

if (!cellarId || !sharesNum) {
res.status(400).send({
error: "missing cellar id or shares",
message: "missing cellar id or shares",
})
return
}

const cellar = await queryContract(
cellarDataMap[cellarId]?.config.id,
cellarDataMap[cellarId]?.config.cellar.abi
)

let shareValue: number = 0

if (cellar) {
shareValue = await cellar.previewRedeem(sharesNum)
} else {
throw new Error("failed to load contract")
}

res.setHeader(
"Cache-Control",
"public, maxage=60, s-maxage=60, stale-while-revalidate=120"
)
res.setHeader("Access-Control-Allow-Origin", baseUrl)
res.status(200).json({
sharesValue: shareValue.toString(), // Convert the result to string to ensure it can be serialized in JSON
})
} catch (error) {
res
.status(500)
.send({ error: "failed to fetch data", message: error })
}
}

export default cellarPreviewRedeem
53 changes: 53 additions & 0 deletions src/pages/api/cellar-redeemable-reserves.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NextApiRequest, NextApiResponse } from "next"
import { cellarDataMap } from "data/cellarDataMap"
import { queryContract } from "context/rpc_context"

const baseUrl =
process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"


const cellarRedeemableAssets = async (
req: NextApiRequest,
res: NextApiResponse
) => {
try {
let { cellarId } = req.query
cellarId = cellarId as string

if (!cellarId) {
res.status(400).send({
error: "missing cellar id",
message: "missing cellar id",
})
return
}

const cellar = await queryContract(
cellarDataMap[cellarId]?.config.id,
cellarDataMap[cellarId]?.config.cellar.abi
)

let totalAssets: string = "";

if (cellar) {
totalAssets = await cellar.totalAssetsWithdrawable()
} else {
throw new Error("failed to load contract")
}

res.setHeader(
"Cache-Control",
"public, maxage=60, s-maxage=60, stale-while-revalidate=120"
)
res.setHeader("Access-Control-Allow-Origin", baseUrl)
res.status(200).json({
totalAssetsWithdrawable: totalAssets.toString(), // Convert the result to string to ensure it can be serialized in JSON
})
} catch (error) {
res
.status(500)
.send({ error: "failed to fetch data", message: error })
}
}

export default cellarRedeemableAssets
19 changes: 19 additions & 0 deletions src/queries/get-cellar-preview-redeem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const getUrl = (cellarId: string, shares: BigInt) =>
`/api/cellar-preview-redeem?cellarId=${cellarId}&shares=${shares}`

export const fetchCellarPreviewRedeem = async (
cellarId: string,
shares: BigInt
) => {
const url = getUrl(cellarId, shares)

try {
const data = await fetch(url)
const result = await data.json()

return result ? result.sharesValue : undefined
} catch (error) {
console.log("Error fetching Cellar Preview Redeem", error)
throw Error(error as string)
}
}
16 changes: 16 additions & 0 deletions src/queries/get-cellar-redeemable-asssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const getUrl = (cellarId: string) =>
`/api/cellar-redeemable-reserves?cellarId=${cellarId}`

export const fetchCellarRedeemableReserves = async (cellarId: string) => {
const url = getUrl(cellarId)

try {
const data = await fetch(url)
const result = await data.json()

return result ? result.totalAssetsWithdrawable : undefined
} catch (error) {
console.log("Error fetching Cellar Redeemable Assets", error)
throw Error(error as string)
}
}