前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >web3钱包接入之viem

web3钱包接入之viem

作者头像
Maic
发布于 2025-02-12 06:11:37
发布于 2025-02-12 06:11:37
21300
代码可运行
举报
文章被收录于专栏:Web技术学苑Web技术学苑
运行总次数:0
代码可运行

过去一年,接入了很多第三方钱包,有solana,rainbow,web3Modal【现在是reown】了、aptos相关区块链钱包,第三方钱包接入已经相对非常成熟,API通常来讲非常简易,基本不用考虑更底层的API,常用的hook也会更高效,但通常接入钱包过程中,似乎总有种,知其然而不知所以然的感觉,本文是一篇钱包偏底层的接入流程。

本文主要以evm钱包为例子

首先我们要初步了解viem,我们在项目中,你也许会看到大量的wagmi,实际上这是对viem更上层的封装,wagmi是以太坊交互官方封装便捷好用的react库,让你非常快捷方便的与钱包以及以太坊交互,但本质上所有与以太坊交互的hook依赖于viem

在开始本文之前,你的浏览器得提前安装一个metaMask插件钱包

查看区块block

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 
// test.js
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({
  chain: mainnet,
  transport: http(),
});
console.log(client);
const getBlock = async () => {
  const blockNumber = await client.getBlockNumber();
  console.log(blockNumber);
};
getBlock();

当我们运行node test.js

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 
{
  account: undefined,
  batch: undefined,
  cacheTime: 4000,
  ccipRead: undefined,
  chain: {
    formatters: undefined,
    fees: undefined,
    serializers: undefined,
    id: 1,
    name: 'Ethereum',
    nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
    rpcUrls: { default: [Object] },
    blockExplorers: { default: [Object] },
    contracts: {
      ensRegistry: [Object],
      ensUniversalResolver: [Object],
      multicall3: [Object]
    }
  },
  key: 'public',
  name: 'Public Client',
  pollingInterval: 4000,
  request: [AsyncFunction (anonymous)],
  transport: {
    key: 'http',
    name: 'HTTP JSON-RPC',
    request: [AsyncFunction: request],
    retryCount: 3,
    retryDelay: 150,
    timeout: 10000,
    type: 'http',
    fetchOptions: undefined,
    url: 'https://cloudflare-eth.com'
  },
  type: 'publicClient',
  uid: '2523e6fdcf6',
  extend: [Function (anonymous)],
  call: [Function: call],
  createBlockFilter: [Function: createBlockFilter],
  createContractEventFilter: [Function: createContractEventFilter],
  createEventFilter: [Function: createEventFilter],
  createPendingTransactionFilter: [Function: createPendingTransactionFilter],
  estimateContractGas: [Function: estimateContractGas],
  estimateGas: [Function: estimateGas],
  getBalance: [Function: getBalance],
  getBlobBaseFee: [Function: getBlobBaseFee],
  getBlock: [Function: getBlock],
  getBlockNumber: [Function: getBlockNumber],
  getBlockTransactionCount: [Function: getBlockTransactionCount],
  getBytecode: [Function: getBytecode],
  getChainId: [Function: getChainId],
  getCode: [Function: getCode],
  getContractEvents: [Function: getContractEvents],
  getEip712Domain: [Function: getEip712Domain],
  getEnsAddress: [Function: getEnsAddress],
  getEnsAvatar: [Function: getEnsAvatar],
  getEnsName: [Function: getEnsName],
  getEnsResolver: [Function: getEnsResolver],
  getEnsText: [Function: getEnsText],
  getFeeHistory: [Function: getFeeHistory],
  estimateFeesPerGas: [Function: estimateFeesPerGas],
  getFilterChanges: [Function: getFilterChanges],
  getFilterLogs: [Function: getFilterLogs],
  getGasPrice: [Function: getGasPrice],
  getLogs: [Function: getLogs],
  getProof: [Function: getProof],
  estimateMaxPriorityFeePerGas: [Function: estimateMaxPriorityFeePerGas],
  getStorageAt: [Function: getStorageAt],
  getTransaction: [Function: getTransaction],
  getTransactionConfirmations: [Function: getTransactionConfirmations],
  getTransactionCount: [Function: getTransactionCount],
  getTransactionReceipt: [Function: getTransactionReceipt],
  multicall: [Function: multicall],
  prepareTransactionRequest: [Function: prepareTransactionRequest],
  readContract: [Function: readContract],
  sendRawTransaction: [Function: sendRawTransaction],
  simulateContract: [Function: simulateContract],
  verifyMessage: [Function: verifyMessage],
  verifySiweMessage: [Function: verifySiweMessage],
  verifyTypedData: [Function: verifyTypedData],
  uninstallFilter: [Function: uninstallFilter],
  waitForTransactionReceipt: [Function: waitForTransactionReceipt],
  watchBlocks: [Function: watchBlocks],
  watchBlockNumber: [Function: watchBlockNumber],
  watchContractEvent: [Function: watchContractEvent],
  watchEvent: [Function: watchEvent],
  watchPendingTransactions: [Function: watchPendingTransactions]
}

