Skip to content

Commit

Permalink
feat: EIP-5792 in OnchainKitProvider (#1181)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xAlec authored Sep 4, 2024
1 parent e40f379 commit 63742da
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-cameras-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/onchainkit": patch
---

**feat**: add support for `EIP-5792` (https://eips.ethereum.org/EIPS/eip-5792) in `OnchainKitProvider`. by @0xAlec #1181
5 changes: 5 additions & 0 deletions src/OnchainKitConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export const ONCHAIN_KIT_CONFIG: OnchainKitConfig = {
chain: baseSepolia,
rpcUrl: null,
schemaId: null,
walletCapabilities: {
hasAtomicBatch: false,
hasAuxiliaryFunds: false,
hasPaymasterService: false,
},
};

/**
Expand Down
168 changes: 141 additions & 27 deletions src/OnchainKitProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import { render, screen, waitFor } from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import { base } from 'viem/chains';
import '@testing-library/jest-dom';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import { describe, expect, it, vi } from 'vitest';
import { WagmiProvider } from 'wagmi';
import { http, createConfig } from 'wagmi';
import { mock } from 'wagmi/connectors';
import { setOnchainKitConfig } from './OnchainKitConfig';
import { OnchainKitProvider } from './OnchainKitProvider';
import type { EASSchemaUid } from './identity/types';
import { useCapabilitiesSafe } from './useCapabilitiesSafe';
import { useOnchainKit } from './useOnchainKit';

const queryClient = new QueryClient();
const mockConfig = createConfig({
chains: [base],
connectors: [
mock({
accounts: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'],
}),
],
transports: {
[base.id]: http(),
},
});
vi.mock('./useCapabilitiesSafe');

const TestComponent = () => {
const { schemaId, apiKey } = useOnchainKit();
return (
Expand All @@ -26,18 +46,26 @@ vi.mock('./OnchainKitConfig', () => ({
chain: base,
rpcUrl: null,
schemaId: null,
walletCapabilities: {
hasPaymasterService: false,
hasAtomicBatch: false,
hasAuxiliaryFunds: false,
},
},
}));

describe('OnchainKitProvider', () => {
const schemaId: EASSchemaUid = `0x${'1'.repeat(64)}`;
const apiKey = 'test-api-key';

it('provides the context value correctly', async () => {
render(
<OnchainKitProvider chain={base} schemaId={schemaId} apiKey={apiKey}>
<TestComponent />
</OnchainKitProvider>,
<WagmiProvider config={mockConfig}>
<QueryClientProvider client={queryClient}>
<OnchainKitProvider chain={base} schemaId={schemaId} apiKey={apiKey}>
<TestComponent />
</OnchainKitProvider>
</QueryClientProvider>
</WagmiProvider>,
);

await waitFor(() => {
Expand All @@ -47,47 +75,133 @@ describe('OnchainKitProvider', () => {
});

it('throws an error if schemaId does not meet the required length', () => {
expect(() =>
expect(() => {
render(
<OnchainKitProvider chain={base} schemaId={'0x123'} apiKey={apiKey}>
<TestComponent />
</OnchainKitProvider>,
),
).toThrow('EAS schemaId must be 64 characters prefixed with "0x"');
<WagmiProvider config={mockConfig}>
<QueryClientProvider client={queryClient}>
<OnchainKitProvider chain={base} schemaId={'0x123'} apiKey={apiKey}>
<TestComponent />
</OnchainKitProvider>
</QueryClientProvider>
</WagmiProvider>,
);
}).toThrow('EAS schemaId must be 64 characters prefixed with "0x"');
});

it('does not throw an error if schemaId is not provided', () => {
expect(() =>
expect(() => {
render(
<OnchainKitProvider chain={base} apiKey={apiKey}>
<TestComponent />
</OnchainKitProvider>,
),
).not.toThrow();
<WagmiProvider config={mockConfig}>
<QueryClientProvider client={queryClient}>
<OnchainKitProvider chain={base} apiKey={apiKey}>
<TestComponent />
</OnchainKitProvider>
</QueryClientProvider>
</WagmiProvider>,
);
}).not.toThrow();
});

it('does not throw an error if api key is not provided', () => {
expect(() =>
expect(() => {
render(
<OnchainKitProvider chain={base}>
<TestComponent />
</OnchainKitProvider>,
),
).not.toThrow();
<WagmiProvider config={mockConfig}>
<QueryClientProvider client={queryClient}>
<OnchainKitProvider chain={base}>
<TestComponent />
</OnchainKitProvider>
</QueryClientProvider>
</WagmiProvider>,
);
}).not.toThrow();
});

it('should call setOnchainKitConfig with the correct values', async () => {
it('should call setOnchainKitConfig with the correct default values', async () => {
render(
<OnchainKitProvider chain={base} schemaId={schemaId} apiKey={apiKey}>
<TestComponent />
</OnchainKitProvider>,
<WagmiProvider config={mockConfig}>
<QueryClientProvider client={queryClient}>
<OnchainKitProvider chain={base} schemaId={schemaId} apiKey={apiKey}>
<TestComponent />
</OnchainKitProvider>
</QueryClientProvider>
</WagmiProvider>,
);

expect(setOnchainKitConfig).toHaveBeenCalledWith({
address: null,
apiKey,
chain: base,
rpcUrl: null,
schemaId,
walletCapabilities: {
hasPaymasterService: false,
hasAtomicBatch: false,
hasAuxiliaryFunds: false,
},
});
});

it('should call setOnchainKitConfig when capabilities are found', async () => {
const walletCapabilities = {
hasPaymasterService: true,
hasAtomicBatch: true,
hasAuxiliaryFunds: true,
};
vi.mocked(useCapabilitiesSafe).mockReturnValue(walletCapabilities);
await act(async () => {
render(
<WagmiProvider config={mockConfig}>
<QueryClientProvider client={queryClient}>
<OnchainKitProvider
chain={base}
schemaId={schemaId}
apiKey={apiKey}
>
<TestComponent />
</OnchainKitProvider>
</QueryClientProvider>
</WagmiProvider>,
);
});
expect(setOnchainKitConfig).toHaveBeenCalledWith({
address: null,
apiKey,
chain: base,
rpcUrl: null,
schemaId,
walletCapabilities,
});
});

it('should call setOnchainKitConfig when capabilities are not found', async () => {
const walletCapabilities = {
hasPaymasterService: false,
hasAtomicBatch: false,
hasAuxiliaryFunds: false,
};
vi.mocked(useCapabilitiesSafe).mockReturnValue(walletCapabilities);
await act(async () => {
render(
<WagmiProvider config={mockConfig}>
<QueryClientProvider client={queryClient}>
<OnchainKitProvider
chain={base}
schemaId={schemaId}
apiKey={apiKey}
>
<TestComponent />
</OnchainKitProvider>
</QueryClientProvider>
</WagmiProvider>,
);
});
expect(setOnchainKitConfig).toHaveBeenCalledWith({
address: null,
apiKey,
chain: base,
rpcUrl: null,
schemaId,
walletCapabilities,
});
});
});
10 changes: 9 additions & 1 deletion src/OnchainKitProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createContext, useMemo } from 'react';
import { ONCHAIN_KIT_CONFIG, setOnchainKitConfig } from './OnchainKitConfig';
import { checkHashLength } from './internal/utils/checkHashLength';
import type { OnchainKitContextType, OnchainKitProviderReact } from './types';
import { useCapabilitiesSafe } from './useCapabilitiesSafe';

export const OnchainKitContext =
createContext<OnchainKitContextType>(ONCHAIN_KIT_CONFIG);
Expand All @@ -20,17 +21,24 @@ export function OnchainKitProvider({
if (schemaId && !checkHashLength(schemaId, 64)) {
throw Error('EAS schemaId must be 64 characters prefixed with "0x"');
}
const walletCapabilities = useCapabilitiesSafe({ chainId: chain.id });

const value = useMemo(() => {
const onchainKitConfig = {
address: address ?? null,
apiKey: apiKey ?? null,
chain: chain,
rpcUrl: rpcUrl ?? null,
schemaId: schemaId ?? null,
walletCapabilities: walletCapabilities ?? {
hasAtomicBatch: false,
hasAuxiliaryFunds: false,
hasPaymasterService: false,
},
};
setOnchainKitConfig(onchainKitConfig);
return onchainKitConfig;
}, [address, chain, schemaId, apiKey, rpcUrl]);
}, [address, apiKey, chain, rpcUrl, schemaId, walletCapabilities]);
return (
<OnchainKitContext.Provider value={value}>
{children}
Expand Down
39 changes: 33 additions & 6 deletions src/identity/components/IdentityProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import '@testing-library/jest-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook } from '@testing-library/react';
import type { Address, Chain } from 'viem';
import { baseSepolia, optimism, sepolia } from 'viem/chains';
import { OnchainKitProvider } from '../../OnchainKitProvider';
import { IdentityProvider, useIdentityContext } from './IdentityProvider';

import { describe, expect, it } from 'vitest';
import { WagmiProvider } from 'wagmi';
import { http, createConfig } from 'wagmi';
import { mock } from 'wagmi/connectors';

const queryClient = new QueryClient();
const mockConfig = createConfig({
chains: [sepolia],
connectors: [
mock({
accounts: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'],
}),
],
transports: {
[sepolia.id]: http(),
},
});

describe('IdentityProvider', () => {
it('provides context values from props', () => {
const address: Address = '0x1234567890abcdef1234567890abcdef12345678';
Expand Down Expand Up @@ -35,9 +54,13 @@ describe('IdentityProvider', () => {
it('use onchainkit context chain if provided', () => {
const { result } = renderHook(() => useIdentityContext(), {
wrapper: ({ children }) => (
<OnchainKitProvider chain={optimism}>
<IdentityProvider>{children}</IdentityProvider>
</OnchainKitProvider>
<WagmiProvider config={mockConfig}>
<QueryClientProvider client={queryClient}>
<OnchainKitProvider chain={optimism}>
<IdentityProvider>{children}</IdentityProvider>
</OnchainKitProvider>
</QueryClientProvider>
</WagmiProvider>
),
});
expect(result.current.address).toEqual('');
Expand All @@ -48,9 +71,13 @@ describe('IdentityProvider', () => {
it('use identity context chain over onchainkit context if both are provided', () => {
const { result } = renderHook(() => useIdentityContext(), {
wrapper: ({ children }) => (
<OnchainKitProvider chain={optimism}>
<IdentityProvider chain={sepolia}>{children}</IdentityProvider>
</OnchainKitProvider>
<WagmiProvider config={mockConfig}>
<QueryClientProvider client={queryClient}>
<OnchainKitProvider chain={optimism}>
<IdentityProvider chain={sepolia}>{children}</IdentityProvider>
</OnchainKitProvider>
</QueryClientProvider>
</WagmiProvider>
),
});
expect(result.current.address).toEqual('');
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export type {
OnchainKitConfig,
OnchainKitContextType,
OnchainKitProviderReact,
WalletCapabilities,
} from './types';
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type OnchainKitConfig = {
rpcUrl: string | null; // RPC URL for onchain requests. Defaults to using CDP Node if the API Key is set
chain: Chain; // Chain must be provided as we need to know which chain to use
schemaId: EASSchemaUid | null; // SchemaId is optional as not all apps need to use EAS
walletCapabilities: WalletCapabilities; // Capabilities of the wallet - see EIP-5792
};

export type SetOnchainKitConfig = Partial<OnchainKitConfig>;
Expand All @@ -45,3 +46,16 @@ export type OnchainKitProviderReact = {
rpcUrl?: string;
schemaId?: EASSchemaUid;
};

export type UseCapabilitiesSafeParams = {
chainId: number;
};

/**
* Note: exported as public Type
*/
export type WalletCapabilities = {
hasPaymasterService: boolean; // If the wallet supports ERC-4337 Paymasters for gas sponsorship
hasAtomicBatch: boolean; // If the wallet supports atomic batching of transactions
hasAuxiliaryFunds: boolean; // If the wallet supports auxiliary funding of accounts (e.g. Magic Spend)
};
Loading

0 comments on commit 63742da

Please sign in to comment.