You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Hardhat+ethers.js调用UniswapV3 exactInput报错:deployer.sendTransaction非函数

问题:Hardhat环境调用Uniswap V3兑换UNI时出现deployer.sendTransaction is not a function错误

我在Goerli网络用Hardhat部署,通过UniswapV3接口兑换购买UNI,调用getToken()时触发TypeError: deployer.sendTransaction is not a function错误,但很多示例都这么写。另外,查询UNI余额的balanceOf()功能正常,需要错误原因分析、修复方案和代码改进建议。

原Hardhat代码

const { ethers, getNamedAccounts, network } = require("hardhat")
const {abi: V3SwapRouterABI} = require('@uniswap/v3-periphery/artifacts/contracts/interfaces/ISwapRouter.sol/ISwapRouter.json')

const FEE_SIZE = 3

function encodePath(path, fees) {
    if (path.length != fees.length + 1) {
        throw new Error('path/fee lengths do not match')
    }

    let encoded = '0x'
    for (let i = 0; i < fees.length; i++) {
        // 20 byte encoding of the address
        encoded += path[i].slice(2)
        // 3 byte encoding of the fee
        encoded += fees[i].toString(16).padStart(2 * FEE_SIZE, '0')
    }
    // encode the final token
    encoded += path[path.length - 1].slice(2)

    return encoded.toLowerCase()
    }

async function getToken() {

    // const signer = provider.getSigner()
    const deadline = Math.floor(Date.now()/1000) + (60*10)
    const wethToken= "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
    const Uni= "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"
    const UniswapRouter="0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
    const path = encodePath([wethToken, Uni], [3000])
    console.log(path)
    
    const { deployer } = await getNamedAccounts()
    const UniV3Contract = await ethers.getContractAt(
        V3SwapRouterABI,
        UniswapRouter,
        deployer
    )

    const params = {
        path: path,
        recipient:deployer,
        deadline: deadline,
        amountIn: ethers.utils.parseEther('0.01'),
        amountOutMinimum: 0
    }

    const encodedData = UniV3Contract.interface.encodeFunctionData("exactInput", [params])

    const txArg = {
        to: UniswapRouter,
        from: deployer,
        data: encodedData,
        gasLimit: ethers.utils.hexlify(1000000)
    }

    const tx = await deployer.sendTransaction(txArg)
    console.log('tx: ', tx)

    const IUni = await ethers.getContractAt(
        "IERC20",
        Uni,
        deployer
    )
    const UniBlance = await IUni.balanceOf(deployer)
    console.log(`Got ${UniBlance.toString()} UNI`)
}

module.exports = { getToken }

非Hardhat环境对比代码

const {abi: V3SwapRouterABI} = require('@uniswap/v3-periphery/artifacts/contracts/interfaces/ISwapRouter.sol/ISwapRouter.json')
const { ethers } = require("ethers")
require("dotenv").config()

const INFURA_URL_TESTNET = process.env.INFURA_URL_TESTNET
const PRIVATE_KEY = process.env.PRIVATE_KEY
const WALLET_ADDRESS = process.env.WALLET_ADDRESS
// now you can call sendTransaction 


const wethToken= "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"
const Uni= "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"
const UniswapRouter="0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
const UniV3Contract = new ethers.Contract(
    UniswapRouter,
    V3SwapRouterABI
)

const provider = new ethers.providers.JsonRpcProvider(INFURA_URL_TESTNET)
const wallet = new ethers.Wallet(PRIVATE_KEY)
const signer = wallet.connect(provider)


const FEE_SIZE = 3

function encodePath(path, fees) {
    if (path.length != fees.length + 1) {
        throw new Error('path/fee lengths do not match')
    }

    let encoded = '0x'
    for (let i = 0; i < fees.length; i++) {
        // 20 byte encoding of the address
        encoded += path[i].slice(2)
        // 3 byte encoding of the fee
        encoded += fees[i].toString(16).padStart(2 * FEE_SIZE, '0')
    }
    // encode the final token
    encoded += path[path.length - 1].slice(2)

    return encoded.toLowerCase()
    }

async function getToken() {

    const path = encodePath([wethToken, Uni], [3000])
    const deadline = Math.floor(Date.now()/1000) + (60*10)  

    const params = {
        path: path,
        recipient: WALLET_ADDRESS,
        deadline: deadline,
        amountIn: ethers.utils.parseEther('0.01'),
        amountOutMinimum: 0
    }

    const encodedData = UniV3Contract.interface.encodeFunctionData("exactInput", [params])

    const txArg = {
        to: UniswapRouter,
        from: WALLET_ADDRESS,
        data: encodedData,
        gasLimit: ethers.utils.hexlify(1000000)
    }

    const tx = await signer.sendTransaction(txArg)
    console.log('tx: ', tx)
    const receipt = tx.wait()
    console.log('receipt: ', receipt)

}

module.exports = { getToken }

错误原因

