diff --git a/.changeset/fast-cooks-provide.md b/.changeset/fast-cooks-provide.md new file mode 100644 index 0000000000..30e3c5c1b0 --- /dev/null +++ b/.changeset/fast-cooks-provide.md @@ -0,0 +1,5 @@ +--- +"@wagmi/core": minor +--- + +Added `addChain` action. diff --git a/.changeset/thin-rings-carry.md b/.changeset/thin-rings-carry.md new file mode 100644 index 0000000000..a4cbd790cc --- /dev/null +++ b/.changeset/thin-rings-carry.md @@ -0,0 +1,5 @@ +--- +"wagmi": minor +--- + +Added `useAddChain` hook. diff --git a/packages/core/src/actions/addChain.test.ts b/packages/core/src/actions/addChain.test.ts new file mode 100644 index 0000000000..9a6614f6bd --- /dev/null +++ b/packages/core/src/actions/addChain.test.ts @@ -0,0 +1,25 @@ +import { config } from '@wagmi/test' +import { avalanche } from 'viem/chains' +import { test } from 'vitest' + +import { addChain } from './addChain.js' +import { connect } from './connect.js' +import { disconnect } from './disconnect.js' + +const connector = config.connectors[0]! + +test('default', async () => { + await connect(config, { connector }) + await addChain(config, { + chain: avalanche, + }) + await disconnect(config, { connector }) +}) + +test('no block explorer', async () => { + await connect(config, { connector }) + await addChain(config, { + chain: { ...avalanche, blockExplorers: undefined }, + }) + await disconnect(config, { connector }) +}) diff --git a/packages/core/src/actions/addChain.ts b/packages/core/src/actions/addChain.ts new file mode 100644 index 0000000000..567ef9c8f4 --- /dev/null +++ b/packages/core/src/actions/addChain.ts @@ -0,0 +1,38 @@ +import { + type AddChainErrorType as viem_AddChainErrorType, + type AddChainParameters as viem_AddChainParameters, + addChain as viem_addChain, +} from 'viem/actions' + +import type { Config } from '../createConfig.js' +import type { BaseErrorType, ErrorType } from '../errors/base.js' +import type { ConnectorParameter } from '../types/properties.js' +import type { Compute } from '../types/utils.js' +import { getAction } from '../utils/getAction.js' +import { + type GetConnectorClientErrorType, + getConnectorClient, +} from './getConnectorClient.js' + +export type AddChainParameters = Compute< + viem_AddChainParameters & ConnectorParameter +> + +export type AddChainErrorType = + // getConnectorClient() + | GetConnectorClientErrorType + // base + | BaseErrorType + | ErrorType + // viem + | viem_AddChainErrorType + +/** https://wagmi.sh/core/api/actions/addChain */ +export async function addChain(config: Config, parameters: AddChainParameters) { + const { connector, ...rest } = parameters + + const client = await getConnectorClient(config, { connector }) + + const action = getAction(client, viem_addChain, 'addChain') + return action(rest) +} diff --git a/packages/core/src/connectors/mock.ts b/packages/core/src/connectors/mock.ts index 8c6fa4d57b..a0dccee442 100644 --- a/packages/core/src/connectors/mock.ts +++ b/packages/core/src/connectors/mock.ts @@ -28,6 +28,7 @@ export type MockParameters = { features?: | { connectError?: boolean | Error | undefined + addChainError?: boolean | Error | undefined switchChainError?: boolean | Error | undefined signMessageError?: boolean | Error | undefined signTypedDataError?: boolean | Error | undefined @@ -145,6 +146,17 @@ export function mock(parameters: MockParameters) { } // wallet methods + if (method === 'wallet_addEthereumChain') { + if (features.addChainError) { + if (typeof features.addChainError === 'boolean') + throw new UserRejectedRequestError( + new Error('Failed to add chain.'), + ) + throw features.addChainError + } + return + } + if (method === 'wallet_switchEthereumChain') { if (features.switchChainError) { if (typeof features.switchChainError === 'boolean') diff --git a/packages/core/src/exports/actions.test.ts b/packages/core/src/exports/actions.test.ts index 77f805d96c..a3f1d4cb45 100644 --- a/packages/core/src/exports/actions.test.ts +++ b/packages/core/src/exports/actions.test.ts @@ -5,6 +5,7 @@ import * as actions from './actions.js' test('exports', () => { expect(Object.keys(actions)).toMatchInlineSnapshot(` [ + "addChain", "call", "connect", "deployContract", diff --git a/packages/core/src/exports/actions.ts b/packages/core/src/exports/actions.ts index d5d6b21760..bbedfb528b 100644 --- a/packages/core/src/exports/actions.ts +++ b/packages/core/src/exports/actions.ts @@ -3,6 +3,12 @@ //////////////////////////////////////////////////////////////////////////////// // biome-ignore lint/performance/noBarrelFile: entrypoint module +export { + type AddChainErrorType, + type AddChainParameters, + addChain, +} from '../actions/addChain.js' + export { type CallErrorType, type CallParameters, diff --git a/packages/core/src/exports/index.test.ts b/packages/core/src/exports/index.test.ts index 2d6953b9cc..47896ac343 100644 --- a/packages/core/src/exports/index.test.ts +++ b/packages/core/src/exports/index.test.ts @@ -5,6 +5,7 @@ import * as core from './index.js' test('exports', () => { expect(Object.keys(core)).toMatchInlineSnapshot(` [ + "addChain", "call", "connect", "deployContract", diff --git a/packages/core/src/exports/index.ts b/packages/core/src/exports/index.ts index 69dae7cc17..aa686c8cff 100644 --- a/packages/core/src/exports/index.ts +++ b/packages/core/src/exports/index.ts @@ -3,6 +3,12 @@ //////////////////////////////////////////////////////////////////////////////// // biome-ignore lint/performance/noBarrelFile: entrypoint module +export { + type AddChainErrorType, + type AddChainParameters, + addChain, +} from '../actions/addChain.js' + export { type CallErrorType, type CallParameters, diff --git a/packages/core/src/exports/query.test.ts b/packages/core/src/exports/query.test.ts index 777bfe9abe..3919aaf66a 100644 --- a/packages/core/src/exports/query.test.ts +++ b/packages/core/src/exports/query.test.ts @@ -5,6 +5,7 @@ import * as query from './query.js' test('exports', () => { expect(Object.keys(query)).toMatchInlineSnapshot(` [ + "addChainMutationOptions", "callQueryKey", "callQueryOptions", "connectMutationOptions", diff --git a/packages/core/src/exports/query.ts b/packages/core/src/exports/query.ts index 5746148bab..c0eb28c526 100644 --- a/packages/core/src/exports/query.ts +++ b/packages/core/src/exports/query.ts @@ -3,6 +3,14 @@ //////////////////////////////////////////////////////////////////////////////// // biome-ignore lint/performance/noBarrelFile: entrypoint module +export { + type AddChainData, + type AddChainVariables, + type AddChainMutate, + type AddChainMutateAsync, + addChainMutationOptions, +} from '../query/addChain.js' + export { type CallData, type CallOptions, diff --git a/packages/core/src/query/addChain.test.ts b/packages/core/src/query/addChain.test.ts new file mode 100644 index 0000000000..ab61caa1ba --- /dev/null +++ b/packages/core/src/query/addChain.test.ts @@ -0,0 +1,15 @@ +import { config } from '@wagmi/test' +import { expect, test } from 'vitest' + +import { addChainMutationOptions } from './addChain.js' + +test('default', () => { + expect(addChainMutationOptions(config)).toMatchInlineSnapshot(` + { + "mutationFn": [Function], + "mutationKey": [ + "addChain", + ], + } + `) +}) diff --git a/packages/core/src/query/addChain.ts b/packages/core/src/query/addChain.ts new file mode 100644 index 0000000000..a36c8c27ba --- /dev/null +++ b/packages/core/src/query/addChain.ts @@ -0,0 +1,40 @@ +import type { MutationOptions } from '@tanstack/query-core' + +import { + type AddChainErrorType, + type AddChainParameters, + addChain, +} from '../actions/addChain.js' +import type { Config } from '../createConfig.js' +import type { Mutate, MutateAsync } from './types.js' + +export function addChainMutationOptions(config: Config) { + return { + mutationFn(variables) { + return addChain(config, variables) + }, + mutationKey: ['addChain'], + } as const satisfies MutationOptions< + AddChainData, + AddChainErrorType, + AddChainVariables + > +} + +export type AddChainData = void + +export type AddChainVariables = AddChainParameters + +export type AddChainMutate = Mutate< + AddChainData, + AddChainErrorType, + AddChainVariables, + context +> + +export type AddChainMutateAsync = MutateAsync< + AddChainData, + AddChainErrorType, + AddChainVariables, + context +> diff --git a/packages/react/src/exports/actions.test.ts b/packages/react/src/exports/actions.test.ts index 77f805d96c..a3f1d4cb45 100644 --- a/packages/react/src/exports/actions.test.ts +++ b/packages/react/src/exports/actions.test.ts @@ -5,6 +5,7 @@ import * as actions from './actions.js' test('exports', () => { expect(Object.keys(actions)).toMatchInlineSnapshot(` [ + "addChain", "call", "connect", "deployContract", diff --git a/packages/react/src/exports/index.test.ts b/packages/react/src/exports/index.test.ts index cba2fa8d59..fbfd36e0f0 100644 --- a/packages/react/src/exports/index.test.ts +++ b/packages/react/src/exports/index.test.ts @@ -13,6 +13,7 @@ test('exports', () => { "WagmiProviderNotFoundError", "useAccount", "useAccountEffect", + "useAddChain", "useBalance", "useBlock", "useBlockNumber", diff --git a/packages/react/src/exports/index.ts b/packages/react/src/exports/index.ts index 7a08838ce4..a444123c63 100644 --- a/packages/react/src/exports/index.ts +++ b/packages/react/src/exports/index.ts @@ -39,6 +39,12 @@ export { useAccountEffect, } from '../hooks/useAccountEffect.js' +export { + type UseAddChainParameters, + type UseAddChainReturnType, + useAddChain, +} from '../hooks/useAddChain.js' + export { type UseBalanceParameters, type UseBalanceReturnType, diff --git a/packages/react/src/exports/query.test.ts b/packages/react/src/exports/query.test.ts index fe4130815d..ddb3797bbf 100644 --- a/packages/react/src/exports/query.test.ts +++ b/packages/react/src/exports/query.test.ts @@ -5,6 +5,7 @@ import * as query from './query.js' test('exports', () => { expect(Object.keys(query)).toMatchInlineSnapshot(` [ + "addChainMutationOptions", "callQueryKey", "callQueryOptions", "connectMutationOptions", diff --git a/packages/react/src/hooks/useAddChain.test-d.ts b/packages/react/src/hooks/useAddChain.test-d.ts new file mode 100644 index 0000000000..1e3cf78979 --- /dev/null +++ b/packages/react/src/hooks/useAddChain.test-d.ts @@ -0,0 +1,62 @@ +import type { AddChainErrorType } from '@wagmi/core' +import type { AddChainVariables } from '@wagmi/core/query' +import { expectTypeOf, test } from 'vitest' + +import { avalanche } from 'viem/chains' +import { useAddChain } from './useAddChain.js' + +const contextValue = { foo: 'bar' } as const + +test('context', () => { + const { context, data, error, addChain, variables } = useAddChain({ + mutation: { + onMutate(variables) { + expectTypeOf(variables).toEqualTypeOf() + return contextValue + }, + onError(error, variables, context) { + expectTypeOf(variables).toEqualTypeOf() + expectTypeOf(error).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + }, + onSuccess(data, variables, context) { + expectTypeOf(variables).toEqualTypeOf() + expectTypeOf(data).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + }, + onSettled(data, error, variables, context) { + expectTypeOf(data).toEqualTypeOf() + expectTypeOf(error).toEqualTypeOf() + expectTypeOf(variables).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + }, + }, + }) + + expectTypeOf(data).toEqualTypeOf() + expectTypeOf(error).toEqualTypeOf() + expectTypeOf(variables).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + + addChain( + { chain: avalanche }, + { + onError(error, variables, context) { + expectTypeOf(variables).toEqualTypeOf() + expectTypeOf(error).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + }, + onSuccess(data, variables, context) { + expectTypeOf(variables).toEqualTypeOf() + expectTypeOf(data).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + }, + onSettled(data, error, variables, context) { + expectTypeOf(data).toEqualTypeOf() + expectTypeOf(error).toEqualTypeOf() + expectTypeOf(variables).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + }, + }, + ) +}) diff --git a/packages/react/src/hooks/useAddChain.test.ts b/packages/react/src/hooks/useAddChain.test.ts new file mode 100644 index 0000000000..70384a8ef8 --- /dev/null +++ b/packages/react/src/hooks/useAddChain.test.ts @@ -0,0 +1,33 @@ +import { connect, disconnect } from '@wagmi/core' +import { config } from '@wagmi/test' +import { renderHook, waitFor } from '@wagmi/test/react' +import { avalanche } from 'viem/chains' +import { expect, test } from 'vitest' + +import { useAddChain } from './useAddChain.js' + +const connector = config.connectors[0]! + +test('default', async () => { + await connect(config, { connector }) + + const { result } = renderHook(() => useAddChain()) + + result.current.addChain({ chain: avalanche }) + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()) + + await disconnect(config, { connector }) +}) + +test('no block explorer', async () => { + await connect(config, { connector }) + + const { result } = renderHook(() => useAddChain()) + + result.current.addChain({ + chain: { ...avalanche, blockExplorers: undefined }, + }) + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()) + + await disconnect(config, { connector }) +}) diff --git a/packages/react/src/hooks/useAddChain.ts b/packages/react/src/hooks/useAddChain.ts new file mode 100644 index 0000000000..2952f7f2c7 --- /dev/null +++ b/packages/react/src/hooks/useAddChain.ts @@ -0,0 +1,65 @@ +'use client' + +import { useMutation } from '@tanstack/react-query' +import type { AddChainErrorType } from '@wagmi/core' +import type { Compute } from '@wagmi/core/internal' +import { + type AddChainData, + type AddChainMutate, + type AddChainMutateAsync, + type AddChainVariables, + addChainMutationOptions, +} from '@wagmi/core/query' + +import type { ConfigParameter } from '../types/properties.js' +import type { + UseMutationParameters, + UseMutationReturnType, +} from '../utils/query.js' +import { useConfig } from './useConfig.js' + +export type UseAddChainParameters = Compute< + ConfigParameter & { + mutation?: + | UseMutationParameters< + AddChainData, + AddChainErrorType, + AddChainVariables, + context + > + | undefined + } +> + +export type UseAddChainReturnType = Compute< + UseMutationReturnType< + AddChainData, + AddChainErrorType, + AddChainVariables, + context + > & { + addChain: AddChainMutate + addChainAsync: AddChainMutateAsync + } +> + +/** https://wagmi.sh/react/api/hooks/useAddChain */ +export function useAddChain( + parameters: UseAddChainParameters = {}, +): UseAddChainReturnType { + const { mutation } = parameters + + const config = useConfig(parameters) + + const mutationOptions = addChainMutationOptions(config) + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutation, + ...mutationOptions, + }) + + return { + ...result, + addChain: mutate, + addChainAsync: mutateAsync, + } +} diff --git a/packages/vue/src/exports/actions.test.ts b/packages/vue/src/exports/actions.test.ts index 77f805d96c..a3f1d4cb45 100644 --- a/packages/vue/src/exports/actions.test.ts +++ b/packages/vue/src/exports/actions.test.ts @@ -5,6 +5,7 @@ import * as actions from './actions.js' test('exports', () => { expect(Object.keys(actions)).toMatchInlineSnapshot(` [ + "addChain", "call", "connect", "deployContract", diff --git a/packages/vue/src/exports/query.test.ts b/packages/vue/src/exports/query.test.ts index d2b3386c33..e50b49e1e8 100644 --- a/packages/vue/src/exports/query.test.ts +++ b/packages/vue/src/exports/query.test.ts @@ -5,6 +5,7 @@ import * as query from './query.js' test('exports', () => { expect(Object.keys(query)).toMatchInlineSnapshot(` [ + "addChainMutationOptions", "callQueryKey", "callQueryOptions", "connectMutationOptions", diff --git a/site/.vitepress/sidebar.ts b/site/.vitepress/sidebar.ts index e08ff57314..42c9ffa67e 100644 --- a/site/.vitepress/sidebar.ts +++ b/site/.vitepress/sidebar.ts @@ -143,6 +143,7 @@ export function getSidebar() { text: 'useAccountEffect', link: '/react/api/hooks/useAccountEffect', }, + { text: 'useAddChain', link: '/react/api/hooks/useAddChain' }, { text: 'useBalance', link: '/react/api/hooks/useBalance' }, { text: 'useBlockNumber', @@ -744,6 +745,7 @@ export function getSidebar() { text: 'Actions', link: '/core/api/actions', items: [ + { text: 'addChain', link: '/core/api/actions/addChain' }, { text: 'call', link: '/core/api/actions/call', diff --git a/site/core/api/actions/addChain.md b/site/core/api/actions/addChain.md new file mode 100644 index 0000000000..5a37eadeb3 --- /dev/null +++ b/site/core/api/actions/addChain.md @@ -0,0 +1,68 @@ + + +# addChain + +Action for adding an EVM chain to the wallet. + +## Import + +```ts +import { addChain } from '@wagmi/core' +``` + +## Usage + +::: code-group +```ts [index.ts] +import { avalanche } from 'viem/chains' +import { addChain } from '@wagmi/core' +import { config } from './config' + +await addChain(config, { chain: avalanche }) +``` +<<< @/snippets/core/config.ts[config.ts] +::: + +## Parameters + +```ts +import { type AddChainParameters } from '@wagmi/core' +``` + +### connector + +`Connector | undefined` + +[Connector](/core/api/connectors) to add chain with. + +::: code-group +```ts [index.ts] +import { avalanche } from 'viem/chains' +import { getAccount, addChain } from '@wagmi/core' +import { config } from './config' + +const { connector } = getAccount(config) +const result = await addChain(config, { + connector, // [!code focus] + chain: avalanche +}) +``` +<<< @/snippets/core/config.ts[config.ts] +::: + +## Error + +```ts +import { type AddChainErrorType } from '@wagmi/core' +``` + + + +## Viem + +- [`addChain`](https://viem.sh/docs/actions/wallet/addChain.html) + diff --git a/site/react/api/hooks/useAddChain.md b/site/react/api/hooks/useAddChain.md new file mode 100644 index 0000000000..d4ea08b3fb --- /dev/null +++ b/site/react/api/hooks/useAddChain.md @@ -0,0 +1,92 @@ +--- +title: useAddChain +description: Hook for requesting the user to add an EVM chain to the wallet. +--- + + + +# useAddChain + +Hook for requesting the user to add an EVM chain to the wallet. + +## Import + +```ts +import { useAddChain } from 'wagmi' +``` + +## Usage + +::: code-group +```tsx [index.tsx] +import { avalanche } from 'viem/chains' +import { useAddchain } from 'wagmi' + +function App() { + const { addChain } = useAddChain() + + return ( + + ) +} +``` +<<< @/snippets/react/config.ts[config.ts] +::: + +## Parameters + +```ts +import { type UseAddChainParameters } from 'wagmi' +``` + +### config + +`Config | undefined` + +[`Config`](/react/api/createConfig#config) to use instead of retrieving from the from nearest [`WagmiProvider`](/react/api/WagmiProvider). + +::: code-group +```tsx [index.tsx] +import { avalanche } from 'viem/chains' +import { useAddChain } from 'wagmi' +import { config } from './config' // [!code focus] + +function App() { + const result = useAddChain({ + config, // [!code focus] + chain: avalanche, + }) +} +``` +<<< @/snippets/react/config.ts[config.ts] +::: + + + +## Return Type + +```ts +import { type UseAddChainReturnType } from 'wagmi' +``` + + + + + +## Action + +- [`addChain`](/core/api/actions/addChain)