diff --git a/v3/components/BorrowModal/BorrowModal.tsx b/v3/components/BorrowModal/BorrowModal.tsx new file mode 100644 index 000000000..ac92c3455 --- /dev/null +++ b/v3/components/BorrowModal/BorrowModal.tsx @@ -0,0 +1,156 @@ +import { + Box, + Button, + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + Spinner, + Text, +} from '@chakra-ui/react'; +import { Amount } from '@snx-v3/Amount'; +import Wei from '@synthetixio/wei'; +import { TransactionStatus } from '@snx-v3/txnReducer'; +import { CheckIcon, CloseIcon } from '@snx-v3/Multistep'; +import { PropsWithChildren, useContext } from 'react'; +import { useParams } from '@snx-v3/useParams'; +import { ManagePositionContext } from '@snx-v3/ManagePositionContext'; +import { useCollateralType } from '@snx-v3/useCollateralTypes'; +import { useBorrow } from '../../lib/useBorrow'; + +function StepIcon({ txnStatus, children }: PropsWithChildren<{ txnStatus: TransactionStatus }>) { + switch (txnStatus) { + case 'error': + return ; + case 'success': + return ; + case 'prompting': + case 'pending': + return ; + default: + return ( + + {children} + + ); + } +} + +const statusColor = (txnStatus: TransactionStatus) => { + if (txnStatus === 'error' || txnStatus === 'success') return txnStatus; + return 'gray.700'; +}; +export const BorrowModalUi: React.FC<{ + onClose: () => void; + debtChange: Wei; + isOpen: boolean; + txnStatus: TransactionStatus; + execBorrow: () => void; +}> = ({ onClose, isOpen, debtChange, txnStatus, execBorrow }) => { + return ( + + + + Complete this action + + + + + 1 + + + Borrow + + + + + + + ); +}; + +export const BorrowModal: React.FC<{ + onClose: () => void; + isOpen: boolean; +}> = ({ onClose, isOpen }) => { + const { debtChange } = useContext(ManagePositionContext); + const params = useParams(); + const collateralType = useCollateralType(params.collateralSymbol); + const { + exec: execBorrow, + txnState, + settle: settleBorrow, + } = useBorrow({ + accountId: params.accountId, + poolId: params.poolId, + collateralTypeAddress: collateralType?.tokenAddress, + debtChange, + }); + const { txnStatus } = txnState; + if (!params.poolId || !params.accountId || !collateralType) return null; + return ( + { + settleBorrow(); + onClose(); + }} + isOpen={isOpen} + /> + ); +}; diff --git a/v3/components/BorrowModal/index.ts b/v3/components/BorrowModal/index.ts new file mode 100644 index 000000000..a6ffcffb1 --- /dev/null +++ b/v3/components/BorrowModal/index.ts @@ -0,0 +1 @@ +export * from './BorrowModal'; diff --git a/v3/components/BorrowModal/package.json b/v3/components/BorrowModal/package.json new file mode 100644 index 000000000..d7b00b4c7 --- /dev/null +++ b/v3/components/BorrowModal/package.json @@ -0,0 +1,17 @@ +{ + "name": "@snx-v3/BorrowModal", + "private": true, + "main": "index.ts", + "version": "0.0.1", + "dependencies": { + "@chakra-ui/react": "^2.2.8", + "@snx-v3/Amount": "workspace:*", + "@snx-v3/ManagePositionContext": "workspace:*", + "@snx-v3/Multistep": "workspace:*", + "@snx-v3/txnReducer": "workspace:*", + "@snx-v3/useCollateralTypes": "workspace:*", + "@snx-v3/useParams": "workspace:*", + "@synthetixio/wei": "workspace:*", + "react": "^18.2.0" + } +} diff --git a/v3/lib/useBorrow/index.ts b/v3/lib/useBorrow/index.ts new file mode 100644 index 000000000..dad6f3218 --- /dev/null +++ b/v3/lib/useBorrow/index.ts @@ -0,0 +1 @@ +export * from './useBorrow'; diff --git a/v3/lib/useBorrow/package.json b/v3/lib/useBorrow/package.json new file mode 100644 index 000000000..1f68de079 --- /dev/null +++ b/v3/lib/useBorrow/package.json @@ -0,0 +1,18 @@ +{ + "name": "@snx-v3/useBorrow", + "private": true, + "main": "index.ts", + "version": "0.0.1", + "dependencies": { + "@snx-v3/txnReducer": "workspace:*", + "@snx-v3/useBlockchain": "workspace:*", + "@snx-v3/useCoreProxy": "workspace:*", + "@snx-v3/useGasOptions": "workspace:*", + "@snx-v3/useGasPrice": "workspace:*", + "@snx-v3/useGasSpeed": "workspace:*", + "@synthetixio/wei": "workspace:*", + "@tanstack/react-query": "^4.3.4", + "ethers": "^5.7.2", + "react": "^18.2.0" + } +} diff --git a/v3/lib/useBorrow/useBorrow.tsx b/v3/lib/useBorrow/useBorrow.tsx new file mode 100644 index 000000000..0b308e955 --- /dev/null +++ b/v3/lib/useBorrow/useBorrow.tsx @@ -0,0 +1,106 @@ +import { useReducer } from 'react'; +import { useCoreProxy, CoreProxyContractType } from '@snx-v3/useCoreProxy'; +import { formatGasPriceForTransaction } from '@snx-v3/useGasOptions'; +import { useMutation } from '@tanstack/react-query'; +import { useNetwork, useSigner } from '@snx-v3/useBlockchain'; +import { initialState, reducer } from '@snx-v3/txnReducer'; +import Wei from '@synthetixio/wei'; +import { BigNumber } from 'ethers'; +import { getGasPrice } from '@snx-v3/useGasPrice'; +import { useGasSpeed } from '@snx-v3/useGasSpeed'; + +const createPopulateTransaction = ({ + CoreProxy, + accountId, + poolId, + collateralTypeAddress, + debtChange, +}: { + CoreProxy?: CoreProxyContractType; + accountId?: string; + poolId?: string; + collateralTypeAddress?: string; + debtChange: Wei; +}) => { + if (!(CoreProxy && poolId && accountId && collateralTypeAddress)) return; + if (debtChange.eq(0)) return; + + return () => + CoreProxy.populateTransaction.mintUsd( + BigNumber.from(accountId), + BigNumber.from(poolId), + collateralTypeAddress, + debtChange.toBN(), + { + gasLimit: CoreProxy.estimateGas.mintUsd( + BigNumber.from(accountId), + BigNumber.from(poolId), + collateralTypeAddress, + debtChange.toBN() + ), + } + ); +}; +export const useBorrow = ( + { + accountId, + poolId, + collateralTypeAddress, + debtChange, + }: { + accountId?: string; + poolId?: string; + collateralTypeAddress?: string; + debtChange: Wei; + }, + eventHandlers?: { onSuccess?: () => void; onMutate?: () => void; onError?: (e: Error) => void } +) => { + const [txnState, dispatch] = useReducer(reducer, initialState); + const { gasSpeed } = useGasSpeed(); + const { data: CoreProxy } = useCoreProxy(); + const populateTransaction = createPopulateTransaction({ + CoreProxy, + accountId, + poolId, + collateralTypeAddress, + debtChange, + }); + const signer = useSigner(); + const { name: networkName, id: networkId } = useNetwork(); + + const mutation = useMutation(async () => { + if (!signer || !populateTransaction) return; + try { + dispatch({ type: 'prompting' }); + + const [populatedTxn, gasPrices] = await Promise.all([ + populateTransaction(), + getGasPrice({ networkId, networkName }), + ]); + const gasLimit = populatedTxn.gasLimit || BigNumber.from(0); + const gasOptionsForTransaction = formatGasPriceForTransaction({ + gasLimit, + gasPrices, + gasSpeed, + }); + const txn = await signer.sendTransaction({ + ...populatedTxn, + ...gasOptionsForTransaction, + }); + dispatch({ type: 'pending', payload: { txnHash: txn.hash } }); + + await txn.wait(); + dispatch({ type: 'success' }); + } catch (error: any) { + dispatch({ type: 'error', payload: { error } }); + throw error; + } + }, eventHandlers); + return { + mutation, + txnState, + settle: () => dispatch({ type: 'settled' }), + isLoading: mutation.isLoading, + exec: mutation.mutateAsync, + }; +}; diff --git a/v3/ui/package.json b/v3/ui/package.json index c72e69921..c2172e89b 100644 --- a/v3/ui/package.json +++ b/v3/ui/package.json @@ -26,6 +26,7 @@ "@snx-v3/Amount": "workspace:*", "@snx-v3/Balance": "workspace:*", "@snx-v3/BorderBox": "workspace:*", + "@snx-v3/BorrowModal": "workspace:*", "@snx-v3/CollateralTypeSelector": "workspace:*", "@snx-v3/Constants": "workspace:*", "@snx-v3/DepositModal": "workspace:*", diff --git a/v3/ui/src/pages/Manage/Manage.tsx b/v3/ui/src/pages/Manage/Manage.tsx index 56b47e4da..e8c9814f1 100644 --- a/v3/ui/src/pages/Manage/Manage.tsx +++ b/v3/ui/src/pages/Manage/Manage.tsx @@ -61,7 +61,7 @@ export const ManageUi: FC<{ collateralType: CollateralType }> = ({ collateralTyp export const Manage = () => { const params = useParams(); - const collateralType = useCollateralType(params.collateral); + const collateralType = useCollateralType(params.collateralSymbol); if (!collateralType) { return ; // TODO skeleton diff --git a/v3/ui/src/pages/Manage/ManageActions.tsx b/v3/ui/src/pages/Manage/ManageActions.tsx index b9e611de3..d27391eff 100644 --- a/v3/ui/src/pages/Manage/ManageActions.tsx +++ b/v3/ui/src/pages/Manage/ManageActions.tsx @@ -25,6 +25,7 @@ import { Withdraw } from './Withdraw'; import { Deposit } from './Deposit'; import { z } from 'zod'; import { RepayModal } from '@snx-v3/RepayModal'; +import { BorrowModal } from '@snx-v3/BorrowModal'; import { DepositModal } from '@snx-v3/DepositModal'; const validActions = ['borrow', 'deposit', 'repay', 'withdraw'] as const; @@ -115,6 +116,7 @@ export const ManageAction = () => { const [txnModalOpen, setTxnModalOpen] = useState(null); const { debtChange, collateralChange, setCollateralChange, setDebtChange } = useContext(ManagePositionContext); + const collateralType = useCollateralType(params.collateralSymbol); const liquidityPosition = useLiquidityPosition({ accountId: params.accountId, @@ -154,7 +156,7 @@ export const ManageAction = () => { if (!form.reportValidity() || !isValid) { return; } - if (parsedAction === 'repay' || parsedAction === 'deposit') { + if (parsedAction === 'repay' || parsedAction === 'deposit' || parsedAction === 'borrow') { setTxnModalOpen(parsedAction); } else { // TODO add more hooks for all actions and remove this @@ -205,6 +207,15 @@ export const ManageAction = () => { }} isOpen={txnModalOpen === 'repay'} /> + { + liquidityPosition.refetch(); + setCollateralChange(wei(0)); + setDebtChange(wei(0)); + setTxnModalOpen(null); + }} + isOpen={txnModalOpen === 'borrow'} + /> { diff --git a/v3/ui/src/pages/Manage/ManageStats.tsx b/v3/ui/src/pages/Manage/ManageStats.tsx index e3f93f5a7..da9db5190 100644 --- a/v3/ui/src/pages/Manage/ManageStats.tsx +++ b/v3/ui/src/pages/Manage/ManageStats.tsx @@ -104,7 +104,7 @@ export const ManageStats = () => { const params = useParams(); const { debtChange, collateralChange } = useContext(ManagePositionContext); - const collateralType = useCollateralType(params.collateral); + const collateralType = useCollateralType(params.collateralSymbol); const { data: liquidityPosition } = useLiquidityPosition({ tokenAddress: collateralType?.tokenAddress, accountId: params.accountId, diff --git a/yarn.lock b/yarn.lock index d8fb9595b..f534fde48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7557,6 +7557,22 @@ __metadata: languageName: unknown linkType: soft +"@snx-v3/BorrowModal@workspace:*, @snx-v3/BorrowModal@workspace:v3/components/BorrowModal": + version: 0.0.0-use.local + resolution: "@snx-v3/BorrowModal@workspace:v3/components/BorrowModal" + dependencies: + "@chakra-ui/react": ^2.2.8 + "@snx-v3/Amount": "workspace:*" + "@snx-v3/ManagePositionContext": "workspace:*" + "@snx-v3/Multistep": "workspace:*" + "@snx-v3/txnReducer": "workspace:*" + "@snx-v3/useCollateralTypes": "workspace:*" + "@snx-v3/useParams": "workspace:*" + "@synthetixio/wei": "workspace:*" + react: ^18.2.0 + languageName: unknown + linkType: soft + "@snx-v3/CollateralTypeSelector@workspace:*, @snx-v3/CollateralTypeSelector@workspace:v3/components/CollateralTypeSelector": version: 0.0.0-use.local resolution: "@snx-v3/CollateralTypeSelector@workspace:v3/components/CollateralTypeSelector" @@ -7808,6 +7824,7 @@ __metadata: "@snx-v3/Amount": "workspace:*" "@snx-v3/Balance": "workspace:*" "@snx-v3/BorderBox": "workspace:*" + "@snx-v3/BorrowModal": "workspace:*" "@snx-v3/CollateralTypeSelector": "workspace:*" "@snx-v3/Constants": "workspace:*" "@snx-v3/DepositModal": "workspace:*" @@ -7949,6 +7966,23 @@ __metadata: languageName: unknown linkType: soft +"@snx-v3/useBorrow@workspace:v3/lib/useBorrow": + version: 0.0.0-use.local + resolution: "@snx-v3/useBorrow@workspace:v3/lib/useBorrow" + dependencies: + "@snx-v3/txnReducer": "workspace:*" + "@snx-v3/useBlockchain": "workspace:*" + "@snx-v3/useCoreProxy": "workspace:*" + "@snx-v3/useGasOptions": "workspace:*" + "@snx-v3/useGasPrice": "workspace:*" + "@snx-v3/useGasSpeed": "workspace:*" + "@synthetixio/wei": "workspace:*" + "@tanstack/react-query": ^4.3.4 + ethers: ^5.7.2 + react: ^18.2.0 + languageName: unknown + linkType: soft + "@snx-v3/useCollateralTypes@workspace:*, @snx-v3/useCollateralTypes@workspace:v3/lib/useCollateralTypes": version: 0.0.0-use.local resolution: "@snx-v3/useCollateralTypes@workspace:v3/lib/useCollateralTypes"