Skip to content

Commit

Permalink
🚧 roll debt
Browse files Browse the repository at this point in the history
  • Loading branch information
dieguezguille committed Dec 30, 2024
1 parent aafcc2c commit 26b9074
Show file tree
Hide file tree
Showing 6 changed files with 349 additions and 151 deletions.
2 changes: 1 addition & 1 deletion src/components/add-funds/AddCrypto.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function AddCrypto() {
const copy = useCallback(() => {
if (!address) return;
setStringAsync(address).catch(handleError);
toast.show("Account address copied!");
toast.show("Account address copied!", { customData: { type: "success" } });
setAlertShown(false);
}, [address, toast]);

Expand Down
98 changes: 92 additions & 6 deletions src/components/payments/NextPayment.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import { marketUSDCAddress } from "@exactly/common/generated/chain";
import { WAD } from "@exactly/lib";
import { Coins, Info } from "@tamagui/lucide-icons";
import { exaPluginAbi, marketUSDCAddress, upgradeableModularAccountAbi } from "@exactly/common/generated/chain";
import { MATURITY_INTERVAL, WAD } from "@exactly/lib";
import { Coins, Info, RefreshCcw } from "@tamagui/lucide-icons";
import { useToastController } from "@tamagui/toast";
import { useQuery } from "@tanstack/react-query";
import { format, formatDistance, isAfter } from "date-fns";
import { router } from "expo-router";
import React from "react";
import React, { useCallback } from "react";
import { Pressable } from "react-native";
import { ms } from "react-native-size-matters";
import { XStack } from "tamagui";
import { Spinner, XStack } from "tamagui";
import { useSimulateContract, useWriteContract } from "wagmi";

import { auditorAbi, marketAbi } from "../../generated/contracts";
import handleError from "../../utils/handleError";
import queryClient from "../../utils/queryClient";
import useIntercom from "../../utils/useIntercom";
import useMarketAccount from "../../utils/useMarketAccount";
import Button from "../shared/Button";
import Text from "../shared/Text";
import View from "../shared/View";

export default function NextPayment() {
const toast = useToastController();
const { presentArticle } = useIntercom();
const { data: hidden } = useQuery<boolean>({ queryKey: ["settings", "sensitive"] });
const { market: USDCMarket } = useMarketAccount(marketUSDCAddress);
const { market: USDCMarket, account, queryKey } = useMarketAccount(marketUSDCAddress);
const usdDue = new Map<bigint, { previewValue: bigint; position: bigint }>();
if (USDCMarket) {
const { fixedBorrowPositions, usdPrice, decimals } = USDCMarket;
Expand All @@ -33,6 +38,48 @@ export default function NextPayment() {
const maturity = usdDue.keys().next().value;
const duePayment = usdDue.get(maturity ?? 0n);
const discount = duePayment ? Number(WAD - (duePayment.previewValue * WAD) / duePayment.position) / 1e18 : 0;
const followingMaturity = Number(maturity) + MATURITY_INTERVAL;

const repayMaturity = maturity ?? 0n;
const borrowMaturity = BigInt(followingMaturity);
const maxRepayAssets = duePayment ? duePayment.position : 0n;
const maxBorrowAssets = duePayment ? duePayment.position : 0n;
const percentage = WAD;

const {
data: rolloverSimulation,
isPending: isSimulatingRollover,
error: rolloverSimulationError,
} = useSimulateContract({
abi: [...exaPluginAbi, ...upgradeableModularAccountAbi, ...auditorAbi, ...marketAbi],
args: [repayMaturity, borrowMaturity, maxRepayAssets, maxBorrowAssets, percentage],
query: { retry: false, enabled: maxRepayAssets !== 0n && maxBorrowAssets !== 0n }, // TODO remove
functionName: "rollDebt",
address: account,
account,
});

const { writeContract: rollover, isPending: isRollingDebt } = useWriteContract({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey }).catch(handleError);
toast.show("Success", { customData: { type: "success" } });
},
onError: (error) => {
if (error.name === "ContractFunctionExecutionError" && error.details === "AA23 reverted (or OOG)") {
toast.show("Cancelled", { customData: { type: "error" } });
return;
}
toast.show("An error ocurred", { customData: { type: "error" } });
},
},
});

