From 5ae09105c090869aef3e33f83c8b0244f644cc8d Mon Sep 17 00:00:00 2001 From: Felipe Mendes Date: Mon, 17 Apr 2023 14:23:17 -0300 Subject: [PATCH] feat: UI create functionality to resolve address (#227) * feat: add ENS slice * feat: add resolved address hook * feat: add resolved address component --- ui/src/components/index.ts | 1 + ui/src/components/resolved-address/index.ts | 1 + .../resolved-address.styles.ts | 21 ++++++++ .../resolved-address/resolved-address.tsx | 31 +++++++++++ .../store/features/ens/async-thunk/index.ts | 1 + .../ens/async-thunk/resolve-address.ts | 37 +++++++++++++ ui/src/store/features/ens/ens-slice.ts | 52 +++++++++++++++++++ ui/src/store/features/ens/hooks/index.ts | 1 + .../ens/hooks/use-resolved-address.ts | 21 ++++++++ ui/src/store/features/ens/index.ts | 2 + ui/src/store/features/index.ts | 1 + ui/src/store/store.ts | 2 + .../views/components-test/components-test.tsx | 5 +- 13 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 ui/src/components/resolved-address/index.ts create mode 100644 ui/src/components/resolved-address/resolved-address.styles.ts create mode 100644 ui/src/components/resolved-address/resolved-address.tsx create mode 100644 ui/src/store/features/ens/async-thunk/index.ts create mode 100644 ui/src/store/features/ens/async-thunk/resolve-address.ts create mode 100644 ui/src/store/features/ens/ens-slice.ts create mode 100644 ui/src/store/features/ens/hooks/index.ts create mode 100644 ui/src/store/features/ens/hooks/use-resolved-address.ts create mode 100644 ui/src/store/features/ens/index.ts diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index 69e8429..8e58b16 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -7,3 +7,4 @@ export * from './toast'; export * from './step'; export * from './nfa-card'; export * from './nfa-preview'; +export * from './resolved-address'; diff --git a/ui/src/components/resolved-address/index.ts b/ui/src/components/resolved-address/index.ts new file mode 100644 index 0000000..c5b1f78 --- /dev/null +++ b/ui/src/components/resolved-address/index.ts @@ -0,0 +1 @@ +export * from './resolved-address'; diff --git a/ui/src/components/resolved-address/resolved-address.styles.ts b/ui/src/components/resolved-address/resolved-address.styles.ts new file mode 100644 index 0000000..5b3fb23 --- /dev/null +++ b/ui/src/components/resolved-address/resolved-address.styles.ts @@ -0,0 +1,21 @@ +import { keyframes, styled } from '@/theme'; + +const Loading = keyframes({ + '0%': { + opacity: 1, + }, + '50%': { + opacity: 0.5, + }, + '100%': { + opacity: 1, + }, +}); + +export const ResolvedAddressStyles = { + Container: styled('span', { + '&[data-loading="true"]': { + animation: `${Loading} 1s ease-in-out infinite`, + }, + }), +}; diff --git a/ui/src/components/resolved-address/resolved-address.tsx b/ui/src/components/resolved-address/resolved-address.tsx new file mode 100644 index 0000000..58edc30 --- /dev/null +++ b/ui/src/components/resolved-address/resolved-address.tsx @@ -0,0 +1,31 @@ +import { useMemo } from 'react'; + +import { useResolvedAddress } from '@/store'; +import { forwardStyledRef } from '@/theme'; + +import { ResolvedAddressStyles as RAS } from './resolved-address.styles'; + +export type ResolvedAddressProps = React.ComponentPropsWithRef< + typeof RAS.Container +> & { + children: string; +}; + +export const ResolvedAddress = forwardStyledRef< + HTMLSpanElement, + ResolvedAddressProps +>(({ children, ...props }, ref) => { + const [resolvedAddress, loading] = useResolvedAddress(children); + + const text = useMemo(() => { + if (!resolvedAddress.endsWith('.eth')) + return `${resolvedAddress.slice(0, 6)}...${resolvedAddress.slice(-4)}`; + return resolvedAddress; + }, [resolvedAddress]); + + return ( + + {text} + + ); +}); diff --git a/ui/src/store/features/ens/async-thunk/index.ts b/ui/src/store/features/ens/async-thunk/index.ts new file mode 100644 index 0000000..29a1344 --- /dev/null +++ b/ui/src/store/features/ens/async-thunk/index.ts @@ -0,0 +1 @@ +export * from './resolve-address'; diff --git a/ui/src/store/features/ens/async-thunk/resolve-address.ts b/ui/src/store/features/ens/async-thunk/resolve-address.ts new file mode 100644 index 0000000..6178b9a --- /dev/null +++ b/ui/src/store/features/ens/async-thunk/resolve-address.ts @@ -0,0 +1,37 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { ethers } from 'ethers'; + +import { env } from '@/constants'; +import { ENSActions, RootState } from '@/store'; +import { AppLog } from '@/utils'; + +export const resolveAddress = createAsyncThunk( + 'ENS/fetchAddress', + async (address, { dispatch, getState }) => { + const { addressMap } = (getState() as RootState).ENS; + const stored = addressMap[address] || {}; + + if (stored.state === 'loading') return; + + try { + dispatch( + ENSActions.setAddress({ key: address, value: { state: 'loading' } }) + ); + + const provider = new ethers.providers.JsonRpcProvider(env.goerli.rpc); + const value = (await provider.lookupAddress(address)) || undefined; + + dispatch( + ENSActions.setAddress({ + key: address, + value: { state: 'success', value }, + }) + ); + } catch (error) { + AppLog.error('Failed to resolve ENS name by address', error); + dispatch( + ENSActions.setAddress({ key: address, value: { state: 'loading' } }) + ); + } + } +); diff --git a/ui/src/store/features/ens/ens-slice.ts b/ui/src/store/features/ens/ens-slice.ts new file mode 100644 index 0000000..4a9d501 --- /dev/null +++ b/ui/src/store/features/ens/ens-slice.ts @@ -0,0 +1,52 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { RootState } from '@/store'; +import { useAppSelector } from '@/store/hooks'; + +import * as asyncThunk from './async-thunk'; + +export namespace ENSState { + export type QueryState = undefined | 'loading' | 'failed' | 'success'; + + export type Address = { + state: QueryState; + value?: string; + }; + + export type AddressMap = Record; +} + +export interface ENSState { + addressMap: ENSState.AddressMap; +} + +const initialState: ENSState = { + addressMap: {}, +}; + +export const ENSSlice = createSlice({ + name: 'ENSSLice', + initialState, + reducers: { + setAddress: ( + state, + action: PayloadAction<{ + key: string; + value: ENSState.Address; + }> + ) => { + state.addressMap[action.payload.key] = action.payload.value; + }, + }, +}); + +export const ENSActions = { + ...ENSSlice.actions, + ...asyncThunk, +}; + +const selectENSState = (state: RootState): ENSState => state.ENS; + +export const useENSStore = (): ENSState => useAppSelector(selectENSState); + +export default ENSSlice.reducer; diff --git a/ui/src/store/features/ens/hooks/index.ts b/ui/src/store/features/ens/hooks/index.ts new file mode 100644 index 0000000..1920088 --- /dev/null +++ b/ui/src/store/features/ens/hooks/index.ts @@ -0,0 +1 @@ +export * from './use-resolved-address'; diff --git a/ui/src/store/features/ens/hooks/use-resolved-address.ts b/ui/src/store/features/ens/hooks/use-resolved-address.ts new file mode 100644 index 0000000..edf2256 --- /dev/null +++ b/ui/src/store/features/ens/hooks/use-resolved-address.ts @@ -0,0 +1,21 @@ +import { useEffect, useMemo } from 'react'; + +import { useAppDispatch } from '@/store/hooks'; + +import { ENSActions, useENSStore } from '../ens-slice'; + +export const useResolvedAddress = (address: string): [string, boolean] => { + const { addressMap } = useENSStore(); + const dispatch = useAppDispatch(); + + useEffect(() => { + const stored = addressMap[address] || {}; + if (typeof stored.state !== 'undefined') return; + dispatch(ENSActions.resolveAddress(address)); + }, [address, dispatch, addressMap]); + + return useMemo(() => { + const stored = addressMap[address] || {}; + return [stored.value || address, addressMap[address]?.state === 'loading']; + }, [address, addressMap]); +}; diff --git a/ui/src/store/features/ens/index.ts b/ui/src/store/features/ens/index.ts new file mode 100644 index 0000000..27b3bab --- /dev/null +++ b/ui/src/store/features/ens/index.ts @@ -0,0 +1,2 @@ +export * from './ens-slice'; +export * from './hooks'; diff --git a/ui/src/store/features/index.ts b/ui/src/store/features/index.ts index 2e74368..bf09fad 100644 --- a/ui/src/store/features/index.ts +++ b/ui/src/store/features/index.ts @@ -1,3 +1,4 @@ export * from './fleek-erc721'; export * from './github'; export * from './toasts'; +export * from './ens'; diff --git a/ui/src/store/store.ts b/ui/src/store/store.ts index 6144369..79511cc 100644 --- a/ui/src/store/store.ts +++ b/ui/src/store/store.ts @@ -1,5 +1,6 @@ import { configureStore } from '@reduxjs/toolkit'; +import ENSReducer from './features/ens/ens-slice'; import fleekERC721Reducer from './features/fleek-erc721/fleek-erc721-slice'; import githubReducer from './features/github/github-slice'; import toastsReducer from './features/toasts/toasts-slice'; @@ -9,6 +10,7 @@ export const store = configureStore({ fleekERC721: fleekERC721Reducer, github: githubReducer, toasts: toastsReducer, + ENS: ENSReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/ui/src/views/components-test/components-test.tsx b/ui/src/views/components-test/components-test.tsx index bce37f7..ec9655f 100644 --- a/ui/src/views/components-test/components-test.tsx +++ b/ui/src/views/components-test/components-test.tsx @@ -1,4 +1,4 @@ -import { Flex } from '@/components'; +import { Flex, ResolvedAddress } from '@/components'; import { ColorPickerTest } from './color-picker'; import { ComboboxTest } from './combobox-test'; @@ -7,6 +7,9 @@ import { ToastTest } from './toast-test'; export const ComponentsTest: React.FC = () => { return ( + + {'0x7ed735b7095c05d78df169f991f2b7f1a1f1a049'} +