21074983n

我们发现client这个对象返回出了很多与以太坊交互的接口

http()其实会默认选择rpc就是cloudflare-eth.comcontracts也提供了一个默认的address,默认的交易对就是ETH,精度18

连接钱包

我们知道与以太坊交互去中心化,所有的交易账户需要一个账号与链上产生交互,所以需要链接钱包,当我们插件安装了metaMask后,浏览器的window对象就已经绑定了ethereum这个对象,所以基本上所有与钱包交互的接口的前提是你必须在插件商店安装对应的一个钱包才行

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 
   function App() {
        const handleConnetWallet = async () => {
        const [account] = await window.ethereum.request({ method: 'eth_requestAccounts' });
        console.log(account, '==account')
      }
  
   return (<div>
            <h1 onClick={handleConnetWallet}>Content Wallet</h1>
      </div>);
   }

当前的web端唤起小狐狸钱包或者其他钱包,基本就是依靠这个window.ethereum.request这个接口

我们注意到一个钱包有很多个账号,而且我们默认的就是ETH主网,因为我们的钱包会默认选择一个ETH主网,当你选择确认对应钱包后,此时的account就是我们选择的钱包地址

SignMessage

通常我们在连接钱包后,我们需要做一个签名,这个签名一般由客户端主动发起,当签名成功后,就会获取一个hash,然后这个hash一般会是通过一个后端登录的接口进行验证这个账户

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 
import  {memo, useRef} from "react";
...
function App() {
     const accountRef = useRef(null);
    const handleSignMessage = async () => {
    const walletClient = createWalletClient({
        account: accountRef.current,
        chain: mainnet,
        transport:custom(window.ethereum),
    });
    const hash = await walletClient.signMessage({
        message: "hello world"
    });
    console.log(hash);
  }
  const handleConnetWallet = async () => {
    const [account] = await window.ethereum.request({ method: 'eth_requestAccounts' });
    accountRef.current = account;
  }
  return (<div>
     <h1 onClick={handleConnetWallet}>Content Wallet</h1>
     <h1 onClick={handleSignMessage}>SignMessage</h1>
  </div>)
}

注意签名的前提必须是你已经链接钱包

返回的hash是这这样的

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 
0xaeedca0a7745ee7dbbabdf69226dd7c27aa0f17c8bdcf58616ced8f1b9fe7bc60da3fcdb850af23d1379c2a0b57349862db2010ceb50f8a33322aed68119b14b1c

我们会发现签名值的变化与message这个传入的字符串有关,如果这个字符串发生变化,那么签名后的hash就不回一样,反之当message不发生变化时,产生的hash是不会变化的

isAddress

