使用 React+ethers.js 开发简单加密钱包(二)

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 使用 React+ethers.js 开发简单加密钱包

整体架构设计


由于业务并不复杂,我们可以将它简单划分为几个组件。使用 Context 足够应对这个场景,而不需要额外导入状态管理库来增加复杂性。

image.png

Wallet 是根组件,内部维护了很多 state,以 Context 的方式将数据和操作注入到子组件。

Connect 负责连接钱包和断开连接。

Details 负责显示钱包信息,是纯展示型组件。

Transfer 负责向其他账户进行转账。

Loading 负责渲染加载动画,是纯展示型组件。


创建上下文


type IWalletCtx = {
  walletProvider: any;
  setWalletProvider: (walletProvider: any) => void;
  msgIsOpen: boolean;
  setMsgIsOpen: (msgIsOpen: boolean) => void;
  msg: string;
  setMsg: (msg: string) => void;
  account: string;
  setAccount: (account: string) => void;
  networkName: string;
  setNetworkName: (networkName: string) => void;
  balance: string;
  setBalance: (balance: string) => void;
  showMessage: (message: string) => void;
  refresh: boolean;
  setRefresh: (refresh: boolean) => void;
};
const WalletCtx = createContext<IWalletCtx>({} as IWalletCtx);

通过初始化一个对象,然后断言为 IWalletCtx 的方式,可以避免在使用 WalletCtx 时添加是否为 undefined 或 null 的判断。因为我们一定会注入数据。


Loading 组件


Loading 作为纯展示型组件,是最简单的组件。SVG 的代码是直接从 tailwindcss 文档中搬运过来的。仅仅是添加了一个 size 属性,用来展示不同大小的尺寸。


function Loading({ size = "md" }: { size?: "sm" | "md" | "lg" | "xl" }) {
  const sizes = {
    sm: "h-3 w-3",
    md: "h-5 w-5",
    lg: "h-7 w-7",
    xl: "h-9 w-9",
  };
  return (
    <svg
      className={`animate-spin -ml-1 mr-3 ${sizes[size]} text-black`}
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 24 24"
    >
      <circle
        className="opacity-25"
        cx="12"
        cy="12"
        r="10"
        stroke="currentColor"
        strokeWidth="4"
      ></circle>
      <path
        className="opacity-75"
        fill="currentColor"
        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
      ></path>
    </svg>
  );
}


Wallet 组件


在 Wallet 组件中创建这些 state,并注入到 context 中。


export default function Wallet() {
  const [walletProvider, setWalletProvider] = useState<any>(null);
  const [msgIsOpen, setMsgIsOpen] = useState(false);
  const [msg, setMsg] = useState("");
  const [account, setAccount] = useState<string>("");
  const [networkName, setNetworkName] = useState<string>("");
  const [balance, setBalance] = useState<string>("");
  const [refresh, setRefresh] = useState<boolean>(false);
  useEffect(() => {
    setWalletProvider(new ethers.providers.Web3Provider(window.ethereum));
  }, []);
  const showMessage = (message: string) => {
    setMsg(message);
    setMsgIsOpen(true);
    setTimeout(() => {
      setMsg("");
      setMsgIsOpen(false);
    }, 2000);
  };
  return (
    <WalletCtx.Provider
      value={{
        walletProvider,
        setWalletProvider,
        msgIsOpen,
        setMsgIsOpen,
        msg,
        setMsg,
        account,
        setAccount,
        networkName,
        setNetworkName,
        balance,
        setBalance,
        showMessage,
        refresh,
        setRefresh,
      }}
    >
      <div className="flex flex-col gap-4 p-4">
        <Dialog open={msgIsOpen} as={"div"} onClose={() => setMsgIsOpen(false)}>
          <div className="fixed flex justify-center items-center w-full top-2">
            <Dialog.Panel className="inline-flex flex-col bg-green-400 text-slate-600 p-4 shadow-xl rounded-3xl">
              <Dialog.Title>{msg}</Dialog.Title>
            </Dialog.Panel>
          </div>
        </Dialog>
        <Connect />
        <Details />
        <Transfer />
      </div>
    </WalletCtx.Provider>
  );
}

Wallet 组件基本上没有什么逻辑,它的主要作用有三个:

  • 向 context 注入数据。
  • 创建 ethers.provider。
  • 使用 Dialog 组件作为全局消息提示。


Connect 组件


在 styles/globals.css 中添加按钮样式。


@layer components {
  .btn {
    @apply bg-black text-white py-2 px-4 rounded-3xl;
  }
}

在 index.tsx 中编写逻辑。


