Web3-React
Web3-React 是一个简单、可最大程度扩展、依赖性最小化的框架,用于构建现代以太坊 dApp 的链接组件
快速开始
1.配置 Connector:在 Web3-React packages 内添加 BitgetWallet
js
import detectEthereumProvider from "@akkafinance/bitkeep-detect-provider";
import type {
Actions,
AddEthereumChainParameter,
Provider,
ProviderConnectInfo,
ProviderRpcError,
WatchAssetParameters,
} from "@web3-react/types";
import { Connector } from "@web3-react/types";
type BitGetWalletProvider = Provider & {
isBitGetWallet?: boolean;
isConnected?: () => boolean;
providers?: BitGetWalletProvider[];
};
export class NoBitGetWalletError extends Error {
public constructor() {
super("BitGetWallet not installed");
this.name = NoBitGetWalletError.name;
Object.setPrototypeOf(this, NoBitGetWalletError.prototype);
}
}
function parseChainId(chainId: string) {
return Number.parseInt(chainId, 16);
}
/**
* @param options - Options to pass to `./detect-provider`
* @param onError - Handler to report errors thrown from eventListeners.
*/
export interface BitGetWalletpConstructorArgs {
actions: Actions;
options?: Parameters<typeof detectEthereumProvider>[0];
onError?: (error: Error) => void;
}
export class BitGetWallet extends Connector {
/** {@inheritdoc Connector.provider} */
public provider?: BitGetWalletProvider;
private readonly options?: Parameters<typeof detectEthereumProvider>[0];
private eagerConnection?: Promise<void>;
constructor({ actions, options, onError }: BitGetWalletConstructorArgs) {
super(actions, onError);
this.options = options;
}
private async isomorphicInitialize(): Promise<void> {
if (this.eagerConnection) return;
return (this.eagerConnection = import(
"@akkafinance/bitkeep-detect-provider"
).then(async (m) => {
const provider = await m.default(this.options);
if (provider) {
this.provider = provider as unknown as BitGetWalletProvider;
if (this.provider.providers?.length) {
this.provider =
this.provider.providers.find((p) => p.isBitGetWallet) ??
this.provider.providers[0];
}
this.provider.on(
"connect",
({ chainId }: ProviderConnectInfo): void => {
this.actions.update({ chainId: parseChainId(chainId) });
}
);
this.provider.on("disconnect", (error: ProviderRpcError): void => {
this.actions.resetState();
this.onError?.(error);
});
this.provider.on("chainChanged", (chainId: string): void => {
this.actions.update({ chainId: parseChainId(chainId) });
});
this.provider.on("accountsChanged", (accounts: string[]): void => {
if (accounts.length === 0) {
// handle this edge case by disconnecting
this.actions.resetState();
} else {
this.actions.update({ accounts });
}
});
}
}));
}
/** {@inheritdoc Connector.connectEagerly} */
public async connectEagerly(): Promise<void> {
const cancelActivation = this.actions.startActivation();
await this.isomorphicInitialize();
if (!this.provider) return cancelActivation();
return Promise.all([
this.provider.request({ method: "eth_chainId" }) as Promise<string>,
this.provider.request({ method: "eth_accounts" }) as Promise<string[]>,
])
.then(([chainId, accounts]) => {
if (accounts.length) {
this.actions.update({ chainId: parseChainId(chainId), accounts });
} else {
throw new Error("No accounts returned");
}
})
.catch((error) => {
console.debug("Could not connect eagerly", error);
// we should be able to use `cancelActivation` here, but on mobile, metamask emits a 'connect'
// event, meaning that chainId is updated, and cancelActivation doesn't work because an intermediary
// update has occurred, so we reset state instead
this.actions.resetState();
});
}
/**
* Initiates a connection.
*
* @param desiredChainIdOrChainParameters - If defined, indicates the desired chain to connect to. If the user is
* already connected to this chain, no additional steps will be taken. Otherwise, the user will be prompted to switch
* to the chain, if one of two conditions is met: either they already have it added in their extension, or the
* argument is of type AddEthereumChainParameter, in which case the user will be prompted to add the chain with the
* specified parameters first, before being prompted to switch.
*/
public async activate(
desiredChainIdOrChainParameters?: number | AddEthereumChainParameter
): Promise<void> {
let cancelActivation: () => void;
if (!this.provider?.isConnected?.())
cancelActivation = this.actions.startActivation();
return this.isomorphicInitialize()
.then(async () => {
if (!this.provider) throw new NoBitGetWalletError();
return Promise.all([
this.provider.request({ method: "eth_chainId" }) as Promise<string>,
this.provider.request({ method: "eth_requestAccounts" }) as Promise<
string[]
>,
]).then(([chainId, accounts]) => {
const receivedChainId = parseChainId(chainId);
const desiredChainId =
typeof desiredChainIdOrChainParameters === "number"
? desiredChainIdOrChainParameters
: desiredChainIdOrChainParameters?.chainId;
// if there's no desired chain, or it's equal to the received, update
if (!desiredChainId || receivedChainId === desiredChainId)
return this.actions.update({ chainId: receivedChainId, accounts });
const desiredChainIdHex = `0x${desiredChainId.toString(16)}`;
// if we're here, we can try to switch networks
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.provider!.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: desiredChainIdHex }],
})
.catch((error: ProviderRpcError) => {
if (
error.code === 4902 &&
typeof desiredChainIdOrChainParameters !== "number"
) {
// if we're here, we can try to add a new network
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.provider!.request({
method: "wallet_addEthereumChain",
params: [
{
...desiredChainIdOrChainParameters,
chainId: desiredChainIdHex,
},
],
});
}
throw error;
})
.then(() => this.activate(desiredChainId));
});
})
.catch((error) => {
cancelActivation?.();
throw error;
});
}
public async watchAsset({
address,
symbol,
decimals,
image,
}: WatchAssetParameters): Promise<true> {
if (!this.provider) throw new Error("No provider");
return this.provider
.request({
method: "wallet_watchAsset",
params: {
type: "ERC20", // Initially only supports ERC20, but eventually more!
options: {
address, // The address that the token is at.
symbol, // A ticker symbol or shorthand, up to 5 chars.
decimals, // The number of decimals in the token
image, // A string url of the token logo
},
},
})
.then((success) => {
if (!success) throw new Error("Rejected");
return true;
});
}
}
import detectEthereumProvider from "@akkafinance/bitkeep-detect-provider";
import type {
Actions,
AddEthereumChainParameter,
Provider,
ProviderConnectInfo,
ProviderRpcError,
WatchAssetParameters,
} from "@web3-react/types";
import { Connector } from "@web3-react/types";
type BitGetWalletProvider = Provider & {
isBitGetWallet?: boolean;
isConnected?: () => boolean;
providers?: BitGetWalletProvider[];
};
export class NoBitGetWalletError extends Error {
public constructor() {
super("BitGetWallet not installed");
this.name = NoBitGetWalletError.name;
Object.setPrototypeOf(this, NoBitGetWalletError.prototype);
}
}
function parseChainId(chainId: string) {
return Number.parseInt(chainId, 16);
}
/**
* @param options - Options to pass to `./detect-provider`
* @param onError - Handler to report errors thrown from eventListeners.
*/
export interface BitGetWalletpConstructorArgs {
actions: Actions;
options?: Parameters<typeof detectEthereumProvider>[0];
onError?: (error: Error) => void;
}
export class BitGetWallet extends Connector {
/** {@inheritdoc Connector.provider} */
public provider?: BitGetWalletProvider;
private readonly options?: Parameters<typeof detectEthereumProvider>[0];
private eagerConnection?: Promise<void>;
constructor({ actions, options, onError }: BitGetWalletConstructorArgs) {
super(actions, onError);
this.options = options;
}
private async isomorphicInitialize(): Promise<void> {
if (this.eagerConnection) return;
return (this.eagerConnection = import(
"@akkafinance/bitkeep-detect-provider"
).then(async (m) => {
const provider = await m.default(this.options);
if (provider) {
this.provider = provider as unknown as BitGetWalletProvider;
if (this.provider.providers?.length) {
this.provider =
this.provider.providers.find((p) => p.isBitGetWallet) ??
this.provider.providers[0];
}
this.provider.on(
"connect",
({ chainId }: ProviderConnectInfo): void => {
this.actions.update({ chainId: parseChainId(chainId) });
}
);
this.provider.on("disconnect", (error: ProviderRpcError): void => {
this.actions.resetState();
this.onError?.(error);
});
this.provider.on("chainChanged", (chainId: string): void => {
this.actions.update({ chainId: parseChainId(chainId) });
});
this.provider.on("accountsChanged", (accounts: string[]): void => {
if (accounts.length === 0) {
// handle this edge case by disconnecting
this.actions.resetState();
} else {
this.actions.update({ accounts });
}
});
}
}));
}
/** {@inheritdoc Connector.connectEagerly} */
public async connectEagerly(): Promise<void> {
const cancelActivation = this.actions.startActivation();
await this.isomorphicInitialize();
if (!this.provider) return cancelActivation();
return Promise.all([
this.provider.request({ method: "eth_chainId" }) as Promise<string>,
this.provider.request({ method: "eth_accounts" }) as Promise<string[]>,
])
.then(([chainId, accounts]) => {
if (accounts.length) {
this.actions.update({ chainId: parseChainId(chainId), accounts });
} else {
throw new Error("No accounts returned");
}
})
.catch((error) => {
console.debug("Could not connect eagerly", error);
// we should be able to use `cancelActivation` here, but on mobile, metamask emits a 'connect'
// event, meaning that chainId is updated, and cancelActivation doesn't work because an intermediary
// update has occurred, so we reset state instead
this.actions.resetState();
});
}
/**
* Initiates a connection.
*
* @param desiredChainIdOrChainParameters - If defined, indicates the desired chain to connect to. If the user is
* already connected to this chain, no additional steps will be taken. Otherwise, the user will be prompted to switch
* to the chain, if one of two conditions is met: either they already have it added in their extension, or the
* argument is of type AddEthereumChainParameter, in which case the user will be prompted to add the chain with the
* specified parameters first, before being prompted to switch.
*/
public async activate(
desiredChainIdOrChainParameters?: number | AddEthereumChainParameter
): Promise<void> {
let cancelActivation: () => void;
if (!this.provider?.isConnected?.())
cancelActivation = this.actions.startActivation();
return this.isomorphicInitialize()
.then(async () => {
if (!this.provider) throw new NoBitGetWalletError();
return Promise.all([
this.provider.request({ method: "eth_chainId" }) as Promise<string>,
this.provider.request({ method: "eth_requestAccounts" }) as Promise<
string[]
>,
]).then(([chainId, accounts]) => {
const receivedChainId = parseChainId(chainId);
const desiredChainId =
typeof desiredChainIdOrChainParameters === "number"
? desiredChainIdOrChainParameters
: desiredChainIdOrChainParameters?.chainId;
// if there's no desired chain, or it's equal to the received, update
if (!desiredChainId || receivedChainId === desiredChainId)
return this.actions.update({ chainId: receivedChainId, accounts });
const desiredChainIdHex = `0x${desiredChainId.toString(16)}`;
// if we're here, we can try to switch networks
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.provider!.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: desiredChainIdHex }],
})
.catch((error: ProviderRpcError) => {
if (
error.code === 4902 &&
typeof desiredChainIdOrChainParameters !== "number"
) {
// if we're here, we can try to add a new network
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.provider!.request({
method: "wallet_addEthereumChain",
params: [
{
...desiredChainIdOrChainParameters,
chainId: desiredChainIdHex,
},
],
});
}
throw error;
})
.then(() => this.activate(desiredChainId));
});
})
.catch((error) => {
cancelActivation?.();
throw error;
});
}
public async watchAsset({
address,
symbol,
decimals,
image,
}: WatchAssetParameters): Promise<true> {
if (!this.provider) throw new Error("No provider");
return this.provider
.request({
method: "wallet_watchAsset",
params: {
type: "ERC20", // Initially only supports ERC20, but eventually more!
options: {
address, // The address that the token is at.
symbol, // A ticker symbol or shorthand, up to 5 chars.
decimals, // The number of decimals in the token
image, // A string url of the token logo
},
},
})
.then((success) => {
if (!success) throw new Error("Rejected");
return true;
});
}
}
2.在 connectors 目录内添加 bitgetWallet.ts
ts
import { initializeConnector } from "@web3-react/core";
import { BitGetWallet } from "@akkafinance/web3-react-bitkeep";
export const [bitgetWallet, hooks] = initializeConnector<BitGetWallet>(
(actions) => new BitGetWallet({ actions })
);
import { initializeConnector } from "@web3-react/core";
import { BitGetWallet } from "@akkafinance/web3-react-bitkeep";
export const [bitgetWallet, hooks] = initializeConnector<BitGetWallet>(
(actions) => new BitGetWallet({ actions })
);
3.在 connectorCards 目录内添加 bitgetWalletCard.tsx
tsx
import { useEffect, useState } from "react";
import { hooks, bitgetWallet } from "../../connectors/bitgetWallet";
import { Card } from "../Card";
const {
useChainId,
useAccounts,
useIsActivating,
useIsActive,
useProvider,
useENSNames,
} = hooks;
export default function BitgetWalletCard() {
const chainId = useChainId();
const accounts = useAccounts();
const isActivating = useIsActivating();
const isActive = useIsActive();
const provider = useProvider();
const ENSNames = useENSNames(provider);
const [error, setError] = useState(undefined);
// attempt to connect eagerly on mount
useEffect(() => {
void bitgetWallet.connectEagerly().catch(() => {
console.debug("Failed to connect eagerly to BitgetWallet");
});
}, []);
return (
<Card
connector={bitgetWallet}
activeChainId={chainId}
isActivating={isActivating}
isActive={isActive}
error={error}
setError={setError}
accounts={accounts}
provider={provider}
ENSNames={ENSNames}
/>
);
}
import { useEffect, useState } from "react";
import { hooks, bitgetWallet } from "../../connectors/bitgetWallet";
import { Card } from "../Card";
const {
useChainId,
useAccounts,
useIsActivating,
useIsActive,
useProvider,
useENSNames,
} = hooks;
export default function BitgetWalletCard() {
const chainId = useChainId();
const accounts = useAccounts();
const isActivating = useIsActivating();
const isActive = useIsActive();
const provider = useProvider();
const ENSNames = useENSNames(provider);
const [error, setError] = useState(undefined);
// attempt to connect eagerly on mount
useEffect(() => {
void bitgetWallet.connectEagerly().catch(() => {
console.debug("Failed to connect eagerly to BitgetWallet");
});
}, []);
return (
<Card
connector={bitgetWallet}
activeChainId={chainId}
isActivating={isActivating}
isActive={isActive}
error={error}
setError={setError}
accounts={accounts}
provider={provider}
ENSNames={ENSNames}
/>
);
}
4.ProviderExample 内添加 bitgetWalletHooks
tsx
import {
hooks as bitgetWalletHooks,
bitgetWallet,
} from "../connectors/bitgetWallet";
const connectors: [
MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network,
Web3ReactHooks
][] = [
[bitgetWallet, bitgetWalletHooks],
[metaMask, metaMaskHooks],
[walletConnect, walletConnectHooks],
[walletConnectV2, walletConnectV2Hooks],
[coinbaseWallet, coinbaseWalletHooks],
[network, networkHooks],
];
export default function ProviderExample() {
return (
<Web3ReactProvider connectors={connectors}>
<Child />
</Web3ReactProvider>
)
}
import {
hooks as bitgetWalletHooks,
bitgetWallet,
} from "../connectors/bitgetWallet";
const connectors: [
MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network,
Web3ReactHooks
][] = [
[bitgetWallet, bitgetWalletHooks],
[metaMask, metaMaskHooks],
[walletConnect, walletConnectHooks],
[walletConnectV2, walletConnectV2Hooks],
[coinbaseWallet, coinbaseWalletHooks],
[network, networkHooks],
];
export default function ProviderExample() {
return (
<Web3ReactProvider connectors={connectors}>
<Child />
</Web3ReactProvider>
)
}