diff --git a/package.json b/package.json
index 1cb18207d8..b526bad7bb 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -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",
diff --git a/src/components/common/ImageFallback/index.tsx b/src/components/common/ImageFallback/index.tsx
index 337caa90fd..75e00d885c 100644
--- a/src/components/common/ImageFallback/index.tsx
+++ b/src/components/common/ImageFallback/index.tsx
@@ -11,7 +11,14 @@ const ImageFallback = ({ src, fallbackSrc, fallbackComponent, ...props }: ImageF
if (isError && fallbackComponent) return fallbackComponent
- return setIsError(true)} />
+ return (
+ setIsError(true)}
+ />
+ )
}
export default ImageFallback
diff --git a/src/components/common/TokenIcon/index.tsx b/src/components/common/TokenIcon/index.tsx
index 7fe79303cd..a3a64d80ae 100644
--- a/src/components/common/TokenIcon/index.tsx
+++ b/src/components/common/TokenIcon/index.tsx
@@ -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,
@@ -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 (
{
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])
diff --git a/src/components/settings/SpendingLimits/RemoveSpendingLimit/index.tsx b/src/components/settings/SpendingLimits/RemoveSpendingLimit/index.tsx
index 1a1f16cc07..7645ece499 100644
--- a/src/components/settings/SpendingLimits/RemoveSpendingLimit/index.tsx
+++ b/src/components/settings/SpendingLimits/RemoveSpendingLimit/index.tsx
@@ -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(() => {
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,
diff --git a/src/components/settings/SpendingLimits/SpendingLimitsTable.tsx b/src/components/settings/SpendingLimits/SpendingLimitsTable.tsx
index 18cdb19477..ba47144d79 100644
--- a/src/components/settings/SpendingLimits/SpendingLimitsTable.tsx
+++ b/src/components/settings/SpendingLimits/SpendingLimitsTable.tsx
@@ -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'
@@ -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: (
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ },
+ spent: {
+ rawValue: '0',
+ content: (
+
+
+
+
+
+
+ ),
+ },
+ resetTime: {
+ rawValue: '0',
+ content: (
+
+
+
+ ),
+ },
+ },
+ }
+})
+
+export const SpendingLimitsTable = ({
+ spendingLimits,
+ isLoading,
+}: {
+ spendingLimits: SpendingLimitState[]
+ isLoading: boolean
+}) => {
const [open, setOpen] = useState(false)
const [initialData, setInitialData] = useState()
- const { balances } = useBalances()
const isGranted = useIsGranted()
const shouldHideactions = !isGranted
@@ -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: (
-
- ),
- },
- spent: {
- rawValue: spendingLimit.spent,
- content: (
-
-
- {`${formattedSpent} of ${formattedAmount} ${token?.tokenInfo.symbol}`}
-
- ),
- },
- resetTime: {
- rawValue: spendingLimit.resetTimeMin,
- content: (
-
- ),
- },
- actions: {
- rawValue: '',
- sticky: true,
- hide: shouldHideactions,
- content: (
-
- ),
- },
- },
- }
- }),
- [balances.items, shouldHideactions, spendingLimits],
+ return {
+ cells: {
+ beneficiary: {
+ rawValue: spendingLimit.beneficiary,
+ content: (
+
+ ),
+ },
+ spent: {
+ rawValue: spendingLimit.spent,
+ content: (
+
+
+ {`${formattedSpent} of ${formattedAmount} ${spendingLimit.token.symbol}`}
+
+ ),
+ },
+ resetTime: {
+ rawValue: spendingLimit.resetTimeMin,
+ content: (
+
+ ),
+ },
+ actions: {
+ rawValue: '',
+ sticky: true,
+ hide: shouldHideactions,
+ content: (
+
+ ),
+ },
+ },
+ }
+ }),
+ [isLoading, shouldHideactions, spendingLimits],
)
-
return (
<>
diff --git a/src/components/settings/SpendingLimits/index.tsx b/src/components/settings/SpendingLimits/index.tsx
index 380b13fbaf..39149aa049 100644
--- a/src/components/settings/SpendingLimits/index.tsx
+++ b/src/components/settings/SpendingLimits/index.tsx
@@ -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'
@@ -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)
@@ -33,15 +34,14 @@ const SpendingLimits = () => {
{isGranted && }
- {!spendingLimits.length && }
+ {!spendingLimits.length && !spendingLimitsLoading && }
) : (
The spending limit module is not yet available on this chain.
)}
-
- {spendingLimits.length > 0 && }
+
)
}
diff --git a/src/components/tx/modals/TokenTransferModal/ReviewSpendingLimitTx.tsx b/src/components/tx/modals/TokenTransferModal/ReviewSpendingLimitTx.tsx
index e18ce71fde..9dc374d4d7 100644
--- a/src/components/tx/modals/TokenTransferModal/ReviewSpendingLimitTx.tsx
+++ b/src/components/tx/modals/TokenTransferModal/ReviewSpendingLimitTx.tsx
@@ -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,
diff --git a/src/components/tx/modals/TokenTransferModal/SendAssetsForm.tsx b/src/components/tx/modals/TokenTransferModal/SendAssetsForm.tsx
index 4df8775c17..b5ae2be022 100644
--- a/src/components/tx/modals/TokenTransferModal/SendAssetsForm.tsx
+++ b/src/components/tx/modals/TokenTransferModal/SendAssetsForm.tsx
@@ -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
diff --git a/src/hooks/__tests__/useLoadSpendingLimits.test.ts b/src/hooks/__tests__/useLoadSpendingLimits.test.ts
index 76fa9599df..d7d4080eb4 100644
--- a/src/hooks/__tests__/useLoadSpendingLimits.test.ts
+++ b/src/hooks/__tests__/useLoadSpendingLimits.test.ts
@@ -2,12 +2,16 @@ import * as spendingLimit from '@/services/contracts/spendingLimitContracts'
import { JsonRpcProvider } from '@ethersproject/providers'
import { ZERO_ADDRESS } from '@safe-global/safe-core-sdk/dist/src/utils/constants'
import type { AllowanceModule } from '@/types/contracts'
+import { ERC20__factory } from '@/types/contracts'
import {
getSpendingLimits,
getTokenAllowanceForDelegate,
getTokensForDelegate,
} from '../loadables/useLoadSpendingLimits'
import { BigNumber } from '@ethersproject/bignumber'
+import * as web3 from '../wallets/web3'
+import { keccak256, toUtf8Bytes } from 'ethers/lib/utils'
+import { TokenType } from '@safe-global/safe-gateway-typescript-sdk'
const mockProvider = new JsonRpcProvider()
const mockModule = {
@@ -23,7 +27,7 @@ describe('getSpendingLimits', () => {
it('should return undefined if no spending limit module address was found', async () => {
jest.spyOn(spendingLimit, 'getSpendingLimitModuleAddress').mockReturnValue(undefined)
- const result = await getSpendingLimits(mockProvider, [], ZERO_ADDRESS, '4')
+ const result = await getSpendingLimits(mockProvider, [], ZERO_ADDRESS, '4', [])
expect(result).toBeUndefined()
})
@@ -31,7 +35,7 @@ describe('getSpendingLimits', () => {
it('should return undefined if the safe has no spending limit module', async () => {
jest.spyOn(spendingLimit, 'getSpendingLimitModuleAddress').mockReturnValue('0x1')
- const result = await getSpendingLimits(mockProvider, [], ZERO_ADDRESS, '4')
+ const result = await getSpendingLimits(mockProvider, [], ZERO_ADDRESS, '4', [])
expect(result).toBeUndefined()
})
@@ -51,7 +55,7 @@ describe('getSpendingLimits', () => {
value: '0x1',
}
- await getSpendingLimits(mockProvider, [mockModule], ZERO_ADDRESS, '4')
+ await getSpendingLimits(mockProvider, [mockModule], ZERO_ADDRESS, '4', [])
expect(getDelegatesMock).toHaveBeenCalledWith(ZERO_ADDRESS, 0, 100)
})
@@ -78,7 +82,7 @@ describe('getSpendingLimits', () => {
}),
)
- const result = await getSpendingLimits(mockProvider, [mockModule], ZERO_ADDRESS, '4')
+ const result = await getSpendingLimits(mockProvider, [mockModule], ZERO_ADDRESS, '4', [])
expect(result?.length).toBe(4)
})
@@ -105,7 +109,7 @@ describe('getSpendingLimits', () => {
}),
)
- const result = await getSpendingLimits(mockProvider, [mockModule], ZERO_ADDRESS, '4')
+ const result = await getSpendingLimits(mockProvider, [mockModule], ZERO_ADDRESS, '4', [])
expect(result?.length).toBe(0)
})
@@ -116,7 +120,7 @@ describe('getTokensForDelegate', () => {
const getTokensMock = jest.fn(() => [])
const mockContract = { getTokens: getTokensMock } as unknown as AllowanceModule
- await getTokensForDelegate(mockContract, ZERO_ADDRESS, '0x1')
+ await getTokensForDelegate(mockContract, ZERO_ADDRESS, '0x1', [])
expect(getTokensMock).toHaveBeenCalledWith(ZERO_ADDRESS, '0x1')
})
@@ -133,7 +137,7 @@ describe('getTokenAllowanceForDelegate', () => {
])
const mockContract = { getTokenAllowance: getTokenAllowanceMock } as unknown as AllowanceModule
- const result = await getTokenAllowanceForDelegate(mockContract, ZERO_ADDRESS, '0x1', '0x10')
+ const result = await getTokenAllowanceForDelegate(mockContract, ZERO_ADDRESS, '0x1', '0x10', [])
expect(result.beneficiary).toBe('0x1')
expect(result.nonce).toBe('0')
@@ -142,4 +146,79 @@ describe('getTokenAllowanceForDelegate', () => {
expect(result.lastResetMin).toBe('0')
expect(result.resetTimeMin).toBe('0')
})
+
+ it('should return tokenInfo from balance', async () => {
+ const getTokenAllowanceMock = jest.fn(() => [
+ BigNumber.from(0),
+ BigNumber.from(0),
+ BigNumber.from(0),
+ BigNumber.from(0),
+ BigNumber.from(0),
+ ])
+ const mockContract = { getTokenAllowance: getTokenAllowanceMock } as unknown as AllowanceModule
+
+ const mockTokenInfoFromBalances = [
+ {
+ address: '0x10',
+ name: 'Test',
+ type: TokenType.ERC20,
+ symbol: 'TST',
+ decimals: 10,
+ logoUri: 'https://mock.images/0x10.png',
+ },
+ ]
+
+ const result = await getTokenAllowanceForDelegate(
+ mockContract,
+ ZERO_ADDRESS,
+ '0x1',
+ '0x10',
+ mockTokenInfoFromBalances,
+ )
+
+ expect(result.token.address).toBe('0x10')
+ expect(result.token.decimals).toBe(10)
+ expect(result.token.symbol).toBe('TST')
+ expect(result.token.logoUri).toBe('https://mock.images/0x10.png')
+ })
+
+ it('should return tokenInfo from on-chain if not in balance', async () => {
+ const getTokenAllowanceMock = jest.fn(() => [
+ BigNumber.from(0),
+ BigNumber.from(0),
+ BigNumber.from(0),
+ BigNumber.from(0),
+ BigNumber.from(0),
+ ])
+
+ jest.spyOn(web3, 'getWeb3').mockImplementation(
+ () =>
+ ({
+ call: (tx: { data: string; to: string }) => {
+ {
+ const decimalsSigHash = keccak256(toUtf8Bytes('decimals()')).slice(0, 10)
+ const symbolSigHash = keccak256(toUtf8Bytes('symbol()')).slice(0, 10)
+
+ if (tx.data.startsWith(decimalsSigHash)) {
+ return ERC20__factory.createInterface().encodeFunctionResult('decimals', [10])
+ }
+ if (tx.data.startsWith(symbolSigHash)) {
+ return ERC20__factory.createInterface().encodeFunctionResult('symbol', ['TST'])
+ }
+ }
+ },
+ _isProvider: true,
+ resolveName: (name: string) => name,
+ } as any),
+ )
+
+ const mockContract = { getTokenAllowance: getTokenAllowanceMock } as unknown as AllowanceModule
+
+ const result = await getTokenAllowanceForDelegate(mockContract, ZERO_ADDRESS, '0x1', '0x10', [])
+
+ expect(result.token.address).toBe('0x10')
+ expect(result.token.decimals).toBe(10)
+ expect(result.token.symbol).toBe('TST')
+ expect(result.token.logoUri).toBe(undefined)
+ })
})
diff --git a/src/hooks/loadables/useLoadSpendingLimits.ts b/src/hooks/loadables/useLoadSpendingLimits.ts
index 22defcbbe2..3a881a3116 100644
--- a/src/hooks/loadables/useLoadSpendingLimits.ts
+++ b/src/hooks/loadables/useLoadSpendingLimits.ts
@@ -4,13 +4,23 @@ import useSafeInfo from '../useSafeInfo'
import { Errors, logError } from '@/services/exceptions'
import type { SpendingLimitState } from '@/store/spendingLimitsSlice'
import useChainId from '@/hooks/useChainId'
-import { useWeb3ReadOnly } from '@/hooks/wallets/web3'
+import { getWeb3, useWeb3ReadOnly } from '@/hooks/wallets/web3'
import type { JsonRpcProvider } from '@ethersproject/providers'
import { getSpendingLimitContract, getSpendingLimitModuleAddress } from '@/services/contracts/spendingLimitContracts'
-import type { AddressEx } from '@safe-global/safe-gateway-typescript-sdk'
+import type { AddressEx, TokenInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { sameAddress } from '@/utils/addresses'
+import { ERC20__factory } from '@/types/contracts'
import type { AllowanceModule } from '@/types/contracts'
+
import { sameString } from '@safe-global/safe-core-sdk/dist/src/utils'
+import { useAppSelector } from '@/store'
+import { selectTokens } from '@/store/balancesSlice'
+import { isEqual } from 'lodash'
+
+const DEFAULT_TOKEN_INFO = {
+ decimals: 18,
+ symbol: '',
+}
const isModuleEnabled = (modules: string[], moduleAddress: string): boolean => {
return modules?.some((module) => sameAddress(module, moduleAddress)) ?? false
@@ -19,18 +29,37 @@ const isModuleEnabled = (modules: string[], moduleAddress: string): boolean => {
const discardZeroAllowance = (spendingLimit: SpendingLimitState): boolean =>
!(sameString(spendingLimit.amount, '0') && sameString(spendingLimit.resetTimeMin, '0'))
+const getTokenInfoFromBalances = (tokenInfoFromBalances: TokenInfo[], address: string): TokenInfo | undefined =>
+ tokenInfoFromBalances.find((token) => token.address === address)
+
+const getTokenInfoOnChain = async (
+ address: string,
+): Promise | undefined> => {
+ const web3 = getWeb3()
+ if (!web3) return
+
+ const erc20 = ERC20__factory.connect(address, web3)
+ const [symbol, decimals] = await Promise.all([erc20.symbol(), erc20.decimals()])
+ return {
+ address,
+ symbol,
+ decimals,
+ }
+}
+
export const getTokenAllowanceForDelegate = async (
contract: AllowanceModule,
safeAddress: string,
delegate: string,
token: string,
+ tokenInfoFromBalances: TokenInfo[],
): Promise => {
const tokenAllowance = await contract.getTokenAllowance(safeAddress, delegate, token)
const [amount, spent, resetTimeMin, lastResetMin, nonce] = tokenAllowance
-
return {
beneficiary: delegate,
- token,
+ token: getTokenInfoFromBalances(tokenInfoFromBalances, token) ||
+ (await getTokenInfoOnChain(token)) || { ...DEFAULT_TOKEN_INFO, address: token },
amount: amount.toString(),
spent: spent.toString(),
resetTimeMin: resetTimeMin.toString(),
@@ -39,10 +68,19 @@ export const getTokenAllowanceForDelegate = async (
}
}
-export const getTokensForDelegate = async (contract: AllowanceModule, safeAddress: string, delegate: string) => {
+export const getTokensForDelegate = async (
+ contract: AllowanceModule,
+ safeAddress: string,
+ delegate: string,
+ tokenInfoFromBalances: TokenInfo[],
+) => {
const tokens = await contract.getTokens(safeAddress, delegate)
- return Promise.all(tokens.map(async (token) => getTokenAllowanceForDelegate(contract, safeAddress, delegate, token)))
+ return Promise.all(
+ tokens.map(async (token) =>
+ getTokenAllowanceForDelegate(contract, safeAddress, delegate, token, tokenInfoFromBalances),
+ ),
+ )
}
export const getSpendingLimits = async (
@@ -50,6 +88,7 @@ export const getSpendingLimits = async (
safeModules: AddressEx[],
safeAddress: string,
chainId: string,
+ tokenInfoFromBalances: TokenInfo[],
): Promise => {
const spendingLimitModuleAddress = getSpendingLimitModuleAddress(chainId)
if (!spendingLimitModuleAddress) return
@@ -64,7 +103,9 @@ export const getSpendingLimits = async (
const delegates = await contract.getDelegates(safeAddress, 0, 100)
const spendingLimits = await Promise.all(
- delegates.results.map(async (delegate) => getTokensForDelegate(contract, safeAddress, delegate)),
+ delegates.results.map(async (delegate) =>
+ getTokensForDelegate(contract, safeAddress, delegate, tokenInfoFromBalances),
+ ),
)
return spendingLimits.flat().filter(discardZeroAllowance)
}
@@ -73,12 +114,13 @@ export const useLoadSpendingLimits = (): AsyncResult => {
const { safeAddress, safe, safeLoaded } = useSafeInfo()
const chainId = useChainId()
const provider = useWeb3ReadOnly()
+ const tokenInfoFromBalances = useAppSelector(selectTokens, isEqual)
const [data, error, loading] = useAsync(() => {
- if (!provider || !safeLoaded || !safe.modules) return
+ if (!provider || !safeLoaded || !safe.modules || !tokenInfoFromBalances) return
- return getSpendingLimits(provider, safe.modules, safeAddress, chainId)
- }, [provider, safeLoaded, safe.modules?.length, safeAddress, chainId, safe.txHistoryTag])
+ return getSpendingLimits(provider, safe.modules, safeAddress, chainId, tokenInfoFromBalances)
+ }, [provider, safeLoaded, safe.modules?.length, safeAddress, chainId, safe.txHistoryTag, tokenInfoFromBalances])
useEffect(() => {
if (error) {
diff --git a/src/hooks/useSpendingLimit.ts b/src/hooks/useSpendingLimit.ts
index 68f63dd8d4..373f60cdab 100644
--- a/src/hooks/useSpendingLimit.ts
+++ b/src/hooks/useSpendingLimit.ts
@@ -11,7 +11,7 @@ const useSpendingLimit = (selectedToken?: TokenInfo): SpendingLimitState | undef
return spendingLimits.find(
(spendingLimit) =>
- sameAddress(spendingLimit.token, selectedToken?.address) &&
+ sameAddress(spendingLimit.token.address, selectedToken?.address) &&
sameAddress(spendingLimit.beneficiary, wallet?.address),
)
}
diff --git a/src/services/tx/__tests__/spendingLimitParams.test.ts b/src/services/tx/__tests__/spendingLimitParams.test.ts
index 6e73b008c0..eb8b6e4b4f 100644
--- a/src/services/tx/__tests__/spendingLimitParams.test.ts
+++ b/src/services/tx/__tests__/spendingLimitParams.test.ts
@@ -72,7 +72,7 @@ describe('createNewSpendingLimitTx', () => {
const mockSpendingLimits: SpendingLimitState[] = [
{
beneficiary: ZERO_ADDRESS,
- token: '0x10',
+ token: { address: '0x10', decimals: 18, symbol: 'TST' },
amount: '1',
resetTimeMin: '0',
lastResetMin: '0',
@@ -90,7 +90,7 @@ describe('createNewSpendingLimitTx', () => {
it('creates a tx to reset an existing allowance if some of the allowance was already spent', async () => {
const existingSpendingLimitMock = {
beneficiary: ZERO_ADDRESS,
- token: '0x10',
+ token: { address: '0x10', decimals: 18, symbol: 'TST' },
amount: '1',
resetTimeMin: '0',
lastResetMin: '0',
@@ -107,7 +107,7 @@ describe('createNewSpendingLimitTx', () => {
it('does not create a tx to reset an existing allowance if none was spent', async () => {
const existingSpendingLimitMock = {
beneficiary: ZERO_ADDRESS,
- token: '0x10',
+ token: { address: '0x10', decimals: 18, symbol: 'TST' },
amount: '1',
resetTimeMin: '0',
lastResetMin: '0',
diff --git a/src/store/spendingLimitsSlice.ts b/src/store/spendingLimitsSlice.ts
index 854b11071b..f742a352ab 100644
--- a/src/store/spendingLimitsSlice.ts
+++ b/src/store/spendingLimitsSlice.ts
@@ -3,7 +3,12 @@ import { makeLoadableSlice } from './common'
export type SpendingLimitState = {
beneficiary: string
- token: string
+ token: {
+ address: string
+ symbol: string
+ decimals: number
+ logoUri?: string
+ }
amount: string
nonce: string
resetTimeMin: string
@@ -17,6 +22,5 @@ const { slice, selector } = makeLoadableSlice('spendingLimits', initialState)
export const spendingLimitSlice = slice
-export const selectSpendingLimits = createSelector(selector, (spendingLimits) => {
- return spendingLimits.data
-})
+export const selectSpendingLimits = createSelector(selector, (spendingLimits) => spendingLimits.data)
+export const selectSpendingLimitsLoading = createSelector(selector, (spendingLimits) => spendingLimits.loading)
diff --git a/yarn.lock b/yarn.lock
index 7b88209f07..05e255fc43 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2897,6 +2897,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@openzeppelin/contracts@^4.8.1":
+ version "4.8.1"
+ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.1.tgz#709cfc4bbb3ca9f4460d60101f15dac6b7a2d5e4"
+ integrity sha512-xQ6eUZl+RDyb/FiZe1h+U7qr/f4p/SrTSQcTPH2bjur3C5DbuW/zFgCU/b1P/xcIaEqJep+9ju4xDRi3rmChdQ==
+
"@pkgr/utils@^2.3.1":
version "2.3.1"
resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.3.1.tgz#0a9b06ffddee364d6642b3cd562ca76f55b34a03"