Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-flies-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ant-design/web3-icp': major
---

功能:集成 ICP 链的钱包连接能力
17 changes: 17 additions & 0 deletions packages/assets/src/icp/chains.tsx
Original file line number Diff line number Diff line change
@@ -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: <IcpColorful />,
browser: {
icon: <IcpColorful />,
getBrowserLink: createGetBrowserLink('https://dashboard.internetcomputer.org'),
},
Comment on lines +12 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ICP Dashboard 浏览器链接格式可能不正确。

createGetBrowserLink 工具函数生成的 URL 格式为 /address/{address}/tx/{hash},但 ICP Dashboard 使用的是 /account/{principal}/transaction/{txId} 格式。这会导致生成的浏览器链接无法正确跳转。

建议自定义 getBrowserLink 函数以匹配 ICP Dashboard 的 URL 结构:

   browser: {
     icon: <IcpColorful />,
-    getBrowserLink: createGetBrowserLink('https://dashboard.internetcomputer.org'),
+    getBrowserLink: (address: string, type: string) => {
+      if (type === 'address') {
+        return `https://dashboard.internetcomputer.org/account/${address}`;
+      }
+      if (type === 'transaction') {
+        return `https://dashboard.internetcomputer.org/transaction/${address}`;
+      }
+      throw new Error(`getBrowserLink unsupported type ${type}`);
+    },
   },

Committable suggestion skipped: line range outside the PR's diff.

nativeCurrency: { name: 'ICP', symbol: 'ICP', decimals: 8 },
};
1 change: 1 addition & 0 deletions packages/assets/src/icp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './chains';
1 change: 1 addition & 0 deletions packages/assets/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './wallets';
export * from './chains/ethereum';
export * from './tokens';
export * from './icp';
1 change: 1 addition & 0 deletions packages/assets/src/wallets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from './mobile-wallet';
export * from './slush';
export * from './suiet';
export * from './solflare';
export * from './plug';
20 changes: 20 additions & 0 deletions packages/assets/src/wallets/plug.tsx
Original file line number Diff line number Diff line change
@@ -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: <ChromeCircleColorful />,
browserName: 'Chrome',
link: 'https://chromewebstore.google.com/detail/plug/cfbfdhimifdmdehjmkdobpcjfefblkjm',
description: 'Access your wallet right from your favorite web browser.',
},
],
};
4 changes: 4 additions & 0 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export enum SuiChainIds {
Localnet = 4,
}

export enum IcpChainIds {
Mainnet = 1,
}

export type BrowserLinkType = 'address' | 'transaction';

export type BalanceMetadata = {
Expand Down
63 changes: 63 additions & 0 deletions packages/icp/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
156 changes: 156 additions & 0 deletions packages/icp/src/icp-provider/config-provider.tsx
Original file line number Diff line number Diff line change
@@ -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<Account>;
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<void>;
onDisconnect: () => Promise<void>;
}

