diff --git a/package.json b/package.json index b853b67..56d2a94 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "prepare": "husky install", "generate-types": "typechain --target=ethers-v5 --out-dir src/contracts './node_modules/@openzeppelin/contracts/build/contracts/ERC20.json' './customabis/ERC721.json' './node_modules/@openzeppelin/contracts/build/contracts/ERC1155.json' './node_modules/@openzeppelin/contracts/build/contracts/ERC165.json'", "postinstall": "yarn generate-types", - "serve": "npx -y serve build -p 3001" + "serve": "npx -y serve build -p 3001", + "update-safe-apps-sdk": "yarn add @safe-global/safe-apps-provider@latest @safe-global/safe-apps-react-sdk@latest @safe-global/safe-apps-sdk" }, "dependencies": { "@emotion/cache": "^11.10.1", @@ -29,8 +30,8 @@ "@openzeppelin/contracts": "^5.0.2", "@reduxjs/toolkit": "^1.9.5", "@safe-global/safe-apps-provider": "^0.18.2", - "@safe-global/safe-apps-react-sdk": "^4.6.4", - "@safe-global/safe-apps-sdk": "^8.1.0", + "@safe-global/safe-apps-react-sdk": "^4.7.1", + "@safe-global/safe-apps-sdk": "^9.0.0", "@safe-global/safe-gateway-typescript-sdk": "^3.13.2", "@safe-global/safe-react-components": "2.0.5", "@types/jest": "^29.5.12", diff --git a/src/AppInitializer.tsx b/src/AppInitializer.tsx index 57dd62b..4f22e29 100644 --- a/src/AppInitializer.tsx +++ b/src/AppInitializer.tsx @@ -2,11 +2,13 @@ import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; import { useLoadAssets, useLoadCollectibles } from "./hooks/useBalances"; import { useLoadChains } from "./hooks/useChains"; +import { useLoadAddressbook } from "./hooks/useLoadAddressbook"; export const AppInitializer = () => { const { safe } = useSafeAppsSDK(); useLoadChains(); useLoadAssets(safe); useLoadCollectibles(safe); + useLoadAddressbook(); return <>; }; diff --git a/src/hooks/useBalances.ts b/src/hooks/useBalances.ts index 0f6ce02..f498450 100644 --- a/src/hooks/useBalances.ts +++ b/src/hooks/useBalances.ts @@ -74,12 +74,14 @@ const useCollectibleBalances = (safeAddress?: string, chainId?: number) => { }); // We load up to 10 pages of NFTs for performance reasons - if (data && data.length > 0) { - const totalPages = Math.max(COLLECTIBLE_MAX_PAGES, Math.ceil(data[0].count / COLLECTIBLE_LIMIT)); - if (totalPages > size) { - setSize(Math.ceil(data[0].count / COLLECTIBLE_LIMIT)); + useEffect(() => { + if (data && data.length > 0) { + const totalPages = Math.max(COLLECTIBLE_MAX_PAGES, Math.ceil(data[0].count / COLLECTIBLE_LIMIT)); + if (totalPages > size && size !== COLLECTIBLE_LIMIT) { + setSize(Math.ceil(data[0].count / COLLECTIBLE_LIMIT)); + } } - } + }, [data, setSize, size]); const flatData = useMemo(() => { if (data === undefined) { diff --git a/src/hooks/useEnsResolver.ts b/src/hooks/useEnsResolver.ts index 78a07d7..12f8479 100644 --- a/src/hooks/useEnsResolver.ts +++ b/src/hooks/useEnsResolver.ts @@ -2,6 +2,8 @@ import { SafeAppProvider } from "@safe-global/safe-apps-provider"; import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; import { ethers } from "ethers"; import { useCallback, useMemo } from "react"; +import { selectAddressbook } from "src/stores/slices/addressbookSlice"; +import { useAppSelector } from "src/stores/store"; export interface EnsResolver { /** @@ -35,6 +37,23 @@ export const useEnsResolver: () => EnsResolver = () => { const lookupCache = useMemo(() => new Map(), []); + const addressbook = useAppSelector(selectAddressbook); + + const lookupAddress = useCallback( + async (address: string) => { + const nameFromAb = addressbook.find((abItem) => { + return abItem.address.toLowerCase() === address.toLowerCase() && abItem.chainId === safe.chainId.toString(); + })?.name; + + if (nameFromAb) { + return Promise.resolve(nameFromAb); + } + + return await web3Provider.lookupAddress(address); + }, + [addressbook, safe.chainId, web3Provider], + ); + const cachedResolveName = useCallback( async (ensName: string) => { const cachedAddress = resolveCache.get(ensName); @@ -50,14 +69,13 @@ export const useEnsResolver: () => EnsResolver = () => { const cachedLookupAddress = useCallback( async (address: string) => { const cachedAddress = lookupCache.get(address); - const resolvedEnsName = - typeof cachedAddress !== "undefined" ? cachedAddress : await web3Provider.lookupAddress(address); + const resolvedEnsName = typeof cachedAddress !== "undefined" ? cachedAddress : await lookupAddress(address); if (!lookupCache.has(address)) { lookupCache.set(address, resolvedEnsName); } return resolvedEnsName; }, - [lookupCache, web3Provider], + [lookupAddress, lookupCache], ); const isEnsEnabled = useCallback(async () => { diff --git a/src/hooks/useLoadAddressbook.ts b/src/hooks/useLoadAddressbook.ts new file mode 100644 index 0000000..02f3b88 --- /dev/null +++ b/src/hooks/useLoadAddressbook.ts @@ -0,0 +1,32 @@ +import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; +import { useCallback, useEffect } from "react"; +import { setAddressbook } from "src/stores/slices/addressbookSlice"; +import { useAppDispatch } from "src/stores/store"; + +/** + * This hook requests the address book and stores it if the permission is granted. + */ +export const useLoadAddressbook = () => { + const { sdk } = useSafeAppsSDK(); + const dispatch = useAppDispatch(); + + const request = useCallback(async () => sdk.safe.requestAddressBook(), [sdk.safe]); + + useEffect(() => { + let isMounted = true; + + request() + .then((ab) => { + if (isMounted) { + dispatch(setAddressbook(ab)); + } + }) + .catch(() => { + console.error("Addressbook request was rejected."); + }); + + return () => { + isMounted = false; + }; + }, [request, dispatch]); +}; diff --git a/src/stores/slices/addressbookSlice.ts b/src/stores/slices/addressbookSlice.ts new file mode 100644 index 0000000..932cc6b --- /dev/null +++ b/src/stores/slices/addressbookSlice.ts @@ -0,0 +1,33 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +import { RootState } from "../store"; + +export type AddressbookEntry = { + address: string; + chainId: string; + name: string; +}; + +export interface AddressbookState { + namedAddresses: AddressbookEntry[]; +} + +const initialState: AddressbookState = { + namedAddresses: [], +}; + +export const addressbookSlice = createSlice({ + name: "addressbook", + initialState, + reducers: { + setAddressbook: (state, action: PayloadAction) => { + state.namedAddresses = action.payload; + }, + }, +}); + +export const { setAddressbook } = addressbookSlice.actions; + +export default addressbookSlice.reducer; + +export const selectAddressbook = ({ addressbook }: RootState) => addressbook.namedAddresses; diff --git a/src/stores/store.ts b/src/stores/store.ts index a85f99a..ea4e42c 100644 --- a/src/stores/store.ts +++ b/src/stores/store.ts @@ -8,6 +8,7 @@ import { import { setupListeners } from "@reduxjs/toolkit/dist/query"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; +import addressBookReducer from "./slices/addressbookSlice"; import assetBalanceReducer from "./slices/assetBalanceSlice"; import collectiblesReducer from "./slices/collectiblesSlice"; import csvReducer from "./slices/csvEditorSlice"; @@ -27,6 +28,7 @@ export const store = configureStore({ networks: networksReducer, collectibles: collectiblesReducer, assetBalance: assetBalanceReducer, + addressbook: addressBookReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(listenerMiddlewareInstance.middleware), }); diff --git a/yarn.lock b/yarn.lock index c02a9d0..6fd2f6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,11 +7,6 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== -"@adraffy/ens-normalize@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.4.tgz#aae21cb858bbb0411949d5b7b3051f4209043f62" - integrity sha512-UK0bHA7hh9cR39V+4gl2/NnBBjoXIxkuWAPCaY4X7fbH4L/azIi7ilWOCjMUYfpJgraLUAqkRi2BqrjME8Rynw== - "@ampproject/remapping@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -2779,28 +2774,12 @@ "@safe-global/safe-apps-sdk" "^9.0.0" events "^3.3.0" -"@safe-global/safe-apps-react-sdk@^4.6.4": - version "4.6.4" - resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-react-sdk/-/safe-apps-react-sdk-4.6.4.tgz#7163cb10eb6af82489d199002cc94e0e58b41e97" - integrity sha512-QFrKZldFw0JY5jD/B6X2TzArgNuyWALpTjJair3ooYwRU5ZE6lWJ6W8FQg7XOogfRYoXbDhAtBXoGrX/BHbM4Q== - dependencies: - "@safe-global/safe-apps-sdk" "7.10.0" - -"@safe-global/safe-apps-sdk@7.10.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-7.10.0.tgz#e75fc581126f27c52ec2601da51bca5eb99b61f4" - integrity sha512-is0QAHVoGkP06YfOPcp4X3/YUEA3wRdgFUyKZ4rT47uOEnzxA9Sm8BFJrIZqZOjjqC+aJXRMF0cE2qucS953rg== +"@safe-global/safe-apps-react-sdk@^4.7.1": + version "4.7.1" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-react-sdk/-/safe-apps-react-sdk-4.7.1.tgz#3df442496edee9778ef7217ee78a031cac0e1701" + integrity sha512-sbVroIUxYy9XOzN7769wCTR2ytk6LMp8ECmFlc+d23/7EHOuX5Yw8Si+tf5SYbYhS9ygdSsMcVRQeNvKOhZnEw== dependencies: - "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" - ethers "^5.7.2" - -"@safe-global/safe-apps-sdk@^8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-8.1.0.tgz#d1d0c69cd2bf4eef8a79c5d677d16971926aa64a" - integrity sha512-XJbEPuaVc7b9n23MqlF6c+ToYIS3f7P2Sel8f3cSBQ9WORE4xrSuvhMpK9fDSFqJ7by/brc+rmJR/5HViRr0/w== - dependencies: - "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" - viem "^1.0.0" + "@safe-global/safe-apps-sdk" "^9.0.0" "@safe-global/safe-apps-sdk@^9.0.0": version "9.0.0" @@ -12549,20 +12528,6 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -viem@^1.0.0: - version "1.18.2" - resolved "https://registry.yarnpkg.com/viem/-/viem-1.18.2.tgz#2e9135d593e7a77b6b5a8cbd1800521ffda3b23c" - integrity sha512-ifobXCKwzztmjHbHowAWqTASO6tAqF6udKB9ONXkJQU1cmt830MABiMwJGtTO9Gb9ION1N+324G7nHDbpPn4wg== - dependencies: - "@adraffy/ens-normalize" "1.9.4" - "@noble/curves" "1.2.0" - "@noble/hashes" "1.3.2" - "@scure/bip32" "1.3.2" - "@scure/bip39" "1.2.1" - abitype "0.9.8" - isows "1.0.3" - ws "8.13.0" - viem@^1.6.0: version "1.21.4" resolved "https://registry.yarnpkg.com/viem/-/viem-1.21.4.tgz#883760e9222540a5a7e0339809202b45fe6a842d"