wagmi Vue 组合式函数 useWriteContract 实战:在 Vue 3 中安全地执行合约写交易
【免费下载链接】wagmiReactive primitives for Ethereum apps项目地址: https://gitcode.com/GitHub_Trending/wa/wagmi
useWriteContract是@wagmi/vue提供的核心组合式函数(Composable),用于在 Vue 3 应用中调用 Solidity 合约的「写」函数(write function)并广播交易。本文以 useWriteContract 官方文档 为主线,结合仓库源码(useWriteContract.ts、writeContract.ts 等)逐层拆解它的用法、参数、返回值与底层实现,帮助你写出类型安全、可处理状态与错误、可复用的链上写入逻辑。
什么是「写函数」:为什么需要广播交易
Solidity 合约中的函数分为两类:view/pure等「读」函数不改变链上状态,调用后直接返回结果;而「写」函数(如transferFrom、approve、mint)会修改区块链状态。因为状态变更需要全网共识与矿工打包,这类调用必须消耗 Gas,并以广播一笔交易的方式执行。
useWriteContract正是为这类场景设计:它接收合约 ABI、合约地址、函数名与参数,通过已连接的账户签名并广播交易,最终返回交易哈希(Hash)——交易被确认打包还需要额外等待(详见下文「等待交易上链」)。
安装与导入
useWriteContract属于@wagmi/vue包,直接导入即可:
import { useWriteContract } from '@wagmi/vue'使用它之前,应用需要先完成两件准备工作:
- 使用 createConfig 创建链配置(chains 与 transports);
- 将配置通过 WagmiPlugin 注入应用,例如:
// config.ts import { createConfig, http } from '@wagmi/vue' import { mainnet, sepolia } from '@wagmi/vue/chains' export const config = createConfig({ chains: [mainnet, sepolia], transports: { [mainnet.id]: http(), [sepolia.id]: http(), }, })// main.ts import { WagmiPlugin } from '@wagmi/vue' import { createApp } from 'vue' import { config } from './config' import App from './App.vue' createApp(App).use(WagmiPlugin, { config }).mount('#app')基本用法:在组件中触发一笔合约写交易
以下示例调用 ERC-20 合约的transferFrom(授权转账),完整的 abi.ts 与 config.ts 可在仓库中找到:
<!-- index.vue --> <script setup lang="ts"> import { useWriteContract } from '@wagmi/vue' import { abi } from './abi' const writeContract = useWriteContract() </script> <template> <button @click="writeContract.mutate({ abi, address: '0x6b175474e89094c44da98b954eedeac495271d0f', functionName: 'transferFrom', args: [ '0xd2135CfB216b74109775236E36d4b433F1DF507B', '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', 123n, ], })"> Transfer </button> </template>其中 abi.ts 定义了approve与transferFrom两个函数的签名,注意使用as const断言以保证类型推断(详见「类型推断」一节):
export const abi = [ { type: 'function', name: 'approve', stateMutability: 'nonpayable', inputs: [ { name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }, ], outputs: [{ type: 'bool' }], }, { type: 'function', name: 'transferFrom', stateMutability: 'nonpayable', inputs: [ { name: 'sender', type: 'address' }, { name: 'recipient', type: 'address' }, { name: 'amount', type: 'uint256' }, ], outputs: [{ type: 'bool' }], }, ] as const几个关键点:
- 金额使用
bigint字面量:示例中123n是123 wei,避免 JavaScriptnumber的精度丢失;大额 ETH 可配合 viem 的parseEther('0.01')转换; mutate(variables)触发交易:把合约调用参数传给mutate后,组合式函数内部会完成「获取连接器客户端 → 签名 → 广播」的完整流程;- 返回的是交易哈希而非回执:
data中是0x...形式的哈希,区块确认结果需配合waitForTransactionReceipt获取(见下文)。
参数详解
useWriteContract接受一个UseWriteContractParameters对象:
import { type UseWriteContractParameters } from '@wagmi/vue'从源码看,其类型定义为ConfigParameter<config> & WriteContractOptions<config, context>(见 useWriteContract.ts),即「config 参数 + 合约调用参数 + TanStack Query 的 mutation 参数」。
config
Config | undefined
指定要使用的 Config 实例,覆盖从 WagmiPlugin 中检索到的默认配置。适合测试、多配置并存或需要在组合式函数外部控制配置的场景:
<script setup lang="ts"> import { useWriteContract } from '@wagmi/vue' import { config } from './config' // [!code focus] const writeContract = useWriteContract({ config, // [!code focus] }) </script>若不传,则通过useConfig从插件上下文自动获取——这是 useWriteContract 内部的第一步。
mutation(TanStack Query 参数)
组合式函数内部基于@tanstack/vue-query的useMutation实现,因此大部分 TanStack Query 的 mutation 参数都可用,类型为WriteContractOptions(见 query/writeContract.ts):
注意:
mutationFn与mutationKey被 Wagmi 内部占用(分别绑定writeContractaction 与['writeContract']键),不可覆盖;其余参数均支持(参见 utils/query.ts 中Omit<'mutationFn' | 'mutationKey' | 'throwOnError'>的定义)。
| 参数 | 类型 | 说明 |
|---|---|---|
gcTime | number \| Infinity \| undefined | 未使用/非活跃缓存数据的存活毫秒数,到期后被垃圾回收;设为Infinity禁用回收 |
meta | Record<string, unknown> \| undefined | 附加到 mutation 缓存条目的元信息,可在onError/onSuccess等回调中读取 |
networkMode | 'online' \| 'always' \| 'offlineFirst' \| undefined | 网络模式,默认'online' |
onError | (error, variables, context?) => ... | mutation 出错时触发,接收错误对象 |
onMutate | (variables) => ... | mutation 函数执行前触发,可用于乐观更新;返回值会传给onError/onSettled以便回滚 |
onSuccess | (data, variables, context?) => ... | mutation 成功时触发,接收结果 |
onSettled | (data, error, variables, context?) => ... | mutation 无论成功或失败都会触发 |
queryClient | QueryClient | 自定义QueryClient,否则使用最近上下文中的实例 |
retry | boolean \| number \| ((failureCount, error) => boolean) \| undefined | 失败重试次数,默认0;true无限重试,数字表示最多重试次数 |
retryDelay | number \| ((retryAttempt, error) => number) \| undefined | 重试前等待毫秒数;可用attempt => Math.min(2 ** attempt * 1000, 30_000)实现指数退避 |
完整说明见共享文档 mutation-options.md。
合约调用参数(action 层)
除config与 mutation 参数外,mutate的变量对象(WriteContractVariables)还包含完整的合约调用参数,它们与@wagmi/core的 writeContract action 参数一一对应,此处先列出最常用的一组(完整参考见下文「交易参数完整参考」):
| 参数 | 类型 | 说明 |
|---|---|---|
abi | Abi | 合约 ABI,as const声明可获得最强类型推断 |
address | Address | 合约地址 |
functionName | string | 要调用的合约函数名,由abi推断 |
args | readonly unknown[] \| undefined | 传给函数的参数,由abi与functionName推断 |
chainId | config['chains'][number]['id'] \| undefined | 发送交易前校验的链 ID |
value | bigint \| undefined | 随交易发送的 wei 金额(如parseEther('0.01')) |
返回值详解
import { type UseWriteContractReturnType } from '@wagmi/vue'返回对象是 TanStack QueryuseMutation结果的增强版本(并额外提供两个已废弃别名,见「底层原理」),包含:
| 属性 | 类型 | 说明 |
|---|---|---|
mutate | (variables, options?) => void | 触发 mutation,可传onSuccess/onError/onSettled回调 |
mutateAsync | (variables, options?) => Promise<TData> | 与mutate类似,但返回可await的 Promise |
data | TData \| undefined | 最近一次成功的结果,即交易哈希0x... |
error | TError \| null | mutation 的错误对象 |
failureCount | number | 失败计数,每次失败 +1,成功归零 |
failureReason | TError \| null | 失败重试的原因,成功时重置为null |
isError/isIdle/isPending/isSuccess | boolean | 由status派生的布尔状态 |
isPaused | boolean | mutation 是否被网络模式暂停 |
reset | () => void | 重置 mutation 内部状态 |
status | 'idle' \| 'pending' \| 'error' \| 'success' | 核心状态:idle初始 →pending执行中 →error/success |
submittedAt | number | mutation 提交时间戳,默认0 |
variables | TVariables \| undefined | 最近一次传给mutate的变量对象 |
完整说明见共享文档 mutation-result.md。在实际组件中,常用isPending控制按钮 loading、error展示失败信息:
<template> <div> <button :disabled="writeContract.isPending.value" @click="onTransfer"> {{ writeContract.isPending.value ? '确认中…' : 'Transfer' }} </button> <p v-if="writeContract.error.value"> {{ writeContract.error.value.shortMessage }} </p> <p v-if="writeContract.data.value"> Hash: {{ writeContract.data.value }} </p> </div> </template>注意:在 Vue 模板中,@tanstack/vue-query返回的是ref 包裹的响应式值,模板中直接访问会被自动解包;在<script setup>中则需通过.value读取。
TypeScript 类型推断:让编译器替你把关
只要abi以as const正确声明(参考 abi-write.ts),TypeScript 就会自动推断出:
functionName:只能是 ABI 中存在的函数名;args:数量、顺序、类型与所选函数签名严格一致(例如transferFrom要求[address, address, uint256]);value等参数的合法性校验。
这意味着拼错函数名、传错参数类型都会在编译期直接报错。data的类型同样可通过abi+functionName+args的组合推断出来,详见 TypeScript 文档。
此外,若需要显式引用类型,可以从@wagmi/vue/query导入(参考 mutation-imports.md 的模板模式):
import { type WriteContractData, type WriteContractVariables, type WriteContractMutate, type WriteContractMutateAsync, WriteContractMutationOptions, } from '@wagmi/vue/query'底层原理:从组合式函数到链上交易
useWriteContract的整个实现非常精简(useWriteContract.ts):
export function useWriteContract<config extends Config, context = unknown>( parameters: UseWriteContractParameters<config, context> = {}, ): UseWriteContractReturnType<config, context> { const config = useConfig(parameters) // 1. 取配置(默认来自插件) const options = writeContractMutationOptions(config, parameters) // 2. 组装 mutation 选项 const mutation = useMutation(options) // 3. 交给 TanStack Vue Query return { ...(mutation as Return), writeContract: mutation.mutate, // 4. 兼容别名(已废弃) writeContractAsync: mutation.mutateAsync, // 4. 兼容别名(已废弃) } }对应的writeContractMutationOptions(query/writeContract.ts)把变量直接桥接到 core action:
return { ...(options.mutation as any), mutationFn(variables) { return writeContract(config, variables) // 调用 @wagmi/core 的 writeContract }, mutationKey: ['writeContract'], }而 core 层的writeContractaction(actions/writeContract.ts)核心调用链为:
- 判断
account是否为local类型账户:是则直接config.getClient({ chainId }),否则通过getConnectorClient获取当前连接器(MetaMask、WalletConnect 等)对应的 viemClient; - 校验
chainId(若传入)与当前链是否一致,构造chain上下文; - 通过
getAction(client, viem_writeContract, 'writeContract')复用 viem 的writeContract完成编码 calldata、估算 Gas、签名并广播,返回交易哈希。
因此整个调用链为:Vue 组件 →useWriteContract→writeContractMutationOptions→@wagmi/core的writeContract→ viem 的writeContract→ 连接器签名广播。
两个值得注意的实现细节:
- 返回对象中额外暴露了
writeContract与writeContractAsync两个别名,源码中已标注@deprecated,请统一使用mutate/mutateAsync; WriteContractMutate泛型用functionName收窄了变量联合类型,确保mutate调用时 args 精确匹配所选函数(见 query/writeContract.ts 的注释说明)。
交易参数完整参考
以下参数均可在mutate的变量对象中传入(与 writeContract action 对齐):
| 参数 | 类型 | 说明 |
|---|---|---|
abi | Abi | 合约 ABI,类型推断的核心 |
address | Address | 合约地址 |
functionName | string | 要调用的函数名(nonpayable/payable),由abi推断 |
args | readonly unknown[] \| undefined | 调用参数,由abi与functionName推断 |
chainId | number \| undefined | 发送前校验的链 ID |
connector | Connector \| undefined | 用于签名的连接器,默认取当前连接(可用getConnection(config)获取) |
account | Address \| Account \| undefined | 签名账户;若指定了本地账户则走本地客户端路径 |
gas | bigint \| undefined | 交易执行提供的 Gas 上限,如parseGwei('20') |
gasPrice | bigint \| undefined | 每单位 Gas 价格(仅 Legacy 交易) |
maxFeePerGas | bigint \| undefined | 总费用上限,含优先费(仅 EIP-1559 交易) |
maxPriorityFeePerGas | bigint \| undefined | 最大优先费(仅 EIP-1559 交易) |
nonce | number | 标识该交易的唯一序号 |
type | 'legacy' \| 'eip1559' \| 'eip2930' \| undefined | 可选的交易类型,用于收窄参数 |
value | bigint \| undefined | 随交易发送的 wei 金额 |
accessList | AccessList \| undefined | EIP-2930 访问列表 |
dataSuffix | `0x${string}` \| undefined | 附加到 calldata 末尾的数据(如 Seaport 的 domain tag) |
实战组合:模拟 → 写入 → 等待确认
官方建议将simulateContract与writeContract配对使用:先用simulateContract在本地验证交易是否会成功(不消耗 Gas),成功后再真正广播。结合等待交易确认,一个完整的 Vue 组合式示例为:
<script setup lang="ts"> import { useSimulateContract, useWriteContract } from '@wagmi/vue' import { waitForTransactionReceipt } from '@wagmi/vue/actions' import { abi } from './abi' import { config } from './config' const { data } = useSimulateContract({ abi, address: '0x6b175474e89094c44da98b954eedeac495271d0f', functionName: 'transferFrom', args: [ '0xd2135CfB216b74109775236E36d4b433F1DF507B', '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', 123n, ], }) const { writeContractAsync } = useWriteContract() async function onTransfer() { try { // 1. 模拟:确认交易会成功 const { request } = data.value! // 2. 广播交易,得到哈希 const hash = await writeContractAsync(request) // 3. 等待交易上链,拿到回执 const receipt = await waitForTransactionReceipt(config, { hash }) console.log('Transaction confirmed:', receipt.transactionHash) } catch (error) { console.error('Transaction failed:', error) } } </script>simulateContract返回的request可直接透传给writeContract,这是官方推荐的稳健模式,相关用法详见 core actions 文档。
延伸阅读
- 组合式函数源码:useWriteContract.ts
- core action 实现与完整参数:actions/writeContract.ts 与 writeContract action 文档
- mutation 桥接层:query/writeContract.ts
- 共享文档:mutation-options.md、mutation-result.md、mutation-imports.md
- 示例代码片段:abi-write.ts、vue/config.ts
- 类型安全配置指南:TypeScript 文档
【免费下载链接】wagmiReactive primitives for Ethereum apps项目地址: https://gitcode.com/GitHub_Trending/wa/wagmi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考