const rollDebt = useCallback(() => {
if (!rolloverSimulation || rolloverSimulationError || isRollingDebt) throw new Error("no rollover simulation");
rollover(rolloverSimulation.request);
}, [isRollingDebt, rollover, rolloverSimulation, rolloverSimulationError]);

return (
<View backgroundColor="$backgroundSoft" paddingTop="$s8">
{maturity ? (
Expand Down Expand Up @@ -131,6 +178,37 @@ export default function NextPayment() {
>
Pay
</Button>
<Button
main
spaced
halfWidth
{...outlined}
onPress={rollDebt}
iconAfter={
isRollingDebt || isSimulatingRollover ? (
<Spinner color="$interactiveOnDisabled" />
) : (
<RefreshCcw color="$interactiveOnBaseBrandSoft" strokeWidth={2.5} />
)
}
backgroundColor="$interactiveBaseBrandSoftDefault"
disabled={isRollingDebt || isSimulatingRollover}
color={
isRollingDebt || isSimulatingRollover ? "$interactiveOnDisabled" : "$interactiveOnBaseBrandSoft"
}
>
<Text
fontSize={ms(15)}
emphasized
numberOfLines={1}
adjustsFontSizeToFit
color={
isRollingDebt || isSimulatingRollover ? "$interactiveOnDisabled" : "$interactiveOnBaseBrandSoft"
}
>
Rollover
</Text>
</Button>
</View>
</View>
)}
Expand All @@ -145,3 +223,11 @@ export default function NextPayment() {
</View>
);
}

const outlined = {
hoverStyle: { backgroundColor: "$interactiveBaseBrandSoftHover" },
pressStyle: {
backgroundColor: "$interactiveBaseBrandSoftPressed",
color: "$interactiveOnBaseBrandSoft",
},
};
143 changes: 5 additions & 138 deletions src/components/payments/PaymentSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,11 @@
import { marketUSDCAddress } from "@exactly/common/generated/chain";
import { WAD } from "@exactly/lib";
import { Coins, Info } from "@tamagui/lucide-icons";
import { useQuery } from "@tanstack/react-query";
import { format, formatDistance, isAfter } from "date-fns";
import { router, useLocalSearchParams } from "expo-router";
import { useLocalSearchParams } from "expo-router";
import React from "react";
import { Pressable } from "react-native";
import { ms } from "react-native-size-matters";
import { Sheet, XStack } from "tamagui";
import { nonEmpty, pipe, safeParse, string } from "valibot";
import { Sheet } from "tamagui";

import handleError from "../../utils/handleError";
import useIntercom from "../../utils/useIntercom";
import useMarketAccount from "../../utils/useMarketAccount";
import Button from "../shared/Button";
import SafeView from "../shared/SafeView";
import Text from "../shared/Text";
import View from "../shared/View";
import PaymentSheetContent from "./PaymentSheetContent";

export default function PaymentSheet({ open, onClose }: { open: boolean; onClose: () => void }) {
const { presentArticle } = useIntercom();
const { data: hidden } = useQuery<boolean>({ queryKey: ["settings", "sensitive"] });
const { market: USDCMarket } = useMarketAccount(marketUSDCAddress);
const { maturity: currentMaturity } = useLocalSearchParams();
const { success, output: maturity } = safeParse(pipe(string(), nonEmpty("no maturity")), currentMaturity);
if (!success || !USDCMarket) return;

const { fixedBorrowPositions, usdPrice, decimals } = USDCMarket;
const borrow = fixedBorrowPositions.find((b) => b.maturity === BigInt(maturity));

if (!borrow) return;

const previewValue = (borrow.previewValue * usdPrice) / 10n ** BigInt(decimals);
const positionValue = ((borrow.position.principal + borrow.position.fee) * usdPrice) / 10n ** BigInt(decimals);
const discount = Number(WAD - (previewValue * WAD) / positionValue) / 1e18;
const maturity = useLocalSearchParams().maturity as string | undefined;
return (
<Sheet
open={open}
Expand All @@ -54,112 +26,7 @@ export default function PaymentSheet({ open, onClose }: { open: boolean; onClose
exitStyle={{ opacity: 0 }} // eslint-disable-line react-native/no-inline-styles
/>
<Sheet.Handle />
<Sheet.Frame>
<SafeView paddingTop={0} fullScreen borderTopLeftRadius="$r4" borderTopRightRadius="$r4">
<View padded paddingTop="$s6" fullScreen flex={1}>
<>
<View gap="$s5">
<XStack alignItems="center" justifyContent="center" gap="$s3">
<Text
secondary
textAlign="center"
emphasized
subHeadline
color={
isAfter(new Date(Number(maturity) * 1000), new Date())
? "$uiNeutralSecondary"
: "$uiErrorSecondary"
}
>
{isAfter(new Date(Number(maturity) * 1000), new Date())
? `Due in ${formatDistance(new Date(), new Date(Number(maturity) * 1000))}`
: `${formatDistance(new Date(Number(maturity) * 1000), new Date())} past due`}
<Text secondary textAlign="center" emphasized subHeadline color="$uiNeutralSecondary">
&nbsp;-&nbsp;{format(new Date(Number(maturity) * 1000), "MMM dd, yyyy")}
</Text>
</Text>
<Pressable
onPress={() => {
presentArticle("10245778").catch(handleError);
}}
hitSlop={ms(15)}
>
<Info size={16} color="$uiNeutralPrimary" />
</Pressable>
</XStack>
<View flexDirection="column" justifyContent="center" alignItems="center" gap="$s4">
<Text
sensitive
textAlign="center"
fontFamily="$mono"
fontSize={ms(40)}
fontWeight="bold"
overflow="hidden"
color={
isAfter(new Date(Number(maturity) * 1000), new Date()) ? "$uiNeutralPrimary" : "$uiErrorSecondary"
}
>
{(Number(previewValue) / 1e18).toLocaleString(undefined, {
style: "currency",
currency: "USD",
currencyDisplay: "narrowSymbol",
})}
</Text>
{discount >= 0 && (
<Text sensitive body strikeThrough color="$uiNeutralSecondary">
{(Number(positionValue) / 1e18).toLocaleString(undefined, {
style: "currency",
currency: "USD",
currencyDisplay: "narrowSymbol",
})}
</Text>
)}
{!hidden && (
<Text
pill
caption2
padding="$s2"
backgroundColor={
discount >= 0 ? "$interactiveBaseSuccessSoftDefault" : "$interactiveBaseErrorSoftDefault"
}
color={discount >= 0 ? "$uiSuccessSecondary" : "$uiErrorSecondary"}
>
{discount >= 0 ? "PAY NOW AND SAVE " : "DAILY PENALTIES "}
{(discount >= 0 ? discount : discount * -1).toLocaleString(undefined, {
style: "percent",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</Text>
)}
</View>
<View
flexDirection="row"
display="flex"
gap={ms(10)}
justifyContent="center"
alignItems="center"
paddingVertical={ms(10)}
>
<Button
onPress={() => {
onClose();
router.push({ pathname: "/pay", params: { maturity: maturity.toString() } });
}}
contained
main
spaced
halfWidth
iconAfter={<Coins color="$interactiveOnBaseBrandDefault" strokeWidth={2.5} />}
>
Pay
</Button>
</View>
</View>
</>
</View>
</SafeView>
</Sheet.Frame>
<Sheet.Frame>{maturity && <PaymentSheetContent maturity={maturity} onClose={onClose} />}</Sheet.Frame>
</Sheet>
);
}
Loading

0 comments on commit 26b9074

Please sign in to comment.