Skip to content

Commit

Permalink
Convert() method on the bond (#17)
Browse files Browse the repository at this point in the history
Closes #13
  • Loading branch information
luckyrobot committed Mar 25, 2022
1 parent da303be commit 02f5ec4
Show file tree
Hide file tree
Showing 7 changed files with 434 additions and 241 deletions.
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint-staged
5 changes: 0 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,6 @@
"yarn run prettier:fix"
]
},
"husky": {
"hooks": {
"pre-commit": "yarn lint-staged"
}
},
"jest": {
"transformIgnorePatterns": [
"/node_modules/(?!@amcharts)"
Expand Down
266 changes: 266 additions & 0 deletions src/components/bond/BondAction/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import React, { useState } from 'react'
import { useParams } from 'react-router-dom'
import styled from 'styled-components'

import { formatUnits, parseUnits } from '@ethersproject/units'
import { TokenAmount } from '@josojo/honeyswap-sdk'
import { useTokenBalance } from '@usedapp/core'
import { useWeb3React } from '@web3-react/core'

import { ApprovalState, useApproveCallback } from '../../../hooks/useApproveCallback'
import { useBondDetails } from '../../../hooks/useBondDetails'
import { useBondContract } from '../../../hooks/useContract'
import { useConvertBond } from '../../../hooks/useConvertBond'
import { useIsBondRepaid } from '../../../hooks/useIsBondRepaid'
import { usePreviewBond } from '../../../hooks/usePreviewBond'
import { useRedeemBond } from '../../../hooks/useRedeemBond'
import { BondActions } from '../../../pages/Bond'
import { useActivePopups } from '../../../state/application/hooks'
import { useFetchTokenByAddress } from '../../../state/user/hooks'
import { ChainId, EASY_AUCTION_NETWORKS } from '../../../utils'
import { Button } from '../../buttons/Button'
import AmountInputPanel from '../../form/AmountInputPanel'
import ConfirmationModal from '../../modals/ConfirmationModal'

const ActionButton = styled(Button)`
flex-shrink: 0;
height: 40px;
margin-top: auto;
`

const ActionPanel = styled.div`
margin-bottom: 20px;
max-width: 300px;
`

const BondAction = ({ actionType }: { actionType: BondActions }) => {
const { account, chainId } = useWeb3React()
const fetchTok = useFetchTokenByAddress()
const activePopups = useActivePopups()

const bondIdentifier = useParams()
const { data: derivedBondInfo, loading: isLoading } = useBondDetails(bondIdentifier?.bondId)
const [bondTokenInfo, setBondTokenInfo] = useState(null)
const [collateralTokenInfo, setCollateralTokenInfo] = useState(null)
const [paymentTokenInfo, setPaymentTokenInfo] = useState(null)

const collateralTokenBalance = useTokenBalance(derivedBondInfo?.collateralToken, account, {
chainId,
})

const paymentTokenBalance = useTokenBalance(derivedBondInfo?.paymentToken, account, {
chainId,
})

const isRepaid = !!useIsBondRepaid(bondIdentifier?.bondId)
const isMatured = derivedBondInfo && new Date() > new Date(derivedBondInfo.maturityDate * 1000)

const [isOwner, setIsOwner] = useState(false)
const [bondsToRedeem, setBondsToRedeem] = useState('0')
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirmed
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
const [txHash, setTxHash] = useState<string>('')

const bigg = bondTokenInfo && parseUnits(bondsToRedeem, bondTokenInfo.decimals)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore its a big number i swear
const tokenAmount = bondTokenInfo && new TokenAmount(bondTokenInfo, bigg)
const [approval, approveCallback] = useApproveCallback(
tokenAmount,
EASY_AUCTION_NETWORKS[chainId as ChainId],
chainId as ChainId,
)

const bondContract = useBondContract(bondIdentifier?.bondId)
const { redeem } = useRedeemBond(tokenAmount, bondIdentifier?.bondId)
const { convert } = useConvertBond(tokenAmount, bondIdentifier?.bondId)

const [totalBalance, setTotalBalance] = useState('0')
const isApproved = approval !== ApprovalState.NOT_APPROVED && approval !== ApprovalState.PENDING

const onUserSellAmountInput = (theInput) => {
setBondsToRedeem(theInput || '0')
}

const resetModal = () => {
if (!pendingConfirmation) {
onUserSellAmountInput('')
}
setPendingConfirmation(true)
setAttemptingTxn(false)
}

React.useEffect(() => {
if (txHash && activePopups.length) {
onUserSellAmountInput('')
setPendingConfirmation(false)
setAttemptingTxn(false)
}
}, [activePopups, txHash])

const doTheAction = async () => {
let hash

setAttemptingTxn(true)

if (actionType === BondActions.Convert) {
hash = await convert().catch(() => {
resetModal()
})
}

if (actionType === BondActions.Redeem) {
hash = await redeem().catch(() => {
resetModal()
})
}

if (hash) {
setTxHash(hash)
setPendingConfirmation(false)
}
}

React.useEffect(() => {
if (!derivedBondInfo || !account || (!bondTokenInfo && bondContract)) return

if (actionType === BondActions.Redeem) {
setTotalBalance(formatUnits(paymentTokenBalance || 0, paymentTokenInfo?.decimals))
}

if (actionType === BondActions.Convert) {
setTotalBalance(formatUnits(collateralTokenBalance || 0, collateralTokenInfo?.decimals))
}
}, [
collateralTokenInfo,
paymentTokenInfo,
collateralTokenBalance,
paymentTokenBalance,
derivedBondInfo,
actionType,
account,
bondContract,
bondTokenInfo,
attemptingTxn,
])

const invalidBond = React.useMemo(
() => !bondIdentifier || !derivedBondInfo,
[bondIdentifier, derivedBondInfo],
)

React.useEffect(() => {
if (!isLoading && !invalidBond && account && derivedBondInfo) {
setIsOwner(derivedBondInfo.owner.toLowerCase() === account.toLowerCase())

fetchTok(bondIdentifier?.bondId).then((r) => {
setBondTokenInfo(r)
})
fetchTok(bondIdentifier?.collateralToken).then((r) => {
setCollateralTokenInfo(r)
})
fetchTok(bondIdentifier?.paymentToken).then((r) => {
setPaymentTokenInfo(r)
})
}
}, [derivedBondInfo, isLoading, invalidBond, account, fetchTok, bondIdentifier])

const isConvertable = React.useMemo(() => {
if (isMatured) return false

const hasBonds =
account &&
isOwner &&
isApproved &&
parseUnits(bondsToRedeem, collateralTokenInfo?.decimals).gt(0) &&
parseUnits(totalBalance, collateralTokenInfo?.decimals).gt(0) &&
parseUnits(bondsToRedeem, collateralTokenInfo?.decimals).lte(
parseUnits(totalBalance, collateralTokenInfo?.decimals),
)

return hasBonds
}, [
account,
totalBalance,
collateralTokenInfo?.decimals,
bondsToRedeem,
isApproved,
isMatured,
isOwner,
])

const isRedeemable = React.useMemo(() => {
const hasBonds =
account &&
isOwner &&
isApproved &&
parseUnits(bondsToRedeem, paymentTokenInfo?.decimals).gt(0) &&
parseUnits(totalBalance, paymentTokenInfo?.decimals).gt(0) &&
parseUnits(bondsToRedeem, paymentTokenInfo?.decimals).lte(
parseUnits(totalBalance, paymentTokenInfo?.decimals),
)

return hasBonds && (isRepaid || isMatured)
}, [
account,
totalBalance,
paymentTokenInfo?.decimals,
bondsToRedeem,
isApproved,
isMatured,
isOwner,
isRepaid,
])

if (isLoading || invalidBond || !bondTokenInfo) return null

return (
<ActionPanel>
<AmountInputPanel
balance={totalBalance}
chainId={bondTokenInfo.chainId}
onMax={() => {
setBondsToRedeem(totalBalance)
}}
onUserSellAmountInput={onUserSellAmountInput}
token={bondTokenInfo}
unlock={{ isLocked: !isApproved, onUnlock: approveCallback, unlockState: approval }}
value={bondsToRedeem}
wrap={{ isWrappable: false, onClick: null }}
/>
<div>
<div>{!isOwner && "You don't own this bond"}</div>
<ActionButton
disabled={actionType === BondActions.Convert ? !isConvertable : !isRedeemable}
onClick={doTheAction}
>
{actionType === BondActions.Redeem && 'Redeem'}
{actionType === BondActions.Convert && 'Convert'}
</ActionButton>
</div>

{actionType === BondActions.Redeem && (
<div>redeemable for this number of payment tokens </div>
)}
<div>redeemable for this number of collateral tokens</div>

<ConfirmationModal
attemptingTxn={attemptingTxn}
content={null}
hash={txHash}
isOpen={attemptingTxn}
onDismiss={() => {
resetModal()
}}
pendingConfirmation={pendingConfirmation}
pendingText={
actionType === BondActions.Redeem ? 'Placing redeem order' : 'Placing convert order'
}
title="Confirm Order"
width={504}
/>
</ActionPanel>
)
}

export default BondAction
64 changes: 64 additions & 0 deletions src/components/bond/BondHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react'
import styled, { css } from 'styled-components'

import { ButtonCopy } from '../../buttons/ButtonCopy'
import { NetworkIcon } from '../../icons/NetworkIcon'
import { PageTitle } from '../../pureStyledComponents/PageTitle'

const BondHeader = ({ bondId }) => (
<>
<Title>Bond Details</Title>
<SubTitleWrapperStyled>
<SubTitle>
<Network>
<NetworkIconStyled />
</Network>
<BondId>Bond Id #{bondId}</BondId>
</SubTitle>
<CopyButton copyValue={bondId} title="Copy bond id" />
</SubTitleWrapperStyled>
</>
)
const Title = styled(PageTitle)`
margin-bottom: 2px;
`

const SubTitleWrapperStyled = styled.div`
align-items: center;
display: flex;
margin-bottom: 20px;
`

const SubTitle = styled.h2`
align-items: center;
color: ${({ theme }) => theme.text1};
display: flex;
font-size: 15px;
font-weight: 400;
line-height: 1.2;
margin: 0 8px 0 0;
`
const BondId = styled.span`
align-items: center;
display: flex;
`

const IconCSS = css`
height: 14px;
width: 14px;
`

const CopyButton = styled(ButtonCopy)`
${IconCSS}
`

const Network = styled.span`
align-items: center;
display: flex;
margin-right: 5px;
`

const NetworkIconStyled = styled(NetworkIcon)`
${IconCSS}
`
export default BondHeader
44 changes: 44 additions & 0 deletions src/hooks/useConvertBond.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useCallback } from 'react'

import { TransactionResponse } from '@ethersproject/providers'
import { TokenAmount } from '@josojo/honeyswap-sdk'

import { useTransactionAdder } from '../state/transactions/hooks'
import { getLogger } from '../utils/logger'
import { useBondContract } from './useContract'

const logger = getLogger('useConvertBond')

// returns a variable indicating the state of the approval and a function which converts if necessary or early returns
export function useConvertBond(amountToConvert?: TokenAmount | null, addressToConvert?: string) {
const tokenContract = useBondContract(amountToConvert?.token?.address)
const addTransaction = useTransactionAdder()

const convert = useCallback(async (): Promise<string> => {
if (!tokenContract) {
logger.error('tokenContract is null')
return ''
}

if (!amountToConvert) {
logger.error('missing amount to convert')
return ''
}

const response: TransactionResponse = await tokenContract
.convert(amountToConvert.raw.toString())
.catch((error: Error) => {
logger.debug('Failed to convert token', error)
throw error
})

addTransaction(response, {
summary: 'Convert ' + amountToConvert?.token?.symbol,
approval: { tokenAddress: amountToConvert.token.address, spender: addressToConvert },
})

return response.hash
}, [tokenContract, addressToConvert, amountToConvert, addTransaction])

return { convert }
}
Loading

1 comment on commit 02f5ec4

@vercel
Copy link

@vercel vercel bot commented on 02f5ec4 Mar 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.