diff --git a/src/abi/CoolerConsolidation.json b/src/abi/CoolerConsolidation.json index 74190aaaad..876846cbef 100644 --- a/src/abi/CoolerConsolidation.json +++ b/src/abi/CoolerConsolidation.json @@ -1,434 +1,579 @@ { "abi": [ { - "type": "constructor", "inputs": [ { - "name": "gohm_", - "type": "address", - "internalType": "address" + "internalType": "address", + "name": "kernel_", + "type": "address" }, { - "name": "sdai_", - "type": "address", - "internalType": "address" - }, - { - "name": "dai_", - "type": "address", - "internalType": "address" - }, + "internalType": "uint256", + "name": "feePercentage_", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ { - "name": "owner_", - "type": "address", - "internalType": "address" - }, + "internalType": "address", + "name": "caller_", + "type": "address" + } + ], + "name": "KernelAdapter_OnlyKernel", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyConsolidatorActive", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyCoolerOwner", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyLender", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyPolicyActive", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyThis", + "type": "error" + }, + { + "inputs": [], + "name": "Params_FeePercentageOutOfRange", + "type": "error" + }, + { + "inputs": [], + "name": "Params_InsufficientCoolerCount", + "type": "error" + }, + { + "inputs": [], + "name": "Params_InvalidAddress", + "type": "error" + }, + { + "inputs": [], + "name": "Params_InvalidClearinghouse", + "type": "error" + }, + { + "inputs": [], + "name": "Params_InvalidCooler", + "type": "error" + }, + { + "inputs": [ { - "name": "lender_", - "type": "address", - "internalType": "address" - }, + "internalType": "Keycode", + "name": "keycode_", + "type": "bytes5" + } + ], + "name": "Policy_ModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ { - "name": "collector_", - "type": "address", - "internalType": "address" - }, + "internalType": "bytes", + "name": "expected_", + "type": "bytes" + } + ], + "name": "Policy_WrongModuleVersion", + "type": "error" + }, + { + "anonymous": false, + "inputs": [], + "name": "ConsolidatorActivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "ConsolidatorDeactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ { - "name": "feePercentage_", - "type": "uint256", - "internalType": "uint256" + "indexed": false, + "internalType": "uint256", + "name": "feePercentage", + "type": "uint256" } ], - "stateMutability": "nonpayable" + "name": "FeePercentageSet", + "type": "event" }, { - "type": "function", + "inputs": [], "name": "ONE_HUNDRED_PERCENT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], + "name": "ROLES", "outputs": [ { + "internalType": "contract ROLESv1", "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "address" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "VERSION", "inputs": [], + "name": "ROLE_ADMIN", "outputs": [ { + "internalType": "bytes32", "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "bytes32" } ], - "stateMutability": "pure" + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ROLE_EMERGENCY_SHUTDOWN", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "VERSION", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "activate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract Kernel", + "name": "newKernel_", + "type": "address" + } + ], + "name": "changeKernel", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "collateralRequired", "inputs": [ { + "internalType": "address", "name": "clearinghouse_", - "type": "address", - "internalType": "address" + "type": "address" }, { + "internalType": "address", "name": "cooler_", - "type": "address", - "internalType": "address" + "type": "address" }, { + "internalType": "uint256[]", "name": "ids_", - "type": "uint256[]", - "internalType": "uint256[]" + "type": "uint256[]" } ], + "name": "collateralRequired", "outputs": [ { + "internalType": "uint256", "name": "consolidatedLoanCollateral", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" }, { + "internalType": "uint256", "name": "existingLoanCollateral", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" }, { + "internalType": "uint256", "name": "additionalCollateral", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "collector", "inputs": [], + "name": "configureDependencies", "outputs": [ { - "name": "", - "type": "address", - "internalType": "address" + "internalType": "Keycode[]", + "name": "dependencies", + "type": "bytes5[]" } ], - "stateMutability": "view" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "consolidateWithFlashLoan", "inputs": [ { - "name": "clearinghouse_", - "type": "address", - "internalType": "address" + "internalType": "address", + "name": "clearinghouseFrom_", + "type": "address" }, { - "name": "cooler_", - "type": "address", - "internalType": "address" + "internalType": "address", + "name": "clearinghouseTo_", + "type": "address" + }, + { + "internalType": "address", + "name": "coolerFrom_", + "type": "address" + }, + { + "internalType": "address", + "name": "coolerTo_", + "type": "address" }, { + "internalType": "uint256[]", "name": "ids_", - "type": "uint256[]", - "internalType": "uint256[]" + "type": "uint256[]" + } + ], + "name": "consolidate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clearinghouseFrom_", + "type": "address" + }, + { + "internalType": "address", + "name": "clearinghouseTo_", + "type": "address" }, { - "name": "useFunds_", - "type": "uint256", - "internalType": "uint256" + "internalType": "address", + "name": "coolerFrom_", + "type": "address" }, { - "name": "sdai_", - "type": "bool", - "internalType": "bool" + "internalType": "address", + "name": "coolerTo_", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "ids_", + "type": "uint256[]" } ], + "name": "consolidateWithNewOwner", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "dai", "inputs": [], + "name": "consolidatorActive", "outputs": [ { + "internalType": "bool", "name": "", - "type": "address", - "internalType": "contract IERC20" + "type": "bool" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "feePercentage", "inputs": [], + "name": "deactivate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "feePercentage", "outputs": [ { + "internalType": "uint256", "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getProtocolFee", "inputs": [ { + "internalType": "address", + "name": "clearinghouseTo_", + "type": "address" + }, + { + "internalType": "address", + "name": "coolerFrom_", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "ids_", + "type": "uint256[]" + } + ], + "name": "fundsRequired", + "outputs": [ + { + "internalType": "address", + "name": "reserveTo", + "type": "address" + }, + { + "internalType": "uint256", + "name": "interest", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lenderFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "protocolFee", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", "name": "totalDebt_", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" } ], + "name": "getProtocolFee", "outputs": [ { + "internalType": "uint256", "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "gohm", "inputs": [], + "name": "isActive", "outputs": [ { + "internalType": "bool", "name": "", - "type": "address", - "internalType": "contract IERC20" + "type": "bool" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "lender", "inputs": [], + "name": "kernel", "outputs": [ { + "internalType": "contract Kernel", "name": "", - "type": "address", - "internalType": "contract IERC3156FlashLender" + "type": "address" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "onFlashLoan", "inputs": [ { + "internalType": "address", "name": "initiator_", - "type": "address", - "internalType": "address" + "type": "address" }, { + "internalType": "address", "name": "", - "type": "address", - "internalType": "address" + "type": "address" }, { + "internalType": "uint256", "name": "amount_", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" }, { + "internalType": "uint256", "name": "lenderFee_", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" }, { + "internalType": "bytes", "name": "params_", - "type": "bytes", - "internalType": "bytes" + "type": "bytes" } ], + "name": "onFlashLoan", "outputs": [ { + "internalType": "bytes32", "name": "", - "type": "bytes32", - "internalType": "bytes32" + "type": "bytes32" } ], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "owner", "inputs": [], + "name": "requestPermissions", "outputs": [ { - "name": "", - "type": "address", - "internalType": "address" + "components": [ + { + "internalType": "Keycode", + "name": "keycode", + "type": "bytes5" + }, + { + "internalType": "bytes4", + "name": "funcSelector", + "type": "bytes4" + } + ], + "internalType": "struct Permissions[]", + "name": "requests", + "type": "tuple[]" } ], - "stateMutability": "view" + "stateMutability": "pure", + "type": "function" }, { - "type": "function", - "name": "requiredApprovals", "inputs": [ { - "name": "clearinghouse_", - "type": "address", - "internalType": "address" + "internalType": "address", + "name": "clearinghouseTo_", + "type": "address" }, { - "name": "cooler_", - "type": "address", - "internalType": "address" + "internalType": "address", + "name": "coolerFrom_", + "type": "address" }, { + "internalType": "uint256[]", "name": "ids_", - "type": "uint256[]", - "internalType": "uint256[]" + "type": "uint256[]" } ], + "name": "requiredApprovals", "outputs": [ { + "internalType": "address", "name": "", - "type": "address", - "internalType": "address" + "type": "address" }, { + "internalType": "uint256", "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" }, { + "internalType": "address", "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "address" }, { + "internalType": "uint256", "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" }, { + "internalType": "uint256", "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "sdai", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract IERC4626" + "type": "uint256" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "setCollector", - "inputs": [ - { - "name": "collector_", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "setFeePercentage", "inputs": [ { + "internalType": "uint256", "name": "feePercentage_", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "transferOwnership", - "inputs": [ - { - "name": "newOwner", - "type": "address", - "internalType": "address" + "type": "uint256" } ], + "name": "setFeePercentage", "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "OwnershipTransferred", - "inputs": [ - { - "name": "user", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "newOwner", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "InsufficientCoolerCount", - "inputs": [] - }, - { - "type": "error", - "name": "OnlyCoolerOwner", - "inputs": [] - }, - { - "type": "error", - "name": "OnlyLender", - "inputs": [] - }, - { - "type": "error", - "name": "OnlyThis", - "inputs": [] - }, - { - "type": "error", - "name": "Params_FeePercentageOutOfRange", - "inputs": [] - }, - { - "type": "error", - "name": "Params_InvalidAddress", - "inputs": [] - }, - { - "type": "error", - "name": "Params_UseFundsOutOfBounds", - "inputs": [] + "stateMutability": "nonpayable", + "type": "function" } ] } diff --git a/src/components/TokenAllowanceGuard/TokenAllowanceGuard.tsx b/src/components/TokenAllowanceGuard/TokenAllowanceGuard.tsx index a11db0984c..fcc533a58c 100644 --- a/src/components/TokenAllowanceGuard/TokenAllowanceGuard.tsx +++ b/src/components/TokenAllowanceGuard/TokenAllowanceGuard.tsx @@ -120,7 +120,7 @@ export const TokenAllowanceGuard: React.FC<{ loading={approveMutation.isLoading} fullWidth className="" - onClick={() => approveMutation.mutate({ spenderAddressMap })} + onClick={() => approveMutation.mutate({ spenderAddressMap, spendAmount })} disabled={approveMutation.isLoading} > {approveMutation.isLoading ? `${approvalPendingText}` : `${approvalText}`} diff --git a/src/components/TokenAllowanceGuard/hooks/useApproveToken.ts b/src/components/TokenAllowanceGuard/hooks/useApproveToken.ts index 66161d7507..c47a967f44 100644 --- a/src/components/TokenAllowanceGuard/hooks/useApproveToken.ts +++ b/src/components/TokenAllowanceGuard/hooks/useApproveToken.ts @@ -3,6 +3,7 @@ import { ContractReceipt } from "@ethersproject/contracts"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import toast from "react-hot-toast"; import { AddressMap } from "src/constants/addresses"; +import { DecimalBigNumber } from "src/helpers/DecimalBigNumber/DecimalBigNumber"; import { useDynamicTokenContract } from "src/hooks/useContract"; import { contractAllowanceQueryKey } from "src/hooks/useContractAllowance"; import { EthersError } from "src/lib/EthersTypes"; @@ -15,14 +16,14 @@ export const useApproveToken = (tokenAddressMap: AddressMap) => { const { chain = { id: 1 } } = useNetwork(); const token = useDynamicTokenContract(tokenAddressMap, true); - return useMutation( - async ({ spenderAddressMap }) => { + return useMutation( + async ({ spenderAddressMap, spendAmount }) => { const contractAddress = spenderAddressMap[chain.id as keyof typeof spenderAddressMap]; if (!token) throw new Error("Token doesn't exist on current network. Please switch networks."); if (!contractAddress) throw new Error("Contract doesn't exist on current network. Please switch networks."); - const transaction = await token.approve(contractAddress, MaxUint256); + const transaction = await token.approve(contractAddress, spendAmount?.toBigNumber() || MaxUint256); return transaction.wait(); }, diff --git a/src/constants/addresses.ts b/src/constants/addresses.ts index 77d209dae2..872693a408 100644 --- a/src/constants/addresses.ts +++ b/src/constants/addresses.ts @@ -299,7 +299,7 @@ export const OLYMPUS_GOVERNANCE_ADDRESSES = { }; export const COOLER_CONSOLIDATION_ADDRESSES = { - [NetworkId.MAINNET]: "0xB15bcb1b6593d85890f5287Baa2245B8A29F464a", + [NetworkId.MAINNET]: "0x784cA0C006b8651BAB183829A99fA46BeCe50dBc", [NetworkId.TESTNET_GOERLI]: "", }; diff --git a/src/hooks/wagmi.ts b/src/hooks/wagmi.ts index 962be57b88..97e55b2898 100644 --- a/src/hooks/wagmi.ts +++ b/src/hooks/wagmi.ts @@ -23,7 +23,10 @@ export const { chains, provider, webSocketProvider } = configureChains( [ { ...mainnet, - rpcUrls: { default: { http: ["https://rpc.ankr.com/eth"] }, public: { http: ["https://rpc.ankr.com/eth"] } }, + rpcUrls: { + default: { http: ["https://rpc.ankr.com/eth"] }, + public: { http: ["https://rpc.ankr.com/eth"] }, + }, }, { ...polygon, diff --git a/src/views/Lending/Cooler/hooks/useCheckConsolidatorActive.tsx b/src/views/Lending/Cooler/hooks/useCheckConsolidatorActive.tsx new file mode 100644 index 0000000000..86b8950647 --- /dev/null +++ b/src/views/Lending/Cooler/hooks/useCheckConsolidatorActive.tsx @@ -0,0 +1,19 @@ +import { useQuery } from "@tanstack/react-query"; +import { COOLER_CONSOLIDATION_ADDRESSES } from "src/constants/addresses"; +import { useTestableNetworks } from "src/hooks/useTestableNetworks"; +import { CoolerConsolidation__factory } from "src/typechain"; +import { useProvider } from "wagmi"; + +export const useCheckConsolidatorActive = () => { + const networks = useTestableNetworks(); + const provider = useProvider(); + + return useQuery({ + queryKey: ["consolidatorActive"], + queryFn: async () => { + const contract = CoolerConsolidation__factory.connect(COOLER_CONSOLIDATION_ADDRESSES[networks.MAINNET], provider); + const isActive = await contract.isActive(); + return isActive; + }, + }); +}; diff --git a/src/views/Lending/Cooler/hooks/useConsolidateCooler.tsx b/src/views/Lending/Cooler/hooks/useConsolidateCooler.tsx index 5b34d0c4f3..a7b1c71361 100644 --- a/src/views/Lending/Cooler/hooks/useConsolidateCooler.tsx +++ b/src/views/Lending/Cooler/hooks/useConsolidateCooler.tsx @@ -15,20 +15,31 @@ export const useConsolidateCooler = () => { return useMutation( async ({ - coolerAddress, - clearingHouseAddress, + fromCoolerAddress, + toCoolerAddress, + fromClearingHouseAddress, + toClearingHouseAddress, loanIds, }: { - coolerAddress: string; - clearingHouseAddress: string; + fromCoolerAddress: string; + toCoolerAddress: string; + fromClearingHouseAddress: string; + toClearingHouseAddress: string; loanIds: number[]; }) => { if (!signer) throw new Error(`Please connect a wallet`); const contractAddress = COOLER_CONSOLIDATION_CONTRACT.addresses[networks.MAINNET]; const contract = CoolerConsolidation__factory.connect(contractAddress, signer); - const cooler = await contract.consolidateWithFlashLoan(clearingHouseAddress, coolerAddress, loanIds, 0, false, { - gasLimit: loanIds.length <= 30 ? loanIds.length * 1000000 : 30000000, - }); + const cooler = await contract.consolidate( + fromClearingHouseAddress, + toClearingHouseAddress, + fromCoolerAddress, + toCoolerAddress, + loanIds, + { + gasLimit: loanIds.length <= 15 ? loanIds.length * 2000000 : 30000000, + }, + ); const receipt = await cooler.wait(); return receipt; }, diff --git a/src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx b/src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx new file mode 100644 index 0000000000..bb2603cf08 --- /dev/null +++ b/src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx @@ -0,0 +1,44 @@ +import { useQuery } from "@tanstack/react-query"; +import { COOLER_CONSOLIDATION_CONTRACT } from "src/constants/contracts"; +import { DecimalBigNumber } from "src/helpers/DecimalBigNumber/DecimalBigNumber"; +import { useTestableNetworks } from "src/hooks/useTestableNetworks"; +import { CoolerConsolidation__factory } from "src/typechain"; +import { useProvider } from "wagmi"; + +export const useGetConsolidationAllowances = ({ + clearingHouseAddress, + coolerAddress, + loanIds, +}: { + clearingHouseAddress: string; + coolerAddress: string; + loanIds: number[]; +}) => { + const provider = useProvider(); + const networks = useTestableNetworks(); + + console.log("useGetConsolidationAllowances", clearingHouseAddress, coolerAddress, loanIds); + + const { data, isFetched, isLoading } = useQuery( + ["useGetConsolidationAllowances", clearingHouseAddress, coolerAddress], + async () => { + try { + const contractAddress = COOLER_CONSOLIDATION_CONTRACT.addresses[networks.MAINNET]; + const contract = CoolerConsolidation__factory.connect(contractAddress, provider); + const requiredApprovals = await contract.requiredApprovals(clearingHouseAddress, coolerAddress, loanIds); + const totalDebtWithFee = requiredApprovals[3].add(requiredApprovals[4]); + return { + consolidatedLoanCollateral: new DecimalBigNumber(requiredApprovals[1], 18), + totalDebtWithFee: new DecimalBigNumber(totalDebtWithFee, 18), + }; + } catch { + return { + consolidatedLoanCollateral: new DecimalBigNumber("0", 18), + totalDebtWithFee: new DecimalBigNumber("0", 18), + }; + } + }, + { enabled: !!coolerAddress }, + ); + return { data, isFetched, isLoading }; +}; diff --git a/src/views/Lending/Cooler/positions/ConsolidateLoan.tsx b/src/views/Lending/Cooler/positions/ConsolidateLoan.tsx index 0658835c70..c9c11d9f42 100644 --- a/src/views/Lending/Cooler/positions/ConsolidateLoan.tsx +++ b/src/views/Lending/Cooler/positions/ConsolidateLoan.tsx @@ -1,28 +1,97 @@ +import { Box, FormControl, MenuItem, Select, SelectChangeEvent, SvgIcon, Typography } from "@mui/material"; +import { InfoNotification, Modal, PrimaryButton } from "@olympusdao/component-library"; import { BigNumber } from "ethers"; import { formatEther } from "ethers/lib/utils.js"; import { useEffect, useState } from "react"; +import lendAndBorrowIcon from "src/assets/icons/lendAndBorrow.svg?react"; +import { TokenAllowanceGuard } from "src/components/TokenAllowanceGuard/TokenAllowanceGuard"; +import { WalletConnectedGuard } from "src/components/WalletConnectedGuard"; +import { COOLER_CONSOLIDATION_ADDRESSES, GOHM_ADDRESSES } from "src/constants/addresses"; +import { formatNumber } from "src/helpers"; import { useBalance } from "src/hooks/useBalance"; import { useTestableNetworks } from "src/hooks/useTestableNetworks"; +import { NetworkId } from "src/networkDetails"; +import { useCheckConsolidatorActive } from "src/views/Lending/Cooler/hooks/useCheckConsolidatorActive"; import { useConsolidateCooler } from "src/views/Lending/Cooler/hooks/useConsolidateCooler"; +import { useCreateCooler } from "src/views/Lending/Cooler/hooks/useCreateCooler"; +import { useGetClearingHouse } from "src/views/Lending/Cooler/hooks/useGetClearingHouse"; +import { useGetConsolidationAllowances } from "src/views/Lending/Cooler/hooks/useGetConsolidationAllowances"; import { useGetCoolerLoans } from "src/views/Lending/Cooler/hooks/useGetCoolerLoans"; export const ConsolidateLoans = ({ - coolerAddress, - clearingHouseAddress, - loans, - duration, - debtAddress, + v3CoolerAddress, + v2CoolerAddress, + v1CoolerAddress, + clearingHouseAddresses, + v1Loans, + v2Loans, + v3Loans, + factoryAddress, }: { - coolerAddress: string; - clearingHouseAddress: string; - loans: NonNullable["data"]>; - duration: string; - debtAddress: string; + v3CoolerAddress?: string; + v2CoolerAddress?: string; + v1CoolerAddress?: string; + clearingHouseAddresses: { + v1: NonNullable["data"]>; + v2: NonNullable["data"]>; + v3: NonNullable["data"]>; + }; + v1Loans?: NonNullable["data"]>; + v2Loans?: NonNullable["data"]>; + v3Loans?: NonNullable["data"]>; + factoryAddress: string; }) => { const coolerMutation = useConsolidateCooler(); + const { data: consolidatorActive } = useCheckConsolidatorActive(); + const createCooler = useCreateCooler(); const networks = useTestableNetworks(); const [open, setOpen] = useState(false); + + // Determine which versions are available for consolidation + const hasV1Loans = v1Loans && v1Loans.length > 0; + const hasV2Loans = v2Loans && v2Loans.length > 0; + const hasMultipleV3Loans = v3Loans && v3Loans.length > 1; + + // Calculate available versions + const availableVersions = [hasV1Loans && "v1", hasV2Loans && "v2", hasMultipleV3Loans && "v3"].filter(Boolean) as ( + | "v1" + | "v2" + | "v3" + )[]; + + // If there's only one option, use it automatically + const [selectedVersion, setSelectedVersion] = useState<"v1" | "v2" | "v3" | "">( + availableVersions.length === 1 ? availableVersions[0] : "", + ); + + // Get the appropriate loans based on selection + const selectedLoans = + selectedVersion === "v1" ? v1Loans : selectedVersion === "v2" ? v2Loans : selectedVersion === "v3" ? v3Loans : []; + + const loans = selectedLoans || []; const loanIds = loans.map(loan => loan.loanId); + + // Set the appropriate addresses based on selection + const coolerAddress = + selectedVersion === "v1" + ? v1CoolerAddress + : selectedVersion === "v2" + ? v2CoolerAddress + : selectedVersion === "v3" + ? v3CoolerAddress + : ""; + const selectedClearingHouse = + selectedVersion === "v1" + ? clearingHouseAddresses.v1 + : selectedVersion === "v2" + ? clearingHouseAddresses.v2 + : clearingHouseAddresses.v3; + const duration = clearingHouseAddresses.v3.duration; // Standard duration for consolidated loans + const debtAddress = clearingHouseAddresses.v3.debtAddress; + + // Show button only if there are loans that can be consolidated + const showConsolidateButton = hasV1Loans || hasV2Loans || hasMultipleV3Loans; + const totals = loans.reduce( (acc, loan) => { acc.principal = acc.principal.add(loan.principal); @@ -32,23 +101,223 @@ export const ConsolidateLoans = ({ }, { principal: BigNumber.from(0), interest: BigNumber.from(0), collateral: BigNumber.from(0) }, ); + const { data: allowances } = useGetConsolidationAllowances({ + clearingHouseAddress: clearingHouseAddresses.v3.clearingHouseAddress, + coolerAddress: coolerAddress || "", + loanIds, + }); + const maturityDate = new Date(); maturityDate.setDate(maturityDate.getDate() + Number(duration || 0)); - const { data: daiBalance } = useBalance({ [networks.MAINNET]: debtAddress || "" })[networks.MAINNET]; + const { data: debtBalance } = useBalance({ [networks.MAINNET]: debtAddress || "" })[networks.MAINNET]; const [insufficientCollateral, setInsufficientCollateral] = useState(); useEffect(() => { - if (!daiBalance) { + if (!debtBalance) { setInsufficientCollateral(undefined); return; } - if (Number(daiBalance) < parseFloat(formatEther(totals.interest))) { + if (Number(debtBalance) < parseFloat(formatEther(totals.interest))) { setInsufficientCollateral(true); } else { setInsufficientCollateral(false); } - }, [daiBalance, totals.interest]); + }, [debtBalance, totals.interest]); + + const handleVersionChange = (event: SelectChangeEvent) => { + setSelectedVersion(event.target.value as "v1" | "v2" | "v3"); + }; + + const handleConsolidate = () => { + if (!coolerAddress || !v3CoolerAddress) return; + coolerMutation.mutate( + { + fromCoolerAddress: coolerAddress, + toCoolerAddress: v3CoolerAddress, + fromClearingHouseAddress: selectedClearingHouse.clearingHouseAddress, + toClearingHouseAddress: clearingHouseAddresses.v3.clearingHouseAddress, + loanIds, + }, + { + onSuccess: () => { + setOpen(false); + setSelectedVersion(""); + }, + }, + ); + }; - console.log("consolidate loans"); - return <>; + const needsCoolerCreation = !v3CoolerAddress; + + if (!showConsolidateButton) return null; + + return ( + <> + {consolidatorActive && ( + setOpen(!open)}> + Consolidate Loans to {clearingHouseAddresses.v3.debtAssetName} + + )} + + Consolidate Loans + + } + onClose={() => setOpen(false)} + > + <> + {availableVersions.length > 1 && ( + + + + + + )} + {selectedVersion && ( + <> + + All existing open loans for this Cooler and Clearinghouse will be repaid and consolidated into a new + loan with a {duration} day duration. You must hold enough {clearingHouseAddresses.v3.debtAssetName} in + your wallet to cover the interest owed at consolidation. + + + Loans to Consolidate + + {loans.length} + + + + New Principal Amount + + + {formatNumber(parseFloat(formatEther(totals.principal)), 4)}{" "} + {clearingHouseAddresses.v3.debtAssetName} + + + + + Interest Owed At Consolidation + + + {formatNumber(parseFloat(formatEther(totals.interest)), 4)}{" "} + {clearingHouseAddresses.v3.debtAssetName} + + + + + New Maturity Date + + + {maturityDate.toLocaleDateString([], { + month: "long", + day: "numeric", + year: "numeric", + }) || ""}{" "} + {maturityDate.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + + + + + )} + {selectedVersion && !insufficientCollateral ? ( + + {needsCoolerCreation ? ( + + createCooler.mutate({ + collateralAddress: clearingHouseAddresses.v3.collateralAddress, + debtAddress: clearingHouseAddresses.v3.debtAddress, + factoryAddress, + }) + } + loading={createCooler.isLoading} + disabled={createCooler.isLoading} + > + Create Cooler & Consolidate + + ) : ( + Approve {clearingHouseAddresses.v3.debtAssetName} for Spending on the Consolidation Contract + } + spendAmount={allowances?.totalDebtWithFee} + approvalText={`Approve ${clearingHouseAddresses.v3.debtAssetName} for Spending`} + > + Approve gOHM for Spending on the Consolidation Contract} + spendAmount={allowances?.consolidatedLoanCollateral} + approvalText="Approve gOHM for Spending" + > + + Consolidate Loans + + + + )} + + ) : ( + + {insufficientCollateral + ? `Insufficient ${clearingHouseAddresses.v3.debtAssetName} Balance` + : "Select loans to consolidate"} + + )} + + + + ); }; diff --git a/src/views/Lending/Cooler/positions/Positions.tsx b/src/views/Lending/Cooler/positions/Positions.tsx index f69e0d1b80..9036ea866c 100644 --- a/src/views/Lending/Cooler/positions/Positions.tsx +++ b/src/views/Lending/Cooler/positions/Positions.tsx @@ -19,6 +19,7 @@ import { BorrowRate, OutstandingPrincipal, WeeklyCapacityRemaining } from "src/v import { useGetClearingHouse } from "src/views/Lending/Cooler/hooks/useGetClearingHouse"; import { useGetCoolerForWallet } from "src/views/Lending/Cooler/hooks/useGetCoolerForWallet"; import { useGetCoolerLoans } from "src/views/Lending/Cooler/hooks/useGetCoolerLoans"; +import { ConsolidateLoans } from "src/views/Lending/Cooler/positions/ConsolidateLoan"; import { CreateOrRepayLoan } from "src/views/Lending/Cooler/positions/CreateOrRepayLoan"; import { ExtendLoan } from "src/views/Lending/Cooler/positions/ExtendLoan"; import { useAccount } from "wagmi"; @@ -122,7 +123,7 @@ export const CoolerPositions = () => { }; console.log(allLoans, isFetchedLoansV1, isFetchedLoansV2, isFetchedLoansV3, address); - + console.log(coolerAddressV1, coolerAddressV2, coolerAddressV3); return (
@@ -273,6 +274,26 @@ export const CoolerPositions = () => { > Borrow {activeClearingHouse.debtAssetName} & Open Position + {allLoans.length > 1 || + (((loansV1 && loansV1.length > 0) || (loansV2 && loansV2.length > 0)) && + clearingHouses.v1 && + clearingHouses.v2 && + clearingHouses.v3 && ( + + ))} )}