From 603d02cf90c6816deb0cb43708cd792ccb9a6d0a Mon Sep 17 00:00:00 2001 From: alec <93971719+0xAlec@users.noreply.github.com> Date: Mon, 16 Sep 2024 02:03:10 -0400 Subject: [PATCH] feat: re-type `walletCapabilities` object (#1238) --- .changeset/tough-books-study.md | 5 ++ src/OnchainKitConfig.ts | 5 -- src/OnchainKitProvider.test.tsx | 81 +------------------ src/OnchainKitProvider.tsx | 9 +-- src/constants.ts | 6 ++ src/index.ts | 1 - .../hooks}/useCapabilitiesSafe.test.ts | 12 +-- src/internal/hooks/useCapabilitiesSafe.ts | 23 ++++++ .../components/TransactionProvider.test.tsx | 46 +++++------ .../components/TransactionProvider.tsx | 13 ++- .../hooks/useSendWalletTransactions.test.tsx | 40 ++++++++- .../hooks/useSendWalletTransactions.tsx | 5 +- src/transaction/types.ts | 7 +- src/types.ts | 10 --- src/useCapabilitiesSafe.ts | 28 ------- 15 files changed, 115 insertions(+), 176 deletions(-) create mode 100644 .changeset/tough-books-study.md create mode 100644 src/constants.ts rename src/{ => internal/hooks}/useCapabilitiesSafe.test.ts (93%) create mode 100644 src/internal/hooks/useCapabilitiesSafe.ts delete mode 100644 src/useCapabilitiesSafe.ts diff --git a/.changeset/tough-books-study.md b/.changeset/tough-books-study.md new file mode 100644 index 0000000000..b411e1ad60 --- /dev/null +++ b/.changeset/tough-books-study.md @@ -0,0 +1,5 @@ +--- +'@coinbase/onchainkit': patch +--- + +**feat**: re-typed walletCapabilities object in `OnchainKitConfig`. by @0xAlec #1238 diff --git a/src/OnchainKitConfig.ts b/src/OnchainKitConfig.ts index 92335e6067..d774eaffc3 100644 --- a/src/OnchainKitConfig.ts +++ b/src/OnchainKitConfig.ts @@ -9,11 +9,6 @@ export const ONCHAIN_KIT_CONFIG: OnchainKitConfig = { chain: baseSepolia, rpcUrl: null, schemaId: null, - walletCapabilities: { - hasAtomicBatch: false, - hasAuxiliaryFunds: false, - hasPaymasterService: false, - }, }; /** diff --git a/src/OnchainKitProvider.test.tsx b/src/OnchainKitProvider.test.tsx index 731f6aec40..9e788f2b86 100644 --- a/src/OnchainKitProvider.test.tsx +++ b/src/OnchainKitProvider.test.tsx @@ -1,4 +1,4 @@ -import { act, render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { base } from 'viem/chains'; import '@testing-library/jest-dom'; @@ -11,7 +11,6 @@ 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(); @@ -26,7 +25,6 @@ const mockConfig = createConfig({ [base.id]: http(), }, }); -vi.mock('./useCapabilitiesSafe'); const TestComponent = () => { const { schemaId, apiKey } = useOnchainKit(); @@ -46,13 +44,9 @@ 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'; @@ -116,7 +110,7 @@ describe('OnchainKitProvider', () => { }).not.toThrow(); }); - it('should call setOnchainKitConfig with the correct default values', async () => { + it('should call setOnchainKitConfig with the correct values', async () => { render( @@ -133,75 +127,6 @@ describe('OnchainKitProvider', () => { 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( - - - - - - - , - ); - }); - 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( - - - - - - - , - ); - }); - expect(setOnchainKitConfig).toHaveBeenCalledWith({ - address: null, - apiKey, - chain: base, - rpcUrl: null, - schemaId, - walletCapabilities, }); }); }); diff --git a/src/OnchainKitProvider.tsx b/src/OnchainKitProvider.tsx index 4ea6584c89..3a7d0bc2b7 100644 --- a/src/OnchainKitProvider.tsx +++ b/src/OnchainKitProvider.tsx @@ -2,7 +2,6 @@ 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(ONCHAIN_KIT_CONFIG); @@ -21,7 +20,6 @@ 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 = { @@ -30,15 +28,10 @@ export function OnchainKitProvider({ chain: chain, rpcUrl: rpcUrl ?? null, schemaId: schemaId ?? null, - walletCapabilities: walletCapabilities ?? { - hasAtomicBatch: false, - hasAuxiliaryFunds: false, - hasPaymasterService: false, - }, }; setOnchainKitConfig(onchainKitConfig); return onchainKitConfig; - }, [address, apiKey, chain, rpcUrl, schemaId, walletCapabilities]); + }, [address, apiKey, chain, rpcUrl, schemaId]); return ( {children} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000000..f067f19145 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,6 @@ +// Capabilities +export enum Capabilities { + AtomicBatch = 'atomicBatch', + AuxiliaryFunds = 'auxiliaryFunds', + PaymasterService = 'paymasterService', +} diff --git a/src/index.ts b/src/index.ts index 1bc952ab7e..d80ebcc6ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,5 +11,4 @@ export type { OnchainKitConfig, OnchainKitContextType, OnchainKitProviderReact, - WalletCapabilities, } from './types'; diff --git a/src/useCapabilitiesSafe.test.ts b/src/internal/hooks/useCapabilitiesSafe.test.ts similarity index 93% rename from src/useCapabilitiesSafe.test.ts rename to src/internal/hooks/useCapabilitiesSafe.test.ts index e0d7a019f9..fa8f5a14f4 100644 --- a/src/useCapabilitiesSafe.test.ts +++ b/src/internal/hooks/useCapabilitiesSafe.test.ts @@ -15,15 +15,11 @@ vi.mock('wagmi/experimental', () => ({ describe('useCapabilitiesSafe', () => { const mockChainId = 1; const walletCapabilitiesTrue = { - hasAtomicBatch: true, - hasAuxiliaryFunds: true, - hasPaymasterService: true, - }; - const walletCapabilitiesFalse = { - hasAtomicBatch: false, - hasAuxiliaryFunds: false, - hasPaymasterService: false, + atomicBatch: { supported: true }, + paymasterService: { supported: true }, + auxiliaryFunds: { supported: true }, }; + const walletCapabilitiesFalse = {}; beforeEach(() => { vi.resetAllMocks(); diff --git a/src/internal/hooks/useCapabilitiesSafe.ts b/src/internal/hooks/useCapabilitiesSafe.ts new file mode 100644 index 0000000000..cc191b957a --- /dev/null +++ b/src/internal/hooks/useCapabilitiesSafe.ts @@ -0,0 +1,23 @@ +import { useMemo } from 'react'; +import type { WalletCapabilities } from 'viem'; +import { useAccount } from 'wagmi'; +import { useCapabilities } from 'wagmi/experimental'; +import type { UseCapabilitiesSafeParams } from '../../types'; + +export function useCapabilitiesSafe({ + chainId, +}: UseCapabilitiesSafeParams): WalletCapabilities { + const { isConnected } = useAccount(); + + const { data: capabilities, error } = useCapabilities({ + query: { enabled: isConnected }, + }); + + return useMemo(() => { + if (error || !capabilities || !capabilities[chainId]) { + return {}; + } + + return capabilities[chainId]; + }, [capabilities, chainId, error]); +} diff --git a/src/transaction/components/TransactionProvider.test.tsx b/src/transaction/components/TransactionProvider.test.tsx index 77fa6fcf47..8e9c2d07b5 100644 --- a/src/transaction/components/TransactionProvider.test.tsx +++ b/src/transaction/components/TransactionProvider.test.tsx @@ -6,7 +6,7 @@ import { useWaitForTransactionReceipt, } from 'wagmi'; import { waitForTransactionReceipt } from 'wagmi/actions'; -import { useOnchainKit } from '../../useOnchainKit'; +import { useCapabilitiesSafe } from '../../internal/hooks/useCapabilitiesSafe'; import { useCallsStatus } from '../hooks/useCallsStatus'; import { useSendCall } from '../hooks/useSendCall'; import { useSendCalls } from '../hooks/useSendCalls'; @@ -55,8 +55,8 @@ vi.mock('../hooks/useSendWalletTransactions', () => ({ useSendWalletTransactions: vi.fn(), })); -vi.mock('../../useOnchainKit', () => ({ - useOnchainKit: vi.fn(), +vi.mock('../../internal/hooks/useCapabilitiesSafe', () => ({ + useCapabilitiesSafe: vi.fn(), })); const silenceError = () => { @@ -156,13 +156,7 @@ describe('TransactionProvider', () => { (useWaitForTransactionReceipt as ReturnType).mockReturnValue({ receipt: undefined, }); - (useOnchainKit as ReturnType).mockReturnValue({ - walletCapabilities: { - hasAtomicBatch: false, - hasPaymasterService: false, - hasAuxiliaryFunds: false, - }, - }); + (useCapabilitiesSafe as ReturnType).mockReturnValue({}); }); it('should emit onError when setLifecycleStatus is called with error', async () => { @@ -293,10 +287,10 @@ describe('TransactionProvider', () => { status: 'pending', writeContractsAsync: writeContractsAsyncMock, }); - (useOnchainKit as ReturnType).mockReturnValue({ - walletCapabilities: { - hasAtomicBatch: true, - }, + (useCapabilitiesSafe as ReturnType).mockReturnValue({ + atomicBatch: { supported: true }, + paymasterService: { supported: true }, + auxiliaryFunds: { supported: true }, }); render( @@ -336,10 +330,10 @@ describe('TransactionProvider', () => { it('should update context on handleSubmit', async () => { const sendWalletTransactionsMock = vi.fn(); - (useOnchainKit as ReturnType).mockReturnValue({ - walletCapabilities: { - hasAtomicBatch: true, - }, + (useCapabilitiesSafe as ReturnType).mockReturnValue({ + atomicBatch: { supported: true }, + paymasterService: { supported: true }, + auxiliaryFunds: { supported: true }, }); (useSendWalletTransactions as ReturnType).mockReturnValue( sendWalletTransactionsMock, @@ -364,10 +358,10 @@ describe('TransactionProvider', () => { status: 'idle', writeContractsAsync: writeContractsAsyncMock, }); - (useOnchainKit as ReturnType).mockReturnValue({ - walletCapabilities: { - hasAtomicBatch: true, - }, + (useCapabilitiesSafe as ReturnType).mockReturnValue({ + atomicBatch: { supported: true }, + paymasterService: { supported: true }, + auxiliaryFunds: { supported: true }, }); render( @@ -435,10 +429,10 @@ describe('TransactionProvider', () => { (useSendWalletTransactions as ReturnType).mockReturnValue( sendWalletTransactionsMock, ); - (useOnchainKit as ReturnType).mockReturnValue({ - walletCapabilities: { - hasAtomicBatch: true, - }, + (useCapabilitiesSafe as ReturnType).mockReturnValue({ + atomicBatch: { supported: true }, + paymasterService: { supported: true }, + auxiliaryFunds: { supported: true }, }); render( diff --git a/src/transaction/components/TransactionProvider.tsx b/src/transaction/components/TransactionProvider.tsx index d32a709e81..6c75bc2ec5 100644 --- a/src/transaction/components/TransactionProvider.tsx +++ b/src/transaction/components/TransactionProvider.tsx @@ -7,6 +7,7 @@ import { useState, } from 'react'; import type { Address } from 'viem'; +import { baseSepolia } from 'viem/chains'; import { useAccount, useConfig, @@ -14,8 +15,9 @@ import { useWaitForTransactionReceipt, } from 'wagmi'; import { waitForTransactionReceipt } from 'wagmi/actions'; +import { Capabilities } from '../../constants'; +import { useCapabilitiesSafe } from '../../internal/hooks/useCapabilitiesSafe'; import { useValue } from '../../internal/hooks/useValue'; -import { useOnchainKit } from '../../useOnchainKit'; import { GENERIC_ERROR_MESSAGE, TRANSACTION_TYPE_CALLS, @@ -77,7 +79,9 @@ export function TransactionProvider({ : TRANSACTION_TYPE_CONTRACTS; // Retrieve wallet capabilities - const { walletCapabilities } = useOnchainKit(); + const walletCapabilities = useCapabilitiesSafe({ + chainId: chainId || baseSepolia.id, + }); // defaults to Base Sepolia if not provided const { switchChainAsync } = useSwitchChain(); @@ -127,7 +131,8 @@ export function TransactionProvider({ // For batched, use statusSendCalls or statusWriteContracts // For single, use statusSendCall or statusWriteContract const transactionStatus = useMemo(() => { - const transactionStatuses = walletCapabilities.hasAtomicBatch + const transactionStatuses = walletCapabilities[Capabilities.AtomicBatch] + ?.supported ? { [TRANSACTION_TYPE_CALLS]: statusSendCalls, [TRANSACTION_TYPE_CONTRACTS]: statusWriteContracts, @@ -143,7 +148,7 @@ export function TransactionProvider({ statusSendCall, statusWriteContract, transactionType, - walletCapabilities.hasAtomicBatch, + walletCapabilities[Capabilities.AtomicBatch], ]); // Transaction hash for single transaction (non-batched) diff --git a/src/transaction/hooks/useSendWalletTransactions.test.tsx b/src/transaction/hooks/useSendWalletTransactions.test.tsx index 8954d64122..77db8c940b 100644 --- a/src/transaction/hooks/useSendWalletTransactions.test.tsx +++ b/src/transaction/hooks/useSendWalletTransactions.test.tsx @@ -19,7 +19,7 @@ describe('useSendWalletTransactions', () => { it('should handle batched transactions', async () => { const transactions = [{ to: '0x123', data: '0x456' }]; - const capabilities = { someCapability: true }; + const capabilities = {}; const { result } = renderHook(() => useSendWalletTransactions({ transactions, @@ -29,7 +29,11 @@ describe('useSendWalletTransactions', () => { writeContractAsync: vi.fn(), sendCallsAsync: vi.fn(), sendCallAsync: vi.fn(), - walletCapabilities: { hasAtomicBatch: true }, + walletCapabilities: { + atomicBatch: { + supported: true, + }, + }, }), ); await result.current(); @@ -56,7 +60,11 @@ describe('useSendWalletTransactions', () => { writeContractAsync: vi.fn(), sendCallsAsync: vi.fn(), sendCallAsync: vi.fn(), - walletCapabilities: { hasAtomicBatch: false }, + walletCapabilities: { + atomicBatch: { + supported: false, + }, + }, }), ); await result.current(); @@ -85,4 +93,30 @@ describe('useSendWalletTransactions', () => { expect(sendBatchedTransactions).not.toHaveBeenCalled(); expect(sendSingleTransactions).not.toHaveBeenCalled(); }); + + it('should handle empty walletCapabilities', async () => { + const transactions = [ + { to: '0x123', data: '0x456' }, + { to: '0x789', data: '0xabc' }, + ]; + const { result } = renderHook(() => + useSendWalletTransactions({ + transactions, + transactionType: TRANSACTION_TYPE_CALLS, + capabilities: undefined, + writeContractsAsync: vi.fn(), + writeContractAsync: vi.fn(), + sendCallsAsync: vi.fn(), + sendCallAsync: vi.fn(), + walletCapabilities: {}, + }), + ); + await result.current(); + expect(sendSingleTransactions).toHaveBeenCalledWith({ + sendCallAsync: expect.any(Function), + transactions, + transactionType: TRANSACTION_TYPE_CALLS, + writeContractAsync: expect.any(Function), + }); + }); }); diff --git a/src/transaction/hooks/useSendWalletTransactions.tsx b/src/transaction/hooks/useSendWalletTransactions.tsx index f2c395363e..dbbe0ebda7 100644 --- a/src/transaction/hooks/useSendWalletTransactions.tsx +++ b/src/transaction/hooks/useSendWalletTransactions.tsx @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { Capabilities } from '../../constants'; import type { UseSendWalletTransactionsParams } from '../types'; import { sendBatchedTransactions } from '../utils/sendBatchedTransactions'; import { sendSingleTransactions } from '../utils/sendSingleTransactions'; @@ -18,7 +19,7 @@ export const useSendWalletTransactions = ({ if (!transactions) { return; } - if (walletCapabilities.hasAtomicBatch) { + if (walletCapabilities[Capabilities.AtomicBatch]?.supported) { // Batched transactions await sendBatchedTransactions({ capabilities, @@ -44,6 +45,6 @@ export const useSendWalletTransactions = ({ capabilities, transactions, transactionType, - walletCapabilities.hasAtomicBatch, + walletCapabilities, ]); }; diff --git a/src/transaction/types.ts b/src/transaction/types.ts index 10f1656844..fb0d6a6b05 100644 --- a/src/transaction/types.ts +++ b/src/transaction/types.ts @@ -6,12 +6,13 @@ import type { Hex, TransactionReceipt, } from 'viem'; +import type { WalletCapabilities as ViemWalletCapabilities } from 'viem'; import type { Config } from 'wagmi'; import type { SendTransactionMutateAsync, WriteContractMutateAsync, } from 'wagmi/query'; -import type { WalletCapabilities as OnchainKitWalletCapabilities } from '../types'; +// 🌲☀🌲 import { TRANSACTION_TYPE_CALLS, TRANSACTION_TYPE_CONTRACTS, @@ -246,7 +247,7 @@ export type UseSendWalletTransactionsParams = { sendCallAsync: SendTransactionMutateAsync | (() => void); transactions?: Call[] | ContractFunctionParameters[]; transactionType: string; - walletCapabilities: OnchainKitWalletCapabilities; + walletCapabilities: ViemWalletCapabilities; // biome-ignore lint: cannot find module 'wagmi/experimental/query' writeContractsAsync: any; writeContractAsync: WriteContractMutateAsync | (() => void); @@ -265,7 +266,7 @@ export type UseTransactionTypeParams = { batch: string; }; }; - walletCapabilities: OnchainKitWalletCapabilities; + walletCapabilities: ViemWalletCapabilities; }; /** diff --git a/src/types.ts b/src/types.ts index dc57ce6cfb..7b9f1d9527 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,7 +27,6 @@ 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; @@ -52,12 +51,3 @@ export type OnchainKitProviderReact = { 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) -}; diff --git a/src/useCapabilitiesSafe.ts b/src/useCapabilitiesSafe.ts deleted file mode 100644 index b12bdaafed..0000000000 --- a/src/useCapabilitiesSafe.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useAccount } from 'wagmi'; -import { useCapabilities } from 'wagmi/experimental'; -import type { UseCapabilitiesSafeParams, WalletCapabilities } from './types'; - -export function useCapabilitiesSafe({ - chainId, -}: UseCapabilitiesSafeParams): WalletCapabilities { - const { isConnected } = useAccount(); - - const { data: capabilities, error } = useCapabilities({ - query: { enabled: isConnected }, - }); - - if (error || !capabilities || !capabilities[chainId]) { - return { - hasPaymasterService: false, - hasAtomicBatch: false, - hasAuxiliaryFunds: false, - }; - } - - return { - hasPaymasterService: - capabilities[chainId].paymasterService?.supported ?? false, - hasAtomicBatch: capabilities[chainId].atomicBatch?.supported ?? false, - hasAuxiliaryFunds: capabilities[chainId].auxiliaryFunds?.supported ?? false, - }; -}