From 84f94fee5dcdc0f4a9622eb7b2a30d7e6593cf21 Mon Sep 17 00:00:00 2001 From: Daniel Simon Date: Mon, 10 Nov 2025 17:05:10 +0700 Subject: [PATCH] feat: ability to redeem BOLD --- contracts/addresses/1.json | 2 +- frontend/app/.env | 2 +- .../app/src/comps/AppLayout/BottomBar.tsx | 2 +- frontend/app/src/constants.ts | 4 + frontend/app/src/content.tsx | 4 +- frontend/app/src/liquity-utils.ts | 42 +- .../EarnPoolScreen/PanelClaimRewards.tsx | 2 +- .../screens/EarnPoolScreen/PanelCompound.tsx | 2 +- .../src/screens/RedeemScreen/RedeemScreen.tsx | 491 ++++++++++-------- frontend/app/src/services/Prices.tsx | 28 +- .../app/src/tx-flows/redeemCollateral.tsx | 234 +++------ frontend/app/src/utils.ts | 3 + 12 files changed, 421 insertions(+), 395 deletions(-) diff --git a/contracts/addresses/1.json b/contracts/addresses/1.json index 7d4d785b8..6e5615b93 100644 --- a/contracts/addresses/1.json +++ b/contracts/addresses/1.json @@ -15,7 +15,7 @@ "debtInFrontHelper": "0x4bb5e28fdb12891369b560f2fab3c032600677c6", "exchangeHelpers": "0x2f60bab0072abec7058017f48d7256ec288c8686", "exchangeHelpersV2": "0xe453b864d3841469763bda2437e3dd0e38dca222", - "redemptionHelper": "0x0000000000000000000000000000000000000000", + "redemptionHelper": "0xb366256d033ae7e4f7bddec822a5adec9df07b80", "branches": [ { "collSymbol": "WETH", diff --git a/frontend/app/.env b/frontend/app/.env index 7e68802ae..8bf4d32ca 100644 --- a/frontend/app/.env +++ b/frontend/app/.env @@ -121,7 +121,7 @@ NEXT_PUBLIC_CONTRACT_LQTY_STAKING=0x4f9fbb3f1e99b56e0fe2892e623ed36a76fc605d NEXT_PUBLIC_CONTRACT_LQTY_TOKEN=0x6dea81c8171d0ba574754ef6f8b412f2ed88c54d NEXT_PUBLIC_CONTRACT_LUSD_TOKEN=0x5f98805a4e8be255a32880fdec7f6728c6568ba0 NEXT_PUBLIC_CONTRACT_MULTI_TROVE_GETTER=0xfa61db085510c64b83056db3a7acf3b6f631d235 -NEXT_PUBLIC_CONTRACT_REDEMPTION_HELPER=0x0000000000000000000000000000000000000000 +NEXT_PUBLIC_CONTRACT_REDEMPTION_HELPER=0xb366256d033ae7e4f7bddec822a5adec9df07b80 NEXT_PUBLIC_CONTRACT_WETH=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 ########### diff --git a/frontend/app/src/comps/AppLayout/BottomBar.tsx b/frontend/app/src/comps/AppLayout/BottomBar.tsx index 0b694273a..0ae479f60 100644 --- a/frontend/app/src/comps/AppLayout/BottomBar.tsx +++ b/frontend/app/src/comps/AppLayout/BottomBar.tsx @@ -15,7 +15,7 @@ import Image from "next/image"; import { AboutButton } from "./AboutButton"; const DISPLAYED_PRICES = ["LQTY", "BOLD", "ETH"] as const; -const ENABLE_REDEEM = false; +const ENABLE_REDEEM = true; export function BottomBar() { const account = useAccount(); diff --git a/frontend/app/src/constants.ts b/frontend/app/src/constants.ts index 4a0a7fdf4..048d62a89 100644 --- a/frontend/app/src/constants.ts +++ b/frontend/app/src/constants.ts @@ -69,6 +69,10 @@ export const TROVE_STATUS_CLOSED_BY_OWNER = 2; export const TROVE_STATUS_CLOSED_BY_LIQUIDATION = 3; export const TROVE_STATUS_ZOMBIE = 4; +export const REDEMPTION_MAX_ITERATIONS_PER_COLL = 25; +export const REDEMPTION_FEE_HIGH = 0.01; // 1% +export const REDEMPTION_SLIPPAGE_TOLERANCE = 0.001; // 0.1% + // XXX what is the point of this? export const MAX_COLLATERAL_DEPOSITS: Record = { ETH: dn.from(100_000_000n, 18), diff --git a/frontend/app/src/content.tsx b/frontend/app/src/content.tsx index 49cfe2272..e0f913019 100644 --- a/frontend/app/src/content.tsx +++ b/frontend/app/src/content.tsx @@ -368,13 +368,13 @@ export default { action: "Next: Summary", }, rewardsPanel: { - boldRewardsLabel: (collateral: N) => <>Your BOLD rewards will be paid out, + boldRewardsLabel: "Your BOLD rewards will be paid out", collRewardsLabel: (collateral: N) => <>Your {collateral} rewards will be paid out, expectedGasFeeLabel: "Expected gas fee", action: "Next: Summary", }, compoundPanel: { - boldRewardsLabel: (collateral: N) => <>Your BOLD rewards will be used to top-up your deposit, + boldRewardsLabel: "Your BOLD rewards will be used to top-up your deposit", collRewardsLabel: (collateral: N) => <>Your {collateral} rewards will remain in your deposit, expectedGasFeeLabel: "Expected gas fee", action: "Next: Summary", diff --git a/frontend/app/src/liquity-utils.ts b/frontend/app/src/liquity-utils.ts index 093d79270..df7bc2f64 100644 --- a/frontend/app/src/liquity-utils.ts +++ b/frontend/app/src/liquity-utils.ts @@ -34,6 +34,7 @@ import { dnum18, DNUM_0, dnumOrNull, jsonStringifyWithDnum } from "@/src/dnum-ut import { CHAIN_BLOCK_EXPLORER, ENV_BRANCHES, LEGACY_CHECK, LIQUITY_STATS_URL } from "@/src/env"; import { getRedemptionRisk } from "@/src/liquity-math"; import { combineStatus } from "@/src/query-utils"; +import { useDebounced } from "@/src/react-utils"; import { usePrice } from "@/src/services/Prices"; import { getAllInterestRateBrackets, @@ -52,7 +53,7 @@ import * as dn from "dnum"; import { useMemo } from "react"; import * as v from "valibot"; import { encodeAbiParameters, erc20Abi, isAddressEqual, keccak256, parseAbiParameters, zeroAddress } from "viem"; -import { useBalance, useConfig as useWagmiConfig, useReadContract, useReadContracts } from "wagmi"; +import { useBalance, useConfig as useWagmiConfig, useReadContract, useReadContracts, useSimulateContract } from "wagmi"; import { readContract, readContracts } from "wagmi/actions"; export function shortenTroveId(troveId: TroveId, chars = 8) { @@ -1479,3 +1480,42 @@ export function useRedemptionRiskOfInterestRate( return { status, data: getRedemptionRisk(data.debtInFront, data.totalDebt) }; }, [status, data]); } + +export interface RedemptionSimulationParams { + boldAmount: Dnum; + maxIterationsPerCollateral: number; +} + +export function useRedemptionSimulation(params: RedemptionSimulationParams) { + const boldAmount = dn.from(params.boldAmount, 18)[0]; + const maxIterationsPerCollateral = BigInt(params.maxIterationsPerCollateral); + + const values = useMemo(() => ({ + boldAmount, + maxIterationsPerCollateral, + }), [boldAmount, maxIterationsPerCollateral]); + + const [debounced, bouncing] = useDebounced(values); + const RedemptionHelper = getProtocolContract("RedemptionHelper"); + + // We'd love to use `useReadContract()` for this, but wagmi/viem won't let us + // do that for mutating functions, even though it's a perfectly valid use case. + // We could hack the ABI, but that's yucky. + return useSimulateContract({ + ...RedemptionHelper, + functionName: "truncateRedemption", + args: [debounced.boldAmount, debounced.maxIterationsPerCollateral], + + query: { + refetchInterval: 12_000, + enabled: !bouncing, + + select: ({ result: [truncatedBold, feePct, output] }) => ({ + bouncing, + truncatedBold: dnum18(truncatedBold), + feePct: dnum18(feePct), + collRedeemed: output.map(({ coll }) => dnum18(coll)), + }), + }, + }); +} diff --git a/frontend/app/src/screens/EarnPoolScreen/PanelClaimRewards.tsx b/frontend/app/src/screens/EarnPoolScreen/PanelClaimRewards.tsx index 5c45fb7d7..ecd7553b6 100644 --- a/frontend/app/src/screens/EarnPoolScreen/PanelClaimRewards.tsx +++ b/frontend/app/src/screens/EarnPoolScreen/PanelClaimRewards.tsx @@ -79,7 +79,7 @@ export function PanelClaimRewards({ getCollToken(b.branchId)); +const collTokenNames = collTokens.map((collToken) => collToken.name.replace(/^ETH$/, "WETH")); + +const listOfCollTokenNames = [ + ...( + collTokenNames.length > 1 + ? [collTokenNames.slice(0, -1).join(", ")] + : [] + ), + ...collTokenNames.slice(-1), +].join(" and "); + +const zipWithMul = zipWith(dn.mul); export function RedeemScreen() { const account = useAccount(); - const txFlow = useTransactionFlow(); - const boldBalance = useBalance(account.address, "BOLD"); + const boldPrice = usePrice("BOLD"); + const collPrices = useCollateralPrices(branches.map((b) => b.symbol)); + const boldRedeemed = useInputFieldValue(fmtnum); - const CollateralRegistry = getProtocolContract("CollateralRegistry"); - const redemptionRate = useReadContract({ - ...CollateralRegistry, - functionName: "getRedemptionRateWithDecay", + const simulation = useRedemptionSimulation({ + boldAmount: boldRedeemed.parsed ?? DNUM_0, + maxIterationsPerCollateral, }); - const amount = useInputFieldValue(fmtnum); - const maxFee = useInputFieldValue((value) => `${fmtnum(value, "pct2z")}%`, { - parse: parseInputPercentage, - }); + const boldRedeemedUsd = simulation.data && boldPrice.data + && dn.mul(simulation.data.truncatedBold, boldPrice.data); - const hasUpdatedRedemptionRate = useRef(false); - if (!hasUpdatedRedemptionRate.current && redemptionRate.data) { - if (maxFee.isEmpty) { - maxFee.setValue( - fmtnum( - dn.mul(dnum18(redemptionRate.data), 1.1), - "pct2z", - ), - ); - } - hasUpdatedRedemptionRate.current = true; - } + const collRedeemedUsd = simulation.data && collPrices.data + && zipWithMul(simulation.data.collRedeemed, collPrices.data); + + const totalCollRedeemedUsd = collRedeemedUsd + && collRedeemedUsd.reduce((a, b) => dn.add(a, b)); + + const profitLoss = totalCollRedeemedUsd && boldRedeemedUsd + && dn.sub(totalCollRedeemedUsd, boldRedeemedUsd); + + const isLoss = profitLoss + && dn.lt(profitLoss, DNUM_0); - const branches = getBranches(); + const truncatedAmount = boldRedeemed.parsed && simulation.data?.bouncing === false + && dn.gt(dn.sub(boldRedeemed.parsed, simulation.data.truncatedBold), TRUNCATED_THRESHOLD) + ? simulation.data.truncatedBold + : null; - const allowSubmit = account.isConnected - && amount.parsed - && maxFee.parsed - && boldBalance.data - && dn.gte(boldBalance.data, amount.parsed); + const amount = truncatedAmount ?? boldRedeemed.parsed; + const amountNonZero = amount && dn.gt(amount, DNUM_0); + const balanceSufficient = amount && boldBalance.data && dn.lte(amount, boldBalance.data); + const allowSubmit = account.isConnected && amountNonZero && balanceSufficient; + + const drawer = boldRedeemed.isFocused + ? null + : !balanceSufficient + ? { + mode: "error" as const, + message: `Insufficient BOLD balance. You have ${fmtnum(boldBalance.data)} BOLD.`, + } + : truncatedAmount + ? { + mode: "warning" as const, + message: ( + + Amount capped to avoid excessive costs. + + The number of loans you redeem from will be capped at {maxIterationsPerCollateral}{" "} + per collateral branch. This is to avoid a transaction with unusually large gas usage, which might delay the + execution of your redemption. +
+
+ You will be able to redeem the rest of your BOLD in a follow-up transaction. +
+
+ ), + } + : null; return ( - Redeem BOLD for + Redeem + + BOLD for - {branches.map((b) => getCollToken(b.branchId)).map(({ symbol }) => ( - - ))} - {" "} + {collTokens.map(({ symbol }) => )} + ETH ), }} > -
+ } - drawer={amount.isFocused - ? null - : boldBalance.data - && amount.parsed - && dn.gt(amount.parsed, boldBalance.data) - ? { - mode: "error", - message: `Insufficient BOLD balance. You have ${fmtnum(boldBalance.data)} BOLD.`, - } - : null} - label="Redeeming" + drawer={drawer} + label="You pay" placeholder="0.00" secondary={{ - start: `$${ - amount.parsed - ? fmtnum(amount.parsed) - : "0.00" - }`, + start: fmtnum(boldRedeemedUsd, { prefix: "$", preset: "2z" }), end: ( boldBalance.data && dn.gt(boldBalance.data, 0) && ( { if (boldBalance.data) { - amount.setValue(dn.toString(boldBalance.data)); + boldRedeemed.setValue(dn.toString(boldBalance.data)); } }} /> ) ), }} - {...amount.inputFieldProps} + {...boldRedeemed.inputFieldProps} + // Show trucated amount when input field is not focused + value={!boldRedeemed.isFocused && truncatedAmount + ? fmtnum(truncatedAmount) + : boldRedeemed.inputFieldProps.value} /> } - /> + footer={{ + end: ( + + + + - - } - footer={[ - { - end: ( - - <> - Current redemption rate: - + You will be charged a dynamic redemption fee — the more redemptions, the higher the fee. + During periods of no redemption activity, the fee slowly decreases towards a minimum of + 0.5%. If you see a fee significantly higher than this, it might make sense to try redeeming + at a later time, or to break up your redemption into several smaller ones. + + ), + footerLink: { + label: "Learn more about the fee", + href: "https://docs.liquity.org/v2-faq/redemptions-and-delegation#is-there-a-redemption-fee", + }, + }} /> - - - This is the maximum redemption fee you are willing to pay. The redemption fee is a percentage - of the redeemed amount that is paid to the protocol. The redemption fee must be higher than - the current fee. - - ), - footerLink: { - href: "https://dune.com/queries/4641717/7730245", - label: "Redemption fee on Dune", - }, - }} - /> - - ), - }, - ]} + + } + /> + ), + }} /> -
-
-

