Skip to content

Commit

Permalink
🚧 lifi
Browse files Browse the repository at this point in the history
  • Loading branch information
dieguezguille committed Jan 7, 2025
1 parent fbbae4b commit 60aaec7
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 126 deletions.
18 changes: 5 additions & 13 deletions src/components/payments/AssetSelectionSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Address } from "@exactly/common/validation";
import { Coins } from "@tamagui/lucide-icons";
import React from "react";
import { Sheet, Spinner } from "tamagui";
import { Sheet } from "tamagui";

import AssetSelector from "../shared/AssetSelector";
import Button from "../shared/Button";
Expand All @@ -13,15 +13,12 @@ export default function AssetSelectionSheet({
onClose,
onAssetSelected,
positions,

symbol,
isSimulating,
disabled,
}: {
open: boolean;
onClose: () => void;
onAssetSelected: (market: Address) => void;
isSimulating?: boolean;
symbol?: string;
positions?: {
symbol: string;
Expand Down Expand Up @@ -63,19 +60,14 @@ export default function AssetSelectionSheet({
<Button
onPress={onClose}
contained
disabled={disabled}
main
spaced
fullwidth
iconAfter={
isSimulating ? (
<Spinner color="$interactiveOnDisabled" />
) : (
<Coins
strokeWidth={2.5}
color={disabled ? "$interactiveOnDisabled" : "$interactiveOnBaseBrandDefault"}
/>
)
<Coins
strokeWidth={2.5}
color={disabled ? "$interactiveOnDisabled" : "$interactiveOnBaseBrandDefault"}
/>
}
>
{symbol ? `Pay with ${symbol}` : "Select an asset"}
Expand Down
17 changes: 4 additions & 13 deletions src/components/payments/Failure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,20 +89,11 @@ export default function Failure({
})}
</Text>
<XStack gap="$s2" alignItems="center">
{currency === "USDC" ? (
<Text emphasized secondary subHeadline>
{Number(amount).toLocaleString(undefined, {
maximumFractionDigits: 8,
minimumFractionDigits: 0,
})}
</Text>
) : (
<Text emphasized secondary subHeadline>
Paid with&nbsp;
</Text>
)}
<Text emphasized secondary subHeadline>
{currency}&nbsp;
{Number(amount).toLocaleString(undefined, { maximumFractionDigits: 8 })}
</Text>
<Text emphasized secondary subHeadline>
&nbsp;{currency}&nbsp;
</Text>
<AssetLogo uri={assetLogos[currency as keyof typeof assetLogos]} width={ms(16)} height={ms(16)} />
</XStack>
Expand Down
212 changes: 143 additions & 69 deletions src/components/payments/Pay.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import fixedRate from "@exactly/common/fixedRate";
import { exaPluginAbi, marketUSDCAddress } from "@exactly/common/generated/chain";
import { Address } from "@exactly/common/validation";
import { exaPluginAbi, marketUSDCAddress, usdcAddress } from "@exactly/common/generated/chain";
import { Address, Hex } from "@exactly/common/validation";
import { WAD, withdrawLimit } from "@exactly/lib";
import { ArrowLeft, ChevronRight, Coins } from "@tamagui/lucide-icons";
import { useQuery } from "@tanstack/react-query";
import { format, formatDistance, isAfter } from "date-fns";
import { router, useLocalSearchParams } from "expo-router";
import React, { useCallback, useState } from "react";
Expand All @@ -11,7 +12,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ms } from "react-native-size-matters";
import { ScrollView, Separator, Spinner, XStack, YStack } from "tamagui";
import { nonEmpty, parse, pipe, safeParse, string } from "valibot";
import { maxUint256, zeroAddress } from "viem";
import { parseUnits, zeroAddress } from "viem";
import { useSimulateContract, useWriteContract } from "wagmi";

import AssetSelectionSheet from "./AssetSelectionSheet";
Expand All @@ -25,6 +26,7 @@ import View from "../../components/shared/View";
import { auditorAbi, marketAbi } from "../../generated/contracts";
import assetLogos from "../../utils/assetLogos";
import handleError from "../../utils/handleError";
import { getRoute } from "../../utils/lifi";
import queryClient from "../../utils/queryClient";
import useMarketAccount from "../../utils/useMarketAccount";
import AssetLogo from "../shared/AssetLogo";
Expand All @@ -33,12 +35,10 @@ export default function Pay() {
const insets = useSafeAreaInsets();
const [assetSelectionOpen, setAssetSelectionOpen] = useState(false);
const { account, market: USDCMarket, markets, queryKey: marketAccount } = useMarketAccount(marketUSDCAddress);

const [selectedMarket, setSelectedMarket] = useState<Address>();

const [cachedValues, setCachedValues] = useState<{ previewValue: number; positionValue: number }>({
previewValue: 0,
positionValue: 0,
const [displayValues, setDisplayValues] = useState<{ amount: number; usdAmount: number }>({
amount: 0,
usdAmount: 0,
});

const { success, output: maturity } = safeParse(
Expand All @@ -50,23 +50,6 @@ export default function Pay() {
setSelectedMarket(market);
};

const { data: repaySimulation, isPending: isSimulatingRepay } = useSimulateContract({
address: account,
functionName: "repay",
args: [success ? BigInt(maturity) : 0n, maxUint256, maxUint256], // TODO slippage control
abi: [...exaPluginAbi, ...auditorAbi, ...marketAbi],
query: { enabled: !!account && !!USDCMarket },
});
const { data: crossRepaySimulation, isPending: isSimulatingCrossRepay } = useSimulateContract({
address: account,
functionName: "crossRepay",
args: [success ? BigInt(maturity) : 0n, selectedMarket ?? zeroAddress],
abi: [...exaPluginAbi, ...auditorAbi, ...marketAbi, { type: "error", name: "InsufficientOutputAmount" }],
query: {
enabled: !!maturity && !!account && !!selectedMarket && selectedMarket !== parse(Address, marketUSDCAddress),
},
});

const {
data: repayHash,
writeContract: repay,
Expand Down Expand Up @@ -112,23 +95,6 @@ export default function Pay() {
10n ** BigInt(USDCMarket ? USDCMarket.decimals : 0)
: 0n;

const handlePayment = useCallback(() => {
setCachedValues({
previewValue: Number(previewValue) / 1e18,
positionValue: Number(positionValue) / 1e18,
});

if (isUSDCSelected) {
if (!repaySimulation) throw new Error("no repay simulation");
repay(repaySimulation.request);
} else {
if (!crossRepaySimulation) throw new Error("no cross repay simulation");
crossRepay(crossRepaySimulation.request);
}
}, [previewValue, positionValue, isUSDCSelected, repaySimulation, repay, crossRepaySimulation, crossRepay]);

const currentSimulation = isUSDCSelected ? repaySimulation : selectedMarket ? crossRepaySimulation : undefined;
const isSimulating = isUSDCSelected ? isSimulatingRepay : selectedMarket ? isSimulatingCrossRepay : false;
const isPending = isUSDCSelected ? isRepaying : selectedMarket ? isCrossRepaying : false;
const isSuccess = isUSDCSelected ? isRepaySuccess : isCrossRepaySuccess;
const error = isUSDCSelected ? repayError : crossRepayError;
Expand All @@ -150,6 +116,104 @@ export default function Pay() {
const repayMarket = positions?.find((p) => p.market === selectedMarket);
const repayMarketAvailable = markets && selectedMarket ? withdrawLimit(markets, selectedMarket) : 0n;

const slippage: bigint = parseUnits("1.2", 18); // TODO review
const maxRepay: bigint = borrow ? (borrow.previewValue * slippage) / WAD : 0n; // TODO review

const { data: route } = useQuery({
initialData: { fromAmount: 0n, data: parse(Hex, "0x") },
queryKey: ["lifi", "route", selectedMarket], // eslint-disable-line @tanstack/query/exhaustive-deps
queryFn: async () => {
if (!repayMarket || !borrow || !account) return { fromAmount: 0n, data: parse(Hex, "0x") };
return await getRoute(repayMarket.asset, usdcAddress, maxRepay, account);
},
refetchInterval: 5000,
});

const positionAssets: bigint = borrow ? borrow.position.principal + borrow.position.fee : 0n;
const maxAmountIn: bigint = route.fromAmount; // TODO review

const {
data: repaySimulation,
isPending: isSimulatingRepay,
error: repaySimulationError,
} = useSimulateContract({
address: account,
functionName: "repay",
args: [success ? BigInt(maturity) : 0n, positionAssets, maxRepay], // TODO review
abi: [...exaPluginAbi, ...auditorAbi, ...marketAbi],
query: {
enabled:
!!account && !!USDCMarket && !!success && !!maturity && selectedMarket === parse(Address, marketUSDCAddress),
},
});

const {
data: crossRepaySimulation,
error: crossRepaySimulationError,
isPending: isSimulatingCrossRepay,
} = useSimulateContract({
address: account,
functionName: "crossRepay",
args: [
success ? BigInt(maturity) : 0n, // { name: 'maturity', internalType: 'uint256', type: 'uint256' },
positionAssets, // { name: 'positionAssets', internalType: 'uint256', type: 'uint256' },
maxRepay, // { name: 'maxRepay', internalType: 'uint256', type: 'uint256' },
selectedMarket ?? zeroAddress, // { name: 'collateral', internalType: 'contract IMarket', type: 'address' },
maxAmountIn, // { name: 'maxAmountIn', internalType: 'uint256', type: 'uint256' },
route.data, // { name: 'route', internalType: 'bytes', type: 'bytes' },
],
abi: [...exaPluginAbi, ...auditorAbi, ...marketAbi],
query: {
enabled:
!!success &&
!!maturity &&
!!account &&
!!selectedMarket &&
selectedMarket !== parse(Address, marketUSDCAddress),
},
});

const simulationError = isUSDCSelected ? repaySimulationError : crossRepaySimulationError;

// TODO remove logs
if (simulationError) console.log("Simulation Error >>>", simulationError); // eslint-disable-line no-console
!isUSDCSelected &&
console.log({ maturity, positionAssets, maxRepay, collateral: selectedMarket, maxAmountIn, route: route.data });
isUSDCSelected && console.log({ maturity, positionAssets, maxRepay });

const handlePayment = useCallback(() => {
if (!repayMarket) return;
setDisplayValues({
amount: Number(isUSDCSelected ? positionAssets : route.fromAmount) / 10 ** repayMarket.decimals,
usdAmount: Number(previewValue) / 1e18,
});
if (isUSDCSelected) {
if (!repaySimulation) throw new Error("no repay simulation");
repay(repaySimulation.request);
} else {
if (!crossRepaySimulation) throw new Error("no cross repay simulation");
crossRepay(crossRepaySimulation.request);
}
}, [
crossRepay,
crossRepaySimulation,
isUSDCSelected,
positionAssets,
previewValue,
repay,
repayMarket,
repaySimulation,
route.fromAmount,
]);

const simulation = isUSDCSelected ? repaySimulation : selectedMarket ? crossRepaySimulation : undefined;
const isSimulating = isUSDCSelected ? isSimulatingRepay : selectedMarket ? isSimulatingCrossRepay : false;

if (!selectedMarket && positions?.[0]) {
const { market } = positions[0];
setSelectedMarket(parse(Address, market));
}

if (!success || !repayMarket) return;
if (!isPending && !isSuccess && !error)
return (
Expand Down Expand Up @@ -302,24 +366,36 @@ export default function Pay() {
<Text secondary callout textAlign="left">
Pay with
</Text>
<XStack
gap="$s3"
alignItems="center"
onPress={() => {
setAssetSelectionOpen(true);
}}
>
<AssetLogo
uri={assetLogos[repayMarket.assetSymbol as keyof typeof assetLogos]}
width={ms(16)}
height={ms(16)}
/>
<Text primary emphasized headline textAlign="right">
{repayMarket.assetSymbol}
<YStack>
<XStack
gap="$s3"
alignItems="center"
onPress={() => {
setAssetSelectionOpen(true);
}}
>
<AssetLogo
uri={assetLogos[repayMarket.assetSymbol as keyof typeof assetLogos]}
width={ms(16)}
height={ms(16)}
/>
<Text primary emphasized headline textAlign="right">
{repayMarket.assetSymbol}
</Text>
<ChevronRight size={ms(24)} color="$interactiveBaseBrandDefault" />
</XStack>
<Text secondary footnote textAlign="right">
{`${(
Number(isUSDCSelected ? positionAssets : route.fromAmount) /
10 ** repayMarket.decimals
).toLocaleString(undefined, {
maximumFractionDigits: 8,
useGrouping: false,
})} ${repayMarket.assetSymbol}`}
</Text>
<ChevronRight size={ms(24)} color="$interactiveBaseBrandDefault" />
</XStack>
</YStack>
</XStack>

<XStack justifyContent="space-between" gap="$s3">
<Text secondary callout textAlign="left">
Available
Expand Down Expand Up @@ -352,7 +428,7 @@ export default function Pay() {
flexBasis={ms(60)}
onPress={handlePayment}
contained
disabled={!currentSimulation || isSimulating}
disabled={!simulation || isSimulating}
main
spaced
fullwidth
Expand All @@ -362,19 +438,17 @@ export default function Pay() {
) : (
<Coins
strokeWidth={2.5}
color={currentSimulation ? "$interactiveOnBaseBrandDefault" : "$interactiveOnDisabled"}
color={simulation ? "$interactiveOnBaseBrandDefault" : "$interactiveOnDisabled"}
/>
)
}
>
Confirm payment
{isSimulating ? "Please wait..." : simulationError ? "Cannot proceed" : "Confirm payment"}
</Button>
</YStack>
</View>
<AssetSelectionSheet
positions={positions}
isSimulating={isSimulating}
disabled={!currentSimulation || isSimulating}
symbol={repayMarket.assetSymbol}
onAssetSelected={handleAssetSelect}
open={assetSelectionOpen}
Expand All @@ -389,17 +463,17 @@ export default function Pay() {
return (
<Pending
maturity={maturity}
usdAmount={cachedValues.previewValue}
amount={cachedValues.positionValue}
amount={displayValues.amount}
usdAmount={displayValues.usdAmount}
currency={repayMarket.assetSymbol}
/>
);
if (isSuccess)
return (
<Success
maturity={maturity}
usdAmount={cachedValues.previewValue}
amount={cachedValues.positionValue}
amount={displayValues.amount}
usdAmount={displayValues.usdAmount}
currency={repayMarket.assetSymbol}
hash={hash}
/>
Expand All @@ -408,8 +482,8 @@ export default function Pay() {
return (
<Failure
maturity={maturity}
usdAmount={cachedValues.previewValue}
amount={cachedValues.positionValue}
amount={displayValues.amount}
usdAmount={displayValues.usdAmount}
currency={repayMarket.assetSymbol}
hash={hash}
/>
Expand Down
Loading

0 comments on commit 60aaec7

Please sign in to comment.