function Connect() {
  const {
    walletProvider,
    account,
    setAccount,
    setNetworkName,
    setBalance,
    showMessage,
    refresh,
  } = useContext(WalletCtx);
  const refreshBalance = useCallback(async () => {
    if (!walletProvider || !account) return;
    const balance = await walletProvider.getBalance(account);
    setBalance(ethers.utils.formatEther(balance));
  }, [setBalance, walletProvider, account]);
  useEffect(() => {
    refreshBalance();
  }, [refresh, refreshBalance]);
  const connectToMetamask = async () => {
    try {
      await window.ethereum.enable();
      const accounts = await walletProvider.send("eth_requestAccounts", []);
      const network = await walletProvider.getNetwork();
      const balance = await walletProvider.getBalance(accounts[0]);
      setAccount(accounts[0]);
      setNetworkName(network.name);
      setBalance(ethers.utils.formatEther(balance));
    } catch (error) {
      console.log(error);
      showMessage("failed to connect to metamask");
    }
  };
  const disconnect = async () => {
    setAccount("");
  };
  if (!account) {
    return (
      <div className="flex justify-end p-4">
        {walletProvider ? (
          <button className="btn" onClick={connectToMetamask}>
            connect to metamask
          </button>
        ) : (
          <Loading />
        )}
      </div>
    );
  }
  return (
    <div className="flex justify-end items-center gap-2">
      <h1 className="text-end">Hello, {account}</h1>
      <button className="btn" onClick={disconnect}>
        disconnect
      </button>
    </div>
  );
}

我们连接钱包后会获取 3 个重要的信息:钱包账户地址、连接的网络和余额。

分别通过 walletProvider.listAccounts()、walletProvider.getNetwork() 和 walletProvider.getBalance(accounts[0]) 来获取,需要注意它们都是异步操作。


Details 组件


Details 作为纯展示型组件没有什么逻辑,主要是一些样式。


function Details() {
  const { account, networkName, balance } = useContext(WalletCtx);
  if (!account) {
    return null;
  }
  return (
    <div className="flex flex-col gap-4 w-full bg-slate-800 text-white p-4 rounded-md">
      <div className="flex justify-between">
        <div className="text-2xl font-thin">balance</div>
        <div>network: {networkName}</div>
      </div>
      <div className="flex items-end gap-2">
        <div className="text-2xl">{balance}</div>
        <div>ETH</div>
      </div>
    </div>
  );
}

现在我们看一下 Connect 和 Details 组件一起使用的效果。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/28cd508116b4400392838281c0224819~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp


Transfer 组件


Transfer 的功能比较简单,它在 UI 上仅仅包含两个输入框和一个 send 按钮。

两个输入框分别可以输入 to 和 value,表示转账的钱包地址和转账金额。


function Transfer() {
  const { walletProvider, account, showMessage, refresh, setRefresh } =
    useContext(WalletCtx);
  const [to, setTo] = useState<string>("");
  const [amount, setAmount] = useState<string>("");
  const [transferring, setTransferring] = useState<boolean>(false);
  const transfer = async () => {
    try {
      const value = ethers.utils.parseEther(amount);
      const signer = walletProvider.getSigner();
      const tx = {
        to,
        value,
      };
      setTransferring(true);
      const receipt = await signer.sendTransaction(tx);
      await receipt.wait();
      setTo("");
      setAmount("");
      showMessage("successfully transferred");
    } catch (error) {
      console.log(error);
      showMessage("failed to transfer");
    } finally {
      setTransferring(false);
      setRefresh(!refresh);
    }
  };
  if (!account) {
    return null;
  }
  return (
    <div className="flex flex-col gap-4 mt-4">
      <div className="font-bold text-4xl">Transfer</div>
      {transferring ? (
        <div className="flex flex-col items-center gap-4">
          <div className="text-3xl">transferring...</div>
          <Loading size="xl" />
        </div>
      ) : (
        <div className="flex flex-col gap-2">
          <input
            className="input"
            value={to}
            onInput={(e: any) => setTo(e.target.value)}
            type="text"
            placeholder="address"
          />
          <input
            className="input"
            value={amount}
            onInput={(e: any) => setAmount(e.target.value)}
            type="number"
            placeholder="amount"
          />
          <button className="btn" onClick={transfer}>
            send
          </button>
        </div>
      )}
    </div>
  );
}

转账是通过 signer.sendTransaction 方法进行的,它会返回收据对象 receipt。

在转账时使用到了 ethers.utils.parseEther,因为 value 默认的单位是 wei,它非常小,10 的 18 次方才是一个 ehter。在 JS 中需要使用 BigInt 类型表示,并不方便操作,而我们更喜欢用 ether 来描述货币。所以这个 API 可以帮我们转换货币单位。

需要注意,在测试时需要选择 goerli 网或者其他测试网,否则会浪费 gas 费。

你至少要有两个钱包账户,这样可以从一个钱包账户转到另一个钱包账户。

下面我们来测试一下转账。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d0d13d9d2a2849f691ed629b541020c7~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp

这时 MetaMask 钱包会弹出来签名界面。

image.png

点击确认后,需要等待一段时间上链,大约 1 分钟,或者更久。

