From 8d7eaf303adf2b18ac8d96e0cf53ed8168f4d4f2 Mon Sep 17 00:00:00 2001 From: wangshanshan01 Date: Mon, 1 Dec 2025 18:20:46 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9Eicp=E9=93=BE-plug?= =?UTF-8?q?=E9=92=B1=E5=8C=85=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/icp/package.json | 63 +++++++ packages/icp/src/icp-provider/context.tsx | 75 ++++++++ packages/icp/src/index.ts | 3 + packages/icp/src/wallets/infinity.tsx | 76 ++++++++ packages/icp/src/wallets/plug.tsx | 73 ++++++++ packages/icp/src/wallets/types.ts | 14 ++ packages/icp/tsconfig.json | 4 + packages/web3/src/icp/demos/basic.tsx | 204 ++++++++++++++++++++++ packages/web3/src/icp/index.md | 28 +++ packages/web3/src/icp/index.zh-CN.md | 31 ++++ tsconfig.base.json | 3 +- 11 files changed, 573 insertions(+), 1 deletion(-) create mode 100644 packages/icp/package.json create mode 100644 packages/icp/src/icp-provider/context.tsx create mode 100644 packages/icp/src/index.ts create mode 100644 packages/icp/src/wallets/infinity.tsx create mode 100644 packages/icp/src/wallets/plug.tsx create mode 100644 packages/icp/src/wallets/types.ts create mode 100644 packages/icp/tsconfig.json create mode 100644 packages/web3/src/icp/demos/basic.tsx create mode 100644 packages/web3/src/icp/index.md create mode 100644 packages/web3/src/icp/index.zh-CN.md 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/context.tsx b/packages/icp/src/icp-provider/context.tsx new file mode 100644 index 000000000..89dc6bced --- /dev/null +++ b/packages/icp/src/icp-provider/context.tsx @@ -0,0 +1,75 @@ +import React, { createContext, useContext, useMemo, useState, type PropsWithChildren } from 'react'; + +import { createInfinityWallet } from '../wallets/infinity'; +import { createPlugWallet } from '../wallets/plug'; +import type { IcpWallet, IcpWalletType } from '../wallets/types'; + +const IcpContext = createContext(null); + +export interface IcpContextValue { + wallet: IcpWallet; + principal: string | null; + connecting: boolean; + connect: () => Promise; + disconnect: () => Promise; + installed: boolean; +} + +export interface IcpWeb3ConfigProviderProps extends PropsWithChildren { + /** + * 指定使用哪个 ICP 钱包,默认 plug + */ + walletType?: IcpWalletType; +} + +export const useIcpWallet = (): IcpContextValue => { + const ctx = useContext(IcpContext); + + if (!ctx) { + throw new Error('useIcpWallet must be used within IcpWeb3ConfigProvider'); + } + + return ctx; +}; + +export const IcpWeb3ConfigProvider: React.FC = ({ + children, + walletType = 'plug', +}) => { + const [principal, setPrincipal] = useState(null); + const [connecting, setConnecting] = useState(false); + + const wallet = useMemo(() => { + if (walletType === 'infinity') { + return createInfinityWallet(); + } + return createPlugWallet(); + }, [walletType]); + + const connect = async () => { + setConnecting(true); + try { + await wallet.connect(); + const p = await wallet.getPrincipal(); + setPrincipal(p); + } finally { + setConnecting(false); + } + }; + + const disconnect = async () => { + await wallet.disconnect(); + setPrincipal(null); + }; + + const value: IcpContextValue = { + wallet, + principal, + connecting, + connect, + disconnect, + installed: wallet.installed, + }; + + return {children}; +}; diff --git a/packages/icp/src/index.ts b/packages/icp/src/index.ts new file mode 100644 index 000000000..3e5f2b84e --- /dev/null +++ b/packages/icp/src/index.ts @@ -0,0 +1,3 @@ +export * from './icp-provider/context'; +export * from './wallets/plug'; +export * from './wallets/types'; 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..a7abd0d01 --- /dev/null +++ b/packages/web3/src/icp/demos/basic.tsx @@ -0,0 +1,204 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { ConnectButton, ConnectModal } from '@ant-design/web3'; +import type { Wallet } from '@ant-design/web3'; +import { ConnectStatus } from '@ant-design/web3-common'; +import { IcpWeb3ConfigProvider, isPlugInstalled, useIcpWallet } from '@ant-design/web3-icp'; +import { message, Modal } from 'antd'; + +type WalletType = 'plug'; + +type WalletMeta = { + name: string; + remark: string; + group: string; + color: string; + checkInstalled?: () => Promise; +}; + +const walletMetaMap: Record = { + plug: { + name: 'Plug', + remark: '浏览器扩展,适合日常使用', + group: 'Popular', + color: '#4f64ff', + checkInstalled: async () => isPlugInstalled(), + }, +}; + +const WalletIcon: React.FC<{ color: string; label: string }> = ({ color, label }) => ( + + {label} + +); + +const walletList: Wallet[] = (Object.keys(walletMetaMap) as WalletType[]).map((type) => { + const meta = walletMetaMap[type]; + return { + key: type, + name: meta.name, + remark: meta.remark, + icon: , + group: meta.group, + hasExtensionInstalled: meta.checkInstalled + ? async () => !!(await meta.checkInstalled?.()) + : undefined, + hasWalletReady: meta.checkInstalled ? async () => !!(await meta.checkInstalled?.()) : undefined, + }; +}); + +interface IcpConnectButtonProps { + walletType: WalletType; + onSelectWallet: () => void; + autoConnect: boolean; + onAutoConnectConsumed: () => void; +} + +const IcpConnectButton: React.FC = ({ + walletType, + onSelectWallet, + autoConnect, + onAutoConnectConsumed, +}) => { + const { principal, connecting, connect, disconnect, installed } = useIcpWallet(); + const [messageApi, contextHolder] = message.useMessage(); + const walletName = walletMetaMap[walletType].name; + + const handleConnect = async () => { + try { + if (!installed) { + Modal.info({ + title: `未检测到 ${walletName} 钱包`, + content: ( + <> +

当前浏览器环境未检测到 {walletName} 钱包扩展。

+

请先安装该钱包并完成初始化,再刷新页面重试。

+ + ), + okText: '我知道了', + }); + return; + } + await connect(); + } catch (error: any) { + messageApi.error(error?.message ?? `连接 ${walletName} 钱包失败,请检查插件是否安装。`); + } + }; + + const handleDisconnect = async () => { + try { + await disconnect(); + } catch (error: any) { + messageApi.error(error?.message ?? `断开 ${walletName} 钱包失败,请重试。`); + } + }; + + useEffect(() => { + if (autoConnect) { + void (async () => { + await handleConnect(); + onAutoConnectConsumed(); + })(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [autoConnect, walletType]); + + return ( + <> + {contextHolder} + { + if (!principal) { + onSelectWallet(); + } + }} + onDisconnectClick={() => { + void handleDisconnect(); + }} + account={ + principal + ? { + address: principal, + name: walletName, + status: ConnectStatus.Connected, + } + : undefined + } + /> + + ); +}; + +export default () => { + const [walletType, setWalletType] = useState('plug'); + const [modalOpen, setModalOpen] = useState(false); + const [autoConnect, setAutoConnect] = useState(false); + + const walletGroupOrder = useMemo( + () => (a: string, b: string) => { + if (a === 'Popular') return -1; + if (b === 'Popular') return 1; + return a.localeCompare(b); + }, + [], + ); + + const handleWalletSelected = async (wallet: Wallet) => { + const nextType = (wallet.key as WalletType) ?? 'plug'; + const meta = walletMetaMap[nextType]; + const installed = (await meta.checkInstalled?.()) ?? true; + + if (!installed) { + Modal.info({ + title: `${meta.name} 未检测到浏览器插件`, + content: ( + <> +

请先在浏览器中安装 {meta.name} 钱包扩展或确保插件已启用。

+

安装完成后刷新页面,再次选择该钱包即可连接。

+ + ), + okText: '我知道了', + }); + return; + } + + setWalletType(nextType); + setModalOpen(false); + setAutoConnect(true); + }; + + return ( + + setModalOpen(true)} + autoConnect={autoConnect} + onAutoConnectConsumed={() => setAutoConnect(false)} + /> + + setModalOpen(false)} + walletList={walletList} + group={{ + groupOrder: walletGroupOrder, + }} + onWalletSelected={(wallet) => { + void handleWalletSelected(wallet); + }} + /> + + ); +}; 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/"] } } } From 40f8f10d8bcf7cd32e7a5f0d199208ee5de8b921 Mon Sep 17 00:00:00 2001 From: wangshanshan01 Date: Mon, 1 Dec 2025 20:22:13 +0800 Subject: [PATCH 2/5] feat: update changeset --- .changeset/tidy-flies-accept.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-flies-accept.md 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 链的钱包连接能力 From 13a37009772f6be2d7877156bfa3b616c8854823 Mon Sep 17 00:00:00 2001 From: wangshanshan01 Date: Tue, 9 Dec 2025 09:09:25 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96plug=20wallets?= =?UTF-8?q?=E7=BB=B4=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/assets/src/wallets/index.ts | 1 + packages/assets/src/wallets/plug.tsx | 20 ++++++++++++++++++ packages/icp/src/icp-provider/context.tsx | 22 ++++++++++---------- packages/icp/src/index.ts | 2 ++ packages/icp/src/wallets/built-in.ts | 10 +++++++++ packages/icp/src/wallets/factory.ts | 25 +++++++++++++++++++++++ packages/web3/src/icp/demos/basic.tsx | 16 ++++++++++++--- 7 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 packages/assets/src/wallets/plug.tsx create mode 100644 packages/icp/src/wallets/built-in.ts create mode 100644 packages/icp/src/wallets/factory.ts 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/icp/src/icp-provider/context.tsx b/packages/icp/src/icp-provider/context.tsx index 89dc6bced..8f0db4873 100644 --- a/packages/icp/src/icp-provider/context.tsx +++ b/packages/icp/src/icp-provider/context.tsx @@ -1,8 +1,8 @@ import React, { createContext, useContext, useMemo, useState, type PropsWithChildren } from 'react'; -import { createInfinityWallet } from '../wallets/infinity'; -import { createPlugWallet } from '../wallets/plug'; -import type { IcpWallet, IcpWalletType } from '../wallets/types'; +import { PlugWallet } from '../wallets/built-in'; +import type { IcpWalletFactory } from '../wallets/factory'; +import type { IcpWallet } from '../wallets/types'; const IcpContext = createContext(null); @@ -17,9 +17,9 @@ export interface IcpContextValue { export interface IcpWeb3ConfigProviderProps extends PropsWithChildren { /** - * 指定使用哪个 ICP 钱包,默认 plug + * 指定使用的 ICP 钱包数组,默认使用 PlugWallet */ - walletType?: IcpWalletType; + wallets?: IcpWalletFactory[]; } export const useIcpWallet = (): IcpContextValue => { @@ -34,17 +34,17 @@ export const useIcpWallet = (): IcpContextValue => { export const IcpWeb3ConfigProvider: React.FC = ({ children, - walletType = 'plug', + wallets = [PlugWallet()], }) => { const [principal, setPrincipal] = useState(null); const [connecting, setConnecting] = useState(false); const wallet = useMemo(() => { - if (walletType === 'infinity') { - return createInfinityWallet(); - } - return createPlugWallet(); - }, [walletType]); + // 优先选择已安装的钱包,如果没有已安装的,则选择第一个 + const walletInstances = wallets.map((factory) => factory.create()); + const installedWallet = walletInstances.find((w) => w.installed); + return installedWallet ?? walletInstances[0]; + }, [wallets]); const connect = async () => { setConnecting(true); diff --git a/packages/icp/src/index.ts b/packages/icp/src/index.ts index 3e5f2b84e..d374c1a73 100644 --- a/packages/icp/src/index.ts +++ b/packages/icp/src/index.ts @@ -1,3 +1,5 @@ export * from './icp-provider/context'; export * from './wallets/plug'; 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/web3/src/icp/demos/basic.tsx b/packages/web3/src/icp/demos/basic.tsx index a7abd0d01..db58dd43c 100644 --- a/packages/web3/src/icp/demos/basic.tsx +++ b/packages/web3/src/icp/demos/basic.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { ConnectButton, ConnectModal } from '@ant-design/web3'; import type { Wallet } from '@ant-design/web3'; import { ConnectStatus } from '@ant-design/web3-common'; -import { IcpWeb3ConfigProvider, isPlugInstalled, useIcpWallet } from '@ant-design/web3-icp'; +import { IcpWeb3ConfigProvider, PlugWallet, useIcpWallet } from '@ant-design/web3-icp'; import { message, Modal } from 'antd'; type WalletType = 'plug'; @@ -21,7 +21,10 @@ const walletMetaMap: Record = { remark: '浏览器扩展,适合日常使用', group: 'Popular', color: '#4f64ff', - checkInstalled: async () => isPlugInstalled(), + checkInstalled: async () => { + const wallet = PlugWallet().create(); + return wallet.installed; + }, }, }; @@ -179,8 +182,15 @@ export default () => { setAutoConnect(true); }; + const wallets = useMemo(() => { + if (walletType === 'plug') { + return [PlugWallet()]; + } + return [PlugWallet()]; + }, [walletType]); + return ( - + setModalOpen(true)} From 63140d3b3bb2c884d7bb60930eba1fa526fde6bd Mon Sep 17 00:00:00 2001 From: wangshanshan01 Date: Tue, 9 Dec 2025 09:24:51 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E6=8F=90=E4=BE=9Bicp-provider?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=AE=9E=E7=8E=B0=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/assets/src/icp/chains.tsx | 17 ++ packages/assets/src/icp/index.ts | 1 + packages/assets/src/index.ts | 1 + packages/common/src/types.ts | 4 + .../icp/src/icp-provider/config-provider.tsx | 153 ++++++++++++ packages/icp/src/icp-provider/context.tsx | 75 ------ packages/icp/src/icp-provider/index.tsx | 81 +++++++ packages/icp/src/index.ts | 3 +- packages/web3/src/icp/demos/basic.tsx | 219 +----------------- 9 files changed, 268 insertions(+), 286 deletions(-) create mode 100644 packages/assets/src/icp/chains.tsx create mode 100644 packages/assets/src/icp/index.ts create mode 100644 packages/icp/src/icp-provider/config-provider.tsx delete mode 100644 packages/icp/src/icp-provider/context.tsx create mode 100644 packages/icp/src/icp-provider/index.tsx 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/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/src/icp-provider/config-provider.tsx b/packages/icp/src/icp-provider/config-provider.tsx new file mode 100644 index 000000000..2b27d44d4 --- /dev/null +++ b/packages/icp/src/icp-provider/config-provider.tsx @@ -0,0 +1,153 @@ +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); + onCurrentChainChange?.(foundChain ?? chainList[0]); + }} + 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/context.tsx b/packages/icp/src/icp-provider/context.tsx deleted file mode 100644 index 8f0db4873..000000000 --- a/packages/icp/src/icp-provider/context.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { createContext, useContext, useMemo, useState, type PropsWithChildren } from 'react'; - -import { PlugWallet } from '../wallets/built-in'; -import type { IcpWalletFactory } from '../wallets/factory'; -import type { IcpWallet } from '../wallets/types'; - -const IcpContext = createContext(null); - -export interface IcpContextValue { - wallet: IcpWallet; - principal: string | null; - connecting: boolean; - connect: () => Promise; - disconnect: () => Promise; - installed: boolean; -} - -export interface IcpWeb3ConfigProviderProps extends PropsWithChildren { - /** - * 指定使用的 ICP 钱包数组,默认使用 PlugWallet - */ - wallets?: IcpWalletFactory[]; -} - -export const useIcpWallet = (): IcpContextValue => { - const ctx = useContext(IcpContext); - - if (!ctx) { - throw new Error('useIcpWallet must be used within IcpWeb3ConfigProvider'); - } - - return ctx; -}; - -export const IcpWeb3ConfigProvider: React.FC = ({ - children, - wallets = [PlugWallet()], -}) => { - const [principal, setPrincipal] = useState(null); - const [connecting, setConnecting] = useState(false); - - const wallet = useMemo(() => { - // 优先选择已安装的钱包,如果没有已安装的,则选择第一个 - const walletInstances = wallets.map((factory) => factory.create()); - const installedWallet = walletInstances.find((w) => w.installed); - return installedWallet ?? walletInstances[0]; - }, [wallets]); - - const connect = async () => { - setConnecting(true); - try { - await wallet.connect(); - const p = await wallet.getPrincipal(); - setPrincipal(p); - } finally { - setConnecting(false); - } - }; - - const disconnect = async () => { - await wallet.disconnect(); - setPrincipal(null); - }; - - const value: IcpContextValue = { - wallet, - principal, - connecting, - connect, - disconnect, - installed: wallet.installed, - }; - - return {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..f4e03b138 --- /dev/null +++ b/packages/icp/src/icp-provider/index.tsx @@ -0,0 +1,81 @@ +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 () => { + 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 index d374c1a73..b2ff4fcab 100644 --- a/packages/icp/src/index.ts +++ b/packages/icp/src/index.ts @@ -1,5 +1,4 @@ -export * from './icp-provider/context'; -export * from './wallets/plug'; +export * from './icp-provider'; export * from './wallets/types'; export * from './wallets/factory'; export * from './wallets/built-in'; diff --git a/packages/web3/src/icp/demos/basic.tsx b/packages/web3/src/icp/demos/basic.tsx index db58dd43c..597f74c66 100644 --- a/packages/web3/src/icp/demos/basic.tsx +++ b/packages/web3/src/icp/demos/basic.tsx @@ -1,214 +1,15 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { ConnectButton, ConnectModal } from '@ant-design/web3'; -import type { Wallet } from '@ant-design/web3'; -import { ConnectStatus } from '@ant-design/web3-common'; -import { IcpWeb3ConfigProvider, PlugWallet, useIcpWallet } from '@ant-design/web3-icp'; -import { message, Modal } from 'antd'; - -type WalletType = 'plug'; - -type WalletMeta = { - name: string; - remark: string; - group: string; - color: string; - checkInstalled?: () => Promise; -}; - -const walletMetaMap: Record = { - plug: { - name: 'Plug', - remark: '浏览器扩展,适合日常使用', - group: 'Popular', - color: '#4f64ff', - checkInstalled: async () => { - const wallet = PlugWallet().create(); - return wallet.installed; - }, - }, -}; - -const WalletIcon: React.FC<{ color: string; label: string }> = ({ color, label }) => ( - - {label} - -); - -const walletList: Wallet[] = (Object.keys(walletMetaMap) as WalletType[]).map((type) => { - const meta = walletMetaMap[type]; - return { - key: type, - name: meta.name, - remark: meta.remark, - icon: , - group: meta.group, - hasExtensionInstalled: meta.checkInstalled - ? async () => !!(await meta.checkInstalled?.()) - : undefined, - hasWalletReady: meta.checkInstalled ? async () => !!(await meta.checkInstalled?.()) : undefined, - }; -}); - -interface IcpConnectButtonProps { - walletType: WalletType; - onSelectWallet: () => void; - autoConnect: boolean; - onAutoConnectConsumed: () => void; -} - -const IcpConnectButton: React.FC = ({ - walletType, - onSelectWallet, - autoConnect, - onAutoConnectConsumed, -}) => { - const { principal, connecting, connect, disconnect, installed } = useIcpWallet(); - const [messageApi, contextHolder] = message.useMessage(); - const walletName = walletMetaMap[walletType].name; - - const handleConnect = async () => { - try { - if (!installed) { - Modal.info({ - title: `未检测到 ${walletName} 钱包`, - content: ( - <> -

