-
Notifications
You must be signed in to change notification settings - Fork 185
feat: 新增icp链-plug钱包链接 #1556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: 新增icp链-plug钱包链接 #1556
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@ant-design/web3-icp': major | ||
| --- | ||
|
|
||
| 功能:集成 ICP 链的钱包连接能力 |
| 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'), | ||
| }, | ||
| nativeCurrency: { name: 'ICP', symbol: 'ICP', decimals: 8 }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './chains'; |
| 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'; |
| 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.', | ||
| }, | ||
| ], | ||
| }; |
| 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" | ||
| ] | ||
| } |
| 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>(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
是否需要我帮助实现余额获取逻辑?或者如果这是后续迭代的功能,建议添加 TODO 注释说明。 🤖 Prompt for AI Agents |
||
|
|
||
| // 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find packages/icp/src -name "config-provider.tsx" -o -name "index.tsx" | head -20Repository: 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.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: cat -n packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '120,160p' packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '40,80p' packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: cat -n packages/icp/src/icp-provider/index.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: rg -n "wallet\." packages/icp/src/icp-provider/index.tsx -A 3Repository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: rg -n "interface.*Wallet|type.*Wallet" packages/icp/src/ -A 5Repository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: rg -n "getPrincipal" packages/icp/src/ -B 2 -A 5Repository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '1,50p' packages/icp/src/icp-provider/index.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '50,100p' packages/icp/src/icp-provider/index.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: rg -n "type IcpWeb3Config|interface IcpWeb3Config" packages/icp/src/ -A 10Repository: 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.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '90,130p' packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: grep -n "onConnect" packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '80,110p' packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '1,50p' packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: grep -n "connectAsyncRef" packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '40,50p' packages/icp/src/icp-provider/config-provider.tsxRepository: 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 2Repository: 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 3Repository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '125,150p' packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '1,30p' packages/icp/src/icp-provider/config-provider.tsxRepository: 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 5Repository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '127,145p' packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '1,80p' packages/icp/src/icp-provider/index.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: wc -l packages/icp/src/icp-provider/index.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: grep -n "principal" packages/icp/src/icp-provider/config-provider.tsx | head -20Repository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '50,75p' packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '45,90p' packages/icp/src/icp-provider/index.tsxRepository: 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 5Repository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '50,95p' packages/icp/src/icp-provider/index.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: rg -n "timeout|setTimeout|catch|error" packages/icp/src/icp-provider/index.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '1,120p' packages/icp/src/icp-provider/index.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '125,150p' packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '55,75p' packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 🏁 Script executed: sed -n '125,155p' packages/icp/src/icp-provider/config-provider.tsxRepository: ant-design/ant-design-web3 Length of output: 196 Fix Promise hanging when wallet returns null principal without throwing The If Add validation in const principal = await wallet.getPrincipal();
if (!principal) {
throw new Error('Failed to get principal from wallet');
}
setPrincipal(principal);This ensures the 🤖 Prompt for AI Agents |
||
| 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> | ||
| ); | ||
| }; | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
const walletInstances = walletFactories.map((factory) => factory.create());
const installedWallet = walletInstances.find((w) => w.installed);
return installedWallet ?? walletInstances[0];当
建议加上与 const disconnect = async () => {
- await wallet.disconnect();
- setPrincipal(null);
+ if (!wallet) {
+ setPrincipal(null);
+ return;
+ }
+ await wallet.disconnect();
+ setPrincipal(null);
};这样即使调用方传入 Also applies to: 62-65 🤖 Prompt for AI Agents |
||
|
|
||
| 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> | ||
| ); | ||
| }; | ||
| 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'; |
| 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 }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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}`); + }, },