diff --git a/src/components/payments/AssetSelectionSheet.tsx b/src/components/payments/AssetSelectionSheet.tsx
index e4a63647..acb673b7 100644
--- a/src/components/payments/AssetSelectionSheet.tsx
+++ b/src/components/payments/AssetSelectionSheet.tsx
@@ -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";
@@ -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;
@@ -63,19 +60,14 @@ export default function AssetSelectionSheet({
- ) : (
-
- )
+
}
>
{symbol ? `Pay with ${symbol}` : "Select an asset"}
diff --git a/src/components/payments/Failure.tsx b/src/components/payments/Failure.tsx
index 604d37fd..34559413 100644
--- a/src/components/payments/Failure.tsx
+++ b/src/components/payments/Failure.tsx
@@ -89,20 +89,11 @@ export default function Failure({
})}
- {currency === "USDC" ? (
-
- {Number(amount).toLocaleString(undefined, {
- maximumFractionDigits: 8,
- minimumFractionDigits: 0,
- })}
-
- ) : (
-
- Paid with
-
- )}
- {currency}
+ {Number(amount).toLocaleString(undefined, { maximumFractionDigits: 8 })}
+
+
+ {currency}
diff --git a/src/components/payments/Pay.tsx b/src/components/payments/Pay.tsx
index c25afe87..0d0010ea 100644
--- a/src/components/payments/Pay.tsx
+++ b/src/components/payments/Pay.tsx
@@ -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, exaPluginAddress, 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";
@@ -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";
@@ -22,9 +23,10 @@ import Button from "../../components/shared/Button";
import SafeView from "../../components/shared/SafeView";
import Text from "../../components/shared/Text";
import View from "../../components/shared/View";
-import { auditorAbi, marketAbi } from "../../generated/contracts";
+import { auditorAbi, marketAbi, useReadUpgradeableModularAccountGetInstalledPlugins } 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";
@@ -33,13 +35,16 @@ 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
();
+ const [displayValues, setDisplayValues] = useState<{ amount: number; usdAmount: number }>({
+ amount: 0,
+ usdAmount: 0,
+ });
- const [cachedValues, setCachedValues] = useState<{ previewValue: number; positionValue: number }>({
- previewValue: 0,
- positionValue: 0,
+ const { data: installedPlugins } = useReadUpgradeableModularAccountGetInstalledPlugins({
+ address: account ?? zeroAddress,
});
+ const isLatestPlugin = installedPlugins?.[0] === exaPluginAddress;
const { success, output: maturity } = safeParse(
pipe(string(), nonEmpty("no maturity")),
@@ -50,23 +55,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,
@@ -112,23 +100,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;
@@ -150,6 +121,141 @@ 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: isLatestPlugin
+ ? [success ? BigInt(maturity) : 0n, positionAssets, maxRepay]
+ : [success ? BigInt(maturity) : 0n], // TODO review
+ abi: [
+ ...exaPluginAbi,
+ ...auditorAbi,
+ ...marketAbi,
+ ...(isLatestPlugin
+ ? []
+ : [
+ {
+ type: "function",
+ inputs: [{ name: "maturity", internalType: "uint256", type: "uint256" }],
+ name: "repay",
+ outputs: [],
+ stateMutability: "nonpayable",
+ },
+ ]),
+ ],
+ query: {
+ enabled:
+ !!account && !!USDCMarket && !!success && !!maturity && selectedMarket === parse(Address, marketUSDCAddress),
+ },
+ });
+
+ const {
+ data: crossRepaySimulation,
+ error: crossRepaySimulationError,
+ isPending: isSimulatingCrossRepay,
+ } = useSimulateContract({
+ address: account,
+ functionName: "crossRepay",
+ args: isLatestPlugin
+ ? [
+ 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' },
+ ]
+ : [success ? BigInt(maturity) : 0n, selectedMarket ?? zeroAddress], // TODO review
+ abi: [
+ ...exaPluginAbi,
+ ...auditorAbi,
+ ...marketAbi,
+ ...(isLatestPlugin
+ ? []
+ : [
+ {
+ type: "function",
+ inputs: [
+ { name: "maturity", internalType: "uint256", type: "uint256" },
+ { name: "collateral", internalType: "contract IMarket", type: "address" },
+ ],
+ name: "crossRepay",
+ outputs: [],
+ stateMutability: "nonpayable",
+ },
+ ]),
+ ],
+ 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 (
@@ -302,24 +408,36 @@ export default function Pay() {
Pay with
- {
- setAssetSelectionOpen(true);
- }}
- >
-
-
- {repayMarket.assetSymbol}
+
+ {
+ setAssetSelectionOpen(true);
+ }}
+ >
+
+
+ {repayMarket.assetSymbol}
+
+
+
+
+ {`${(
+ Number(isUSDCSelected ? positionAssets : route.fromAmount) /
+ 10 ** repayMarket.decimals
+ ).toLocaleString(undefined, {
+ maximumFractionDigits: 8,
+ useGrouping: false,
+ })} ${repayMarket.assetSymbol}`}
-
-
+
+
Available
@@ -352,7 +470,7 @@ export default function Pay() {
flexBasis={ms(60)}
onPress={handlePayment}
contained
- disabled={!currentSimulation || isSimulating}
+ disabled={!simulation || isSimulating}
main
spaced
fullwidth
@@ -362,19 +480,17 @@ export default function Pay() {
) : (
)
}
>
- Confirm payment
+ {isSimulating ? "Please wait..." : simulationError ? "Cannot proceed" : "Confirm payment"}
);
@@ -398,8 +514,8 @@ export default function Pay() {
return (
@@ -408,8 +524,8 @@ export default function Pay() {
return (
diff --git a/src/components/payments/Pending.tsx b/src/components/payments/Pending.tsx
index e1a6fce8..1c785b2a 100644
--- a/src/components/payments/Pending.tsx
+++ b/src/components/payments/Pending.tsx
@@ -43,11 +43,7 @@ export default function Pending({
showsVerticalScrollIndicator={false}
stickyHeaderIndices={[0]}
// eslint-disable-next-line react-native/no-inline-styles
- contentContainerStyle={{
- flexGrow: 1,
- flexDirection: "column",
- justifyContent: "space-between",
- }}
+ contentContainerStyle={{ flexGrow: 1, flexDirection: "column", justifyContent: "space-between" }}
stickyHeaderHiddenOnScroll
>
@@ -81,20 +77,11 @@ export default function Pending({
})}
- {currency === "USDC" ? (
-
- {Number(amount).toLocaleString(undefined, {
- maximumFractionDigits: 8,
- minimumFractionDigits: 0,
- })}
-
- ) : (
-
- Paid with
-
- )}
- {currency}
+ {Number(amount).toLocaleString(undefined, { maximumFractionDigits: 8 })}
+
+
+ {currency}
diff --git a/src/components/payments/Success.tsx b/src/components/payments/Success.tsx
index 5bae005f..a0e5cc0d 100644
--- a/src/components/payments/Success.tsx
+++ b/src/components/payments/Success.tsx
@@ -91,20 +91,11 @@ export default function Success({
})}
- {currency === "USDC" ? (
-
- {Number(amount).toLocaleString(undefined, {
- maximumFractionDigits: 8,
- minimumFractionDigits: 0,
- })}
-
- ) : (
-
- Paid with
-
- )}
- {currency}
+ {Number(amount).toLocaleString(undefined, { maximumFractionDigits: 8 })}
+
+
+ {currency}
diff --git a/src/utils/lifi.ts b/src/utils/lifi.ts
new file mode 100644
index 00000000..8f22f69b
--- /dev/null
+++ b/src/utils/lifi.ts
@@ -0,0 +1,47 @@
+import chain, { mockSwapperAbi, mockSwapperAddress } from "@exactly/common/generated/chain";
+import { Hex } from "@exactly/common/validation";
+import { config, getContractCallsQuote } from "@lifi/sdk";
+import { parse } from "valibot";
+import { encodeFunctionData } from "viem";
+import { optimism, optimismSepolia } from "viem/chains";
+
+import publicClient from "./publicClient";
+
+async function getRoute(fromToken: Hex, toToken: Hex, toAmount: bigint, account: Hex) {
+ if (chain.id === optimismSepolia.id) {
+ const fromAmount = await publicClient.readContract({
+ abi: mockSwapperAbi,
+ functionName: "getAmountIn",
+ address: parse(Hex, mockSwapperAddress),
+ args: [fromToken, toAmount, toToken],
+ });
+ return {
+ fromAmount,
+ data: parse(
+ Hex,
+ encodeFunctionData({
+ abi: mockSwapperAbi,
+ functionName: "swapExactAmountOut",
+ args: [fromToken, fromAmount, toToken, toAmount],
+ }),
+ ),
+ };
+ }
+ config.set({ integrator: "exa_app", userId: account });
+ const { estimate, transactionRequest } = await getContractCallsQuote({
+ fee: 0.0025, // TODO review - 0.25% fee
+ slippage: 0.02, // TODO review - 2% slippage
+ integrator: "exa_app",
+ fromChain: optimism.id,
+ toChain: optimism.id,
+ fromToken: fromToken.toString(),
+ toToken: toToken.toString(),
+ toAmount: toAmount.toString(),
+ fromAddress: account,
+ contractCalls: [],
+ });
+ return { fromAmount: BigInt(estimate.fromAmount), data: parse(Hex, transactionRequest?.data) };
+}
+
+// TODO add necessary exports and remove eslint-disable
+export { getRoute }; // eslint-disable-line import/prefer-default-export