主要是判断这个钱包地址是否是eth地址

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 
import {isAddress} from "viem";
...
function App() {
       const [address, setIsAddress] = useState("");
      const [isEthAddress, setIsEthAddress] = useState(false);
     const handleVerifyAddress = () => {
            const isEthAddress = isAddress(address);
            setIsEthAddress(isEthAddress);
      }
    return <>
        <div>
            <input type="text" onChange={(e) => setIsAddress(e.target.value)} />
            <button onClick={handleVerifyAddress}>Verify address</button>
            <p>{address} {isEthAddress ? 'is': 'is not'} eth address</p>
        </div>
    <>
}

sendTransaction

可以理解成一个钱包地址向另外一个地址转账,在操作前,必须有SignMessage操作

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 
  ...
  const handleSendTransaction = async () => {
    const res = await wallect.current.sendTransaction({
        to: "0x5A39756bAE97685917a292B33ddFb2B54DF1e806",
        value: parseEther("0.001"), // 0.001ETH
    });
    console.log(res);
  }
  return (<>
      <h1 onClick={handleSendTransaction}>sendTransaction</h1>
  </>)

你会发现,我发送2.66$,所需要的gas费用就是0.67$,所以实际上你在钱包中发起的一笔转账,大概总计就需要3.33$

如果你把实际转账的参数改成这样

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 
  const handleSendTransaction = async () => {
    const res = await wallect.current.sendTransaction({
        to: "0x5A39756bAE97685917a292B33ddFb2B54DF1e806",
        value: parseEther("1"), // 1 ETH
    });
    console.log(res);
  }
  return (<>
       <h1 onClick={handleSendTransaction}>sendTransaction</h1>
  </>)

你会发现gas费用是动态变化的,但是跟你当下交易的笔数的大小无关,gas费每次都会变化,如果多笔转账合并,那么会减少不少的gas费用

获取公共操作

主要是查询用户账户余额、检索当前区块号,或者检索当前的交易信息,查询这些并不需要签名等权限

  • getBalance
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 
import {createPublicClient, http, createWalletClient, custom, isAddress, parseEther, formatUnits } from "viem"

import { mainnet, bsc } from "viem/chains";

const client = createPublicClient({
    chain: bsc, // bsc链
    transport: http(),
 })
 
 function App() {
      const handleGetBlance = async () => {
          const res = await client.getBalance({
              address: "0x6c85e349A70791e95fD6D9D5e066C6Ec136E0a92"
          });
           console.log(`余额:${formatUnits(res, 18)} BNB`);
      }
     return <div>
         <div onClick={handleGetBlance}>get Blance</div>
     </div>
 }

我们发现对于获取后的余额实际上精度是18位的,最终我们使用viem中的formatUnits帮我们处理成了与钱包余额一样的余额数值

  • getTransactionReceipt 主要是根本交易hash能查询当前的交易状态,比如
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 
 ...
 const handleGetTranstion = async () => {
    const res = await client.getTransactionReceipt({
        hash: "0xf6914af778b18ca1be98adead61af6351862fe26faad7e64d631df30fe925cd4"
    });
 console.log(res);
 function App() {
     return <>
             <h1 onClick={handleGetTranstion}>get transition hash</h1>
     </>
 }
  

我们会查询这个这交易hash的在区块链的实际交易状态,实际上你在https://bscscan.com/ 中根据交易hash也可查询看到对应的状态

至此一个从一个连接钱包登录,到签名,再到sendTransition的基础功能就基本结束,viem也提供了如何与以太坊合约交互的接口,这些都会有一个ABI的数据相关联,后续也会写个测试网合约进行调试,更多的详细的与以太坊交互的参考viem 文档

总结

  • 了解学习到使用更低层的viem官方接口唤起钱包,进行钱包连接签名sendTranstion操作
  • viem开放出来的公用区块查询,比如钱包余额,区块,以及交易状态
  • code example
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-10-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Web技术学苑 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 查看区块block
  • 连接钱包
  • SignMessage
  • isAddress
  • sendTransaction
  • 获取公共操作
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档