diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 06c15b24e..e96d20d0d 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -42,10 +42,11 @@ export interface UniversalWeb3ProviderInterface { chains?: Chain[]; currentChain?: Chain; + // connect and return conneted accounts requestAccounts?: (wallet?: string) => Promise; disconnect?: () => Promise; + switchChain?: (chain: Chain) => Promise; - getCurrentNetwork?: () => Promise; getNFTMetadata?: (params: { address: string; tokenId: bigint }) => Promise; } @@ -141,8 +142,9 @@ export type Banlance = { export interface ConnectorTriggerProps { address?: string; loading?: boolean; - onConnectClicked?: () => void; - onDisconnectClicked?: () => Promise; + onConnectClick?: () => void; + onDisconnectClick?: () => Promise; + onSwitchChain?: (chain: Chain) => Promise; domain?: string; connected?: boolean; chains?: Chain[]; diff --git a/packages/ethereum/src/universal-provider.ts b/packages/ethereum/src/universal-provider.ts index 4e3e9c8db..42f5f4e1d 100644 --- a/packages/ethereum/src/universal-provider.ts +++ b/packages/ethereum/src/universal-provider.ts @@ -1,13 +1,13 @@ -import { +import type { UniversalWeb3ProviderInterface, NFTMetadata, Account, - requestWeb3Asset, Wallet, } from '@ant-design/web3-common'; +import { requestWeb3Asset } from '@ant-design/web3-common'; import { EventEmitter } from 'eventemitter3'; import { ethers } from 'ethers'; -import { EthereumEIP1193LikeProvider } from './eip1193-provider'; +import type { EthereumEIP1193LikeProvider } from './eip1193-provider'; export enum UniversalWeb3ProviderEventType { AccountsChanged = 'accountsChanged', diff --git a/packages/wagmi/src/wagmi-provider/config-provider.tsx b/packages/wagmi/src/wagmi-provider/config-provider.tsx index eeb5e53a7..0b797e2f5 100644 --- a/packages/wagmi/src/wagmi-provider/config-provider.tsx +++ b/packages/wagmi/src/wagmi-provider/config-provider.tsx @@ -7,21 +7,31 @@ import { requestWeb3Asset, fillAddressWith0x, } from '@ant-design/web3-common'; -import { useAccount, useConnect, useDisconnect, useNetwork } from 'wagmi'; +import { + useAccount, + useConnect, + useDisconnect, + useNetwork, + useSwitchNetwork, + type Chain as WagmiChain, +} from 'wagmi'; import { readContract } from '@wagmi/core'; import type { WalletFactory } from '../interface'; export interface AntDesignWeb3ConfigProviderProps { assets?: (WalletFactory | Chain)[]; children?: React.ReactNode; + chains: WagmiChain[]; } export const AntDesignWeb3ConfigProvider: React.FC = (props) => { - const { children, assets } = props; + const { children, assets, chains } = props; const { address, isDisconnected } = useAccount(); const { connectors, connectAsync } = useConnect(); - const { chain, chains } = useNetwork(); + const { switchNetwork } = useSwitchNetwork(); + const { chain } = useNetwork(); const { disconnectAsync } = useDisconnect(); + const [currentChain, setCurrentChain] = React.useState(undefined); const accounts: Account[] = React.useMemo(() => { if (!address || isDisconnected) { @@ -36,7 +46,9 @@ export const AntDesignWeb3ConfigProvider: React.FC { return connectors.map((connector) => { - const walletFactory = assets?.find((item) => item.name === connector.name) as WalletFactory; + const walletFactory = assets?.find( + (item) => (item as WalletFactory).name === connector.name, + ) as WalletFactory; if (!walletFactory?.create) { throw new Error(`Can not find wallet factory for ${connector.name}`); } @@ -46,26 +58,38 @@ export const AntDesignWeb3ConfigProvider: React.FC { return chains.map((item) => { - const c = assets?.find((asset) => asset.name === item.name) as Chain; + const c = assets?.find((asset) => { + return (asset as Chain).id === item.id; + }) as Chain; if (!c?.id) { - throw new Error(`Can not find chain id for ${item.name}`); + return { + id: c.id, + name: c.name, + }; } return c; }); }, [chains, assets]); - console.log('chains', chains); - - const currentChain = React.useMemo(() => { - if (!chain) { - return undefined; + React.useEffect(() => { + if (!chain && currentChain) { + // not connected any chain, keep current chain + return; } - const c = assets?.find((item) => item.name === chain?.name) as Chain; + const currentWagmiChain = chain ?? chains[0]; + if (!currentWagmiChain) { + return; + } + let c = assets?.find((item) => (item as Chain).id === currentWagmiChain?.id) as Chain; if (!c?.id) { - throw new Error(`Can not find chain id for ${chain.name}`); + c = { + id: currentWagmiChain.id, + name: currentWagmiChain.name, + }; } - return c; - }, [chain, assets]); + setCurrentChain(c); + return; + }, [chain, assets, chains, currentChain]); return ( item.name === wallet); const { account } = await connectAsync({ connector, + chainId: currentChain?.id, }); return [ { @@ -87,6 +112,9 @@ export const AntDesignWeb3ConfigProvider: React.FC { await disconnectAsync(); }} + switchChain={async (c: Chain) => { + switchNetwork?.(c.id); + }} getNFTMetadata={async ({ address: contractAddress, tokenId }) => { const tokenURI = await readContract({ address: fillAddressWith0x(contractAddress), diff --git a/packages/wagmi/src/wagmi-provider/index.test.tsx b/packages/wagmi/src/wagmi-provider/index.test.tsx index 4f6271aca..5b3105fdf 100644 --- a/packages/wagmi/src/wagmi-provider/index.test.tsx +++ b/packages/wagmi/src/wagmi-provider/index.test.tsx @@ -1,8 +1,11 @@ -import { describe, it, expect } from 'vitest'; -import { render } from '@testing-library/react'; -import { createConfig, configureChains, mainnet } from 'wagmi'; +import { describe, it, expect, vi } from 'vitest'; +import { render, fireEvent } from '@testing-library/react'; +import { createConfig, configureChains } from 'wagmi'; +import { polygon, mainnet } from 'wagmi/chains'; import { publicProvider } from 'wagmi/providers/public'; +import { Connector, type ConnectorTriggerProps } from '@ant-design/web3'; import { WagmiWeb3ConfigProvider } from '.'; +import { Mainnet } from '@ant-design/web3-assets'; describe('WagmiWeb3ConfigProvider', () => { it('mount correctly', () => { @@ -21,4 +24,100 @@ describe('WagmiWeb3ConfigProvider', () => { const { baseElement } = render(); expect(baseElement.querySelector('.content')?.textContent).toBe('test'); }); + + it('chains', () => { + const chains = [polygon, mainnet]; + const { publicClient } = configureChains(chains, [publicProvider()]); + const config = createConfig({ + autoConnect: true, + publicClient, + connectors: [], + }); + + const CustomButton: React.FC> = (props) => { + const { currentChain, onSwitchChain } = props; + return ( +
{ + onSwitchChain?.(Mainnet); + }} + className="content" + > + {currentChain?.name} +
+ ); + }; + + const switchChain = vi.fn(); + + const App = () => ( + + + + + + ); + const { baseElement } = render(); + expect(baseElement.querySelector('.content')?.textContent).toBe('Polygon'); + fireEvent.click(baseElement.querySelector('.content')!); + expect(switchChain).toBeCalledWith(Mainnet); + }); + + it('custom assets', () => { + const customChainId = 2333; + const chains = [ + { + ...mainnet, + id: customChainId, + name: 'TEST Chain', + }, + polygon, + ]; + const { publicClient } = configureChains(chains, [publicProvider()]); + const config = createConfig({ + autoConnect: true, + publicClient, + connectors: [], + }); + + const CustomButton: React.FC> = (props) => { + const { currentChain, onSwitchChain } = props; + return ( +
{ + onSwitchChain?.(chains[0]); + }} + className="content" + > + {currentChain?.name} +
+ ); + }; + + const switchChain = vi.fn(); + const assets = [ + { + name: 'TEST Chain show text', + id: customChainId, + icon:
icon
, + nativeCurrency: { + name: 'Matic', + symbol: 'MATIC', + decimals: 18, + }, + }, + ]; + + const App = () => ( + + + + + + ); + const { baseElement } = render(); + expect(baseElement.querySelector('.content')?.textContent).toBe('TEST Chain show text'); + fireEvent.click(baseElement.querySelector('.content')!); + expect(switchChain).toBeCalledWith(chains[0]); + }); }); diff --git a/packages/wagmi/src/wagmi-provider/index.tsx b/packages/wagmi/src/wagmi-provider/index.tsx index a10d0108a..9760670d5 100644 --- a/packages/wagmi/src/wagmi-provider/index.tsx +++ b/packages/wagmi/src/wagmi-provider/index.tsx @@ -1,6 +1,6 @@ -import { WagmiConfig } from 'wagmi'; +import { WagmiConfig, mainnet } from 'wagmi'; -import type { PublicClient, WebSocketPublicClient, Config } from 'wagmi'; +import type { PublicClient, WebSocketPublicClient, Config, Chain as WagmiChain } from 'wagmi'; import { AntDesignWeb3ConfigProvider } from './config-provider'; import type { Chain } from '@ant-design/web3-common'; import type { WalletFactory } from '../interface'; @@ -14,6 +14,7 @@ export type WagmiWeb3ConfigProviderProps< TWebSocketPublicClient extends WebSocketPublicClient = WebSocketPublicClient, > = { config: Config; + chains?: WagmiChain[]; assets?: (Chain | WalletFactory)[]; }; @@ -23,6 +24,7 @@ export function WagmiWeb3ConfigProvider< >({ children, assets = [], + chains = [mainnet], ...restProps }: React.PropsWithChildren< WagmiWeb3ConfigProviderProps @@ -31,6 +33,7 @@ export function WagmiWeb3ConfigProvider< {children} diff --git a/packages/web3/src/connect-button/connect-button.tsx b/packages/web3/src/connect-button/connect-button.tsx index 64b335232..96500996e 100644 --- a/packages/web3/src/connect-button/connect-button.tsx +++ b/packages/web3/src/connect-button/connect-button.tsx @@ -4,7 +4,15 @@ import { Address } from '../address'; import type { ConnectButtonProps } from './interface'; export const ConnectButton: React.FC = (props) => { - const { address, connected, onConnectClicked, onDisconnectClicked, chains } = props; + const { + address, + connected, + onConnectClick, + onDisconnectClick, + chains, + currentChain, + onSwitchChain, + } = props; const buttonProps = { style: props.style, @@ -14,9 +22,9 @@ export const ConnectButton: React.FC = (props) => { ghost: props.ghost, onClick: () => { if (connected) { - onDisconnectClicked?.(); + onDisconnectClick?.(); } else { - onConnectClicked?.(); + onConnectClick?.(); } }, children: connected ?
: 'Connect Wallet', @@ -26,9 +34,14 @@ export const ConnectButton: React.FC = (props) => { if (chains && chains.length > 1) { return ( { return { + onClick: () => { + onSwitchChain?.(item); + }, + icon: item.icon, label: item.name, key: item.id, }; diff --git a/packages/web3/src/connector/__tests__/basic.test.tsx b/packages/web3/src/connector/__tests__/basic.test.tsx index 70adadd05..2829ee558 100644 --- a/packages/web3/src/connector/__tests__/basic.test.tsx +++ b/packages/web3/src/connector/__tests__/basic.test.tsx @@ -32,14 +32,14 @@ describe('Connector', () => { const onConnectCallTest = vi.fn(); const onDisconnected = vi.fn(); const CustomButton: React.FC> = (props) => { - const { address, connected, onConnectClicked, onDisconnectClicked, children } = props; + const { address, connected, onConnectClick, onDisconnectClick, children } = props; return ( ; + }; + + const App = () => ( + + + + ); + const { baseElement } = render(); + expect(baseElement.querySelector('.ant-btn')?.textContent).toBe('Ethereum'); + }); + + it('chains', () => { + const CustomButton: React.FC> = (props) => { + const { chains } = props; + return ; + }; + + const App = () => ( + + + + ); + const { baseElement } = render(); + expect(baseElement.querySelector('.ant-btn')?.textContent).toBe('Ethereum,Polygon'); + }); + + it('onSwitchChain', async () => { + const CustomButton: React.FC> = (props) => { + const { currentChain, onSwitchChain } = props; + return ( + + ); + }; + + const onChainSwitched = vi.fn(); + + const App = () => { + const [currentChain, setCurrentChain] = React.useState(Mainnet); + const chains = [Mainnet, Polygon]; + return ( + { + setCurrentChain(chain); + }} + > + + + ); + }; + const { baseElement } = render(); + expect(baseElement.querySelector('.ant-btn')?.textContent).toBe('Ethereum'); + fireEvent.click(baseElement.querySelector('.ant-btn') as HTMLElement); + await vi.waitFor(() => { + expect(baseElement.querySelector('.ant-btn')?.textContent).toBe('Polygon'); + expect(onChainSwitched).toBeCalledWith(Polygon); + }); + }); +}); diff --git a/packages/web3/src/connector/conector.tsx b/packages/web3/src/connector/conector.tsx index 3d905cf10..8cb6bd4e9 100644 --- a/packages/web3/src/connector/conector.tsx +++ b/packages/web3/src/connector/conector.tsx @@ -1,13 +1,21 @@ import React from 'react'; import { ConnectModal } from '@ant-design/web3'; -import type { Wallet, ConnectorTriggerProps } from '@ant-design/web3-common'; +import type { Wallet, ConnectorTriggerProps, Chain } from '@ant-design/web3-common'; import { message } from 'antd'; import type { ConnectorProps } from './interface'; import useProvider from '../hooks/useProvider'; export const Connector: React.FC = (props) => { - const { children, modalProps, onConnect, onConnected, onDisconnect, onDisconnected } = props; - const { wallets, requestAccounts, disconnect, accounts, chains, currentChain } = + const { + children, + modalProps, + onConnect, + onConnected, + onDisconnect, + onDisconnected, + onChainSwitched, + } = props; + const { wallets, requestAccounts, disconnect, accounts, chains, currentChain, switchChain } = useProvider(props); const currentAccount = accounts?.[0]; const [open, setOpen] = React.useState(false); @@ -36,10 +44,10 @@ export const Connector: React.FC = (props) => { address: currentAccount?.address, connected: !!currentAccount, loading, - onConnectClicked: () => { + onConnectClick: () => { setOpen(true); }, - onDisconnectClicked: async () => { + onDisconnectClick: async () => { setLoading(true); onDisconnect?.(); await disconnect?.(); @@ -48,6 +56,10 @@ export const Connector: React.FC = (props) => { }, chains, currentChain, + onSwitchChain: async (chain: Chain) => { + await switchChain?.(chain); + onChainSwitched?.(chain); + }, })} { return ( - + diff --git a/packages/web3/src/connector/demos/wagmi.tsx b/packages/web3/src/connector/demos/wagmi.tsx index 1bb2712c5..c2abf1b81 100644 --- a/packages/web3/src/connector/demos/wagmi.tsx +++ b/packages/web3/src/connector/demos/wagmi.tsx @@ -11,7 +11,9 @@ const config = createConfig({ autoConnect: true, publicClient, connectors: [ - new MetaMaskConnector(), + new MetaMaskConnector({ + chains, + }), new WalletConnectConnector({ chains, options: { diff --git a/packages/web3/src/connector/index.zh-CN.md b/packages/web3/src/connector/index.zh-CN.md index 18cff3ef8..797e08551 100644 --- a/packages/web3/src/connector/index.zh-CN.md +++ b/packages/web3/src/connector/index.zh-CN.md @@ -33,10 +33,13 @@ group: 组件 | onDisconnect | 触发断开连接时的回调 | `() => Promise` | - | - | | onConnected | 连接成功时的回调 | `(account: Account) => Promise` | - | - | | onDisconnected | 断开连接时的回调 | `() => Promise` | - | - | +| onChainSwitched | 切换网络时的回调 | `(chain: Chain) => Promise` | - | - | | wallets | 钱包列表 | `Wallet[]` | - | - | | accounts | 账户列表 | `Account[]` | - | - | +| chains | 网络列表 | `Chain[]` | - | - | | requestAccounts | 请求账户列表的方法 | `() => Promise` | - | - | | disconnect | 断开连接的方法 | `() => Promise` | - | - | +| switchChain | 切换网络的方法 | `(chain: Chain) => Promise` | - | - | ### ConnectorTriggerProps @@ -45,8 +48,9 @@ group: 组件 | 属性 | 描述 | 类型 | 默认值 | 版本 | | --- | --- | --- | --- | --- | | address | 当前连接的账户地址 | `string` | - | - | -| onConnectClicked | 连接事件 | `React.MouseEventHandler` | - | - | -| onDisconnectClicked | 断开连接事件 | `React.MouseEventHandler` | - | - | +| onConnectClick | 连接事件 | `React.MouseEventHandler` | - | - | +| onDisconnectClick | 断开连接事件 | `React.MouseEventHandler` | - | - | +| onSwitchChain | 切换网络事件 | `(chain: Chain) => Promise` | - | - | | domain | address 对应的域名,通常就是指 ENS | `string` | - | - | | connected | 是否已连接 | `boolean` | - | - | | chains | 当前连接的网络列表 | `ChainSelectItem[]` | - | - | diff --git a/packages/web3/src/connector/interface.ts b/packages/web3/src/connector/interface.ts index 74d57667f..d183970d0 100644 --- a/packages/web3/src/connector/interface.ts +++ b/packages/web3/src/connector/interface.ts @@ -1,5 +1,5 @@ import type { ConnectModalProps } from '@ant-design/web3'; -import type { Account, Wallet } from '@ant-design/web3-common'; +import type { Account, Wallet, Chain } from '@ant-design/web3-common'; export interface ConnectorProps { children: React.ReactNode; @@ -9,9 +9,13 @@ export interface ConnectorProps { onDisconnect?: () => Promise; onConnected?: (accounts?: Account[]) => void; onDisconnected?: () => void; + onChainSwitched?: (chain?: Chain) => void; wallets?: Wallet[]; accounts?: Account[]; + chains?: Chain[]; + currentChain?: Chain; requestAccounts?: (wallet?: string) => Promise; disconnect?: () => Promise; + switchChain?: (chain: Chain) => Promise; } diff --git a/packages/web3/src/web3-config-provider/demos/simple.tsx b/packages/web3/src/web3-config-provider/demos/simple.tsx index ff0a426cf..252d717c4 100644 --- a/packages/web3/src/web3-config-provider/demos/simple.tsx +++ b/packages/web3/src/web3-config-provider/demos/simple.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Web3ConfigProvider, ConnectButton, Account } from '@ant-design/web3'; +import { Web3ConfigProvider, ConnectButton, type Account } from '@ant-design/web3'; const App: React.FC = () => { const [accounts, setAccounts] = React.useState([]);