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 {props.alt} setIsError(true)} /> + return ( + {props.alt} 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: ( - - onRemove(spendingLimit)} color="error" size="small"> - - - - ), - }, - }, - } - }), - [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: ( + + onRemove(spendingLimit)} color="error" size="small"> + + + + ), + }, + }, + } + }), + [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"