getNamedAccounts()返回的deployer只是字符串格式的钱包地址,不是ethers.js的Signer对象,而sendTransaction()是Signer对象的专属方法,直接调用自然会报错。非Hardhat环境里用的signer.sendTransaction()是通过钱包私钥生成的Signer实例,所以能正常工作。

修复方案

方案1:将deployer地址转为Signer对象

替换原代码中获取deployer的部分,生成Signer后再调用交易:

const { deployer } = await getNamedAccounts();
const signer = await ethers.getSigner(deployer); // 把地址转为可签名的Signer对象
// 后续把deployer.sendTransaction(txArg)改成signer.sendTransaction(txArg)

方案2:直接用Signer初始化合约(推荐)

不需要手动编码交易,直接用Signer初始化合约后调用exactInput方法,更简洁:

const { deployer } = await getNamedAccounts();
const signer = await ethers.getSigner(deployer);

// 用Signer初始化合约,而非地址
const UniV3Contract = await ethers.getContractAt(
    V3SwapRouterABI,
    UniswapRouter,
    signer
);

// 直接调用合约方法,ETH金额通过value参数传入(原生代币交易必须这么做)
const tx = await UniV3Contract.exactInput(params, {
    value: ethers.utils.parseEther('0.01'),
    gasLimit: ethers.utils.hexlify(1000000)
});

注意:用ETH兑换ERC20时,Uniswap V3的amountIn参数无效,必须通过交易的value字段传入ETH金额。


代码改进建议

  • 修正网络地址:原代码用了主网WETH地址0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2,Goerli网络的WETH地址是0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6,必须替换。
  • 添加滑点保护:不要把amountOutMinimum设为0,可通过callStatic估算预期输出后,扣减1%-5%作为最小输出,防止MEV攻击或滑点过大导致损失。
  • 动态Gas估算:不要固定gasLimit,用ethers.provider.estimateGas()动态计算,避免gas浪费或不足。
  • 错误捕获:添加try-catch块处理交易失败的情况(比如滑点超限、gas不足)。
  • 环境变量管理:把合约地址、网络配置放到.env文件,不要硬编码,方便切换网络。

修复后的完整Hardhat代码

const { ethers, getNamedAccounts, network } = require("hardhat")
const {abi: V3SwapRouterABI} = require('@uniswap/v3-periphery/artifacts/contracts/interfaces/ISwapRouter.sol/ISwapRouter.json')
require("dotenv").config()

const FEE_SIZE = 3

function encodePath(path, fees) {
    if (path.length != fees.length + 1) {
        throw new Error('path/fee lengths do not match')
    }

    let encoded = '0x'
    for (let i = 0; i < fees.length; i++) {
        encoded += path[i].slice(2)
        encoded += fees[i].toString(16).padStart(2 * FEE_SIZE, '0')
    }
    encoded += path[path.length - 1].slice(2)

    return encoded.toLowerCase()
}

async function getToken() {
    try {
        const deadline = Math.floor(Date.now()/1000) + (60*10)
        // Goerli网络的WETH和UNI地址
        const wethToken= "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"
        const uniToken= "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"
        const uniswapRouter="0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
        const path = encodePath([wethToken, uniToken], [3000])
        
        const { deployer } = await getNamedAccounts()
        const signer = await ethers.getSigner(deployer)

        // 用Signer初始化合约
        const uniV3Contract = await ethers.getContractAt(
            V3SwapRouterABI,
            uniswapRouter,
            signer
        )

        const ethAmount = ethers.utils.parseEther('0.01')
        // 1%滑点保护:估算预期输出后扣减1%
        const estimatedOutput = await uniV3Contract.callStatic.exactInput({
            path,
            recipient: ethers.constants.AddressZero,
            deadline: Math.floor(Date.now()/1000) + 60,
            amountIn: ethAmount,
            amountOutMinimum: 0
        })
        const amountOutMinimum = estimatedOutput.mul(99).div(100)

        const params = {
            path: path,
            recipient: deployer,
            deadline: deadline,
            amountIn: ethAmount,
            amountOutMinimum: amountOutMinimum
        }

        console.log(`开始兑换:输入${ethers.utils.formatEther(ethAmount)} ETH,最小预期输出${ethers.utils.formatUnits(amountOutMinimum, 18)} UNI`)
        
        // 动态估算gas并发送交易
        const tx = await uniV3Contract.exactInput(params, {
            value: ethAmount,
            gasLimit: await ethers.provider.estimateGas({
                to: uniswapRouter,
                data: uniV3Contract.interface.encodeFunctionData("exactInput", [params]),
                value: ethAmount
            })
        })

        console.log(`交易哈希:${tx.hash}`)
        await tx.wait()
        console.log("交易已确认")

        const iUni = await ethers.getContractAt("IERC20", uniToken, signer)
        const uniBalance = await iUni.balanceOf(deployer)
        console.log(`当前UNI余额:${ethers.utils.formatUnits(uniBalance, 18)}`)
    } catch (error) {
        console.error("兑换失败:", error)
    }
}

module.exports = { getToken }

内容的提问来源于stack exchange,提问作者Oriok

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 02:25:16