转账成功后,当前账户的余额会刷新。

image.png


完成


现在一个简单的加密钱包就完成了。通过这个项目的学习,相信你已经学会了 ethers.js 常规 API 的使用。

源码链接:github.com/luzhenqian/…

线上地址:web3-examples.vercel.app/

我一直在深入研究 Web3 的最新趋势,相信这些趋势会使我们的生活变得更好。

如果你对 Web3 感兴趣,欢迎联系我,一起打造一个更公平的世界吧。

同时也可以关注我的 Web3 专栏,我会持续更新更多 Web3 相关内容。



相关文章
|
2月前
|
前端开发 JavaScript API
React开发需要了解的10个库
本文首发于微信公众号“前端徐徐”,介绍了React及其常用库。React是由Meta开发的JavaScript库,用于构建动态用户界面,广泛应用于Facebook、Instagram等知名网站。文章详细讲解了Axios、Formik、React Helmet、React-Redux、React Router DOM、Dotenv、ESLint、Storybook、Framer Motion和React Bootstrap等库的使用方法和应用场景,帮助开发者提升开发效率和代码质量。
126 4
React开发需要了解的10个库
|
23天前
|
资源调度 前端开发 JavaScript
vite3+vue3 实现前端部署加密混淆 javascript-obfuscator
【11月更文挑战第10天】本文介绍了在 Vite 3 + Vue 3 项目中使用 `javascript-obfuscator` 实现前端代码加密混淆的详细步骤,包括安装依赖、创建混淆脚本、修改 `package.json` 脚本命令、构建项目并执行混淆,以及在 HTML 文件中引用混淆后的文件。通过这些步骤,可以有效提高代码的安全性。
|
1月前
|
监控 前端开发 JavaScript
React 静态网站生成工具 Next.js 入门指南
【10月更文挑战第20天】Next.js 是一个基于 React 的服务器端渲染框架,由 Vercel 开发。本文从基础概念出发,逐步探讨 Next.js 的常见问题、易错点及解决方法,并通过具体代码示例进行说明,帮助开发者快速构建高性能的 Web 应用。
79 10
|
1月前
|
资源调度 前端开发 数据可视化
构建高效的数据可视化仪表板:D3.js与React的融合之道
【10月更文挑战第25天】在数据驱动的时代,将复杂的数据集转换为直观、互动式的可视化表示已成为一项至关重要的技能。本文深入探讨了如何结合D3.js的强大可视化功能和React框架的响应式特性来构建高效、动态的数据可视化仪表板。文章首先介绍了D3.js和React的基础知识,然后通过一个实际的项目案例,详细阐述了如何将两者结合使用,并提供了实用的代码示例。无论你是数据科学家、前端开发者还是可视化爱好者,这篇文章都将为你提供宝贵的洞见和实用技能。
57 5
|
26天前
|
前端开发 JavaScript 安全
vite3+vue3 实现前端部署加密混淆 javascript-obfuscator
【11月更文挑战第7天】本文介绍了在 Vite 3 + Vue 3 项目中使用 `javascript-obfuscator` 实现前端代码加密混淆的详细步骤。包括项目准备、安装 `javascript-obfuscator`、配置 Vite 构建以应用混淆,以及最终构建项目进行混淆。通过这些步骤,可以有效提升前端代码的安全性,防止被他人轻易分析和盗用。
|
2月前
|
前端开发 JavaScript 开发者
React 组件化开发最佳实践
【10月更文挑战第4天】React 组件化开发最佳实践
57 4
|
2月前
|
开发框架 前端开发 JavaScript
React、Vue.js 和 Angular主流前端框架和选择指南
在当今的前端开发领域,选择合适的框架对于项目的成功至关重要。本文将介绍几个主流的前端框架——React、Vue.js 和 Angular,探讨它们各自的特点、开发场景、优缺点,并提供选择框架的建议。
52 6
|
3月前
|
前端开发 JavaScript 开发者
React 和 Vue.js 框架的区别是什么?
React 和 Vue.js 框架的区别是什么?
|
3月前
|
XML 移动开发 前端开发
使用duxapp开发 React Native App 事半功倍
对于Taro的壳子,或者原生React Native,都会存在 `android` `ios`这两个文件夹,而在duxapp中,这些文件夹的内容是自动生成的,那么对于需要在这些文件夹中修改的配置内容,例如包名、版本号、新架构开关等,都通过配置文件的方式配置了,而不需要需修改具体的文件
|
3月前
|
前端开发 JavaScript API
React、Vue.js 和 Angular前端三大框架对比与选择
前端框架是用于构建用户界面的工具和库,它提供组件化结构、数据绑定、路由管理和状态管理等功能,帮助开发者高效地创建和维护 web 应用的前端部分。常见的前端框架如 React、Vue.js 和 Angular,能够提高开发效率并促进团队协作。
140 4