过去一年,接入了很多第三方钱包,有solana,rainbow,web3Modal【现在是reown】了、aptos相关区块链钱包,第三方钱包接入已经相对非常成熟,API通常来讲非常简易,基本不用考虑更底层的API,常用的hook也会更高效,但通常接入钱包过程中,似乎总有种,知其然而不知所以然的感觉,本文是一篇钱包偏底层的接入流程。
本文主要以evm钱包为例子
首先我们要初步了解viem
,我们在项目中,你也许会看到大量的wagmi
,实际上这是对viem
更上层的封装,wagmi
是以太坊交互官方封装便捷好用的react库,让你非常快捷方便的与钱包以及以太坊交互,但本质上所有与以太坊交互的hook
依赖于viem
在开始本文之前,你的浏览器得提前安装一个metaMask
插件钱包
// 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
时
{
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
这个对象,所以基本上所有与钱包交互的接口的前提是你必须在插件商店安装对应的一个钱包才行
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
就是我们选择的钱包地址
通常我们在连接钱包后,我们需要做一个签名,这个签名一般由客户端主动发起,当签名成功后,就会获取一个hash
,然后这个hash
一般会是通过一个后端登录的接口进行验证这个账户
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
是这这样的
0xaeedca0a7745ee7dbbabdf69226dd7c27aa0f17c8bdcf58616ced8f1b9fe7bc60da3fcdb850af23d1379c2a0b57349862db2010ceb50f8a33322aed68119b14b1c
我们会发现签名值的变化与message
这个传入的字符串有关,如果这个字符串发生变化,那么签名后的hash
就不回一样,反之当message
不发生变化时,产生的hash
是不会变化的
主要是判断这个钱包地址是否是eth地址
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>
<>
}
可以理解成一个钱包地址向另外一个地址转账,在操作前,必须有SignMessage
操作
...
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$
如果你把实际转账的参数改成这样
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
费用
主要是查询用户账户余额、检索当前区块号,或者检索当前的交易信息,查询这些并不需要签名等权限
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
帮我们处理成了与钱包余额一样的余额数值
hash
能查询当前的交易状态,比如
...
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
开放出来的公用区块查询,比如钱包余额,区块,以及交易状态扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有