- Important note -

-
-

- You will be charged a dynamic redemption fee (the more redemptions, the higher the fee). Trading BOLD on an - exchange could be more favorable.{" "} - - Learn more about redemptions. - -

-
- -
- -
-
+ + + +
); } + +function RedemptionOutput(props: { + branchId: BranchId; + amount: Dnum | null | undefined; + amountUsd: Dnum | null | undefined; +}) { + const collateralToken = getCollToken(props.branchId); + const collateralTokenName = collateralToken.symbol === "ETH" ? "WETH" : collateralToken.name; + + return ( + + + {collateralTokenName} + {collateralTokenName === "WETH" && ( + + You will receive{" "} + WETH, which is an ERC-20 tokenized version of ETH that is equivalent in + value. + + )} + + + + + + + + +
+ +
+
+
+ ); +} + +function InfoBox(props: { + title?: ReactNode; + children?: ReactNode; +}) { + return ( +
+ {props.title && ( +
+

{props.title}

+
+ )} + + {props.children} +
+ ); +} diff --git a/frontend/app/src/services/Prices.tsx b/frontend/app/src/services/Prices.tsx index f8867e8f2..0a8a0b4cf 100644 --- a/frontend/app/src/services/Prices.tsx +++ b/frontend/app/src/services/Prices.tsx @@ -10,23 +10,15 @@ import { dnum18, jsonStringifyWithDnum } from "@/src/dnum-utils"; import { useLiquityStats } from "@/src/liquity-utils"; import { isCollateralSymbol } from "@liquity2/uikit"; import { useQuery } from "@tanstack/react-query"; -import { useConfig as useWagmiConfig } from "wagmi"; +import { useConfig as useWagmiConfig, useReadContracts } from "wagmi"; import { readContract } from "wagmi/actions"; async function fetchCollateralPrice( symbol: CollateralSymbol, config: ReturnType, ): Promise { - const PriceFeed = getBranchContract(symbol, "PriceFeed"); - - const FetchPriceAbi = PriceFeed.abi.find((fn) => fn.name === "fetchPrice"); - if (!FetchPriceAbi) { - throw new Error("fetchPrice ABI not found"); - } - const [price] = await readContract(config, { - abi: [{ ...FetchPriceAbi, stateMutability: "view" }] as const, - address: PriceFeed.address, + ...getBranchContract(symbol, "PriceFeed"), functionName: "fetchPrice", }); @@ -64,3 +56,19 @@ export function usePrice(symbol: string | null): UseQueryResult { refetchInterval: PRICE_REFRESH_INTERVAL, }); } + +export function useCollateralPrices(symbols: CollateralSymbol[]) { + return useReadContracts({ + allowFailure: false, + + contracts: symbols.map((symbol) => ({ + ...getBranchContract(symbol, "PriceFeed"), + functionName: "fetchPrice", + } as const)), + + query: { + select: (data) => data.map(([price]) => dnum18(price)), + refetchInterval: 12_000, + }, + }); +} diff --git a/frontend/app/src/tx-flows/redeemCollateral.tsx b/frontend/app/src/tx-flows/redeemCollateral.tsx index 2381d2a0e..ed892898f 100644 --- a/frontend/app/src/tx-flows/redeemCollateral.tsx +++ b/frontend/app/src/tx-flows/redeemCollateral.tsx @@ -1,27 +1,27 @@ import type { FlowDeclaration } from "@/src/services/TransactionFlow"; -import type { Address } from "@/src/types"; import { Amount } from "@/src/comps/Amount/Amount"; -import { LOCAL_STORAGE_PREFIX } from "@/src/constants"; import { getProtocolContract } from "@/src/contracts"; -import { dnum18, jsonParseWithDnum, jsonStringifyWithDnum } from "@/src/dnum-utils"; -import { getBranches } from "@/src/liquity-utils"; +import { dnum18, DNUM_1 } from "@/src/dnum-utils"; +import { getBranches, getCollToken } from "@/src/liquity-utils"; import { TransactionDetailsRow } from "@/src/screens/TransactionsScreen/TransactionsScreen"; import { TransactionStatus } from "@/src/screens/TransactionsScreen/TransactionStatus"; +import { useCollateralPrices, usePrice } from "@/src/services/Prices"; import { vDnum } from "@/src/valibot-utils"; -import { useQuery } from "@tanstack/react-query"; +import { HFlex, InfoTooltip } from "@liquity2/uikit"; import * as dn from "dnum"; -import { Fragment } from "react"; import * as v from "valibot"; -import { createPublicClient } from "viem"; -import { http, useConfig as useWagmiConfig } from "wagmi"; +import { REDEMPTION_SLIPPAGE_TOLERANCE } from "../constants"; import { createRequestSchema, verifyTransaction } from "./shared"; const RequestSchema = createRequestSchema( "redeemCollateral", { amount: vDnum(), - maxFee: vDnum(), + maxIterationsPerCollateral: v.number(), + feePct: vDnum(), + collRedeemed: v.array(vDnum()), + slippageTolerance: vDnum(), }, ); @@ -30,55 +30,63 @@ export type RedeemCollateralRequest = v.InferOutput; export const redeemCollateral: FlowDeclaration = { title: "Review & Send Transaction", Summary: () => null, + Details(ctx) { - const estimatedGains = useSimulatedBalancesChange(ctx); + const { amount, collRedeemed } = ctx.request; const branches = getBranches(); - const boldChange = estimatedGains.data?.find(({ symbol }) => symbol === "BOLD")?.change; - const collChanges = estimatedGains.data?.filter(({ symbol }) => symbol !== "BOLD"); + const boldPrice = usePrice("BOLD"); + const collPrices = useCollateralPrices(branches.map((b) => b.symbol)); + return ( <> , + + + + This is the estimated fee you will pay. The actual fee may be up to{" "} + {" "} + higher than this due to slippage. + + , ]} /> + , - - Estimated BOLD that will be redeemed. - , + + + + This is the estimated amount of BOLD you will pay. The actual amount may be slightly lower than this. + + , + boldPrice.data && , ]} /> - {branches.map(({ symbol }) => { - const collChange = collChanges?.find((change) => symbol === change.symbol)?.change; - const symbol_ = symbol === "ETH" ? "WETH" : symbol; + + {branches.map(({ branchId }, i) => { + const collateralToken = getCollToken(branchId); + const collateralTokenName = collateralToken.symbol === "ETH" ? "WETH" : collateralToken.name; + return ( 1 ? ` #${i + 1}` : "")} value={[ - , - - Estimated {symbol_} you will receive. - , + + + + This is the estimated amount of {collateralTokenName}{" "} + you will receive. The actual amount may be up to{" "} + {" "} + lower than this due to slippage. + + , + collRedeemed[branchId] && collPrices.data?.[branchId] && ( + + ), ]} /> ); @@ -86,37 +94,43 @@ export const redeemCollateral: FlowDeclaration = { ); }, + steps: { approve: { name: () => "Approve BOLD", Status: TransactionStatus, + async commit({ request, writeContract }) { - const CollateralRegistry = getProtocolContract("CollateralRegistry"); + const RedemptionHelper = getProtocolContract("RedemptionHelper"); const BoldToken = getProtocolContract("BoldToken"); return writeContract({ ...BoldToken, functionName: "approve", - args: [CollateralRegistry.address, request.amount[0]], + args: [RedemptionHelper.address, request.amount[0]], }); }, async verify(ctx, hash) { await verifyTransaction(ctx.wagmiConfig, hash, ctx.isSafe); }, }, + redeemCollateral: { name: () => "Redeem BOLD", Status: TransactionStatus, + async commit({ request, writeContract }) { - const CollateralRegistry = getProtocolContract("CollateralRegistry"); + const bold = dn.from(request.amount, 18)[0]; + const maxIterationsPerCollateral = BigInt(request.maxIterationsPerCollateral); + const maxFeePct = dn.add(request.feePct, request.slippageTolerance, 18)[0]; + const slippageFactor = dn.sub(DNUM_1, request.slippageTolerance); + const minCollRedeemed = request.collRedeemed.map((collRedeemed) => dn.mul(collRedeemed, slippageFactor, 18)[0]); + const RedemptionHelper = getProtocolContract("RedemptionHelper"); + return writeContract({ - ...CollateralRegistry, + ...RedemptionHelper, functionName: "redeemCollateral", - args: [ - request.amount[0], - 0n, - request.maxFee[0], - ], + args: [bold, maxIterationsPerCollateral, maxFeePct, minCollRedeemed], }); }, async verify(ctx, hash) { @@ -134,15 +148,15 @@ export const redeemCollateral: FlowDeclaration = { functionName: "allowance", args: [ ctx.account, - getProtocolContract("CollateralRegistry").address, + getProtocolContract("RedemptionHelper").address, ], }); + if (dn.gt(ctx.request.amount, dnum18(boldAllowance))) { steps.push("approve"); } steps.push("redeemCollateral"); - return steps; }, @@ -158,115 +172,3 @@ export const StoredBalancesChangeSchema = v.object({ change: vDnum(), })), }); - -export function useSimulatedBalancesChange({ - account, - request, -}: { - account: Address; - request: RedeemCollateralRequest; -}) { - const wagmiConfig = useWagmiConfig(); - return useQuery({ - queryKey: ["simulatedBalancesChange", account, jsonStringifyWithDnum(request)], - queryFn: async () => { - const CollateralRegistry = getProtocolContract("CollateralRegistry"); - const BoldToken = getProtocolContract("BoldToken"); - - let stored: v.InferOutput | null = null; - try { - stored = v.parse( - StoredBalancesChangeSchema, - jsonParseWithDnum( - localStorage.getItem( - `${LOCAL_STORAGE_PREFIX}:simulatedBalancesChange`, - ) ?? "", - ), - ); - } catch (_) { - stored = null; - } - - if (stored && stored.stringifiedRequest === jsonStringifyWithDnum(request)) { - return stored.balanceChanges; - } - - const [chain] = wagmiConfig.chains; - const [rpcUrl] = chain.rpcUrls.default.http; - const client = createPublicClient({ chain, transport: http(rpcUrl) }); - - const branches = getBranches(); - const branchesBalanceCalls = branches.map((branch) => ({ - to: branch.contracts.CollToken.address, - abi: branch.contracts.CollToken.abi, - functionName: "balanceOf" as const, - args: [account], - } as const)); - - const boldBalanceCall = { - to: BoldToken.address, - abi: BoldToken.abi, - functionName: "balanceOf" as const, - args: [account], - } as const; - - const simulation = await client.simulateCalls({ - account, - calls: [ - // 1. get balances before - boldBalanceCall, - ...branchesBalanceCalls, - - // 2. redeem - { - to: CollateralRegistry.address, - abi: CollateralRegistry.abi, - functionName: "redeemCollateral", - args: [request.amount[0], 0n, request.maxFee[0]], - }, - - // 3. get balances after - boldBalanceCall, - ...branchesBalanceCalls, - ], - - // This is needed to avoid a “nonce too low” error with certain RPCs - stateOverrides: [{ address: account, nonce: 0 }], - }); - - const getBalancesFromSimulated = (position: number) => { - return simulation.results - .slice(position, position + branches.length + 1) - .map((result, index) => { - const symbol = index === 0 ? "BOLD" : branches[index - 1]?.symbol; - return { - symbol, - balance: dnum18(result.data ?? 0n), - }; - }); - }; - - const balancesBefore = getBalancesFromSimulated(0); - const balancesAfter = getBalancesFromSimulated(branches.length + 2); - - const balanceChanges = balancesBefore.map((balanceBefore, index) => { - const balanceAfter = balancesAfter[index]; - if (!balanceAfter) throw new Error(); - return { - symbol: balanceBefore.symbol, - change: dn.sub(balanceAfter.balance, balanceBefore.balance), - }; - }); - - localStorage.setItem( - `${LOCAL_STORAGE_PREFIX}:simulatedBalancesChange`, - jsonStringifyWithDnum({ - stringifiedRequest: jsonStringifyWithDnum(request), - balanceChanges, - }), - ); - - return balanceChanges; - }, - }); -} diff --git a/frontend/app/src/utils.ts b/frontend/app/src/utils.ts index a912bff32..5cec50b9f 100644 --- a/frontend/app/src/utils.ts +++ b/frontend/app/src/utils.ts @@ -53,3 +53,6 @@ export function tokenIconUrl(chainId: ChainId, address: Address) { export function panic(errorMessage: string): T { throw new Error(errorMessage); } + +export const zipWith = (f: (t: T, u: U) => V) => (ts: T[], us: U[]) => + ts.slice(0, us.length).map((t, i) => f(t, us[i]!));