export const AntDesignWeb3ConfigProvider: React.FC<
React.PropsWithChildren<AntDesignWeb3ConfigProviderProps>
> = (props) => {
const {
wallet,
principal,
connecting,
onConnect,
onDisconnect,
availableChains,
chainAssets,
currentChain,
availableWallets,
balance,
locale,
onCurrentChainChange,
} = props;

const connectAsyncRef = useRef<ConnectAsync>();
const [account, setAccount] = useState<Account>();
const [balanceData, setBalanceData] = useState<bigint>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

balanceData 状态未被更新,余额功能不完整

balanceData 状态被定义但 setBalanceData 从未被调用,导致传递给 Web3ConfigProviderbalance.value 始终为 undefined。如果 balance 功能是预期的,需要添加获取和更新余额的逻辑。

是否需要我帮助实现余额获取逻辑?或者如果这是后续迭代的功能,建议添加 TODO 注释说明。

🤖 Prompt for AI Agents
In packages/icp/src/icp-provider/config-provider.tsx around line 49, the
balanceData state is defined but never updated; add logic to fetch the account
balance and call setBalanceData so Web3ConfigProvider receives a real value.
Specifically, add a useEffect that runs when the active wallet/account or
provider changes, call the appropriate ICP actor/provider method to get the
balance, convert the returned value to bigint if needed, and invoke
setBalanceData with that result; handle errors and loading cases and clean up
any subscriptions. If balance is intentionally out of scope for now, add a TODO
comment at this location stating that balance fetching will be implemented in a
follow-up and leave balanceData undefined until then.


// 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]);

Comment on lines +47 to +75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find packages/icp/src -name "config-provider.tsx" -o -name "index.tsx" | head -20

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

fd -e tsx "config-provider" packages/icp/src/

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

fd -e tsx "index" packages/icp/src/icp-provider/

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

ls -la packages/icp/src/icp-provider/

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

wc -l packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

cat -n packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '120,160p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '40,80p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

cat -n packages/icp/src/icp-provider/index.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

rg -n "wallet\." packages/icp/src/icp-provider/index.tsx -A 3

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

rg -n "interface.*Wallet|type.*Wallet" packages/icp/src/ -A 5

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

rg -n "getPrincipal" packages/icp/src/ -B 2 -A 5

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '1,50p' packages/icp/src/icp-provider/index.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '50,100p' packages/icp/src/icp-provider/index.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

rg -n "type IcpWeb3Config|interface IcpWeb3Config" packages/icp/src/ -A 10

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

find packages/icp/src -name "*.ts" -o -name "*.tsx" | xargs rg -l "interface.*Wallet"

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '100,145p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '90,130p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

grep -n "onConnect" packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '80,110p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '1,50p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

grep -n "connectAsyncRef" packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '40,50p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

rg -n "connectAsyncRef" packages/icp/src/icp-provider/config-provider.tsx -B 2 -A 2

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

rg -n "type ConnectAsync|interface ConnectAsync" packages/icp/src/

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

rg -n "ConnectAsync" packages/icp/src/ -B 1 -A 3

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '125,150p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '1,30p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

fd -e ts -e tsx packages/icp/src/ | xargs grep -l "ConnectAsync"

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

rg "ConnectAsync" packages/icp/src/icp-provider/ -B 5 -A 5

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '127,145p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '1,80p' packages/icp/src/icp-provider/index.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

wc -l packages/icp/src/icp-provider/index.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

grep -n "principal" packages/icp/src/icp-provider/config-provider.tsx | head -20

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '50,75p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '45,90p' packages/icp/src/icp-provider/index.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

grep -n "wallet.connect" packages/icp/src/icp-provider/index.tsx -B 5 -A 5

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '50,95p' packages/icp/src/icp-provider/index.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

rg -n "timeout|setTimeout|catch|error" packages/icp/src/icp-provider/index.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '1,120p' packages/icp/src/icp-provider/index.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '125,150p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '55,75p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


🏁 Script executed:

sed -n '125,155p' packages/icp/src/icp-provider/config-provider.tsx

Repository: ant-design/ant-design-web3

Length of output: 196


Fix Promise hanging when wallet returns null principal without throwing

The connect() function creates a Promise that stores its resolve/reject in connectAsyncRef and waits for resolution from the useEffect([principal, connecting]). However, this effect only calls resolve when both principal && connectAsyncRef.current are truthy.

If wallet.getPrincipal() in the parent component's onConnect() returns null without throwing an error, the subsequent setPrincipal(null) causes the effect condition to fail—the Promise hangs indefinitely with no error feedback.

Add validation in onConnect():

const principal = await wallet.getPrincipal();
if (!principal) {
  throw new Error('Failed to get principal from wallet');
}
setPrincipal(principal);

This ensures the .catch() block in the connect() function is triggered, allowing reject to be called on the stored Promise so the caller receives proper error feedback instead of an indefinite hang.

🤖 Prompt for AI Agents
In packages/icp/src/icp-provider/config-provider.tsx around lines 47 to 75, the
connect() Promise can hang when the wallet returns a null principal because the
useEffect only resolves when principal is truthy; modify the onConnect() logic
where wallet.getPrincipal() is called to validate the result and throw an Error
if it is null/undefined (e.g., if (!principal) throw new Error('Failed to get
principal from wallet')), so the connect() caller's .catch() runs and the stored
Promise can be rejected instead of left hanging.

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 (
<Web3ConfigProvider
locale={locale}
account={account}
chain={currentChainData}
balance={
balance
? {
symbol: currency?.symbol,
decimals: currency?.decimals,
value: balanceData,
icon: currency?.icon,
}
: undefined
}
addressPrefix={false}
availableChains={chainList}
availableWallets={availableWallets}
switchChain={async (_chain) => {
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<Account>((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}
</Web3ConfigProvider>
);
};
85 changes: 85 additions & 0 deletions packages/icp/src/icp-provider/index.tsx
Original file line number Diff line number Diff line change
@@ -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<PropsWithChildren<IcpWeb3ConfigProviderProps>> = ({
locale,
chains = [Icp],
wallets: walletFactories = [PlugWallet()],
balance,
children,
}) => {
const [currentChain, setCurrentChain] = useState<IcpChain | undefined>(chains[0]);
const [principal, setPrincipal] = useState<string | null>(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]);
Comment on lines +40 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

walletFactories 允许为空时,disconnect 会在运行时抛异常

wallet 的计算逻辑:

const walletInstances = walletFactories.map((factory) => factory.create());
const installedWallet = walletInstances.find((w) => w.installed);
return installedWallet ?? walletInstances[0];

walletFactories 为空数组时,walletundefined。目前:

  • connect 已经在 Line 47–50 做了 if (!wallet) { return; } 防御;
  • disconnect 中直接执行 await wallet.disconnect();,会在 walletundefined 时抛出运行时错误。

建议加上与 connect 一致的防御逻辑,例如:

 const disconnect = async () => {
-  await wallet.disconnect();
-  setPrincipal(null);
+  if (!wallet) {
+    setPrincipal(null);
+    return;
+  }
+  await wallet.disconnect();
+  setPrincipal(null);
 };

这样即使调用方传入 wallets={[]} 也不会导致崩溃。

Also applies to: 62-65

🤖 Prompt for AI Agents
packages/icp/src/icp-provider/index.tsx around lines 40-45 (and also apply same
fix at lines 62-65): the computed wallet can be undefined when walletFactories
is an empty array, so add the same defensive null-check used in connect to
disconnect; before calling await wallet.disconnect(); add if (!wallet) { return;
} (or guard early) so disconnect no longer throws when wallet is undefined—apply
the identical guard wherever wallet is used without a prior null check.


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 (
<AntDesignWeb3ConfigProvider
locale={locale}
chainAssets={[Icp]}
availableWallets={availableWallets}
balance={balance}
currentChain={currentChain}
onCurrentChainChange={(chain) => setCurrentChain(chain)}
availableChains={chains}
wallet={wallet}
principal={principal}
connecting={connecting}
onConnect={connect}
onDisconnect={disconnect}
>
{children}
</AntDesignWeb3ConfigProvider>
);
};
4 changes: 4 additions & 0 deletions packages/icp/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './icp-provider';
export * from './wallets/types';
export * from './wallets/factory';
export * from './wallets/built-in';
10 changes: 10 additions & 0 deletions packages/icp/src/wallets/built-in.ts
Original file line number Diff line number Diff line change
@@ -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<WalletMetadata>): IcpWalletFactory =>
WalletFactory(createPlugWallet, { ...metadata_Plug, ...metadata });
Loading