Skip to content

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>
  )
}

API 参考

Web3-React
web3-react-bitkeep
web3-react-example