diff --git a/.changeset/friendly-pandas-burn.md b/.changeset/friendly-pandas-burn.md new file mode 100644 index 000000000..3c477ffec --- /dev/null +++ b/.changeset/friendly-pandas-burn.md @@ -0,0 +1,7 @@ +--- +'@ant-design/web3-common': minor +'@ant-design/web3-wagmi': minor +'@ant-design/web3': minor +--- + +feat: wagmi add siwe config diff --git a/packages/common/src/locale/en_US.ts b/packages/common/src/locale/en_US.ts index 05daa72d7..2c5b3846f 100644 --- a/packages/common/src/locale/en_US.ts +++ b/packages/common/src/locale/en_US.ts @@ -8,6 +8,7 @@ const localeValues: RequiredLocale = { copied: 'Copied!', walletAddress: 'Wallet address', moreWallets: 'More Wallets', + sign: 'Sign', }, ConnectModal: { title: 'Connect Wallet', diff --git a/packages/common/src/locale/zh_CN.ts b/packages/common/src/locale/zh_CN.ts index ee6fce984..48b971abe 100644 --- a/packages/common/src/locale/zh_CN.ts +++ b/packages/common/src/locale/zh_CN.ts @@ -8,6 +8,7 @@ const localeValues: RequiredLocale = { copied: '复制成功!', walletAddress: '钱包地址', moreWallets: '更多钱包', + sign: '签名', }, ConnectModal: { title: '连接钱包', diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index b12fc309f..794dc38dc 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -1,8 +1,15 @@ +export const enum ConnectStatus { + Connected = 'connected', + Disconnected = 'disconnected', + Signed = 'signed', +} + export interface Account { address: string; name?: string; avatar?: string; - addresses?: readonly [`0x${string}`, ...`0x${string}`[]]; + addresses?: [`0x${string}`, ...`0x${string}`[]] | readonly `0x${string}`[]; + status?: ConnectStatus; } export enum ChainIds { @@ -119,6 +126,9 @@ export interface UniversalWeb3ProviderInterface { // For Bitcoin, tokenId is undefined. getNFTMetadata?: (params: { address: string; tokenId?: bigint }) => Promise; + + // For Sign + sign?: SignConfig; } export interface Wallet extends WalletMetadata { @@ -247,6 +257,7 @@ export interface RequiredLocale { copied: string; walletAddress: string; moreWallets: string; + sign: string; }; ConnectModal: { title: string; @@ -328,3 +339,13 @@ export type Token = { contract?: string; }[]; }; + +export interface SignConfig { + // required + signIn: (address: string) => Promise; + signOut?: () => Promise; + + // signOutOnDisconnect?: boolean; // defaults true + // signOutOnAccountChange?: boolean; // defaults true + // signOutOnNetworkChange?: boolean; // defaults true +} diff --git a/packages/common/src/web3-config-provider/index.tsx b/packages/common/src/web3-config-provider/index.tsx index a30839bb3..e69f2c79e 100644 --- a/packages/common/src/web3-config-provider/index.tsx +++ b/packages/common/src/web3-config-provider/index.tsx @@ -8,7 +8,6 @@ const ProviderChildren: React.FC< ConfigConsumerProps & { children?: React.ReactNode; parentContext?: ConfigConsumerProps } > = (props) => { const { children, parentContext, ...rest } = props; - const config = { ...parentContext }; Object.keys(rest).forEach((key) => { diff --git a/packages/wagmi/src/interface.ts b/packages/wagmi/src/interface.ts index 818c148f8..6d084718b 100644 --- a/packages/wagmi/src/interface.ts +++ b/packages/wagmi/src/interface.ts @@ -6,6 +6,7 @@ import type { WalletMetadata, } from '@ant-design/web3-common'; import type { Chain as WagmiChain } from 'viem'; +import type { CreateSiweMessageParameters } from 'viem/siwe'; import type { Connector, CreateConnectorFn } from 'wagmi'; export interface WalletUseInWagmiAdapter extends Wallet { @@ -31,3 +32,9 @@ export interface WalletFactory { export type EIP6963Config = boolean | UniversalEIP6963Config; export type ChainAssetWithWagmiChain = Chain & { wagmiChain?: WagmiChain }; + +export interface SIWEConfig { + getNonce: (address: string, chainId?: number) => Promise; + createMessage: (args: CreateSiweMessageParameters) => string; + verifyMessage: (message: string, signature: string) => Promise; +} diff --git a/packages/wagmi/src/wagmi-provider/__mocks__/wagmiBaseMock.ts b/packages/wagmi/src/wagmi-provider/__mocks__/wagmiBaseMock.ts new file mode 100644 index 000000000..1e3a3c250 --- /dev/null +++ b/packages/wagmi/src/wagmi-provider/__mocks__/wagmiBaseMock.ts @@ -0,0 +1,28 @@ +import { mainnet } from 'wagmi/chains'; + +const mockConnector = { + name: 'MetaMask', +}; + +export const wagmiBaseMock = { + useAccount: () => { + return { + chain: mainnet, + address: '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', + connector: mockConnector, + }; + }, + useConfig: () => ({}), + useBalance: () => ({ data: {} }), + useSwitchChain: () => ({ switchChain: () => {} }), + useConnect: () => ({ + connectors: [mockConnector], + connectAsync: async () => ({}), + }), + useDisconnect: () => ({ + disconnectAsync: () => {}, + }), + useEnsName: () => ({ data: null }), + useEnsAvatar: () => ({ data: null }), + useSignMessage: () => ({ signMessageAsync: async () => 'signMessage' }), +}; diff --git a/packages/wagmi/src/wagmi-provider/__tests__/balance.test.tsx b/packages/wagmi/src/wagmi-provider/__tests__/balance.test.tsx index 57c7972ef..49c805e50 100644 --- a/packages/wagmi/src/wagmi-provider/__tests__/balance.test.tsx +++ b/packages/wagmi/src/wagmi-provider/__tests__/balance.test.tsx @@ -5,6 +5,7 @@ import { describe, expect, it, vi } from 'vitest'; import type * as Wagmi from 'wagmi'; import { mainnet } from 'wagmi/chains'; +import { wagmiBaseMock } from '../__mocks__/wagmiBaseMock'; import { MetaMask } from '../../wallets'; import { AntDesignWeb3ConfigProvider } from '../config-provider'; @@ -21,32 +22,8 @@ vi.mock('wagmi', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - useConfig: () => { - return {}; - }, + ...wagmiBaseMock, // https://wagmi.sh/react/hooks/useAccount - useAccount: () => { - return { - chain: mainnet, - address: '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', - connector: mockConnector, - }; - }, - useConnect: () => { - return { - connectors: [mockConnector], - }; - }, - useDisconnect: () => { - return { - disconnectAsync: () => {}, - }; - }, - useSwitchChain: () => { - return { - switchChain: () => {}, - }; - }, useBalance: () => { return { data: { @@ -56,8 +33,6 @@ vi.mock('wagmi', async (importOriginal) => { }, }; }, - useEnsName: () => ({}), - useEnsAvatar: () => ({}), }; }); diff --git a/packages/wagmi/src/wagmi-provider/__tests__/connect-with-universal-wallet.test.tsx b/packages/wagmi/src/wagmi-provider/__tests__/connect-with-universal-wallet.test.tsx index 9dede74e3..85cb56232 100644 --- a/packages/wagmi/src/wagmi-provider/__tests__/connect-with-universal-wallet.test.tsx +++ b/packages/wagmi/src/wagmi-provider/__tests__/connect-with-universal-wallet.test.tsx @@ -8,6 +8,7 @@ import type { Connector, Config as WagmiConfig } from 'wagmi'; import type * as Wagmi from 'wagmi'; import { mainnet } from 'wagmi/chains'; +import { wagmiBaseMock } from '../__mocks__/wagmiBaseMock'; import { TokenPocket } from '../../wallets'; import { AntDesignWeb3ConfigProvider } from '../config-provider'; @@ -37,9 +38,7 @@ vi.mock('wagmi', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - useConfig: () => { - return {}; - }, + ...wagmiBaseMock, useAccount: () => { const [connected, setConnected] = React.useState(false); useEffect(() => { @@ -72,16 +71,6 @@ vi.mock('wagmi', async (importOriginal) => { }, }; }, - useSwitchChain: () => { - return { - switchChain: () => {}, - }; - }, - useBalance: () => { - return { data: {} }; - }, - useEnsName: () => ({ data: null }), - useEnsAvatar: () => ({ data: null }), }; }); diff --git a/packages/wagmi/src/wagmi-provider/__tests__/connect.test.tsx b/packages/wagmi/src/wagmi-provider/__tests__/connect.test.tsx index e063af88f..460f21b5d 100644 --- a/packages/wagmi/src/wagmi-provider/__tests__/connect.test.tsx +++ b/packages/wagmi/src/wagmi-provider/__tests__/connect.test.tsx @@ -8,6 +8,7 @@ import type { Connector, Config as WagmiConfig } from 'wagmi'; import type * as Wagmi from 'wagmi'; import { mainnet } from 'wagmi/chains'; +import { wagmiBaseMock } from '../__mocks__/wagmiBaseMock'; import { MetaMask } from '../../wallets'; import { AntDesignWeb3ConfigProvider } from '../config-provider'; @@ -32,9 +33,7 @@ vi.mock('wagmi', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - useConfig: () => { - return {}; - }, + ...wagmiBaseMock, // https://wagmi.sh/react/hooks/useAccount useAccount: () => { const [connected, setConnected] = React.useState(false); @@ -73,16 +72,6 @@ vi.mock('wagmi', async (importOriginal) => { }, }; }, - useSwitchChain: () => { - return { - switchChain: () => {}, - }; - }, - useBalance: () => { - return {}; - }, - useEnsName: () => ({}), - useEnsAvatar: () => ({}), }; }); diff --git a/packages/wagmi/src/wagmi-provider/__tests__/ens.test.tsx b/packages/wagmi/src/wagmi-provider/__tests__/ens.test.tsx index 10bb8cedb..a1b51a210 100644 --- a/packages/wagmi/src/wagmi-provider/__tests__/ens.test.tsx +++ b/packages/wagmi/src/wagmi-provider/__tests__/ens.test.tsx @@ -5,6 +5,7 @@ import { describe, expect, it, vi } from 'vitest'; import type * as Wagmi from 'wagmi'; import { mainnet } from 'wagmi/chains'; +import { wagmiBaseMock } from '../__mocks__/wagmiBaseMock'; import { MetaMask } from '../../wallets'; import { AntDesignWeb3ConfigProvider } from '../config-provider'; @@ -19,9 +20,7 @@ vi.mock('wagmi', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - useConfig: () => { - return {}; - }, + ...wagmiBaseMock, // https://wagmi.sh/react/hooks/useAccount useAccount: () => { return { @@ -30,27 +29,6 @@ vi.mock('wagmi', async (importOriginal) => { address: '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', }; }, - useConnect: () => { - return { - connectors: [], - connectAsync: async () => { - return {}; - }, - }; - }, - useDisconnect: () => { - return { - disconnectAsync: () => {}, - }; - }, - useSwitchChain: () => { - return { - switchChain: () => {}, - }; - }, - useBalance: () => { - return {}; - }, useEnsName: ({ address }: { address: string }) => { if (address === '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B') { return { data: 'wanderingearth.eth' }; diff --git a/packages/wagmi/src/wagmi-provider/__tests__/nft.test.tsx b/packages/wagmi/src/wagmi-provider/__tests__/nft.test.tsx index f632d2b36..4ee45512e 100644 --- a/packages/wagmi/src/wagmi-provider/__tests__/nft.test.tsx +++ b/packages/wagmi/src/wagmi-provider/__tests__/nft.test.tsx @@ -5,6 +5,7 @@ import { describe, expect, it, vi } from 'vitest'; import type * as Wagmi from 'wagmi'; import { mainnet } from 'wagmi/chains'; +import { wagmiBaseMock } from '../__mocks__/wagmiBaseMock'; import { MetaMask } from '../../wallets'; import { AntDesignWeb3ConfigProvider } from '../config-provider'; @@ -20,9 +21,7 @@ vi.mock('wagmi', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - useConfig: () => { - return {}; - }, + ...wagmiBaseMock, // https://wagmi.sh/react/hooks/useAccount useAccount: () => { return { @@ -31,29 +30,6 @@ vi.mock('wagmi', async (importOriginal) => { address: '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', }; }, - useConnect: () => { - return { - connectors: [], - connectAsync: async () => { - return {}; - }, - }; - }, - useDisconnect: () => { - return { - disconnectAsync: () => {}, - }; - }, - useSwitchChain: () => { - return { - switchChain: () => {}, - }; - }, - useBalance: () => { - return {}; - }, - useEnsName: () => ({}), - useEnsAvatar: () => ({}), }; }); diff --git a/packages/wagmi/src/wagmi-provider/__tests__/siwe-with-empty-address.test.tsx b/packages/wagmi/src/wagmi-provider/__tests__/siwe-with-empty-address.test.tsx new file mode 100644 index 000000000..0af5f5e60 --- /dev/null +++ b/packages/wagmi/src/wagmi-provider/__tests__/siwe-with-empty-address.test.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { Connector, Web3ConfigProvider, type ConnectorTriggerProps } from '@ant-design/web3'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { Button } from 'antd'; +import { describe, expect, it, vi } from 'vitest'; +import type * as Wagmi from 'wagmi'; +import { mainnet } from 'wagmi/chains'; + +vi.mock('wagmi', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useConfig: () => { + return {}; + }, + // https://wagmi.sh/react/hooks/useAccount + useAccount: () => { + return { + chain: mainnet, + isDisconnected: false, + }; + }, + useConnect: () => { + return { + connectors: [], + connectAsync: async () => { + return { + accounts: ['0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B'], + }; + }, + }; + }, + useDisconnect: () => { + return { + disconnectAsync: () => {}, + }; + }, + useSwitchChain: () => { + return { + switchChain: () => {}, + }; + }, + useBalance: () => { + return {}; + }, + useEnsName: () => ({}), + useEnsAvatar: () => ({}), + useSignMessage: () => ({ signMessageAsync: async () => 'signMessage' }), + }; +}); + +describe('sign after connect', () => { + it('sign after connect', async () => { + const signIn = vi.fn(async () => {}); + + const CustomButton: React.FC> = (props) => { + const { account, onConnectClick, onDisconnectClick, children } = props; + return ( + + ); + }; + + const App = () => { + return ( + ({ + address: '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', + })} + sign={{ signIn }} + account={undefined} + chain={{ + id: 2, + name: 'custom', + }} + > + + Custom + + + ); + }; + const { baseElement } = render(); + const btn = baseElement.querySelector('.ant-btn')!; + + expect(btn.textContent).toBe('Custom'); + + fireEvent.click(btn); + + await waitFor(() => { + expect(signIn).toBeCalledWith('0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B'); + }); + }); +}); diff --git a/packages/wagmi/src/wagmi-provider/__tests__/siwe.test.tsx b/packages/wagmi/src/wagmi-provider/__tests__/siwe.test.tsx new file mode 100644 index 000000000..e46dfa87b --- /dev/null +++ b/packages/wagmi/src/wagmi-provider/__tests__/siwe.test.tsx @@ -0,0 +1,324 @@ +import { ConnectButton, Connector } from '@ant-design/web3'; +import { Mainnet } from '@ant-design/web3-assets'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type * as Wagmi from 'wagmi'; +import { mainnet } from 'wagmi/chains'; + +import { wagmiBaseMock } from '../__mocks__/wagmiBaseMock'; +import { MetaMask } from '../../wallets'; +import { AntDesignWeb3ConfigProvider } from '../config-provider'; + +let locationSpy: ReturnType = undefined as any; +const createMessage = vi.fn(() => 'message'); + +vi.mock('wagmi', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + ...wagmiBaseMock, + // https://wagmi.sh/react/hooks/useAccount + useAccount: () => { + return { + chain: { + name: 'Ethereum', + }, + isDisconnected: false, + address: '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', + }; + }, + useSignMessage: () => ({ signMessageAsync: createMessage }), + }; +}); + +describe('Wagmi siwe sign', () => { + beforeEach(() => { + locationSpy?.mockRestore(); + }); + + it('has siwe', async () => { + const { createConfig, http } = await import('wagmi'); + locationSpy = vi.spyOn(window, 'location', 'get').mockImplementation(() => { + return undefined as any; + }); + + const getNonce = vi.fn(async () => '1'); + const verifyMessage = vi.fn(async () => true); + + const config = createConfig({ + chains: [mainnet], + transports: { + [mainnet.id]: http(), + }, + connectors: [], + }); + + const App = () => ( + + + + + + ); + const { baseElement } = render(); + + expect(baseElement.querySelector('.ant-web3-connect-button-text')?.textContent).toBe( + 'Sign: 0x21CD...Fd3B', + ); + + fireEvent.click(baseElement.querySelector('.ant-web3-connect-button')!); + + await waitFor(() => { + expect(getNonce).toBeCalled(); + expect(createMessage).toBeCalled(); + expect(createMessage).toBeCalledWith({ + chainId: 1, + address: '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', + nonce: '1', + uri: '', + domain: '', + version: '1', + }); + expect(verifyMessage).toBeCalled(); + }); + }); + + it('call with diff chainId', async () => { + const { createConfig, http } = await import('wagmi'); + + const getNonce = vi.fn(async () => '1'); + const verifyMessage = vi.fn(async () => true); + + const config = createConfig({ + chains: [ + { + ...mainnet, + id: 2, + }, + ], + transports: { + [2]: http(), + }, + connectors: [], + }); + + const App = () => ( + + + + + + ); + const { baseElement } = render(); + + fireEvent.click(baseElement.querySelector('.ant-web3-connect-button')!); + + await waitFor(() => { + expect(createMessage).toBeCalledWith({ + chainId: 2, + address: '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', + nonce: '1', + uri: 'http://localhost:3000', + domain: 'localhost', + version: '1', + }); + }); + }); + + it('call with no chainId', async () => { + const { createConfig, http } = await import('wagmi'); + + const getNonce = vi.fn(async () => '1'); + const verifyMessage = vi.fn(async () => true); + + const config = createConfig({ + chains: [ + { + ...mainnet, + id: undefined as any, + }, + ], + transports: { + [2]: http(), + }, + connectors: [], + }); + + const App = () => ( + + + + + + ); + const { baseElement } = render(); + + fireEvent.click(baseElement.querySelector('.ant-web3-connect-button')!); + + await waitFor(() => { + expect(createMessage).toBeCalledWith({ + chainId: 1, + address: '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', + nonce: '1', + uri: 'http://localhost:3000', + domain: 'localhost', + version: '1', + }); + }); + }); + + it('has no siwe', async () => { + const { createConfig, http } = await import('wagmi'); + + const config = createConfig({ + chains: [mainnet], + transports: { + [mainnet.id]: http(), + }, + connectors: [], + }); + + const App = () => ( + + + + + + ); + const { baseElement } = render(); + + expect(baseElement.querySelector('.ant-web3-connect-button-text')?.textContent).toBe( + '0x21CD...Fd3B', + ); + }); + + it('siwe', async () => { + const { createConfig, http } = await import('wagmi'); + + const getNonce = vi.fn(() => { + throw new Error('signAddress is required'); + }); + const verifyMessage = vi.fn(async () => true); + + const renderText = vi.fn((defaultDom, account) => `Custom Sign: ${account.address}`); + + const config = createConfig({ + chains: [mainnet], + transports: { + [mainnet.id]: http(), + }, + connectors: [], + }); + + const App = () => ( + + + + + + ); + const { baseElement } = render(); + + const btn = baseElement.querySelector('.ant-web3-connect-button-text'); + + await waitFor(async () => { + fireEvent.click(btn!); + expect(baseElement.querySelector('.ant-message')).not.toBeNull(); + }); + }); + + it('render text', async () => { + const { createConfig, http } = await import('wagmi'); + + const getNonce = vi.fn(async () => '1'); + const verifyMessage = vi.fn(async () => true); + + const config = createConfig({ + chains: [mainnet], + transports: { + [mainnet.id]: http(), + }, + connectors: [], + }); + + const App = () => ( + + + { + return ( +
+ {defaultDom} & Custom Sign: {account?.address} +
+ ); + }} + /> +
+
+ ); + const { baseElement } = render(); + + expect(baseElement.querySelector('.ant-web3-connect-button-text')?.textContent).toBe( + '0x21CD...Fd3B & Custom Sign: 0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', + ); + }); +}); diff --git a/packages/wagmi/src/wagmi-provider/__tests__/switch-chain-unconnected.test.tsx b/packages/wagmi/src/wagmi-provider/__tests__/switch-chain-unconnected.test.tsx index f1a774f7d..59249814b 100644 --- a/packages/wagmi/src/wagmi-provider/__tests__/switch-chain-unconnected.test.tsx +++ b/packages/wagmi/src/wagmi-provider/__tests__/switch-chain-unconnected.test.tsx @@ -5,6 +5,7 @@ import { describe, expect, it, vi } from 'vitest'; import type * as Wagmi from 'wagmi'; import { mainnet, polygon } from 'wagmi/chains'; +import { wagmiBaseMock } from '../__mocks__/wagmiBaseMock'; import { MetaMask } from '../../wallets'; import { AntDesignWeb3ConfigProvider } from '../config-provider'; @@ -21,6 +22,7 @@ vi.mock('wagmi', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, + ...wagmiBaseMock, useConfig: () => { return {}; }, @@ -32,21 +34,6 @@ vi.mock('wagmi', async (importOriginal) => { connector: mockConnector, }; }, - useConnect: () => { - return { - connectors: [mockConnector], - }; - }, - useDisconnect: () => { - return { - disconnectAsync: () => {}, - }; - }, - useSwitchChain: () => { - return { - switchChain: () => {}, - }; - }, useBalance: () => { return { data: { @@ -56,8 +43,6 @@ vi.mock('wagmi', async (importOriginal) => { }, }; }, - useEnsName: () => ({}), - useEnsAvatar: () => ({}), }; }); diff --git a/packages/wagmi/src/wagmi-provider/__tests__/switch-chain.test.tsx b/packages/wagmi/src/wagmi-provider/__tests__/switch-chain.test.tsx index 170044985..c2c0d187d 100644 --- a/packages/wagmi/src/wagmi-provider/__tests__/switch-chain.test.tsx +++ b/packages/wagmi/src/wagmi-provider/__tests__/switch-chain.test.tsx @@ -8,6 +8,7 @@ import type * as Wagmi from 'wagmi'; import type { Chain as WagmiChain } from 'wagmi/chains'; import { mainnet, polygon } from 'wagmi/chains'; +import { wagmiBaseMock } from '../__mocks__/wagmiBaseMock'; import { MetaMask } from '../../wallets'; import { AntDesignWeb3ConfigProvider } from '../config-provider'; @@ -26,11 +27,8 @@ vi.mock('wagmi', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - useConfig: () => { - return {}; - }, + ...wagmiBaseMock, // https://wagmi.sh/react/hooks/useAccount - useAccount: () => { const [currentChain, setCurrentChain] = React.useState(mainnet); useEffect(() => { @@ -44,16 +42,7 @@ vi.mock('wagmi', async (importOriginal) => { connector: mockConnector, }; }, - useConnect: () => { - return { - connectors: [mockConnector], - }; - }, - useDisconnect: () => { - return { - disconnectAsync: () => {}, - }; - }, + useSwitchChain: () => { return { switchChain: ({ chainId }: any) => { @@ -72,8 +61,6 @@ vi.mock('wagmi', async (importOriginal) => { }, }; }, - useEnsName: () => ({}), - useEnsAvatar: () => ({}), }; }); diff --git a/packages/wagmi/src/wagmi-provider/config-provider.tsx b/packages/wagmi/src/wagmi-provider/config-provider.tsx index a6f4ba532..73ae4a6c2 100644 --- a/packages/wagmi/src/wagmi-provider/config-provider.tsx +++ b/packages/wagmi/src/wagmi-provider/config-provider.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { + ConnectStatus, Web3ConfigProvider, type Account, type Chain, @@ -14,12 +15,19 @@ import { useConnect, useEnsAvatar, useEnsName, + useSignMessage, useSwitchChain, type Connector as WagmiConnector, } from 'wagmi'; import { disconnect, getAccount } from 'wagmi/actions'; -import type { EIP6963Config, WalletFactory, WalletUseInWagmiAdapter } from '../interface'; +import { Mainnet } from '../chains'; +import type { + EIP6963Config, + SIWEConfig, + WalletFactory, + WalletUseInWagmiAdapter, +} from '../interface'; import { isEIP6963Connector } from '../utils'; import { EIP6963Wallet } from '../wallets/eip6963'; import { getNFTMetadata } from './methods'; @@ -34,6 +42,7 @@ export interface AntDesignWeb3ConfigProviderProps { eip6963?: EIP6963Config; wagimConfig: WagmiConfig; useWalletConnectOfficialModal?: boolean; + siwe?: SIWEConfig; } export const AntDesignWeb3ConfigProvider: React.FC = (props) => { @@ -47,20 +56,31 @@ export const AntDesignWeb3ConfigProvider: React.FC(ConnectStatus.Disconnected); + + React.useEffect(() => { + setStatus(isDisconnected ? ConnectStatus.Disconnected : ConnectStatus.Connected); + }, [isDisconnected]); + const account: Account | undefined = address && !isDisconnected ? { address, name: ensName && ens ? ensName : undefined, avatar: ensAvatar ?? undefined, + addresses, + status: status, } : undefined; @@ -186,12 +206,48 @@ export const AntDesignWeb3ConfigProvider: React.FC { + const { getNonce, createMessage, verifyMessage } = siwe!; + let msg: string; + let signature: `0x${string}`; + + try { + // get nonce + const nonce = await getNonce(signAddress); + msg = createMessage({ + domain: window?.location ? window.location.hostname : '', + address: signAddress as `0x${string}`, + uri: window?.location ? window.location.origin : '', + nonce, + // Default config + version: '1', + chainId: currentChain?.id ?? Mainnet.id, + }); + if (signMessageAsync) { + signature = await signMessageAsync?.({ message: msg }); + console.log('get signature', signature); + await verifyMessage(msg!, signature!); + setStatus(ConnectStatus.Signed); + } + } catch (error: any) { + throw new Error(error.message); + } + }, + [siwe, currentChain, signMessageAsync], + ); + return ( ; -}; + siwe?: SIWEConfig; +} export function WagmiWeb3ConfigProvider({ children, @@ -54,6 +60,7 @@ export function WagmiWeb3ConfigProvider({ eip6963, walletConnect, transports, + siwe, ...restProps }: React.PropsWithChildren): React.ReactElement { // When user custom config, add Mainnet by default @@ -63,7 +70,6 @@ export function WagmiWeb3ConfigProvider({ : chains?.length ? chains : [Mainnet]; - const generateConfigFlag = () => { return `${JSON.stringify(walletConnect)}-${chains.map((item) => item.id).join(',')}-${wallets.map((item) => item.name).join(',')}`; }; @@ -132,6 +138,7 @@ export function WagmiWeb3ConfigProvider({ = (props) => { locale, quickConnect, addressPrefix: addressPrefixProp, + sign, + signBtnTextRender, ...restProps } = props; const intl = useIntl('ConnectButton', locale); @@ -51,6 +57,7 @@ export const ConnectButton: React.FC = (props) => { const [messageApi, contextHolder] = message.useMessage(); const [showMenu, setShowMenu] = useState(false); + const needSign = !!(sign?.signIn && account?.status === ConnectStatus.Connected && account); let buttonText: React.ReactNode = intl.getMessage(intl.messages.connect); if (account) { buttonText = @@ -75,12 +82,20 @@ export const ConnectButton: React.FC = (props) => { ghost: props.ghost, loading, className: classNames(className, prefixCls, hashId), - onClick: (e) => { + onClick: async (e) => { setShowMenu(false); - if (account && !profileOpen && profileModal) { + if (account && !profileOpen && profileModal && !needSign) { setProfileOpen(true); } onClick?.(e); + + try { + if (needSign) { + await sign?.signIn?.(account?.address); + } + } catch (error: any) { + messageApi.error(error.message); + } }, ...restProps, }; @@ -127,6 +142,17 @@ export const ConnectButton: React.FC = (props) => { const chainSelect = availableChains && availableChains.length > 1 ? : null; + if (needSign) { + buttonText = signBtnTextRender ? ( + signBtnTextRender(buttonText, account) + ) : ( + <> + {`${intl.getMessage(intl.messages.sign)}: `} + {account?.address.slice(0, 6)}...{account?.address.slice(-4)} + + ); + } + const buttonInnerText = (
diff --git a/packages/web3/src/connect-button/interface.ts b/packages/web3/src/connect-button/interface.ts index b7e1f18b6..82a2f2bad 100644 --- a/packages/web3/src/connect-button/interface.ts +++ b/packages/web3/src/connect-button/interface.ts @@ -1,9 +1,15 @@ -import type { ConnectorTriggerProps, Locale } from '@ant-design/web3-common'; +import type { Account, ConnectorTriggerProps, Locale, SignConfig } from '@ant-design/web3-common'; import type { AvatarProps, ButtonProps, GetProp, MenuProps, TooltipProps } from 'antd'; import type { AddressProps } from '../address'; import type { ProfileModalProps } from './profile-modal'; +export const enum ConnectButtonStatus { + Connected = 'connected', + Disconnected = 'disconnected', + Signed = 'signed', +} + export type MenuItemType = Extract[number], { type?: 'item' }>; export type ConnectButtonTooltipProps = TooltipProps & { @@ -18,6 +24,8 @@ export type ConnectButtonProps = ButtonProps & prefixCls?: string; locale?: Locale['ConnectButton']; avatar?: AvatarProps; + sign?: SignConfig; + signBtnTextRender?: (signText?: React.ReactNode, account?: Account) => React.ReactNode; onMenuItemClick?: (e: NonNullable[number]) => void; tooltip?: boolean | ConnectButtonTooltipProps; profileModal?: boolean | ProfileModalProps['modalProps']; diff --git a/packages/web3/src/connector/connector.tsx b/packages/web3/src/connector/connector.tsx index 904b05f54..880287de6 100644 --- a/packages/web3/src/connector/connector.tsx +++ b/packages/web3/src/connector/connector.tsx @@ -27,6 +27,7 @@ export const Connector: React.FC = (props) => { switchChain, balance, addressPrefix, + sign, } = useProvider(props); const [open, setOpen] = React.useState(false); const [connecting, setConnecting] = React.useState(false); @@ -43,6 +44,9 @@ export const Connector: React.FC = (props) => { } const connectedAccount = await connect?.(wallet, options); onConnected?.(connectedAccount ? connectedAccount : undefined); + if (sign?.signIn && connectedAccount?.address) { + await sign.signIn(connectedAccount?.address); + } setOpen(false); } catch (e: any) { if (typeof onConnectError === 'function') { @@ -104,6 +108,7 @@ export const Connector: React.FC = (props) => { availableWallets, chain, addressPrefix, + sign, onSwitchChain: async (c: Chain) => { await switchChain?.(c); onChainSwitched?.(c); diff --git a/packages/web3/src/ethereum/demos/siwe/index.tsx b/packages/web3/src/ethereum/demos/siwe/index.tsx index 3d57719c4..6392da957 100644 --- a/packages/web3/src/ethereum/demos/siwe/index.tsx +++ b/packages/web3/src/ethereum/demos/siwe/index.tsx @@ -1,28 +1,62 @@ +import { Account, ConnectButton, Connector } from '@ant-design/web3'; import { - Mainnet, MetaMask, OkxWallet, + Sepolia, TokenPocket, WagmiWeb3ConfigProvider, WalletConnect, } from '@ant-design/web3-wagmi'; import { QueryClient } from '@tanstack/react-query'; -import { http } from 'wagmi'; +import { Button, Space } from 'antd'; +import { createSiweMessage } from 'viem/siwe'; +import { http, useDisconnect } from 'wagmi'; -import SignBtn from './sign-btn'; +import { getNonce, verifyMessage } from './mock-api'; const queryClient = new QueryClient(); +const DisconnectBtn: React.FC = () => { + const { disconnect } = useDisconnect(); + return ( + + ); +}; + const App: React.FC = () => { + const renderSignBtnText = (defaultDom: React.ReactNode, account?: Account) => { + const { address } = account ?? {}; + const ellipsisAddress = address ? `${address.slice(0, 6)}...${address.slice(-6)}` : ''; + return `Sign in as ${ellipsisAddress}`; + }; + return ( { + return createSiweMessage({ ...props, statement: 'Ant Design Web3' }); + }, + verifyMessage, + }} eip6963={{ autoAddInjectedWallets: true, }} ens - chains={[Mainnet]} + chains={[Sepolia]} transports={{ - [Mainnet.id]: http(), + [Sepolia.id]: http(), }} walletConnect={{ projectId: YOUR_WALLET_CONNECT_PROJECT_ID, @@ -37,7 +71,16 @@ const App: React.FC = () => { ]} queryClient={queryClient} > - + + + + + + ); }; diff --git a/packages/web3/src/ethereum/demos/siwe/sign-btn.tsx b/packages/web3/src/ethereum/demos/siwe/sign-btn.tsx deleted file mode 100644 index b729e43e9..000000000 --- a/packages/web3/src/ethereum/demos/siwe/sign-btn.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useCallback, useState } from 'react'; -import type { Account } from '@ant-design/web3'; -import { ConnectButton, Connector, useAccount } from '@ant-design/web3'; -import { Mainnet } from '@ant-design/web3-wagmi'; -import { Button, message, Space } from 'antd'; -import { createSiweMessage } from 'viem/siwe'; -import { useSignMessage } from 'wagmi'; - -import { getNonce, verifyMessage } from './mock-api'; - -export default function App() { - const { account } = useAccount(); - - const [signed, setSigned] = useState(false); - const [signLoading, setSignLoading] = useState(false); - const { signMessageAsync } = useSignMessage(); - - const signIn = useCallback(async (a?: Account) => { - const address = a?.address as `0x${string}`; - - if (!address) { - message.error('Please connect wallet first.'); - return; - } - - // get nonce - const nonce = await getNonce(address); - if (!nonce) { - message.error('Failed to get nonce.'); - return; - } - - let msg: string; - let signature: `0x${string}`; - - try { - msg = createSiweMessage({ - domain: window.location.hostname, - address, - statement: 'Sign in with Ethereum', - uri: typeof window !== 'undefined' ? window.location.origin : '', - version: '1', - chainId: Mainnet.id, - nonce, - }); - setSignLoading(true); - console.log('signing message'); - signature = await signMessageAsync({ message: msg }); - console.log('get signature', signature); - await verifyMessage(msg!, signature!); - message.success('Sign in successfully.'); - setSigned(true); - setSignLoading(false); - } catch (error: any) { - message.error(error.message); - setSignLoading(false); - } - }, []); - - return ( - - { - setSigned(false); - }} - onConnected={(a) => signIn(a)} - > - - - {!signed && account && ( - - )} - - ); -} diff --git a/packages/web3/src/ethereum/index.md b/packages/web3/src/ethereum/index.md index c9e11f2a4..4e326baac 100644 --- a/packages/web3/src/ethereum/index.md +++ b/packages/web3/src/ethereum/index.md @@ -70,6 +70,8 @@ We have built-in `Mainnet`, and the remaining chains need to configure `chains` SIWE means Sign-In with Ethereum. Your website can verify user login through signatures. Below is an example where the backend interface is mocked. You need to implement it in your project. +To quickly use SIWE, you need to set three key methods: get the nonce value, construct the signature, and verify the signature. + ## Display ENS and Balance @@ -102,6 +104,7 @@ When the `showQrModal` configuration is not `false`, the built-in [web3modal](ht | reconnectOnMount | Whether or not to reconnect previously connected [connectors](https://wagmi.sh/react/api/createConfig#connectors) on mount. | `boolean` \| `undefined` | `true` | - | | walletConnect | WalletConnect configuration | `false` \| [WalletConnectOptions](#walletconnectoptions) | - | `2.8.0` | | transports | [Transport](https://wagmi.sh/core/api/createConfig#transports) configuration | `Transport` | - | `2.8.0` | +| siwe | [SIWEConfig](#siweconfig) | CreateSiweMessageParameters | - | - | ### WalletFactory @@ -139,3 +142,13 @@ export interface WalletConnectOptions useWalletConnectOfficialModal?: boolean; } ``` + +### SIWEConfig + +`CreateSiweMessageParameters` refers to the [definition](https://viem.sh/docs/siwe/utilities/createSiweMessage) in `viem/siwe`. + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| getNonce | Get Nonce value | `(address: string, chainId?: number) => Promise` | - | - | +| createMessage | Construct signature message | `(args: CreateSiweMessageParameters) => string` | - | - | +| verifyMessage | Verify signature message | `(message: string, signature: string) => Promise` | - | - | diff --git a/packages/web3/src/ethereum/index.zh-CN.md b/packages/web3/src/ethereum/index.zh-CN.md index 5ce11da54..85c4c5994 100644 --- a/packages/web3/src/ethereum/index.zh-CN.md +++ b/packages/web3/src/ethereum/index.zh-CN.md @@ -69,6 +69,8 @@ Ant Design Web3 官方提供了 `wagmi`、`ethers` 等多个框架的适配器 SIWE 是指 Sign-In with Ethereum,你的网站可以通过签名来验证用户的登录,下面是一个示例,其中后端接口做了 Mock,你需要在你的项目中自行实现。 +要想要快速使用 SIWE 需要设置三个关键的方法,获取 Nonce 值、构建签名以及验证签名。 + ## 显示 ENS 和余额 @@ -101,6 +103,7 @@ SIWE 是指 Sign-In with Ethereum,你的网站可以通过签名来验证用 | reconnectOnMount | 是否在组件挂载时重新连接之前已连接的[连接器](https://wagmi.sh/react/api/createConfig#connectors) | `boolean` \| `undefined` | `true` | - | | walletConnect | walletConnect 的配置 | `false` \| [WalletConnectOptions](#walletconnectoptions) | - | `2.8.0` | | transports | [Transport](https://wagmi.sh/core/api/createConfig#transports) 网关配置 | `Record;` | - | `2.8.0` | +| siwe | [SIWEConfig](#siweconfig) | CreateSiweMessageParameters | - | - | ### EIP6963Config @@ -144,3 +147,13 @@ export interface WalletConnectOptions useWalletConnectOfficialModal?: boolean; } ``` + +### SIWEConfig + +`CreateSiweMessageParameters` 参考了 `viem/siwe` 的[定义](https://viem.sh/docs/siwe/utilities/createSiweMessage)。 + +| 属性 | 描述 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| getNonce | 获取 Nonce 值 | `(address: string, chainId?: number) => Promise` | - | - | +| createMessage | 构建签名信息 | `(args: CreateSiweMessageParameters) => string` | - | - | +| verifyMessage | 验证签名信息 | `(message: string, signature: string) => Promise` | - | - | diff --git a/vitest.config.mts b/vitest.config.mts index 9006a5198..62933f182 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -72,6 +72,7 @@ export default defineConfig({ '**/src/index.ts', '**/__tests__/*.{ts,tsx}', '**/*.test.{ts,tsx}', + '**/__mocks__/*.{ts,tsx}', ], reporter: ['json-summary', ['text', { skipFull: true }], 'cobertura', 'html'], },