当前浏览器环境未检测到 {walletName} 钱包扩展。

-

请先安装该钱包并完成初始化,再刷新页面重试。

- - ), - okText: '我知道了', - }); - return; - } - await connect(); - } catch (error: any) { - messageApi.error(error?.message ?? `连接 ${walletName} 钱包失败,请检查插件是否安装。`); - } - }; - - const handleDisconnect = async () => { - try { - await disconnect(); - } catch (error: any) { - messageApi.error(error?.message ?? `断开 ${walletName} 钱包失败,请重试。`); - } - }; - - useEffect(() => { - if (autoConnect) { - void (async () => { - await handleConnect(); - onAutoConnectConsumed(); - })(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [autoConnect, walletType]); +import React from 'react'; +import { ConnectButton, Connector } from '@ant-design/web3'; +import { IcpWeb3ConfigProvider, PlugWallet } from '@ant-design/web3-icp'; +const App: React.FC = () => { return ( - <> - {contextHolder} - { - if (!principal) { - onSelectWallet(); - } - }} - onDisconnectClick={() => { - void handleDisconnect(); - }} - account={ - principal - ? { - address: principal, - name: walletName, - status: ConnectStatus.Connected, - } - : undefined - } - /> - - ); -}; - -export default () => { - const [walletType, setWalletType] = useState('plug'); - const [modalOpen, setModalOpen] = useState(false); - const [autoConnect, setAutoConnect] = useState(false); - - const walletGroupOrder = useMemo( - () => (a: string, b: string) => { - if (a === 'Popular') return -1; - if (b === 'Popular') return 1; - return a.localeCompare(b); - }, - [], - ); - - const handleWalletSelected = async (wallet: Wallet) => { - const nextType = (wallet.key as WalletType) ?? 'plug'; - const meta = walletMetaMap[nextType]; - const installed = (await meta.checkInstalled?.()) ?? true; - - if (!installed) { - Modal.info({ - title: `${meta.name} 未检测到浏览器插件`, - content: ( - <> -

请先在浏览器中安装 {meta.name} 钱包扩展或确保插件已启用。

-

安装完成后刷新页面,再次选择该钱包即可连接。

- - ), - okText: '我知道了', - }); - return; - } - - setWalletType(nextType); - setModalOpen(false); - setAutoConnect(true); - }; - - const wallets = useMemo(() => { - if (walletType === 'plug') { - return [PlugWallet()]; - } - return [PlugWallet()]; - }, [walletType]); - - return ( - - setModalOpen(true)} - autoConnect={autoConnect} - onAutoConnectConsumed={() => setAutoConnect(false)} - /> - - setModalOpen(false)} - walletList={walletList} - group={{ - groupOrder: walletGroupOrder, - }} - onWalletSelected={(wallet) => { - void handleWalletSelected(wallet); - }} - /> + + + + ); }; + +export default App; From acf7a64cbb3b67ab2ce2a19fc5058ff2adb36d7a Mon Sep 17 00:00:00 2001 From: wangshanshan01 Date: Tue, 9 Dec 2025 10:25:07 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=98=B2?= =?UTF-8?q?=E5=BE=A1=E6=80=A7=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/icp/src/icp-provider/config-provider.tsx | 5 ++++- packages/icp/src/icp-provider/index.tsx | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/icp/src/icp-provider/config-provider.tsx b/packages/icp/src/icp-provider/config-provider.tsx index 2b27d44d4..1117877a6 100644 --- a/packages/icp/src/icp-provider/config-provider.tsx +++ b/packages/icp/src/icp-provider/config-provider.tsx @@ -120,7 +120,10 @@ export const AntDesignWeb3ConfigProvider: React.FC< availableWallets={availableWallets} switchChain={async (_chain) => { const foundChain = chainList.find((c) => c.id === _chain.id); - onCurrentChainChange?.(foundChain ?? chainList[0]); + const targetChain = foundChain ?? chainList[0]; + if (targetChain) { + onCurrentChainChange?.(targetChain); + } }} connect={async (_wallet, options) => { let resolve: any; diff --git a/packages/icp/src/icp-provider/index.tsx b/packages/icp/src/icp-provider/index.tsx index f4e03b138..20c15cc0a 100644 --- a/packages/icp/src/icp-provider/index.tsx +++ b/packages/icp/src/icp-provider/index.tsx @@ -45,6 +45,10 @@ export const IcpWeb3ConfigProvider: FC { + if (!wallet) { + return; + } + setConnecting(true); try { await wallet.connect();