diff --git a/.changeset/tidy-flies-accept.md b/.changeset/tidy-flies-accept.md
new file mode 100644
index 000000000..c8053d678
--- /dev/null
+++ b/.changeset/tidy-flies-accept.md
@@ -0,0 +1,5 @@
+---
+'@ant-design/web3-icp': major
+---
+
+功能:集成 ICP 链的钱包连接能力
diff --git a/packages/assets/src/icp/chains.tsx b/packages/assets/src/icp/chains.tsx
new file mode 100644
index 000000000..238857767
--- /dev/null
+++ b/packages/assets/src/icp/chains.tsx
@@ -0,0 +1,17 @@
+import { createGetBrowserLink, IcpChainIds, type Chain } from '@ant-design/web3-common';
+import { IcpColorful } from '@ant-design/web3-icons';
+
+export interface IcpChain extends Chain {
+ id: IcpChainIds;
+}
+
+export const Icp: IcpChain = {
+ id: IcpChainIds.Mainnet,
+ name: 'Internet Computer',
+ icon: ,
+ browser: {
+ icon: ,
+ getBrowserLink: createGetBrowserLink('https://dashboard.internetcomputer.org'),
+ },
+ nativeCurrency: { name: 'ICP', symbol: 'ICP', decimals: 8 },
+};
diff --git a/packages/assets/src/icp/index.ts b/packages/assets/src/icp/index.ts
new file mode 100644
index 000000000..c2037b545
--- /dev/null
+++ b/packages/assets/src/icp/index.ts
@@ -0,0 +1 @@
+export * from './chains';
diff --git a/packages/assets/src/index.ts b/packages/assets/src/index.ts
index e916d1641..132b18779 100644
--- a/packages/assets/src/index.ts
+++ b/packages/assets/src/index.ts
@@ -1,3 +1,4 @@
export * from './wallets';
export * from './chains/ethereum';
export * from './tokens';
+export * from './icp';
diff --git a/packages/assets/src/wallets/index.ts b/packages/assets/src/wallets/index.ts
index de82f4a4b..935e25b0a 100644
--- a/packages/assets/src/wallets/index.ts
+++ b/packages/assets/src/wallets/index.ts
@@ -15,3 +15,4 @@ export * from './mobile-wallet';
export * from './slush';
export * from './suiet';
export * from './solflare';
+export * from './plug';
diff --git a/packages/assets/src/wallets/plug.tsx b/packages/assets/src/wallets/plug.tsx
new file mode 100644
index 000000000..c4c6c81bd
--- /dev/null
+++ b/packages/assets/src/wallets/plug.tsx
@@ -0,0 +1,20 @@
+import type { WalletMetadata } from '@ant-design/web3-common';
+import { ChromeCircleColorful } from '@ant-design/web3-icons';
+
+export const metadata_Plug: WalletMetadata = {
+ icon: null,
+ name: 'Plug',
+ remark: 'Plug Wallet',
+ app: {
+ link: 'https://plugwallet.ooo/',
+ },
+ extensions: [
+ {
+ key: 'Chrome',
+ browserIcon: ,
+ browserName: 'Chrome',
+ link: 'https://chromewebstore.google.com/detail/plug/cfbfdhimifdmdehjmkdobpcjfefblkjm',
+ description: 'Access your wallet right from your favorite web browser.',
+ },
+ ],
+};
diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts
index 587197647..b3b69f013 100644
--- a/packages/common/src/types.ts
+++ b/packages/common/src/types.ts
@@ -44,6 +44,10 @@ export enum SuiChainIds {
Localnet = 4,
}
+export enum IcpChainIds {
+ Mainnet = 1,
+}
+
export type BrowserLinkType = 'address' | 'transaction';
export type BalanceMetadata = {
diff --git a/packages/icp/package.json b/packages/icp/package.json
new file mode 100644
index 000000000..beef568c1
--- /dev/null
+++ b/packages/icp/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@ant-design/web3-icp",
+ "version": "0.0.1",
+ "main": "dist/lib/index.js",
+ "module": "dist/esm/index.js",
+ "typings": "dist/esm/index.d.ts",
+ "exports": {
+ "import": "./dist/esm/index.js",
+ "require": "./dist/lib/index.js",
+ "types": "./dist/esm/index.d.ts"
+ },
+ "sideEffects": false,
+ "files": [
+ "dist",
+ "CHANGELOG.md",
+ "README.md"
+ ],
+ "keywords": [
+ "ant",
+ "design",
+ "web3",
+ "antd",
+ "component",
+ "components",
+ "framework",
+ "frontend",
+ "react",
+ "react-component",
+ "ui",
+ "icp",
+ "internet-computer",
+ "plug"
+ ],
+ "homepage": "https://web3.ant.design",
+ "bugs": {
+ "url": "https://github.com/ant-design/ant-design-web3/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/ant-design/ant-design-web3"
+ },
+ "scripts": {
+ "dev": "father dev",
+ "build": "father build"
+ },
+ "dependencies": {
+ "@ant-design/web3-common": "workspace:*"
+ },
+ "devDependencies": {
+ "father": "^4.6.2",
+ "typescript": "^5.6.2"
+ },
+ "publishConfig": {
+ "registry": "https://registry.npmjs.org",
+ "access": "public"
+ },
+ "browserslist": [
+ "last 2 versions",
+ "Firefox ESR",
+ "> 1%",
+ "ie >= 11"
+ ]
+}
diff --git a/packages/icp/src/icp-provider/config-provider.tsx b/packages/icp/src/icp-provider/config-provider.tsx
new file mode 100644
index 000000000..1117877a6
--- /dev/null
+++ b/packages/icp/src/icp-provider/config-provider.tsx
@@ -0,0 +1,156 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import type { IcpChain } from '@ant-design/web3-assets/icp';
+import type { Account, Chain, Locale, Wallet } from '@ant-design/web3-common';
+import { Web3ConfigProvider } from '@ant-design/web3-common';
+
+import type { IcpWallet } from '../wallets/types';
+
+interface ConnectAsync {
+ promise: Promise;
+ resolve: (account?: Account) => void;
+ reject: (reason: any) => void;
+}
+
+export interface AntDesignWeb3ConfigProviderProps {
+ locale?: Locale;
+ chainAssets?: Chain[];
+ availableChains: IcpChain[];
+ balance?: boolean;
+ currentChain?: IcpChain;
+ availableWallets: Wallet[];
+ onCurrentChainChange?: (chain?: IcpChain) => void;
+ wallet: IcpWallet;
+ principal: string | null;
+ connecting: boolean;
+ onConnect: () => Promise;
+ onDisconnect: () => Promise;
+}
+
+export const AntDesignWeb3ConfigProvider: React.FC<
+ React.PropsWithChildren
+> = (props) => {
+ const {
+ wallet,
+ principal,
+ connecting,
+ onConnect,
+ onDisconnect,
+ availableChains,
+ chainAssets,
+ currentChain,
+ availableWallets,
+ balance,
+ locale,
+ onCurrentChainChange,
+ } = props;
+
+ const connectAsyncRef = useRef();
+ const [account, setAccount] = useState();
+ const [balanceData, setBalanceData] = useState();
+
+ // get account address
+ useEffect(() => {
+ if (!principal) {
+ setAccount(undefined);
+ return;
+ }
+
+ setAccount({
+ address: principal,
+ });
+ }, [principal]);
+
+ // connect/disconnect wallet
+ useEffect(() => {
+ if (connecting && connectAsyncRef.current) {
+ // connecting in progress
+ return;
+ }
+
+ if (principal && connectAsyncRef.current) {
+ connectAsyncRef.current.resolve({ address: principal });
+ connectAsyncRef.current = undefined;
+ }
+ }, [principal, connecting]);
+
+ const chainList = useMemo(() => {
+ return availableChains
+ .map((item) => {
+ const c = chainAssets?.find((asset) => {
+ return asset.id === item.id;
+ }) as Chain;
+
+ if (c?.id) {
+ return {
+ ...item,
+ ...c,
+ id: c.id,
+ name: c.name,
+ icon: c.icon,
+ };
+ }
+ return item;
+ })
+ .filter((item) => item !== null) as (Chain & IcpChain)[];
+ }, [availableChains, chainAssets]);
+
+ const currentChainData = useMemo(() => {
+ return chainList.find((c) => c.id === currentChain?.id);
+ }, [currentChain, chainList]);
+
+ const currency = currentChainData?.nativeCurrency;
+
+ return (
+ {
+ const foundChain = chainList.find((c) => c.id === _chain.id);
+ const targetChain = foundChain ?? chainList[0];
+ if (targetChain) {
+ onCurrentChainChange?.(targetChain);
+ }
+ }}
+ connect={async (_wallet, options) => {
+ let resolve: any;
+ let reject: any;
+
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+
+ connectAsyncRef.current = { promise, resolve, reject };
+
+ try {
+ await onConnect();
+ } catch (error) {
+ connectAsyncRef.current.reject(error);
+ connectAsyncRef.current = undefined;
+ throw error;
+ }
+
+ return promise;
+ }}
+ disconnect={async () => {
+ await onDisconnect();
+ }}
+ >
+ {props.children}
+
+ );
+};
diff --git a/packages/icp/src/icp-provider/index.tsx b/packages/icp/src/icp-provider/index.tsx
new file mode 100644
index 000000000..20c15cc0a
--- /dev/null
+++ b/packages/icp/src/icp-provider/index.tsx
@@ -0,0 +1,85 @@
+import React, { useMemo, useState, type FC, type PropsWithChildren } from 'react';
+import { Icp, type IcpChain } from '@ant-design/web3-assets/icp';
+import type { Locale } from '@ant-design/web3-common';
+
+import { PlugWallet } from '../wallets/built-in';
+import type { IcpWalletFactory } from '../wallets/factory';
+import { AntDesignWeb3ConfigProvider } from './config-provider';
+
+export interface IcpWeb3ConfigProviderProps {
+ locale?: Locale;
+ chains?: IcpChain[];
+ wallets?: IcpWalletFactory[];
+ balance?: boolean;
+}
+
+export const IcpWeb3ConfigProvider: FC> = ({
+ locale,
+ chains = [Icp],
+ wallets: walletFactories = [PlugWallet()],
+ balance,
+ children,
+}) => {
+ const [currentChain, setCurrentChain] = useState(chains[0]);
+ const [principal, setPrincipal] = useState(null);
+ const [connecting, setConnecting] = useState(false);
+
+ const availableWallets = useMemo(() => {
+ return walletFactories.map((factory) => {
+ const wallet = factory.create();
+ return {
+ name: wallet.name,
+ remark: wallet.name,
+ icon: wallet.icon,
+ hasExtensionInstalled: async () => wallet.installed,
+ hasWalletReady: async () => wallet.installed,
+ };
+ });
+ }, [walletFactories]);
+
+ const wallet = useMemo(() => {
+ // 优先选择已安装的钱包,如果没有已安装的,则选择第一个
+ const walletInstances = walletFactories.map((factory) => factory.create());
+ const installedWallet = walletInstances.find((w) => w.installed);
+ return installedWallet ?? walletInstances[0];
+ }, [walletFactories]);
+
+ const connect = async () => {
+ if (!wallet) {
+ return;
+ }
+
+ setConnecting(true);
+ try {
+ await wallet.connect();
+ const p = await wallet.getPrincipal();
+ setPrincipal(p);
+ } finally {
+ setConnecting(false);
+ }
+ };
+
+ const disconnect = async () => {
+ await wallet.disconnect();
+ setPrincipal(null);
+ };
+
+ return (
+ setCurrentChain(chain)}
+ availableChains={chains}
+ wallet={wallet}
+ principal={principal}
+ connecting={connecting}
+ onConnect={connect}
+ onDisconnect={disconnect}
+ >
+ {children}
+
+ );
+};
diff --git a/packages/icp/src/index.ts b/packages/icp/src/index.ts
new file mode 100644
index 000000000..b2ff4fcab
--- /dev/null
+++ b/packages/icp/src/index.ts
@@ -0,0 +1,4 @@
+export * from './icp-provider';
+export * from './wallets/types';
+export * from './wallets/factory';
+export * from './wallets/built-in';
diff --git a/packages/icp/src/wallets/built-in.ts b/packages/icp/src/wallets/built-in.ts
new file mode 100644
index 000000000..8f8edde60
--- /dev/null
+++ b/packages/icp/src/wallets/built-in.ts
@@ -0,0 +1,10 @@
+/* v8 ignore start */
+import { metadata_Plug } from '@ant-design/web3-assets';
+import type { WalletMetadata } from '@ant-design/web3-common';
+
+import { WalletFactory } from './factory';
+import type { IcpWalletFactory } from './factory';
+import { createPlugWallet } from './plug';
+
+export const PlugWallet = (metadata?: Partial): IcpWalletFactory =>
+ WalletFactory(createPlugWallet, { ...metadata_Plug, ...metadata });
diff --git a/packages/icp/src/wallets/factory.ts b/packages/icp/src/wallets/factory.ts
new file mode 100644
index 000000000..df569eeb4
--- /dev/null
+++ b/packages/icp/src/wallets/factory.ts
@@ -0,0 +1,25 @@
+import type { WalletMetadata } from '@ant-design/web3-common';
+
+import type { IcpWallet } from './types';
+
+export interface IcpWalletFactory {
+ create: () => IcpWallet;
+}
+
+export type IcpWalletFactoryBuilder = (
+ createWallet: () => IcpWallet,
+ metadata: WalletMetadata,
+) => IcpWalletFactory;
+
+export const WalletFactory: IcpWalletFactoryBuilder = (createWallet, metadata) => {
+ return {
+ create: () => {
+ const wallet = createWallet();
+ return {
+ ...wallet,
+ name: metadata.name,
+ icon: metadata.icon ?? wallet.icon,
+ };
+ },
+ };
+};
diff --git a/packages/icp/src/wallets/infinity.tsx b/packages/icp/src/wallets/infinity.tsx
new file mode 100644
index 000000000..2bbbe874e
--- /dev/null
+++ b/packages/icp/src/wallets/infinity.tsx
@@ -0,0 +1,76 @@
+import type React from 'react';
+
+import type { IcpWallet } from './types';
+
+// NOTE: 这里假设 Infinity 钱包通过 window.ic.infinityWallet 注入,
+// 具体字段请根据官方文档调整。
+declare global {
+ interface Window {
+ ic?: {
+ infinityWallet?: {
+ isConnected: () => Promise;
+ requestConnect: (opts?: unknown) => Promise<{ principalId: string }>;
+ disconnect: () => Promise;
+ getPrincipal: () => Promise<{ toText: () => string }>;
+ };
+ };
+ }
+}
+
+const getInfinity = () =>
+ typeof window === 'undefined' || typeof window.ic === 'undefined'
+ ? undefined
+ : window.ic.infinityWallet;
+
+export const isInfinityInstalled = () => !!getInfinity();
+
+export function createInfinityWallet(): IcpWallet {
+ const getInstalled = () => isInfinityInstalled();
+
+ return {
+ id: 'infinity',
+ name: 'Infinity',
+ icon: null as React.ReactNode | null,
+
+ get installed() {
+ return getInstalled();
+ },
+
+ async connect() {
+ const wallet = getInfinity();
+ if (!wallet) {
+ return;
+ }
+ await wallet.requestConnect();
+ },
+
+ async disconnect() {
+ const wallet = getInfinity();
+ if (!wallet) {
+ return;
+ }
+ await wallet.disconnect();
+ },
+
+ async isConnected() {
+ const wallet = getInfinity();
+ if (!wallet) {
+ return false;
+ }
+ return wallet.isConnected();
+ },
+
+ async getPrincipal() {
+ const wallet = getInfinity();
+ if (!wallet) {
+ return null;
+ }
+ try {
+ const principal = await wallet.getPrincipal();
+ return principal.toText();
+ } catch {
+ return null;
+ }
+ },
+ };
+}
diff --git a/packages/icp/src/wallets/plug.tsx b/packages/icp/src/wallets/plug.tsx
new file mode 100644
index 000000000..738d97dc3
--- /dev/null
+++ b/packages/icp/src/wallets/plug.tsx
@@ -0,0 +1,73 @@
+import type React from 'react';
+
+import type { IcpWallet } from './types';
+
+declare global {
+ interface Window {
+ ic?: {
+ plug?: {
+ isConnected: () => Promise;
+ requestConnect: (opts?: unknown) => Promise<{ principalId: string }>;
+ disconnect: () => Promise;
+ getPrincipal: () => Promise<{ toText: () => string }>;
+ };
+ };
+ }
+}
+
+const getPlug = () =>
+ typeof window === 'undefined' || typeof window.ic === 'undefined' ? undefined : window.ic.plug;
+
+export const isPlugInstalled = () => !!getPlug();
+
+export function createPlugWallet(): IcpWallet {
+ const getInstalled = () => isPlugInstalled();
+
+ return {
+ id: 'plug',
+ name: 'Plug',
+ icon: null,
+
+ get installed() {
+ return getInstalled();
+ },
+
+ async connect() {
+ const plug = getPlug();
+ if (!plug) {
+ // 不抛异常,由上层根据 installed 提示安装钱包
+ return;
+ }
+ await plug.requestConnect();
+ },
+
+ async disconnect() {
+ const plug = getPlug();
+ if (!plug) {
+ return;
+ }
+ await plug.disconnect();
+ },
+
+ async isConnected() {
+ const plug = getPlug();
+ if (!plug) {
+ return false;
+ }
+ return plug.isConnected();
+ },
+
+ async getPrincipal() {
+ const plug = getPlug();
+ if (!plug) {
+ return null;
+ }
+ try {
+ const principal = await plug.getPrincipal();
+ return principal.toText();
+ } catch {
+ return null;
+ }
+ },
+ };
+}
diff --git a/packages/icp/src/wallets/types.ts b/packages/icp/src/wallets/types.ts
new file mode 100644
index 000000000..7356eacd5
--- /dev/null
+++ b/packages/icp/src/wallets/types.ts
@@ -0,0 +1,14 @@
+import type React from 'react';
+
+export interface IcpWallet {
+ id: string;
+ name: string;
+ icon: React.ReactNode | null;
+ readonly installed: boolean;
+ connect: () => Promise;
+ disconnect: () => Promise;
+ isConnected: () => Promise;
+ getPrincipal: () => Promise;
+}
+
+export type IcpWalletType = 'plug' | 'infinity';
diff --git a/packages/icp/tsconfig.json b/packages/icp/tsconfig.json
new file mode 100644
index 000000000..928e5b0ef
--- /dev/null
+++ b/packages/icp/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src", "global.d.ts"]
+}
diff --git a/packages/web3/src/icp/demos/basic.tsx b/packages/web3/src/icp/demos/basic.tsx
new file mode 100644
index 000000000..597f74c66
--- /dev/null
+++ b/packages/web3/src/icp/demos/basic.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { ConnectButton, Connector } from '@ant-design/web3';
+import { IcpWeb3ConfigProvider, PlugWallet } from '@ant-design/web3-icp';
+
+const App: React.FC = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/packages/web3/src/icp/index.md b/packages/web3/src/icp/index.md
new file mode 100644
index 000000000..7ec75cdcf
--- /dev/null
+++ b/packages/web3/src/icp/index.md
@@ -0,0 +1,28 @@
+---
+nav: Components
+subtitle: ICP
+order: 6
+group:
+ title: Chains
+ order: 2
+---
+
+## Introduction
+
+`@ant-design/web3-icp` provides basic adaptation for the ICP chain and currently ships with **Plug** wallet support out of the box.
+
+This page shows how to integrate ICP wallet connection via `IcpWeb3ConfigProvider` together with `ConnectButton`.
+
+> 📎 Need to log in through the official NNS portal first? Visit [https://nns.ic0.app/](https://nns.ic0.app/) to authorize your ICP account / login, then come back to experience the connection flow.
+
+## When to use
+
+- You need to integrate ICP wallet connection into your DApp;
+- You are already using `@ant-design/web3` UI components (such as `ConnectButton`) and want a unified user experience;
+- You only need a minimal Plug integration for now and will extend more wallets later.
+
+## Examples
+
+### Basic usage
+
+
diff --git a/packages/web3/src/icp/index.zh-CN.md b/packages/web3/src/icp/index.zh-CN.md
new file mode 100644
index 000000000..6f1b1398f
--- /dev/null
+++ b/packages/web3/src/icp/index.zh-CN.md
@@ -0,0 +1,31 @@
+---
+nav: 组件
+subtitle: Internet Computer
+order: 6
+group:
+ title: 连接链
+ order: 2
+tag:
+ title: 新增
+ color: success
+---
+
+## 介绍
+
+`@ant-design/web3-icp` 提供了对 ICP 链的基础适配能力,目前内置支持 **Plug** 钱包,后续可以按需扩展更多 ICP 钱包。
+
+本页示例展示如何通过 `IcpWeb3ConfigProvider` 和 `ConnectButton` 快速接入 ICP 链的钱包连接能力。
+
+> 📎 如果需要提前登录官方 NNS 门户,可直接访问 [https://nns.ic0.app/](https://nns.ic0.app/),先在该站点完成 ICP 账号授权与登录,再回到页面体验连接流程。
+
+## 何时使用
+
+- 需要在 DApp 中集成 ICP 链的钱包连接能力;
+- 已经在使用 `@ant-design/web3` 的通用 UI 组件(如 `ConnectButton`),希望统一体验;
+- 只需要 Plug 钱包的最小接入,后续再逐步扩充。
+
+## 代码演示
+
+### 基础用法
+
+
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 8ebe0a74e..c2565f473 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -34,7 +34,8 @@
"@ant-design/web3-sui": ["./packages/sui/src/"],
"@ant-design/web3-ton": ["./packages/ton/src/"],
"@ant-design/web3-bitcoin": ["./packages/bitcoin/src/"],
- "@ant-design/web3-tron": ["./packages/tron/src/"]
+ "@ant-design/web3-tron": ["./packages/tron/src/"],
+ "@ant-design/web3-icp": ["./packages/icp/src/"]
}
}
}