diff --git a/earn/src/components/lend/BorrowingWidget.tsx b/earn/src/components/lend/BorrowingWidget.tsx index 80fb849f..b9f7bbc4 100644 --- a/earn/src/components/lend/BorrowingWidget.tsx +++ b/earn/src/components/lend/BorrowingWidget.tsx @@ -12,6 +12,7 @@ import styled from 'styled-components'; import { computeLTV } from '../../data/BalanceSheet'; import { BorrowerNftBorrower } from '../../data/BorrowerNft'; import { LendingPair, LendingPairBalancesMap } from '../../data/LendingPair'; +import MulticallOperator from '../../data/operations/MulticallOperator'; import { rgba } from '../../util/Colors'; import HealthGauge from '../common/HealthGauge'; import BorrowModal from './modal/BorrowModal'; @@ -120,6 +121,7 @@ export type BorrowingWidgetProps = { tokenBalances: LendingPairBalancesMap; tokenQuotes: Map; tokenColors: Map; + multicallOperator: MulticallOperator; setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; }; @@ -142,7 +144,7 @@ function filterBySelection(lendingPairs: LendingPair[], selection: Token | null) } export default function BorrowingWidget(props: BorrowingWidgetProps) { - const { borrowers, lendingPairs, tokenBalances, tokenColors, setPendingTxn } = props; + const { borrowers, lendingPairs, tokenBalances, tokenColors, multicallOperator, setPendingTxn } = props; // selection/hover state for Available Table const [selectedCollateral, setSelectedCollateral] = useState(null); @@ -442,6 +444,7 @@ export default function BorrowingWidget(props: BorrowingWidgetProps) { selectedCollateral={selectedCollateral} selectedBorrow={selectedBorrows} userBalance={tokenBalances.get(selectedCollateral.address)?.gn ?? GN.zero(selectedCollateral.decimals)} + multicallOperator={multicallOperator} setIsOpen={() => { setSelectedBorrows(null); setSelectedCollateral(null); @@ -454,6 +457,7 @@ export default function BorrowingWidget(props: BorrowingWidgetProps) { isOpen={selectedBorrower != null} borrower={selectedBorrower.borrower} lendingPair={lendingPairs.find((pair) => pair.uniswapPool === selectedBorrower.borrower.uniswapPool)} + multicallOperator={multicallOperator} setIsOpen={() => { setSelectedBorrower(null); }} @@ -464,6 +468,7 @@ export default function BorrowingWidget(props: BorrowingWidgetProps) { { setSelectedBorrower(null); }} diff --git a/earn/src/components/lend/modal/BorrowModal.tsx b/earn/src/components/lend/modal/BorrowModal.tsx index d72cf3a5..7b4d67f4 100644 --- a/earn/src/components/lend/modal/BorrowModal.tsx +++ b/earn/src/components/lend/modal/BorrowModal.tsx @@ -8,7 +8,7 @@ import { borrowerNftAbi } from 'shared/lib/abis/BorrowerNft'; import { factoryAbi } from 'shared/lib/abis/Factory'; import { permit2Abi } from 'shared/lib/abis/Permit2'; import { volatilityOracleAbi } from 'shared/lib/abis/VolatilityOracle'; -import { FilledGradientButton } from 'shared/lib/components/common/Buttons'; +import { FilledStylizedButton } from 'shared/lib/components/common/Buttons'; import { SquareInputWithMax } from 'shared/lib/components/common/Input'; import Modal from 'shared/lib/components/common/Modal'; import TokenAmountInput from 'shared/lib/components/common/TokenAmountInput'; @@ -26,11 +26,12 @@ import { Permit2State, usePermit2 } from 'shared/lib/data/hooks/UsePermit2'; import { Token } from 'shared/lib/data/Token'; import { formatNumberInput } from 'shared/lib/util/Numbers'; import { generateBytes12Salt } from 'shared/lib/util/Salt'; -import { useAccount, useBalance, useContractRead, useContractWrite, usePrepareContractWrite } from 'wagmi'; +import { useAccount, useBalance, useContractRead } from 'wagmi'; import { ChainContext } from '../../../App'; import { computeLTV } from '../../../data/BalanceSheet'; import { LendingPair } from '../../../data/LendingPair'; +import MulticallOperator from '../../../data/operations/MulticallOperator'; import { RateModel, yieldPerSecondToAPR } from '../../../data/RateModel'; const MAX_BORROW_PERCENTAGE = 0.8; @@ -64,7 +65,7 @@ const permit2StateToButtonStateMap = { function getConfirmButton(state: ConfirmButtonState, token: Token): { text: string; enabled: boolean } { switch (state) { case ConfirmButtonState.READY: - return { text: 'Confirm', enabled: true }; + return { text: 'Add Action', enabled: true }; case ConfirmButtonState.LOADING: return { text: 'Loading', enabled: false }; case ConfirmButtonState.PERMIT_ASSET: @@ -85,7 +86,7 @@ function getConfirmButton(state: ConfirmButtonState, token: Token): { text: stri return { text: 'Connect Wallet', enabled: false }; case ConfirmButtonState.DISABLED: default: - return { text: 'Confirm', enabled: false }; + return { text: 'Add Action', enabled: false }; } } @@ -95,17 +96,18 @@ export type BorrowModalProps = { selectedCollateral: Token; selectedBorrow: Token; userBalance: GN; + multicallOperator: MulticallOperator; setIsOpen: (isOpen: boolean) => void; setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; }; export default function BorrowModal(props: BorrowModalProps) { - const { isOpen, selectedLendingPair, selectedCollateral, selectedBorrow, userBalance, setIsOpen, setPendingTxn } = + const { isOpen, selectedLendingPair, selectedCollateral, selectedBorrow, userBalance, multicallOperator, setIsOpen } = props; const [collateralAmountStr, setCollateralAmountStr] = useState(''); const [borrowAmountStr, setBorrowAmountStr] = useState(''); - const { activeChain } = useContext(ChainContext); + const { activeChain } = useContext(ChainContext); const { address: userAddress } = useAccount(); const { data: consultData } = useContractRead({ @@ -265,17 +267,6 @@ export default function BorrowModal(props: BorrowModalProps) { ); }, [permit2Result, predictedAddress, selectedCollateral.address, userAddress]); - // Prepare for actual import/mint transaction - const borrowerNft = useMemo(() => new ethers.utils.Interface(borrowerNftAbi), []); - // First, we `mint` so that they have a `Borrower` to put stuff in - const encodedMint = useMemo(() => { - if (!userAddress || selectedLendingPair?.uniswapPool === undefined) return null; - const to = userAddress; - const pools = [selectedLendingPair.uniswapPool ?? '0x']; - const salts = [generatedSalt]; - return borrowerNft.encodeFunctionData('mint', [to, pools, salts]) as `0x${string}`; - }, [userAddress, selectedLendingPair?.uniswapPool, generatedSalt, borrowerNft]); - const encodedBorrowCall = useMemo(() => { if (!userAddress || !selectedLendingPair || !selectedBorrow) return null; const borrower = new ethers.utils.Interface(borrowerAbi); @@ -287,50 +278,12 @@ export default function BorrowModal(props: BorrowModalProps) { return borrower.encodeFunctionData('borrow', [amount0.toBigNumber(), amount1.toBigNumber(), userAddress]); }, [borrowAmount, selectedBorrow, selectedLendingPair, userAddress]); - const encodedModify = useMemo(() => { - if (!userAddress || nextNftPtrIdx === undefined || ante === undefined || !encodedPermit2 || !encodedBorrowCall) - return null; - const owner = userAddress; - const indices = [nextNftPtrIdx]; - const managers = [ALOE_II_PERMIT2_MANAGER_ADDRESS[activeChain.id]]; - const datas = [encodedPermit2.concat(encodedBorrowCall.slice(2))]; - const antes = [ante.toBigNumber().div(1e13)]; - return borrowerNft.encodeFunctionData('modify', [owner, indices, managers, datas, antes]) as `0x${string}`; - }, [userAddress, nextNftPtrIdx, ante, activeChain.id, encodedPermit2, encodedBorrowCall, borrowerNft]); - - const { - config: configMulticallOps, - isError: isUnableToMulticallOps, - isLoading: isCheckingIfAbleToMulticallOps, - } = usePrepareContractWrite({ - address: ALOE_II_BORROWER_NFT_ADDRESS[activeChain.id], - abi: borrowerNftAbi, - functionName: 'multicall', - args: [[encodedMint ?? '0x', encodedModify ?? '0x']], - overrides: { value: ante?.toBigNumber() }, - chainId: activeChain.id, - enabled: userAddress && Boolean(encodedMint) && Boolean(encodedModify) && parameterData !== undefined, - }); - const gasLimit = configMulticallOps.request?.gasLimit.mul(110).div(100); - const { write: borrow, isLoading: isAskingUserToMulticallOps } = useContractWrite({ - ...configMulticallOps, - request: { - ...configMulticallOps.request, - gasLimit, - }, - onSuccess(data) { - setIsOpen(false); - setPendingTxn(data); - }, - }); - let confirmButtonState: ConfirmButtonState; if (!userAddress) { confirmButtonState = ConfirmButtonState.CONNECT_WALLET; } else if (ante === undefined || maxBorrowSupplyConstraint == null || maxBorrowHealthConstraint == null) { confirmButtonState = ConfirmButtonState.LOADING; } else if ( - isAskingUserToMulticallOps || permit2State === Permit2State.ASKING_USER_TO_SIGN || permit2State === Permit2State.ASKING_USER_TO_APPROVE ) { @@ -427,26 +380,34 @@ export default function BorrowModal(props: BorrowModalProps) { - { - // TODO: clean this up if (permit2State !== Permit2State.DONE) { permit2Action?.(); - } else if ( - confirmButton.enabled && - !isUnableToMulticallOps && - !isCheckingIfAbleToMulticallOps && - configMulticallOps - ) { - borrow?.(); + return; } + if (!userAddress || encodedPermit2 == null || encodedBorrowCall == null || ante === undefined) return; + multicallOperator + .addMintOperation({ + to: userAddress, + pools: [selectedLendingPair.uniswapPool], + salts: [generatedSalt], + }) + .addModifyOperation({ + owner: userAddress, + indices: [nextNftPtrIdx?.toNumber() ?? 0], + managers: [ALOE_II_PERMIT2_MANAGER_ADDRESS[activeChain.id]], + data: [encodedPermit2.concat(encodedBorrowCall.slice(2)) as `0x${string}`], + antes: [ante], + }); + setIsOpen(false); }} > {confirmButton.text} - + By borrowing, you agree to our{' '} diff --git a/earn/src/components/lend/modal/OperationsModal.tsx b/earn/src/components/lend/modal/OperationsModal.tsx new file mode 100644 index 00000000..b45c9f73 --- /dev/null +++ b/earn/src/components/lend/modal/OperationsModal.tsx @@ -0,0 +1,113 @@ +import { useContext, useMemo } from 'react'; + +import { SendTransactionResult } from '@wagmi/core'; +import { ethers } from 'ethers'; +import { borrowerNftAbi } from 'shared/lib/abis/BorrowerNft'; +import { FilledStylizedButton } from 'shared/lib/components/common/Buttons'; +import Modal from 'shared/lib/components/common/Modal'; +import { Text } from 'shared/lib/components/common/Typography'; +import { ALOE_II_BORROWER_NFT_ADDRESS } from 'shared/lib/data/constants/ChainSpecific'; +import { useAccount, useContractWrite, usePrepareContractWrite } from 'wagmi'; + +import { ChainContext } from '../../../App'; +import MulticallOperator from '../../../data/operations/MulticallOperator'; + +export type OperationsModalProps = { + multicallOperator: MulticallOperator; + isOpen: boolean; + setIsOpen: (open: boolean) => void; + setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; +}; + +export default function OperationsModal(props: OperationsModalProps) { + const { multicallOperator, isOpen, setIsOpen, setPendingTxn } = props; + + const { address: userAddress } = useAccount(); + const { activeChain } = useContext(ChainContext); + + const mintOperation = multicallOperator.combineMintOperations(); + const modifyOperation = multicallOperator.combineModifyOperations(); + const combinedAnte = multicallOperator.getCombinedAnte(); + + const borrowerNft = useMemo(() => new ethers.utils.Interface(borrowerNftAbi), []); + const encodedMint = + mintOperation && + (borrowerNft.encodeFunctionData('mint', [ + mintOperation.to, + mintOperation.pools, + mintOperation.salts, + ]) as `0x${string}`); + + const encodedModify = + modifyOperation && + (borrowerNft.encodeFunctionData('modify', [ + modifyOperation.owner, + modifyOperation.indices, + modifyOperation.managers, + modifyOperation.data, + modifyOperation.antes.map((ante) => ante.toBigNumber().div(1e13)), + ]) as `0x${string}`); + + const functionName = mintOperation ? 'multicall' : 'modify'; + + const args = mintOperation + ? [[encodedMint ?? '0x', encodedModify]] + : [ + modifyOperation.owner, + modifyOperation.indices, + modifyOperation.managers, + modifyOperation.data, + modifyOperation.antes.map((ante) => ante.toBigNumber().div(1e13)), + ]; + + const { + config: configMulticallOps, + isError: isUnableToMulticallOps, + isLoading: isCheckingIfAbleToMulticallOps, + } = usePrepareContractWrite({ + address: ALOE_II_BORROWER_NFT_ADDRESS[activeChain.id], + abi: borrowerNftAbi, + functionName: functionName, + args: args as any, + overrides: { + value: combinedAnte.toBigNumber(), + }, + chainId: activeChain.id, + enabled: userAddress !== undefined, + }); + const gasLimit = configMulticallOps.request?.gasLimit.mul(110).div(100); + const { write: call, isLoading: isAskingUserToMulticallOps } = useContractWrite({ + ...configMulticallOps, + request: { + ...configMulticallOps.request, + gasLimit, + }, + onSuccess(data) { + setIsOpen(false); + setPendingTxn(data); + }, + }); + + return ( + +
+ + You are performing {mintOperation?.salts.length ?? 0} mint operations and{' '} + {modifyOperation?.indices.length ?? 0} modify operations in a single transaction. This will save you gas fees. + + { + call?.(); + }} + disabled={ + isAskingUserToMulticallOps || isCheckingIfAbleToMulticallOps || isUnableToMulticallOps || !userAddress + } + > + Confirm + +
+
+ ); +} diff --git a/earn/src/components/lend/modal/UpdateBorrowerModal.tsx b/earn/src/components/lend/modal/UpdateBorrowerModal.tsx index d02c7631..1bbe579b 100644 --- a/earn/src/components/lend/modal/UpdateBorrowerModal.tsx +++ b/earn/src/components/lend/modal/UpdateBorrowerModal.tsx @@ -9,6 +9,7 @@ import styled from 'styled-components'; import { BorrowerNftBorrower } from '../../../data/BorrowerNft'; import { LendingPair } from '../../../data/LendingPair'; +import MulticallOperator from '../../../data/operations/MulticallOperator'; import BorrowModalContent from './content/BorrowModalContent'; import RepayModalContent from './content/RepayModalContent'; @@ -50,13 +51,14 @@ const TabButton = styled.button` export type UpdateBorrowerModalProps = { isOpen: boolean; borrower: BorrowerNftBorrower; + multicallOperator: MulticallOperator; lendingPair?: LendingPair; setIsOpen: (isOpen: boolean) => void; setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; }; export default function UpdateBorrowerModal(props: UpdateBorrowerModalProps) { - const { isOpen, borrower, lendingPair, setIsOpen, setPendingTxn } = props; + const { isOpen, borrower, lendingPair, multicallOperator, setIsOpen, setPendingTxn } = props; const [confirmationType, setConfirmationType] = useState(ConfirmationType.BORROW); return ( @@ -85,6 +87,7 @@ export default function UpdateBorrowerModal(props: UpdateBorrowerModalProps) { void; setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; }; export default function UpdateCollateralModal(props: UpdateCollateralModalProps) { - const { isOpen, borrower, setIsOpen, setPendingTxn } = props; + const { isOpen, borrower, multicallOperator, setIsOpen, setPendingTxn } = props; const [confirmationType, setConfirmationType] = useState(ConfirmationType.DEPOSIT); return ( @@ -83,6 +85,7 @@ export default function UpdateCollateralModal(props: UpdateCollateralModalProps) @@ -90,6 +93,7 @@ export default function UpdateCollateralModal(props: UpdateCollateralModalProps) diff --git a/earn/src/components/lend/modal/content/AddCollateralModalContent.tsx b/earn/src/components/lend/modal/content/AddCollateralModalContent.tsx index d62dce5a..58fea6d3 100644 --- a/earn/src/components/lend/modal/content/AddCollateralModalContent.tsx +++ b/earn/src/components/lend/modal/content/AddCollateralModalContent.tsx @@ -1,23 +1,26 @@ -import { useContext, useState } from 'react'; +import { useContext, useMemo, useState } from 'react'; import { SendTransactionResult } from '@wagmi/core'; -import { erc20Abi } from 'shared/lib/abis/ERC20'; +import { BigNumber, ethers } from 'ethers'; +import { borrowerAbi } from 'shared/lib/abis/Borrower'; +import { permit2Abi } from 'shared/lib/abis/Permit2'; import { FilledStylizedButton } from 'shared/lib/components/common/Buttons'; -import { MODAL_BLACK_TEXT_COLOR } from 'shared/lib/components/common/Modal'; import TokenAmountInput from 'shared/lib/components/common/TokenAmountInput'; import { Text } from 'shared/lib/components/common/Typography'; +import { ALOE_II_PERMIT2_MANAGER_ADDRESS } from 'shared/lib/data/constants/ChainSpecific'; import { TERMS_OF_SERVICE_URL } from 'shared/lib/data/constants/Values'; import { GN, GNFormat } from 'shared/lib/data/GoodNumber'; +import { Permit2State, usePermit2 } from 'shared/lib/data/hooks/UsePermit2'; import { Token } from 'shared/lib/data/Token'; -import { useAccount, useBalance, useContractWrite, usePrepareContractWrite } from 'wagmi'; +import { useAccount, useBalance } from 'wagmi'; import { ChainContext } from '../../../../App'; import { isHealthy } from '../../../../data/BalanceSheet'; import { BorrowerNftBorrower } from '../../../../data/BorrowerNft'; import { Assets } from '../../../../data/MarginAccount'; +import MulticallOperator from '../../../../data/operations/MulticallOperator'; import HealthBar from '../../../borrow/HealthBar'; -const GAS_ESTIMATE_WIGGLE_ROOM = 110; const SECONDARY_COLOR = '#CCDFED'; const TERTIARY_COLOR = '#4b6980'; @@ -44,10 +47,10 @@ function getConfirmButton(state: ConfirmButtonState, token: Token): { text: stri case ConfirmButtonState.WAITING_FOR_USER: return { text: 'Check Wallet', enabled: false }; case ConfirmButtonState.READY: - return { text: 'Confirm', enabled: true }; + return { text: 'Add Action', enabled: true }; case ConfirmButtonState.DISABLED: default: - return { text: 'Confirm', enabled: false }; + return { text: 'Add Action', enabled: false }; } } @@ -56,74 +59,112 @@ type ConfirmButtonProps = { maxDepositAmount: GN; borrower: BorrowerNftBorrower; token: Token; + isDepositingToken0: boolean; + multicallOperator: MulticallOperator; setIsOpen: (isOpen: boolean) => void; setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; }; function ConfirmButton(props: ConfirmButtonProps) { - const { depositAmount, maxDepositAmount, borrower, token, setIsOpen, setPendingTxn } = props; + const { depositAmount, maxDepositAmount, borrower, token, isDepositingToken0, multicallOperator, setIsOpen } = props; + const { address: accountAddress } = useAccount(); const { activeChain } = useContext(ChainContext); - const insufficientAssets = depositAmount.gt(maxDepositAmount); + const { + state: permit2State, + action: permit2Action, + result: permit2Result, + } = usePermit2( + activeChain, + token, + accountAddress ?? '0x', + ALOE_II_PERMIT2_MANAGER_ADDRESS[activeChain.id], + depositAmount + ); - const { config: depositConfig, isLoading: isCheckingIfCanDeposit } = usePrepareContractWrite({ - address: token.address, - abi: erc20Abi, - functionName: 'transfer', - args: [borrower.address, depositAmount.toBigNumber()], - enabled: Boolean(depositAmount) && !insufficientAssets, - chainId: activeChain.id, - }); - const gasLimit = depositConfig.request?.gasLimit.mul(GAS_ESTIMATE_WIGGLE_ROOM).div(100); - const { write: deposit, isLoading: isAskingUserToConfirm } = useContractWrite({ - ...depositConfig, - request: { - ...depositConfig.request, - gasLimit, - }, - onSuccess(data) { - setIsOpen(false); - setPendingTxn(data); - }, - }); + const encodedPermit2 = useMemo(() => { + if (!accountAddress || !permit2Result.signature) return null; + const permit2 = new ethers.utils.Interface(permit2Abi); + return permit2.encodeFunctionData( + 'permitTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes)', + [ + { + permitted: { + token: token.address, + amount: permit2Result.amount.toBigNumber(), + }, + nonce: BigNumber.from(permit2Result.nonce ?? '0'), + deadline: BigNumber.from(permit2Result.deadline), + }, + { + to: borrower.address, + requestedAmount: permit2Result.amount.toBigNumber(), + }, + accountAddress, + permit2Result.signature, + ] + ); + }, [permit2Result, token, accountAddress, borrower.address]); + + const encodedDepositCall = useMemo(() => { + const borrowerInterface = new ethers.utils.Interface(borrowerAbi); + const amount0 = isDepositingToken0 ? depositAmount : GN.zero(borrower.token0.decimals); + const amount1 = isDepositingToken0 ? GN.zero(borrower.token1.decimals) : depositAmount; + + return borrowerInterface.encodeFunctionData('transfer', [ + amount0.toBigNumber(), + amount1.toBigNumber(), + borrower.address, + ]) as `0x${string}`; + }, [isDepositingToken0, depositAmount, borrower.token0.decimals, borrower.token1.decimals, borrower.address]); let confirmButtonState: ConfirmButtonState = ConfirmButtonState.READY; - if (isCheckingIfCanDeposit) { - confirmButtonState = ConfirmButtonState.LOADING; - } else if (depositAmount.isZero()) { + if (depositAmount.isZero()) { confirmButtonState = ConfirmButtonState.DISABLED; } else if (depositAmount.gt(maxDepositAmount)) { confirmButtonState = ConfirmButtonState.INSUFFICIENT_ASSET; - } else if (isAskingUserToConfirm) { - confirmButtonState = ConfirmButtonState.WAITING_FOR_USER; - } else if (!depositConfig.request) { - confirmButtonState = ConfirmButtonState.DISABLED; } const confirmButton = getConfirmButton(confirmButtonState, token); return ( - deposit?.()} - disabled={!confirmButton.enabled} - > - {confirmButton.text} - +
+ { + if (permit2State !== Permit2State.DONE) { + permit2Action?.(); + return; + } + if (!accountAddress || encodedPermit2 == null || encodedDepositCall == null) return; + multicallOperator.addModifyOperation({ + owner: accountAddress, + indices: [borrower.index], + managers: [ALOE_II_PERMIT2_MANAGER_ADDRESS[activeChain.id]], + data: [encodedPermit2.concat(encodedDepositCall.slice(2)) as `0x${string}`], + antes: [GN.zero(18)], + }); + setIsOpen(false); + }} + > + {confirmButton.text} + +
); } export type AddCollateralModalContentProps = { borrower: BorrowerNftBorrower; + multicallOperator: MulticallOperator; setIsOpen: (isOpen: boolean) => void; setPendingTxnResult: (result: SendTransactionResult | null) => void; }; export default function AddCollateralModalContent(props: AddCollateralModalContentProps) { - const { borrower, setIsOpen, setPendingTxnResult } = props; + const { borrower, multicallOperator, setIsOpen, setPendingTxnResult } = props; const [depositAmountStr, setDepositAmountStr] = useState(''); const { activeChain } = useContext(ChainContext); @@ -213,6 +254,8 @@ export default function AddCollateralModalContent(props: AddCollateralModalConte maxDepositAmount={maxDepositAmount} borrower={borrower} token={collateralToken} + isDepositingToken0={isDepositingToken0} + multicallOperator={multicallOperator} setIsOpen={setIsOpen} setPendingTxn={setPendingTxnResult} /> diff --git a/earn/src/components/lend/modal/content/BorrowModalContent.tsx b/earn/src/components/lend/modal/content/BorrowModalContent.tsx index 72c13107..85a42a34 100644 --- a/earn/src/components/lend/modal/content/BorrowModalContent.tsx +++ b/earn/src/components/lend/modal/content/BorrowModalContent.tsx @@ -3,14 +3,11 @@ import { ChangeEvent, useContext, useMemo, useState } from 'react'; import { Address, SendTransactionResult } from '@wagmi/core'; import { ethers } from 'ethers'; import { borrowerAbi } from 'shared/lib/abis/Borrower'; -import { borrowerNftAbi } from 'shared/lib/abis/BorrowerNft'; import { factoryAbi } from 'shared/lib/abis/Factory'; import { FilledStylizedButton } from 'shared/lib/components/common/Buttons'; import { SquareInputWithMax } from 'shared/lib/components/common/Input'; -import { MODAL_BLACK_TEXT_COLOR } from 'shared/lib/components/common/Modal'; import { Display, Text } from 'shared/lib/components/common/Typography'; import { - ALOE_II_BORROWER_NFT_ADDRESS, ALOE_II_BORROWER_NFT_SIMPLE_MANAGER_ADDRESS, ALOE_II_FACTORY_ADDRESS, } from 'shared/lib/data/constants/ChainSpecific'; @@ -18,17 +15,17 @@ import { TERMS_OF_SERVICE_URL } from 'shared/lib/data/constants/Values'; import { GN, GNFormat } from 'shared/lib/data/GoodNumber'; import { Token } from 'shared/lib/data/Token'; import { formatNumberInput } from 'shared/lib/util/Numbers'; -import { useAccount, useBalance, useContractRead, useContractWrite, usePrepareContractWrite } from 'wagmi'; +import { useAccount, useBalance, useContractRead } from 'wagmi'; import { ChainContext } from '../../../../App'; import { isHealthy, maxBorrowAndWithdraw } from '../../../../data/BalanceSheet'; import { BorrowerNftBorrower } from '../../../../data/BorrowerNft'; import { LendingPair } from '../../../../data/LendingPair'; import { Liabilities } from '../../../../data/MarginAccount'; +import MulticallOperator from '../../../../data/operations/MulticallOperator'; import { RateModel, yieldPerSecondToAPR } from '../../../../data/RateModel'; import HealthBar from '../../../borrow/HealthBar'; -const GAS_ESTIMATE_WIGGLE_ROOM = 110; const SECONDARY_COLOR = '#CCDFED'; const TERTIARY_COLOR = '#4b6980'; @@ -47,7 +44,7 @@ function getConfirmButton(state: ConfirmButtonState, token: Token): { text: stri case ConfirmButtonState.PENDING: return { text: 'Pending', enabled: false }; case ConfirmButtonState.READY: - return { text: 'Confirm', enabled: true }; + return { text: 'Add Action', enabled: true }; case ConfirmButtonState.UNHEALTHY: return { text: 'Insufficient Collateral', enabled: false }; case ConfirmButtonState.NOT_ENOUGH_SUPPLY: @@ -58,7 +55,7 @@ function getConfirmButton(state: ConfirmButtonState, token: Token): { text: stri return { text: 'Loading...', enabled: false }; case ConfirmButtonState.DISABLED: default: - return { text: 'Confirm', enabled: false }; + return { text: 'Add Action', enabled: false }; } } @@ -72,6 +69,7 @@ type ConfirmButtonProps = { token: Token; isBorrowingToken0: boolean; accountAddress?: Address; + multicallOperator: MulticallOperator; setIsOpen: (isOpen: boolean) => void; setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; }; @@ -87,12 +85,12 @@ function ConfirmButton(props: ConfirmButtonProps) { token, isBorrowingToken0, accountAddress, + multicallOperator, setIsOpen, - setPendingTxn, } = props; const { activeChain } = useContext(ChainContext); - const encodedBorrowCall = useMemo(() => { + const encodedModify = useMemo(() => { if (!accountAddress) return null; const borrowerInterface = new ethers.utils.Interface(borrowerAbi); const amount0 = isBorrowingToken0 ? borrowAmount : GN.zero(borrower.token0.decimals); @@ -105,77 +103,54 @@ function ConfirmButton(props: ConfirmButtonProps) { ]) as `0x${string}`; }, [borrowAmount, borrower.token0.decimals, borrower.token1.decimals, isBorrowingToken0, accountAddress]); - const { config: borrowConfig, isLoading: isCheckingIfAbleToBorrow } = usePrepareContractWrite({ - address: ALOE_II_BORROWER_NFT_ADDRESS[activeChain.id], - abi: borrowerNftAbi, - functionName: 'modify', - args: [ - accountAddress ?? '0x', - [borrower.index], - [ALOE_II_BORROWER_NFT_SIMPLE_MANAGER_ADDRESS[activeChain.id]], - [encodedBorrowCall ?? '0x'], - [requiredAnte?.toBigNumber().div(1e13).toNumber() ?? 0], - ], - overrides: { value: requiredAnte?.toBigNumber() }, - chainId: activeChain.id, - enabled: - accountAddress && encodedBorrowCall != null && requiredAnte !== undefined && !isUnhealthy && !notEnoughSupply, - }); - const gasLimit = borrowConfig.request?.gasLimit.mul(GAS_ESTIMATE_WIGGLE_ROOM).div(100); - const { write: borrow, isLoading: isAskingUserToConfirm } = useContractWrite({ - ...borrowConfig, - request: { - ...borrowConfig.request, - gasLimit, - }, - onSuccess(data) { - setIsOpen(false); - setPendingTxn(data); - }, - }); - let confirmButtonState: ConfirmButtonState = ConfirmButtonState.READY; if (isLoading) { confirmButtonState = ConfirmButtonState.LOADING; - } else if (isAskingUserToConfirm) { - confirmButtonState = ConfirmButtonState.WAITING_FOR_USER; } else if (borrowAmount.isZero()) { confirmButtonState = ConfirmButtonState.DISABLED; } else if (isUnhealthy) { confirmButtonState = ConfirmButtonState.UNHEALTHY; } else if (notEnoughSupply) { confirmButtonState = ConfirmButtonState.NOT_ENOUGH_SUPPLY; - } else if (isCheckingIfAbleToBorrow && !borrowConfig.request) { - confirmButtonState = ConfirmButtonState.LOADING; - } else if (!borrowConfig.request) { - confirmButtonState = ConfirmButtonState.DISABLED; } const confirmButton = getConfirmButton(confirmButtonState, token); return ( - borrow?.()} - disabled={!confirmButton.enabled} - > - {confirmButton.text} - +
+ { + if (accountAddress === undefined || encodedModify == null || requiredAnte === undefined) return; + multicallOperator.addModifyOperation({ + owner: accountAddress, + indices: [borrower.index], + managers: [ALOE_II_BORROWER_NFT_SIMPLE_MANAGER_ADDRESS[activeChain.id]], + data: [encodedModify], + antes: [requiredAnte], + }); + setIsOpen(false); + }} + > + {confirmButton.text} + +
); } export type BorrowModalContentProps = { borrower: BorrowerNftBorrower; + multicallOperator: MulticallOperator; lendingPair?: LendingPair; setIsOpen: (isOpen: boolean) => void; setPendingTxnResult: (result: SendTransactionResult | null) => void; }; export default function BorrowModalContent(props: BorrowModalContentProps) { - const { borrower, lendingPair, setIsOpen, setPendingTxnResult } = props; + const { borrower, lendingPair, multicallOperator, setIsOpen, setPendingTxnResult } = props; const [additionalBorrowAmountStr, setAdditionalBorrowAmountStr] = useState(''); @@ -340,6 +315,7 @@ export default function BorrowModalContent(props: BorrowModalContentProps) { token={borrowToken} isBorrowingToken0={isBorrowingToken0} accountAddress={accountAddress} + multicallOperator={multicallOperator} setIsOpen={setIsOpen} setPendingTxn={setPendingTxnResult} /> diff --git a/earn/src/components/lend/modal/content/RemoveCollateralModalContent.tsx b/earn/src/components/lend/modal/content/RemoveCollateralModalContent.tsx index 4e6c840f..2e2a095d 100644 --- a/earn/src/components/lend/modal/content/RemoveCollateralModalContent.tsx +++ b/earn/src/components/lend/modal/content/RemoveCollateralModalContent.tsx @@ -3,28 +3,22 @@ import { useContext, useMemo, useState } from 'react'; import { Address, SendTransactionResult } from '@wagmi/core'; import { ethers } from 'ethers'; import { borrowerAbi } from 'shared/lib/abis/Borrower'; -import { borrowerNftAbi } from 'shared/lib/abis/BorrowerNft'; import { FilledStylizedButton } from 'shared/lib/components/common/Buttons'; -import { MODAL_BLACK_TEXT_COLOR } from 'shared/lib/components/common/Modal'; import TokenAmountInput from 'shared/lib/components/common/TokenAmountInput'; import { Text } from 'shared/lib/components/common/Typography'; -import { - ALOE_II_BORROWER_NFT_ADDRESS, - ALOE_II_BORROWER_NFT_MULTI_MANAGER_ADDRESS, - ALOE_II_BORROWER_NFT_SIMPLE_MANAGER_ADDRESS, -} from 'shared/lib/data/constants/ChainSpecific'; +import { ALOE_II_BORROWER_NFT_SIMPLE_MANAGER_ADDRESS } from 'shared/lib/data/constants/ChainSpecific'; import { TERMS_OF_SERVICE_URL } from 'shared/lib/data/constants/Values'; import { GN, GNFormat } from 'shared/lib/data/GoodNumber'; import { Token } from 'shared/lib/data/Token'; -import { useAccount, useBalance, useContractWrite, usePrepareContractWrite } from 'wagmi'; +import { useAccount, useBalance } from 'wagmi'; import { ChainContext } from '../../../../App'; import { isHealthy, maxWithdraws } from '../../../../data/BalanceSheet'; import { BorrowerNftBorrower } from '../../../../data/BorrowerNft'; import { Assets } from '../../../../data/MarginAccount'; +import MulticallOperator from '../../../../data/operations/MulticallOperator'; import HealthBar from '../../../borrow/HealthBar'; -const GAS_ESTIMATE_WIGGLE_ROOM = 110; const SECONDARY_COLOR = '#CCDFED'; const TERTIARY_COLOR = '#4b6980'; @@ -48,10 +42,10 @@ function getConfirmButton(state: ConfirmButtonState, token: Token): { text: stri case ConfirmButtonState.WAITING_FOR_USER: return { text: 'Check Wallet', enabled: false }; case ConfirmButtonState.READY: - return { text: 'Confirm', enabled: true }; + return { text: 'Add Action', enabled: true }; case ConfirmButtonState.DISABLED: default: - return { text: 'Confirm', enabled: false }; + return { text: 'Add Action', enabled: false }; } } @@ -63,6 +57,7 @@ type ConfirmButtonProps = { isWithdrawingToken0: boolean; shouldWithdrawAnte: boolean; accountAddress: Address; + multicallOperator: MulticallOperator; setIsOpen: (isOpen: boolean) => void; setPendingTxn: (pendingTxn: SendTransactionResult | null) => void; }; @@ -76,8 +71,8 @@ function ConfirmButton(props: ConfirmButtonProps) { isWithdrawingToken0, shouldWithdrawAnte, accountAddress, + multicallOperator, setIsOpen, - setPendingTxn, } = props; const { activeChain } = useContext(ChainContext); @@ -120,78 +115,49 @@ function ConfirmButton(props: ConfirmButtonProps) { ) as `0x${string}`; }, [encodedWithdrawCall, encodedWithdrawAnteCall]); - const { config: withdrawConfig, isLoading: isCheckingIfAbleToWithdraw } = usePrepareContractWrite({ - address: ALOE_II_BORROWER_NFT_ADDRESS[activeChain.id], - abi: borrowerNftAbi, - functionName: 'modify', - args: [ - accountAddress ?? '0x', - [borrower.index], - [ - shouldWithdrawAnte - ? ALOE_II_BORROWER_NFT_MULTI_MANAGER_ADDRESS[activeChain.id] - : ALOE_II_BORROWER_NFT_SIMPLE_MANAGER_ADDRESS[activeChain.id], - ], - [(shouldWithdrawAnte ? combinedEncodingsForMultiManager : encodedWithdrawCall) ?? '0x'], - [0], - ], - chainId: activeChain.id, - enabled: - accountAddress && - encodedWithdrawCall != null && - !isRedeemingTooMuch && - !(shouldWithdrawAnte && !encodedWithdrawAnteCall), - }); - const gasLimit = withdrawConfig.request?.gasLimit.mul(GAS_ESTIMATE_WIGGLE_ROOM).div(100); - const { write: withdraw, isLoading: isAskingUserToConfirm } = useContractWrite({ - ...withdrawConfig, - request: { - ...withdrawConfig.request, - gasLimit, - }, - onSuccess(data) { - setIsOpen(false); - setPendingTxn(data); - }, - }); - let confirmButtonState: ConfirmButtonState = ConfirmButtonState.READY; - if (isCheckingIfAbleToWithdraw) { - confirmButtonState = ConfirmButtonState.LOADING; - } else if (withdrawAmount.isZero()) { + if (withdrawAmount.isZero()) { confirmButtonState = ConfirmButtonState.DISABLED; } else if (isRedeemingTooMuch) { confirmButtonState = ConfirmButtonState.REDEEM_TOO_MUCH; - } else if (isAskingUserToConfirm) { - confirmButtonState = ConfirmButtonState.WAITING_FOR_USER; - } else if (!withdrawConfig.request) { - confirmButtonState = ConfirmButtonState.DISABLED; } const confirmButton = getConfirmButton(confirmButtonState, token); return ( - withdraw?.()} - disabled={!confirmButton.enabled} - > - {confirmButton.text} - +
+ { + if (accountAddress === undefined) return; + multicallOperator.addModifyOperation({ + owner: accountAddress, + indices: [borrower.index], + managers: [ALOE_II_BORROWER_NFT_SIMPLE_MANAGER_ADDRESS[activeChain.id]], + data: [(shouldWithdrawAnte ? combinedEncodingsForMultiManager : encodedWithdrawCall) ?? '0x'], + antes: [GN.zero(18)], + }); + setIsOpen(false); + }} + > + {confirmButton.text} + +
); } export type RemoveCollateralModalContentProps = { borrower: BorrowerNftBorrower; + multicallOperator: MulticallOperator; setIsOpen: (isOpen: boolean) => void; setPendingTxnResult: (result: SendTransactionResult | null) => void; }; export default function RemoveCollateralModalContent(props: RemoveCollateralModalContentProps) { - const { borrower, setIsOpen, setPendingTxnResult } = props; + const { borrower, multicallOperator, setIsOpen, setPendingTxnResult } = props; const [withdrawAmountStr, setWithdrawAmountStr] = useState(''); @@ -292,6 +258,7 @@ export default function RemoveCollateralModalContent(props: RemoveCollateralModa isWithdrawingToken0={isWithdrawingToken0} shouldWithdrawAnte={shouldWithdrawAnte} accountAddress={accountAddress || '0x'} + multicallOperator={multicallOperator} setIsOpen={setIsOpen} setPendingTxn={setPendingTxnResult} /> diff --git a/earn/src/data/operations/MintOperation.ts b/earn/src/data/operations/MintOperation.ts new file mode 100644 index 00000000..9d191afc --- /dev/null +++ b/earn/src/data/operations/MintOperation.ts @@ -0,0 +1,7 @@ +import { Address } from 'wagmi'; + +export type MintOperation = { + to: Address; + pools: Address[]; + salts: `0x${string}`[]; +}; diff --git a/earn/src/data/operations/ModifyOperation.ts b/earn/src/data/operations/ModifyOperation.ts new file mode 100644 index 00000000..bbbf9902 --- /dev/null +++ b/earn/src/data/operations/ModifyOperation.ts @@ -0,0 +1,10 @@ +import { GN } from 'shared/lib/data/GoodNumber'; +import { Address } from 'wagmi'; + +export type ModifyOperation = { + owner: Address; + indices: number[]; + managers: Address[]; + data: `0x${string}`[]; + antes: GN[]; +}; diff --git a/earn/src/data/operations/MulticallOperator.ts b/earn/src/data/operations/MulticallOperator.ts new file mode 100644 index 00000000..3740829c --- /dev/null +++ b/earn/src/data/operations/MulticallOperator.ts @@ -0,0 +1,106 @@ +import { GN } from 'shared/lib/data/GoodNumber'; +import { Address } from 'wagmi'; + +import { MintOperation } from './MintOperation'; +import { ModifyOperation } from './ModifyOperation'; + +type MulticallOperatorObserver = () => void; + +export default class MulticallOperator { + private owner: Address | undefined; + private mintOperations: MintOperation[]; + private modifyOperations: ModifyOperation[]; + private observers: MulticallOperatorObserver[]; + + constructor() { + this.mintOperations = []; + this.modifyOperations = []; + this.observers = []; + } + + subscribe(observer: MulticallOperatorObserver) { + this.observers.push(observer); + } + + unsubscribe(observer: MulticallOperatorObserver) { + this.observers = this.observers.filter((o) => o !== observer); + } + + notify() { + this.observers.forEach((observer) => observer()); + } + + addMintOperation(mintOperation: MintOperation): MulticallOperator { + this.mintOperations.push(mintOperation); + this.notify(); + return this; + } + + addModifyOperation(modifyOperation: ModifyOperation): MulticallOperator { + if (this.owner === undefined) { + this.owner = modifyOperation.owner; + } else if (this.owner !== modifyOperation.owner) { + throw new Error('Cannot add modify operation for a different owner'); + } + this.modifyOperations.push(modifyOperation); + this.notify(); + return this; + } + + getMintOperations() { + return this.mintOperations; + } + + getModifyOperations() { + return this.modifyOperations; + } + + combineMintOperations(): MintOperation | undefined { + if (this.mintOperations.length === 0) { + return undefined; + } + let mintOperation: MintOperation = { + to: this.mintOperations[0].to, + pools: [], + salts: [], + }; + for (const op of this.mintOperations) { + if (mintOperation.to !== op.to) { + throw new Error('Cannot combine mint operations for different recipients'); + } + mintOperation.pools.push(...op.pools); + mintOperation.salts.push(...op.salts); + } + return mintOperation; + } + + combineModifyOperations(): ModifyOperation { + if (this.modifyOperations.length === 0 || this.owner === undefined) { + throw new Error('Cannot combine modify operations'); + } + let modifyOperation: ModifyOperation = { + owner: this.owner, + indices: [], + managers: [], + data: [], + antes: [], + }; + for (const op of this.modifyOperations) { + modifyOperation.indices.push(...op.indices); + modifyOperation.managers.push(...op.managers); + modifyOperation.data.push(...op.data); + modifyOperation.antes.push(...op.antes); + } + return modifyOperation; + } + + getCombinedAnte(): GN { + let ante = GN.zero(18); + for (const modifyOperation of this.modifyOperations) { + for (const a of modifyOperation.antes) { + ante = ante.add(a); + } + } + return ante; + } +} diff --git a/earn/src/pages/MarketsPage.tsx b/earn/src/pages/MarketsPage.tsx index 045cc2a7..b5468969 100644 --- a/earn/src/pages/MarketsPage.tsx +++ b/earn/src/pages/MarketsPage.tsx @@ -2,7 +2,9 @@ import { useContext, useEffect, useMemo, useState } from 'react'; import { SendTransactionResult } from '@wagmi/core'; import axios, { AxiosResponse } from 'axios'; +import ShoppingCartIcon from 'shared/lib/assets/svg/ShoppingCart'; import AppPage from 'shared/lib/components/common/AppPage'; +import { FilledGreyButtonWithIcon } from 'shared/lib/components/common/Buttons'; import { Text } from 'shared/lib/components/common/Typography'; import { GREY_400, GREY_600 } from 'shared/lib/data/constants/Colors'; import { GetNumericFeeTier } from 'shared/lib/data/FeeTier'; @@ -15,12 +17,14 @@ import { ChainContext } from '../App'; import PendingTxnModal, { PendingTxnModalStatus } from '../components/common/PendingTxnModal'; import InfoTab from '../components/info/InfoTab'; import BorrowingWidget from '../components/lend/BorrowingWidget'; +import OperationsModal from '../components/lend/modal/OperationsModal'; import SupplyTable, { SupplyTableRow } from '../components/lend/SupplyTable'; import { BorrowerNftBorrower, fetchListOfFuse2BorrowNfts } from '../data/BorrowerNft'; import { API_PRICE_RELAY_LATEST_URL } from '../data/constants/Values'; import { useLendingPairs } from '../data/hooks/UseLendingPairs'; import { getLendingPairBalances, LendingPairBalancesMap } from '../data/LendingPair'; import { fetchBorrowerDatas, UniswapPoolInfo } from '../data/MarginAccount'; +import MulticallOperation from '../data/operations/MulticallOperator'; import { PriceRelayLatestResponse } from '../data/PriceRelayResponse'; import { getProminentColor } from '../util/Colors'; @@ -80,6 +84,9 @@ export default function MarketsPage() { const [isPendingTxnModalOpen, setIsPendingTxnModalOpen] = useState(false); const [pendingTxnModalStatus, setPendingTxnModalStatus] = useState(null); const [selectedHeaderOption, setSelectedHeaderOption] = useState(HeaderOptions.Supply); + const [multicallOperator] = useState(new MulticallOperation()); + const [, forceUpdate] = useState({}); + const [isOperationsModalOpen, setIsOperationsModalOpen] = useState(false); // MARK: custom hooks const { lendingPairs } = useLendingPairs(); @@ -118,6 +125,14 @@ export default function MarketsPage() { return Array.from(tokenSet.values()); }, [lendingPairs]); + useEffect(() => { + const update = () => forceUpdate({}); + multicallOperator.subscribe(update); + return () => { + multicallOperator.unsubscribe(update); + }; + }, [multicallOperator]); + useEffect(() => { (async () => { if (!pendingTxn) return; @@ -281,6 +296,7 @@ export default function MarketsPage() { tokenBalances={balancesMap} tokenQuotes={tokenQuotes} tokenColors={tokenColors} + multicallOperator={multicallOperator} setPendingTxn={setPendingTxn} /> ); @@ -306,31 +322,43 @@ export default function MarketsPage() { {activeChain.name} Markets
-
- setSelectedHeaderOption(HeaderOptions.Supply)} - role='tab' - aria-selected={selectedHeaderOption === HeaderOptions.Supply} +
+
+ setSelectedHeaderOption(HeaderOptions.Supply)} + role='tab' + aria-selected={selectedHeaderOption === HeaderOptions.Supply} + > + Supply + + setSelectedHeaderOption(HeaderOptions.Borrow)} + role='tab' + aria-selected={selectedHeaderOption === HeaderOptions.Borrow} + > + Borrow + + setSelectedHeaderOption(HeaderOptions.Monitor)} + role='tab' + aria-selected={selectedHeaderOption === HeaderOptions.Monitor} + > + Monitor {doesGuardianSenseManipulation ? '🚨' : ''} + +
+ } + position='leading' + svgColorType='stroke' + disabled={multicallOperator.getModifyOperations().length === 0} + onClick={() => setIsOperationsModalOpen(true)} > - Supply - - setSelectedHeaderOption(HeaderOptions.Borrow)} - role='tab' - aria-selected={selectedHeaderOption === HeaderOptions.Borrow} - > - Borrow - - setSelectedHeaderOption(HeaderOptions.Monitor)} - role='tab' - aria-selected={selectedHeaderOption === HeaderOptions.Monitor} - > - Monitor{doesGuardianSenseManipulation ? ' 🚨' : ''} - + {multicallOperator.getModifyOperations().length} Operations +
@@ -351,6 +379,14 @@ export default function MarketsPage() { }} status={pendingTxnModalStatus} /> + {isOperationsModalOpen && ( + + )} ); } diff --git a/shared/src/assets/svg/ShoppingCart.tsx b/shared/src/assets/svg/ShoppingCart.tsx new file mode 100644 index 00000000..b673ba90 --- /dev/null +++ b/shared/src/assets/svg/ShoppingCart.tsx @@ -0,0 +1,30 @@ +/* eslint-disable max-len */ +import { SVGProps } from '.'; + +export default function Power(props: SVGProps) { + return ( + + + + + + ); +} diff --git a/shared/src/data/hooks/UsePermit2.ts b/shared/src/data/hooks/UsePermit2.ts index f62517f9..34a6c910 100644 --- a/shared/src/data/hooks/UsePermit2.ts +++ b/shared/src/data/hooks/UsePermit2.ts @@ -29,6 +29,13 @@ export enum Permit2State { DONE, } +export type Permit2Result = { + amount: GN; + nonce: uint256 | null; + deadline: uint256; + signature: `0x${string}` | undefined; +}; + type uint256 = string; type address = string; @@ -89,7 +96,17 @@ function evmCurrentTimePlus(secondsFromNow: number) { return (Date.now() / 1000 + secondsFromNow).toFixed(0); } -export function usePermit2(chain: Chain, token: Token, owner: Address, spender: Address, amount: GN) { +export function usePermit2( + chain: Chain, + token: Token, + owner: Address, + spender: Address, + amount: GN +): { + state: Permit2State; + action: (() => void) | undefined; + result: Permit2Result; +} { /*////////////////////////////////////////////////////////////// REACT STATE //////////////////////////////////////////////////////////////*/