使用 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 相关内容。



相关文章
|
1天前
|
Web App开发 JavaScript 前端开发
深入浅出Node.js后端开发
【10月更文挑战第36天】本文将引导您探索Node.js的世界,通过实际案例揭示其背后的原理和实践方法。从基础的安装到高级的异步处理,我们将一起构建一个简单的后端服务,并讨论如何优化性能。无论您是新手还是有经验的开发者,这篇文章都将为您提供新的视角和深入的理解。
|
6天前
|
Web App开发 存储 JavaScript
深入浅出Node.js后端开发
【10月更文挑战第31天】本文将引导你进入Node.js的奇妙世界,探索其如何革新后端开发。通过浅显易懂的语言和实际代码示例,我们将一起学习Node.js的核心概念、搭建开发环境,以及实现一个简单但完整的Web应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇通往高效后端开发的大门。
|
3天前
|
运维 监控 JavaScript
鸿蒙next版开发:分析JS Crash(进程崩溃)
在HarmonyOS 5.0中,JS Crash指未处理的JavaScript异常导致应用意外退出。本文详细介绍如何分析JS Crash,包括异常捕获、日志分析和典型案例,帮助开发者定位问题、修复错误,提升应用稳定性。通过DevEco Studio收集日志,结合HiChecker工具,有效解决JS Crash问题。
19 4
|
7天前
|
Web App开发 JavaScript 前端开发
深入浅出Node.js后端开发
【10月更文挑战第30天】本文将通过一个Node.js的简单示例,引导你进入Node.js的世界。我们将从基础概念讲起,然后一步步深入到代码实现,最后总结Node.js在后端开发中的优势和应用场景。无论你是前端开发者还是后端新手,这篇文章都将为你打开一扇了解Node.js的大门。
17 2
|
14天前
|
监控 前端开发 JavaScript
React 静态网站生成工具 Next.js 入门指南
【10月更文挑战第20天】Next.js 是一个基于 React 的服务器端渲染框架,由 Vercel 开发。本文从基础概念出发,逐步探讨 Next.js 的常见问题、易错点及解决方法,并通过具体代码示例进行说明,帮助开发者快速构建高性能的 Web 应用。
41 10
|
12天前
|
资源调度 前端开发 数据可视化
构建高效的数据可视化仪表板:D3.js与React的融合之道
【10月更文挑战第25天】在数据驱动的时代,将复杂的数据集转换为直观、互动式的可视化表示已成为一项至关重要的技能。本文深入探讨了如何结合D3.js的强大可视化功能和React框架的响应式特性来构建高效、动态的数据可视化仪表板。文章首先介绍了D3.js和React的基础知识,然后通过一个实际的项目案例,详细阐述了如何将两者结合使用,并提供了实用的代码示例。无论你是数据科学家、前端开发者还是可视化爱好者,这篇文章都将为你提供宝贵的洞见和实用技能。
30 5
|
15天前
|
开发框架 JavaScript 前端开发
HarmonyOS UI开发:掌握ArkUI(包括Java UI和JS UI)进行界面开发
【10月更文挑战第22天】随着科技发展,操作系统呈现多元化趋势。华为推出的HarmonyOS以其全场景、多设备特性备受关注。本文介绍HarmonyOS的UI开发框架ArkUI,探讨Java UI和JS UI两种开发方式。Java UI适合复杂界面开发,性能较高;JS UI适合快速开发简单界面,跨平台性好。掌握ArkUI可高效打造符合用户需求的界面。
63 8
|
14天前
|
JavaScript 前端开发
javascript开发的简单的弹幕插件
这是一个原生javascript开发的简单的弹幕插件,具有美观、易用,占用的资源较低等特点,可以给弹幕设置内容、颜色、头像、链接地址等属性,鼠标悬停等,简单实用,欢迎下载!
34 5
|
4天前
|
Web App开发 JavaScript 前端开发
探索后端开发:Node.js与Express的完美结合
【10月更文挑战第33天】本文将带领读者深入了解Node.js和Express的强强联手,通过实际案例揭示它们如何简化后端开发流程,提升应用性能。我们将一起探索这两个技术的核心概念、优势以及它们如何共同作用于现代Web开发中。准备好,让我们一起开启这场技术之旅!
14 0
|
11天前
|
JavaScript 前端开发 安全
深入浅出Node.js后端开发
【10月更文挑战第26天】在这篇文章中,我们将一起探索Node.js的奇妙世界。不同于传统的Java或Python,Node.js以其异步非阻塞I/O和事件驱动的特性,在后端开发领域独树一帜。无论你是初学者还是资深开发者,这篇文章都将为你提供新的视角和思考。从基础概念到实际应用,我们一步步深入Node.js的世界,让你了解其不仅仅是JavaScript运行环境那么简单。
下一篇
无影云桌面