Skip to content

Commit

Permalink
feat: Add Base avatar support, Fallback to Mainnet (#986)
Browse files Browse the repository at this point in the history
  • Loading branch information
kirkas authored Aug 7, 2024
1 parent d22bea0 commit 0351295
Show file tree
Hide file tree
Showing 11 changed files with 329 additions and 26 deletions.
6 changes: 6 additions & 0 deletions .changeset/honest-lies-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@coinbase/onchainkit": minor
---

**feat**: Add chain props to `useAvatar` and `getAvatar` to resolve Base avatar. By @kirkas #986
**feat**: Modify `getAvatar` to resolve Base avatar, and fallback to mainnet if none is found. By @kirkas #986
1 change: 1 addition & 0 deletions site/docs/pages/identity/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type GetAttestationsOptions = {

```ts
type GetAvatar = {
chain?: Chain; // Optional chain for domain resolution
ensName: string; // The ENS name to fetch the avatar for.
};
```
Expand Down
1 change: 1 addition & 0 deletions site/docs/pages/identity/use-avatar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const { data: avatar, isLoading } = useAvatar({ ensName: 'vitalik.eth' });
```ts
type UseAvatarOptions = {
ensName: string;
chain?: Chain;
};

type UseAvatarQueryOptions = {
Expand Down
30 changes: 29 additions & 1 deletion src/identity/components/Avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { base } from 'viem/chains';
import { base, baseSepolia } from 'viem/chains';
import { OnchainKitProvider } from '../../OnchainKitProvider';
import { Avatar } from './Avatar';
import { Badge } from './Badge';
Expand Down Expand Up @@ -70,3 +70,31 @@ export const WithBadge: Story = {
children: <Badge />,
},
};

export const Base: Story = {
args: {
address: '0xFd3d8ffE248173B710b4e24a7E75ac4424853503',
chain: base,
},
};

export const BaseSepolia: Story = {
args: {
address: '0xf75ca27C443768EE1876b027272DC8E3d00B8a23',
chain: baseSepolia,
},
};

export const BaseDefaultToMainnet: Story = {
args: {
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
chain: base,
},
};

export const BaseSepoliaDefaultToMainnet: Story = {
args: {
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
chain: baseSepolia,
},
};
4 changes: 2 additions & 2 deletions src/identity/components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ export function Avatar({

// The component first attempts to retrieve the ENS name and avatar for the given Ethereum address.
const { data: name, isLoading: isLoadingName } = useName({
address: address ?? contextAddress,
address: accountAddress,
chain: accountChain,
});

const { data: avatar, isLoading: isLoadingAvatar } = useAvatar(
{ ensName: name ?? '' },
{ ensName: name ?? '', chain: accountChain },
{ enabled: !!name },
);

Expand Down
90 changes: 87 additions & 3 deletions src/identity/hooks/useAvatar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/**
* @vitest-environment jsdom
*/
import { renderHook, waitFor } from '@testing-library/react';
import { base, baseSepolia, mainnet, optimism } from 'viem/chains';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { publicClient } from '../../network/client';
import { getChainPublicClient } from '../../network/getChainPublicClient';
import { getNewReactQueryTestProvider } from './getNewReactQueryTestProvider';
import { useAvatar } from './useAvatar';

vi.mock('../../network/client');

vi.mock('../../network/getChainPublicClient', () => ({
...vi.importActual('../../network/getChainPublicClient'),
getChainPublicClient: vi.fn(() => publicClient),
}));

describe('useAvatar', () => {
const mockGetEnsAvatar = publicClient.getEnsAvatar as vi.Mock;

Expand All @@ -33,6 +38,8 @@ describe('useAvatar', () => {
expect(result.current.data).toBe(testEnsAvatar);
expect(result.current.isLoading).toBe(false);
});

expect(getChainPublicClient).toHaveBeenCalledWith(mainnet);
});

it('returns the loading state true while still fetching ENS avatar', async () => {
Expand All @@ -50,4 +57,81 @@ describe('useAvatar', () => {
expect(result.current.isLoading).toBe(true);
});
});

it('return correct base mainnet avatar', async () => {
const testEnsName = 'shrek.base.eth';
const testEnsAvatar = 'shrekface';

// Mock the getEnsAvatar method of the publicClient
mockGetEnsAvatar.mockResolvedValue(testEnsAvatar);

// Use the renderHook function to create a test harness for the useAvatar hook
const { result } = renderHook(
() => useAvatar({ ensName: testEnsName, chain: base }),
{
wrapper: getNewReactQueryTestProvider(),
},
);

// Wait for the hook to finish fetching the ENS avatar
await waitFor(() => {
// Check that the ENS avatar and loading state are correct
expect(result.current.data).toBe(testEnsAvatar);
expect(result.current.isLoading).toBe(false);
});

expect(getChainPublicClient).toHaveBeenCalledWith(base);
});

it('return correct base sepolia avatar', async () => {
const testEnsName = 'shrek.basetest.eth';
const testEnsAvatar = 'shrektestface';

// Mock the getEnsAvatar method of the publicClient
mockGetEnsAvatar.mockResolvedValue(testEnsAvatar);

// Use the renderHook function to create a test harness for the useAvatar hook
const { result } = renderHook(
() => useAvatar({ ensName: testEnsName, chain: baseSepolia }),
{
wrapper: getNewReactQueryTestProvider(),
},
);

// Wait for the hook to finish fetching the ENS avatar
await waitFor(() => {
// Check that the ENS avatar and loading state are correct
expect(result.current.data).toBe(testEnsAvatar);
expect(result.current.isLoading).toBe(false);
});

expect(getChainPublicClient).toHaveBeenCalledWith(baseSepolia);
});

it('returns error for unsupported chain ', async () => {
const testEnsName = 'shrek.basetest.eth';
const testEnsAvatar = 'shrektestface';

// Mock the getEnsAvatar method of the publicClient
mockGetEnsAvatar.mockResolvedValue(testEnsAvatar);

// Use the renderHook function to create a test harness for the useAvatar hook
const { result } = renderHook(
() => useAvatar({ ensName: testEnsName, chain: optimism }),
{
wrapper: getNewReactQueryTestProvider(),
},
);

// Wait for the hook to finish fetching the ENS name
await waitFor(() => {
// Check that the ENS name and loading state are correct
expect(result.current.data).toBe(undefined);
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(true);
expect(result.current.error).toBe(
'ChainId not supported, avatar resolution is only supported on Ethereum and Base.',
);
});
});
});
23 changes: 10 additions & 13 deletions src/identity/hooks/useAvatar.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
import { useQuery } from '@tanstack/react-query';
import type { GetAvatarReturnType } from '../types';
import { mainnet } from 'viem/chains';
import type {
GetAvatarReturnType,
UseAvatarOptions,
UseAvatarQueryOptions,
} from '../types';
import { getAvatar } from '../utils/getAvatar';

type UseAvatarOptions = {
ensName: string;
};

type UseAvatarQueryOptions = {
enabled?: boolean;
cacheTime?: number;
};

/**
* Gets an ensName and resolves the Avatar
*/
export const useAvatar = (
{ ensName }: UseAvatarOptions,
{ ensName, chain = mainnet }: UseAvatarOptions,
queryOptions?: UseAvatarQueryOptions,
) => {
const { enabled = true, cacheTime } = queryOptions ?? {};
const ensActionKey = `ens-avatar-${ensName}`;
const ensActionKey = `ens-avatar-${ensName}-${chain.id}`;

return useQuery<GetAvatarReturnType>({
queryKey: ['useAvatar', ensActionKey],
queryFn: async () => {
return getAvatar({ ensName });
return getAvatar({ ensName, chain });
},
gcTime: cacheTime,
enabled,
Expand Down
1 change: 1 addition & 0 deletions src/identity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export type {
GetNameReturnType,
IdentityContextType,
IdentityReact,
UseAvatarOptions,
} from './types';
17 changes: 17 additions & 0 deletions src/identity/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export type GetAttestationsOptions = {
*/
export type GetAvatar = {
ensName: string; // The ENS name to fetch the avatar for.
chain?: Chain; // Optional chain for domain resolution
};

/**
Expand Down Expand Up @@ -179,6 +180,22 @@ export type UseAttestations = {
schemaId: Address | null;
};

/**
* Note: exported as public Type
*/
export type UseAvatarOptions = {
ensName: string;
chain?: Chain; // Optional chain for domain resolution
};

/**
* Note: exported as public Type
*/
export type UseAvatarQueryOptions = {
enabled?: boolean;
cacheTime?: number;
};

/**
* Note: exported as public Type
*/
Expand Down
Loading

0 comments on commit 0351295

Please sign in to comment.