Skip to content

Commit

Permalink
fix: spending limits for tokens without balance (#1646)
Browse files Browse the repository at this point in the history
* fix: spending limits for tokens without balance

* fix: remove unused scripts from package.json

* refactor: use tokens selector

* refactor: snake case constant

* refactor: add types, small adjustments

* fix: reloading on balance updates, fallback image
  • Loading branch information
schmanu authored Feb 13, 2023
1 parent 8a1e599 commit 14271ec
Show file tree
Hide file tree
Showing 15 changed files with 278 additions and 95 deletions.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
"cmp": "./scripts/cmp.sh",
"routes": "node scripts/generate-routes.js > src/config/routes.ts && prettier -w src/config/routes.ts && cat src/config/routes.ts",
"css-vars": "ts-node-esm ./scripts/css-vars.ts > ./src/styles/vars.css && prettier -w src/styles/vars.css",
"generate-types:safeDeployments": "typechain --target ethers-v5 --out-dir src/types/contracts ./node_modules/@gnosis.pm/safe-deployments/dist/assets/**/*.json",
"generate-types:spendingLimit": "typechain --target ethers-v5 --out-dir src/types/contracts ./node_modules/@gnosis.pm/safe-modules-deployments/dist/assets/**/*.json",
"generate-types": "yarn generate-types:safeDeployments && yarn generate-types:spendingLimit",
"generate-types": "typechain --target ethers-v5 --out-dir src/types/contracts ./node_modules/@gnosis.pm/safe-deployments/dist/assets/**/*.json ./node_modules/@gnosis.pm/safe-modules-deployments/dist/assets/**/*.json ./node_modules/@openzeppelin/contracts/build/contracts/ERC20.json",
"postinstall": "yarn generate-types && yarn css-vars",
"analyze": "cross-env ANALYZE=true yarn build",
"cypress:open": "cross-env TZ=UTC cypress open",
Expand Down Expand Up @@ -86,6 +84,7 @@
},
"devDependencies": {
"@next/bundle-analyzer": "^13.1.1",
"@openzeppelin/contracts": "^4.8.1",
"@safe-global/safe-core-sdk-types": "^1.9.0",
"@sentry/types": "^7.28.1",
"@svgr/webpack": "^6.3.1",
Expand Down
9 changes: 8 additions & 1 deletion src/components/common/ImageFallback/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ const ImageFallback = ({ src, fallbackSrc, fallbackComponent, ...props }: ImageF

if (isError && fallbackComponent) return fallbackComponent

return <img {...props} alt={props.alt} src={isError ? fallbackSrc : src} onError={() => setIsError(true)} />
return (
<img
{...props}
alt={props.alt}
src={isError || src === undefined ? fallbackSrc : src}
onError={() => setIsError(true)}
/>
)
}

export default ImageFallback
8 changes: 4 additions & 4 deletions src/components/common/TokenIcon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { type ReactElement } from 'react'
import ImageFallback from '../ImageFallback'
import css from './styles.module.css'

const FALLBACK_ICON = '/images/common/token-placeholder.svg'

const TokenIcon = ({
logoUri,
tokenSymbol,
Expand All @@ -12,10 +14,8 @@ const TokenIcon = ({
tokenSymbol?: string
size?: number
fallbackSrc?: string
}): ReactElement | null => {
const FALLBACK_ICON = '/images/common/token-placeholder.svg'

return !logoUri ? null : (
}): ReactElement => {
return (
<ImageFallback
src={logoUri}
alt={tokenSymbol}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export const ReviewSpendingLimit = ({ data, onSubmit }: Props) => {

useEffect(() => {
const existingSpendingLimit = spendingLimits.find(
(spendingLimit) => spendingLimit.beneficiary === data.beneficiary && spendingLimit.token === data.tokenAddress,
(spendingLimit) =>
spendingLimit.beneficiary === data.beneficiary && spendingLimit.token.address === data.tokenAddress,
)
setExistingSpendingLimit(existingSpendingLimit)
}, [spendingLimits, data])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ export const RemoveSpendingLimit = ({ data, onSubmit }: { data: SpendingLimitSta
const chainId = useChainId()
const provider = useWeb3()
const { balances } = useBalances()
const token = balances.items.find((item) => item.tokenInfo.address === data.token)
const token = balances.items.find((item) => item.tokenInfo.address === data.token.address)

const [safeTx, safeTxError] = useAsync<SafeTransaction>(() => {
const spendingLimitAddress = getSpendingLimitModuleAddress(chainId)
if (!provider || !spendingLimitAddress) return

const spendingLimitInterface = getSpendingLimitInterface()
const txData = spendingLimitInterface.encodeFunctionData('deleteAllowance', [data.beneficiary, data.token])
const txData = spendingLimitInterface.encodeFunctionData('deleteAllowance', [data.beneficiary, data.token.address])

const txParams = {
to: spendingLimitAddress,
Expand Down
152 changes: 99 additions & 53 deletions src/components/settings/SpendingLimits/SpendingLimitsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import EnhancedTable from '@/components/common/EnhancedTable'
import useBalances from '@/hooks/useBalances'
import DeleteIcon from '@/public/images/common/delete.svg'
import { safeFormatUnits } from '@/utils/formatters'
import { Box, IconButton, SvgIcon } from '@mui/material'
import { Box, IconButton, Skeleton, SvgIcon, Typography } from '@mui/material'
import { relativeTime } from '@/utils/date'
import EthHashInfo from '@/components/common/EthHashInfo'
import { useMemo, useState } from 'react'
Expand All @@ -24,10 +23,57 @@ const RemoveSpendingLimitSteps: TxStepperProps['steps'] = [
},
]

export const SpendingLimitsTable = ({ spendingLimits }: { spendingLimits: SpendingLimitState[] }) => {
const SKELETON_ROWS = new Array(3).fill('').map(() => {
return {
cells: {
beneficiary: {
rawValue: '0x',
content: (
<Box display="flex" flexDirection="row" gap={1} alignItems="center">
<Skeleton variant="circular" width={26} height={26} />
<div>
<Typography>
<Skeleton width={75} />
</Typography>
<Typography>
<Skeleton width={300} />
</Typography>
</div>
</Box>
),
},
spent: {
rawValue: '0',
content: (
<Box display="flex" flexDirection="row" gap={1} alignItems="center">
<Skeleton variant="circular" width={26} height={26} />
<Typography>
<Skeleton width={100} />
</Typography>
</Box>
),
},
resetTime: {
rawValue: '0',
content: (
<Typography>
<Skeleton />
</Typography>
),
},
},
}
})

export const SpendingLimitsTable = ({
spendingLimits,
isLoading,
}: {
spendingLimits: SpendingLimitState[]
isLoading: boolean
}) => {
const [open, setOpen] = useState<boolean>(false)
const [initialData, setInitialData] = useState<SpendingLimitState>()
const { balances } = useBalances()
const isGranted = useIsGranted()

const shouldHideactions = !isGranted
Expand All @@ -49,58 +95,58 @@ export const SpendingLimitsTable = ({ spendingLimits }: { spendingLimits: Spendi

const rows = useMemo(
() =>
spendingLimits.map((spendingLimit) => {
const token = balances.items.find((item) => item.tokenInfo.address === spendingLimit.token)
const amount = BigNumber.from(spendingLimit.amount)
const formattedAmount = safeFormatUnits(amount, token?.tokenInfo.decimals)
isLoading
? SKELETON_ROWS
: spendingLimits.map((spendingLimit) => {
const amount = BigNumber.from(spendingLimit.amount)
const formattedAmount = safeFormatUnits(amount, spendingLimit.token.decimals)

const spent = BigNumber.from(spendingLimit.spent)
const formattedSpent = safeFormatUnits(spent, token?.tokenInfo.decimals)
const spent = BigNumber.from(spendingLimit.spent)
const formattedSpent = safeFormatUnits(spent, spendingLimit.token.decimals)

return {
cells: {
beneficiary: {
rawValue: spendingLimit.beneficiary,
content: (
<EthHashInfo address={spendingLimit.beneficiary} shortAddress={false} hasExplorer showCopyButton />
),
},
spent: {
rawValue: spendingLimit.spent,
content: (
<Box display="flex" alignItems="center" gap={1}>
<TokenIcon logoUri={token?.tokenInfo.logoUri} tokenSymbol={token?.tokenInfo.symbol} />
{`${formattedSpent} of ${formattedAmount} ${token?.tokenInfo.symbol}`}
</Box>
),
},
resetTime: {
rawValue: spendingLimit.resetTimeMin,
content: (
<SpendingLimitLabel
label={relativeTime(spendingLimit.lastResetMin, spendingLimit.resetTimeMin)}
isOneTime={spendingLimit.resetTimeMin === '0'}
/>
),
},
actions: {
rawValue: '',
sticky: true,
hide: shouldHideactions,
content: (
<Track {...SETTINGS_EVENTS.SPENDING_LIMIT.REMOVE_LIMIT}>
<IconButton onClick={() => onRemove(spendingLimit)} color="error" size="small">
<SvgIcon component={DeleteIcon} inheritViewBox color="error" fontSize="small" />
</IconButton>
</Track>
),
},
},
}
}),
[balances.items, shouldHideactions, spendingLimits],
return {
cells: {
beneficiary: {
rawValue: spendingLimit.beneficiary,
content: (
<EthHashInfo address={spendingLimit.beneficiary} shortAddress={false} hasExplorer showCopyButton />
),
},
spent: {
rawValue: spendingLimit.spent,
content: (
<Box display="flex" alignItems="center" gap={1}>
<TokenIcon logoUri={spendingLimit.token.logoUri} tokenSymbol={spendingLimit.token.symbol} />
{`${formattedSpent} of ${formattedAmount} ${spendingLimit.token.symbol}`}
</Box>
),
},
resetTime: {
rawValue: spendingLimit.resetTimeMin,
content: (
<SpendingLimitLabel
label={relativeTime(spendingLimit.lastResetMin, spendingLimit.resetTimeMin)}
isOneTime={spendingLimit.resetTimeMin === '0'}
/>
),
},
actions: {
rawValue: '',
sticky: true,
hide: shouldHideactions,
content: (
<Track {...SETTINGS_EVENTS.SPENDING_LIMIT.REMOVE_LIMIT}>
<IconButton onClick={() => onRemove(spendingLimit)} color="error" size="small">
<SvgIcon component={DeleteIcon} inheritViewBox color="error" fontSize="small" />
</IconButton>
</Track>
),
},
},
}
}),
[isLoading, shouldHideactions, spendingLimits],
)

return (
<>
<EnhancedTable rows={rows} headCells={headCells} />
Expand Down
8 changes: 4 additions & 4 deletions src/components/settings/SpendingLimits/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Paper, Grid, Typography, Box } from '@mui/material'
import { NoSpendingLimits } from '@/components/settings/SpendingLimits/NoSpendingLimits'
import { SpendingLimitsTable } from '@/components/settings/SpendingLimits/SpendingLimitsTable'
import { useSelector } from 'react-redux'
import { selectSpendingLimits } from '@/store/spendingLimitsSlice'
import { selectSpendingLimits, selectSpendingLimitsLoading } from '@/store/spendingLimitsSlice'
import { NewSpendingLimit } from '@/components/settings/SpendingLimits/NewSpendingLimit'
import { useCurrentChain } from '@/hooks/useChains'
import { hasFeature } from '@/utils/chains'
Expand All @@ -12,6 +12,7 @@ import useIsGranted from '@/hooks/useIsGranted'
const SpendingLimits = () => {
const isGranted = useIsGranted()
const spendingLimits = useSelector(selectSpendingLimits)
const spendingLimitsLoading = useSelector(selectSpendingLimitsLoading)
const currentChain = useCurrentChain()
const isEnabled = currentChain && hasFeature(currentChain, FEATURES.SPENDING_LIMIT)

Expand All @@ -33,15 +34,14 @@ const SpendingLimits = () => {
</Typography>

{isGranted && <NewSpendingLimit />}
{!spendingLimits.length && <NoSpendingLimits />}
{!spendingLimits.length && !spendingLimitsLoading && <NoSpendingLimits />}
</Box>
) : (
<Typography>The spending limit module is not yet available on this chain.</Typography>
)}
</Grid>
</Grid>

{spendingLimits.length > 0 && <SpendingLimitsTable spendingLimits={spendingLimits} />}
<SpendingLimitsTable isLoading={spendingLimitsLoading} spendingLimits={spendingLimits} />
</Paper>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const ReviewSpendingLimitTx = ({ params, onSubmit }: TokenTransferModalProps): R
const txParams: SpendingLimitTxParams = useMemo(
() => ({
safeAddress,
token: spendingLimit?.token || ZERO_ADDRESS,
token: spendingLimit?.token.address || ZERO_ADDRESS,
to: params.recipient,
amount: parseUnits(params.amount, token?.tokenInfo.decimals).toString(),
paymentToken: ZERO_ADDRESS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ const SendAssetsForm = ({
return isOnlySpendingLimitBeneficiary
? balances.items.filter(({ tokenInfo }) => {
return spendingLimits?.some(({ beneficiary, token }) => {
return sameAddress(beneficiary, wallet?.address || '') && sameAddress(tokenInfo.address, token)
return sameAddress(beneficiary, wallet?.address || '') && sameAddress(tokenInfo.address, token.address)
})
})
: balances.items
Expand Down
Loading

0 comments on commit 14271ec

Please sign in to comment.