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

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



相关文章
|
20天前
|
编解码 前端开发 JavaScript
js react antd 实现页面低分变率和高分变率下字体大小自适用,主要是配置antd
在React中结合Ant Design与媒体查询,通过less变量和响应式断点动态调整`@font-size-base`,实现多分辨率下字体自适应,提升跨设备体验。
45 2
|
7月前
|
JSON 自然语言处理 前端开发
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
329 72
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
|
5月前
|
数据采集 监控 API
加密货币 Pump 监测刮刀工具开发原理及实现路径
开发Pump监测刮刀工具需综合运用高频数据采集、波动率建模、跨平台对冲三大核心技术,2025年的技术瓶颈已从基础数据获取转向超低延迟执行与合规适配。建议采用模块化开发策略,优先实现核心监控功能,再逐步接入AI决策与链上套利模块。代码示例需根据最新交易所API文档动态调整,并严格遵守所在地监管法规。
|
6月前
|
存储 JavaScript 前端开发
在NodeJS中使用npm包进行JS代码的混淆加密
总的来说,使用“javascript-obfuscator”包可以帮助我们在Node.js中轻松地混淆JavaScript代码。通过合理的配置,我们可以使混淆后的代码更难以理解,从而提高代码的保密性。
497 9
|
6月前
|
前端开发 JavaScript NoSQL
使用 Node.js、Express 和 React 构建强大的 API
本文详细介绍如何使用 Node.js、Express 和 React 构建强大且动态的 API。从开发环境搭建到集成 React 前端,再到利用 APIPost 高效测试 API,适合各水平开发者。内容涵盖 Node.js 运行时、Express 框架与 React 库的基础知识及协同工作方式,还涉及数据库连接和前后端数据交互。通过实际代码示例,助你快速上手并优化应用性能。
|
7月前
|
安全 网络协议 算法
零基础IM开发入门(五):什么是IM系统的端到端加密?
本篇将通俗易懂地讲解IM系统中的端到端加密原理,为了降低阅读门槛,相关的技术概念会提及但不深入展开。
203 2
|
10月前
|
Web App开发 JavaScript 前端开发
Node.js开发
Node.js开发
195 13
|
10月前
|
Web App开发 JavaScript 前端开发
深入浅出Node.js后端开发
本文将带领读者从零基础开始,一步步深入到Node.js后端开发的精髓。我们将通过通俗易懂的语言和实际代码示例,探索Node.js的强大功能及其在现代Web开发中的应用。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供新的见解和技巧,让你的后端开发技能更上一层楼。

热门文章

最新文章